From 8d91f23f4047be1128c3ae5cb59b396163fca8cb Mon Sep 17 00:00:00 2001 From: Krille Date: Sat, 26 Oct 2024 17:43:38 +0200 Subject: [PATCH] feat: Implement room themes --- lib/pages/chat/chat_view.dart | 444 +++++++++++++++------------- lib/utils/room_theme_extension.dart | 48 +++ 2 files changed, 281 insertions(+), 211 deletions(-) create mode 100644 lib/utils/room_theme_extension.dart diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index beabe749ae..aef6749994 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -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:badges/badges.dart'; @@ -128,7 +130,6 @@ class ChatView extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = Theme.of(context); if (controller.room.membership == Membership.invite) { showFutureLoadingDialog( context: context, @@ -154,231 +155,252 @@ class ChatView extends StatelessWidget { stream: controller.room.client.onRoomState.stream .where((update) => update.roomId == controller.room.id) .rateLimit(const Duration(seconds: 1)), - builder: (context, snapshot) => FutureBuilder( - future: controller.loadTimelineFuture, - builder: (BuildContext context, snapshot) { - var appbarBottomHeight = 0.0; - if (controller.room.pinnedEventIds.isNotEmpty) { - appbarBottomHeight += ChatAppBarListTile.fixedHeight; - } - if (scrollUpBannerEventId != null) { - appbarBottomHeight += ChatAppBarListTile.fixedHeight; - } - final tombstoneEvent = - controller.room.getState(EventTypes.RoomTombstone); - if (tombstoneEvent != null) { - appbarBottomHeight += ChatAppBarListTile.fixedHeight; - } - return Scaffold( - appBar: AppBar( - actionsIconTheme: IconThemeData( - color: controller.selectedEvents.isEmpty - ? null - : theme.colorScheme.primary, - ), - leading: controller.selectMode - ? IconButton( - icon: const Icon(Icons.close), - onPressed: controller.clearSelectedEvents, - tooltip: L10n.of(context).close, - color: theme.colorScheme.primary, - ) - : StreamBuilder( - stream: Matrix.of(context) - .client - .onSync - .stream - .where((syncUpdate) => syncUpdate.hasRoomUpdate), - builder: (context, _) => UnreadRoomsBadge( - 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, + builder: (context, snapshot) { + final roomTheme = controller.room.roomTheme; + final roomColor = roomTheme?.color; + final roomWallpaper = + roomTheme?.wallpaper ?? accountConfig.wallpaperUrl; + return Theme( + data: Theme.of(context).copyWith( + colorScheme: roomColor == null + ? null + : ColorScheme.fromSeed(seedColor: roomColor), + ), + child: FutureBuilder( + future: controller.loadTimelineFuture, + builder: (BuildContext context, snapshot) { + final theme = Theme.of(context); + var appbarBottomHeight = 0.0; + if (controller.room.pinnedEventIds.isNotEmpty) { + appbarBottomHeight += ChatAppBarListTile.fixedHeight; + } + if (scrollUpBannerEventId != null) { + appbarBottomHeight += ChatAppBarListTile.fixedHeight; + } + final tombstoneEvent = + controller.room.getState(EventTypes.RoomTombstone); + if (tombstoneEvent != null) { + appbarBottomHeight += ChatAppBarListTile.fixedHeight; + } + return Scaffold( + appBar: AppBar( + actionsIconTheme: IconThemeData( + color: controller.selectedEvents.isEmpty + ? null + : theme.colorScheme.primary, + ), + leading: controller.selectMode + ? IconButton( icon: const Icon(Icons.close), + onPressed: controller.clearSelectedEvents, tooltip: L10n.of(context).close, - onPressed: () { - controller.discardScrollUpBannerEventId(); - controller.setReadMarker(); - }, - ), - title: L10n.of(context).jumpToLastReadMessage, - trailing: TextButton( - onPressed: () { - controller.scrollToEventId( - scrollUpBannerEventId, - ); - controller.discardScrollUpBannerEventId(); - }, - child: Text(L10n.of(context).jump), + color: theme.colorScheme.primary, + ) + : StreamBuilder( + stream: Matrix.of(context) + .client + .onSync + .stream + .where( + (syncUpdate) => syncUpdate.hasRoomUpdate), + builder: (context, _) => UnreadRoomsBadge( + filter: (r) => r.id != controller.roomId, + badgePosition: + BadgePosition.topEnd(end: 8, top: 4), + 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: [ - 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( + titleSpacing: 0, + title: ChatAppBarTitle(controller), + actions: _appBarActions(context), + bottom: PreferredSize( + preferredSize: Size.fromHeight(appbarBottomHeight), child: Column( - children: [ - 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, + 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), + tooltip: L10n.of(context).close, + onPressed: () { + controller.discardScrollUpBannerEventId(); + controller.setReadMarker(); + }, + ), + title: L10n.of(context).jumpToLastReadMessage, + trailing: TextButton( + onPressed: () { + controller.scrollToEventId( + scrollUpBannerEventId, ); + controller.discardScrollUpBannerEventId(); }, + child: Text(L10n.of(context).jump), ), ), + ], + ), + ), + ), + 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), ), - 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), + ) + : null, + body: DropTarget( + onDragDone: controller.onDragDone, + onDragEntered: controller.onDragEntered, + onDragExited: controller.onDragExited, + child: Stack( + children: [ + 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: [ + 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, + ); + }, + ), ), - child: controller.room.isAbandonedDMRoom == true - ? Row( - mainAxisAlignment: - MainAxisAlignment.spaceEvenly, - children: [ - TextButton.icon( - style: TextButton.styleFrom( - padding: const EdgeInsets.all( - 16, + ), + 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, + ), ), - 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, + 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, + ), ), - ), - 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), + ], ), - ], - ) - : 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, ), - ], - ), + ), + ], ), - if (controller.dragging) - Container( - color: theme.scaffoldBackgroundColor.withOpacity(0.9), - alignment: Alignment.center, - child: const Icon( - Icons.upload_outlined, - size: 100, - ), - ), - ], - ), - ), - ); - }, - ), + ), + ); + }, + ), + ); + }, ), ); } diff --git a/lib/utils/room_theme_extension.dart b/lib/utils/room_theme_extension.dart new file mode 100644 index 0000000000..4a211af9b2 --- /dev/null +++ b/lib/utils/room_theme_extension.dart @@ -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 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 json) { + final colorString = json.tryGet('color'); + final colorInt = colorString == null ? null : int.tryParse(colorString); + final color = + colorInt == null ? null : Result(() => Color(colorInt)).asValue?.value; + final wallpaperString = json.tryGet('wallpaper'); + final wallpaper = + wallpaperString == null ? null : Uri.tryParse(wallpaperString); + + return MatrixRoomTheme( + color: color, + wallpaper: wallpaper, + ); + } + Map toJson() => { + if (color != null) 'color': color?.value, + if (wallpaper != null) 'wallpaper': wallpaper?.toString(), + }; +}