Compare commits

...
Sign in to create a new pull request.

1 commit

Author SHA1 Message Date
Krille
8d91f23f40
feat: Implement room themes 2024-10-26 17:46:35 +02:00
2 changed files with 284 additions and 214 deletions

View file

@ -1,3 +1,5 @@
import 'package:fluffychat/utils/room_theme_extension.dart';
import 'package:fluffychat/utils/string_color.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:badges/badges.dart'; import 'package:badges/badges.dart';
@ -128,7 +130,6 @@ class ChatView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context);
if (controller.room.membership == Membership.invite) { if (controller.room.membership == Membership.invite) {
showFutureLoadingDialog( showFutureLoadingDialog(
context: context, context: context,
@ -154,231 +155,252 @@ class ChatView extends StatelessWidget {
stream: controller.room.client.onRoomState.stream stream: controller.room.client.onRoomState.stream
.where((update) => update.roomId == controller.room.id) .where((update) => update.roomId == controller.room.id)
.rateLimit(const Duration(seconds: 1)), .rateLimit(const Duration(seconds: 1)),
builder: (context, snapshot) => FutureBuilder( builder: (context, snapshot) {
future: controller.loadTimelineFuture, final roomTheme = controller.room.roomTheme;
builder: (BuildContext context, snapshot) { final roomColor = roomTheme?.color;
var appbarBottomHeight = 0.0; final roomWallpaper =
if (controller.room.pinnedEventIds.isNotEmpty) { roomTheme?.wallpaper ?? accountConfig.wallpaperUrl;
appbarBottomHeight += ChatAppBarListTile.fixedHeight; return Theme(
} data: Theme.of(context).copyWith(
if (scrollUpBannerEventId != null) { colorScheme: roomColor == null
appbarBottomHeight += ChatAppBarListTile.fixedHeight; ? null
} : ColorScheme.fromSeed(seedColor: roomColor),
final tombstoneEvent = ),
controller.room.getState(EventTypes.RoomTombstone); child: FutureBuilder(
if (tombstoneEvent != null) { future: controller.loadTimelineFuture,
appbarBottomHeight += ChatAppBarListTile.fixedHeight; builder: (BuildContext context, snapshot) {
} final theme = Theme.of(context);
return Scaffold( var appbarBottomHeight = 0.0;
appBar: AppBar( if (controller.room.pinnedEventIds.isNotEmpty) {
actionsIconTheme: IconThemeData( appbarBottomHeight += ChatAppBarListTile.fixedHeight;
color: controller.selectedEvents.isEmpty }
? null if (scrollUpBannerEventId != null) {
: theme.colorScheme.primary, appbarBottomHeight += ChatAppBarListTile.fixedHeight;
), }
leading: controller.selectMode final tombstoneEvent =
? IconButton( controller.room.getState(EventTypes.RoomTombstone);
icon: const Icon(Icons.close), if (tombstoneEvent != null) {
onPressed: controller.clearSelectedEvents, appbarBottomHeight += ChatAppBarListTile.fixedHeight;
tooltip: L10n.of(context).close, }
color: theme.colorScheme.primary, return Scaffold(
) appBar: AppBar(
: StreamBuilder<Object>( actionsIconTheme: IconThemeData(
stream: Matrix.of(context) color: controller.selectedEvents.isEmpty
.client ? null
.onSync : theme.colorScheme.primary,
.stream ),
.where((syncUpdate) => syncUpdate.hasRoomUpdate), leading: controller.selectMode
builder: (context, _) => UnreadRoomsBadge( ? IconButton(
filter: (r) => r.id != controller.roomId,
badgePosition: BadgePosition.topEnd(end: 8, top: 4),
child: const Center(child: BackButton()),
),
),
titleSpacing: 0,
title: ChatAppBarTitle(controller),
actions: _appBarActions(context),
bottom: PreferredSize(
preferredSize: Size.fromHeight(appbarBottomHeight),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
PinnedEvents(controller),
if (tombstoneEvent != null)
ChatAppBarListTile(
title: tombstoneEvent.parsedTombstoneContent.body,
leading: const Padding(
padding: EdgeInsets.all(8.0),
child: Icon(Icons.upgrade_outlined),
),
trailing: TextButton(
onPressed: controller.goToNewRoomAction,
child: Text(L10n.of(context).goToTheNewRoom),
),
),
if (scrollUpBannerEventId != null)
ChatAppBarListTile(
leading: IconButton(
color: theme.colorScheme.onSurfaceVariant,
icon: const Icon(Icons.close), icon: const Icon(Icons.close),
onPressed: controller.clearSelectedEvents,
tooltip: L10n.of(context).close, tooltip: L10n.of(context).close,
onPressed: () { color: theme.colorScheme.primary,
controller.discardScrollUpBannerEventId(); )
controller.setReadMarker(); : StreamBuilder<Object>(
}, stream: Matrix.of(context)
), .client
title: L10n.of(context).jumpToLastReadMessage, .onSync
trailing: TextButton( .stream
onPressed: () { .where(
controller.scrollToEventId( (syncUpdate) => syncUpdate.hasRoomUpdate),
scrollUpBannerEventId, builder: (context, _) => UnreadRoomsBadge(
); filter: (r) => r.id != controller.roomId,
controller.discardScrollUpBannerEventId(); badgePosition:
}, BadgePosition.topEnd(end: 8, top: 4),
child: Text(L10n.of(context).jump), child: const Center(child: BackButton()),
),
),
],
),
),
),
floatingActionButton: controller.showScrollDownButton &&
controller.selectedEvents.isEmpty
? Padding(
padding: const EdgeInsets.only(bottom: 56.0),
child: FloatingActionButton(
onPressed: controller.scrollDown,
heroTag: null,
mini: true,
child: const Icon(Icons.arrow_downward_outlined),
),
)
: null,
body: DropTarget(
onDragDone: controller.onDragDone,
onDragEntered: controller.onDragEntered,
onDragExited: controller.onDragExited,
child: Stack(
children: <Widget>[
if (accountConfig.wallpaperUrl != null)
Opacity(
opacity: accountConfig.wallpaperOpacity ?? 1,
child: MxcImage(
uri: accountConfig.wallpaperUrl,
fit: BoxFit.cover,
isThumbnail: true,
width: FluffyThemes.columnWidth * 4,
height: FluffyThemes.columnWidth * 4,
placeholder: (_) => Container(),
),
),
SafeArea(
child: Column(
children: <Widget>[
Expanded(
child: GestureDetector(
onTap: controller.clearSingleSelectedEvent,
child: Builder(
builder: (context) {
if (controller.timeline == null) {
return const Center(
child: CircularProgressIndicator.adaptive(
strokeWidth: 2,
),
);
}
return ChatEventList(
controller: controller,
);
},
),
), ),
), ),
if (controller.room.canSendDefaultMessages && titleSpacing: 0,
controller.room.membership == Membership.join) title: ChatAppBarTitle(controller),
Container( actions: _appBarActions(context),
margin: EdgeInsets.only( bottom: PreferredSize(
bottom: bottomSheetPadding, preferredSize: Size.fromHeight(appbarBottomHeight),
left: bottomSheetPadding, child: Column(
right: bottomSheetPadding, mainAxisSize: MainAxisSize.min,
children: [
PinnedEvents(controller),
if (tombstoneEvent != null)
ChatAppBarListTile(
title: tombstoneEvent.parsedTombstoneContent.body,
leading: const Padding(
padding: EdgeInsets.all(8.0),
child: Icon(Icons.upgrade_outlined),
), ),
constraints: const BoxConstraints( trailing: TextButton(
maxWidth: FluffyThemes.columnWidth * 2.5, onPressed: controller.goToNewRoomAction,
child: Text(L10n.of(context).goToTheNewRoom),
), ),
alignment: Alignment.center, ),
child: Material( if (scrollUpBannerEventId != null)
clipBehavior: Clip.hardEdge, ChatAppBarListTile(
color: theme.colorScheme.surfaceContainerHigh, leading: IconButton(
borderRadius: const BorderRadius.all( color: theme.colorScheme.onSurfaceVariant,
Radius.circular(24), icon: const Icon(Icons.close),
), tooltip: L10n.of(context).close,
child: controller.room.isAbandonedDMRoom == true onPressed: () {
? Row( controller.discardScrollUpBannerEventId();
mainAxisAlignment: controller.setReadMarker();
MainAxisAlignment.spaceEvenly, },
children: [ ),
TextButton.icon( title: L10n.of(context).jumpToLastReadMessage,
style: TextButton.styleFrom( trailing: TextButton(
padding: const EdgeInsets.all( onPressed: () {
16, controller.scrollToEventId(
), scrollUpBannerEventId,
foregroundColor: );
theme.colorScheme.error, controller.discardScrollUpBannerEventId();
), },
icon: const Icon( child: Text(L10n.of(context).jump),
Icons.archive_outlined,
),
onPressed: controller.leaveChat,
label: Text(
L10n.of(context).leave,
),
),
TextButton.icon(
style: TextButton.styleFrom(
padding: const EdgeInsets.all(
16,
),
),
icon: const Icon(
Icons.forum_outlined,
),
onPressed: controller.recreateChat,
label: Text(
L10n.of(context).reopenChat,
),
),
],
)
: Column(
mainAxisSize: MainAxisSize.min,
children: [
const ConnectionStatusHeader(),
ReactionsPicker(controller),
ReplyDisplay(controller),
ChatInputRow(controller),
ChatEmojiPicker(controller),
],
),
), ),
), ),
], ],
), ),
), ),
if (controller.dragging) ),
Container( floatingActionButton: controller.showScrollDownButton &&
color: theme.scaffoldBackgroundColor.withOpacity(0.9), controller.selectedEvents.isEmpty
alignment: Alignment.center, ? Padding(
child: const Icon( padding: const EdgeInsets.only(bottom: 56.0),
Icons.upload_outlined, child: FloatingActionButton(
size: 100, onPressed: controller.scrollDown,
heroTag: null,
mini: true,
child: const Icon(Icons.arrow_downward_outlined),
),
)
: null,
body: DropTarget(
onDragDone: controller.onDragDone,
onDragEntered: controller.onDragEntered,
onDragExited: controller.onDragExited,
child: Stack(
children: <Widget>[
if (roomWallpaper != null)
Opacity(
opacity: accountConfig.wallpaperOpacity ?? 1,
child: MxcImage(
uri: roomWallpaper,
fit: BoxFit.cover,
isThumbnail: true,
width: FluffyThemes.columnWidth * 4,
height: FluffyThemes.columnWidth * 4,
placeholder: (_) => Container(),
),
),
SafeArea(
child: Column(
children: <Widget>[
Expanded(
child: GestureDetector(
onTap: controller.clearSingleSelectedEvent,
child: Builder(
builder: (context) {
if (controller.timeline == null) {
return const Center(
child: CircularProgressIndicator
.adaptive(
strokeWidth: 2,
),
);
}
return ChatEventList(
controller: controller,
);
},
),
),
),
if (controller.room.canSendDefaultMessages &&
controller.room.membership == Membership.join)
Container(
margin: EdgeInsets.only(
bottom: bottomSheetPadding,
left: bottomSheetPadding,
right: bottomSheetPadding,
),
constraints: const BoxConstraints(
maxWidth: FluffyThemes.columnWidth * 2.5,
),
alignment: Alignment.center,
child: Material(
clipBehavior: Clip.hardEdge,
color:
theme.colorScheme.surfaceContainerHigh,
borderRadius: const BorderRadius.all(
Radius.circular(24),
),
child: controller.room.isAbandonedDMRoom ==
true
? Row(
mainAxisAlignment:
MainAxisAlignment.spaceEvenly,
children: [
TextButton.icon(
style: TextButton.styleFrom(
padding: const EdgeInsets.all(
16,
),
foregroundColor:
theme.colorScheme.error,
),
icon: const Icon(
Icons.archive_outlined,
),
onPressed: controller.leaveChat,
label: Text(
L10n.of(context).leave,
),
),
TextButton.icon(
style: TextButton.styleFrom(
padding: const EdgeInsets.all(
16,
),
),
icon: const Icon(
Icons.forum_outlined,
),
onPressed:
controller.recreateChat,
label: Text(
L10n.of(context).reopenChat,
),
),
],
)
: Column(
mainAxisSize: MainAxisSize.min,
children: [
const ConnectionStatusHeader(),
ReactionsPicker(controller),
ReplyDisplay(controller),
ChatInputRow(controller),
ChatEmojiPicker(controller),
],
),
),
),
],
),
), ),
), if (controller.dragging)
], Container(
), color:
), theme.scaffoldBackgroundColor.withOpacity(0.9),
); alignment: Alignment.center,
}, child: const Icon(
), Icons.upload_outlined,
size: 100,
),
),
],
),
),
);
},
),
);
},
), ),
); );
} }

View file

@ -0,0 +1,48 @@
import 'package:async/async.dart';
import 'package:flutter/widgets.dart';
import 'package:matrix/matrix.dart' hide Result;
extension RoomThemeExtension on Room {
static const String typeKey = 'im.fluffychat.room.theme';
MatrixRoomTheme? get roomTheme {
final content = getState(typeKey)?.content;
if (content == null) return null;
return MatrixRoomTheme.fromJson(content);
}
Future<void> setRoomTheme(MatrixRoomTheme theme) =>
client.setRoomStateWithKey(
id,
typeKey,
'',
theme.toJson(),
);
}
class MatrixRoomTheme {
final Color? color;
final Uri? wallpaper;
const MatrixRoomTheme({required this.color, required this.wallpaper});
factory MatrixRoomTheme.fromJson(Map<String, Object?> json) {
final colorString = json.tryGet<String>('color');
final colorInt = colorString == null ? null : int.tryParse(colorString);
final color =
colorInt == null ? null : Result(() => Color(colorInt)).asValue?.value;
final wallpaperString = json.tryGet<String>('wallpaper');
final wallpaper =
wallpaperString == null ? null : Uri.tryParse(wallpaperString);
return MatrixRoomTheme(
color: color,
wallpaper: wallpaper,
);
}
Map<String, Object?> toJson() => {
if (color != null) 'color': color?.value,
if (wallpaper != null) 'wallpaper': wallpaper?.toString(),
};
}