feat: Implement room themes

pull/1432/head
Krille 8 months ago
parent cb32608674
commit 8d91f23f40
No known key found for this signature in database
GPG Key ID: E067ECD60F1A0652

@ -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()),
),
), ),
), titleSpacing: 0,
], title: ChatAppBarTitle(controller),
), actions: _appBarActions(context),
), bottom: PreferredSize(
), preferredSize: Size.fromHeight(appbarBottomHeight),
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( child: Column(
children: <Widget>[ mainAxisSize: MainAxisSize.min,
Expanded( children: [
child: GestureDetector( PinnedEvents(controller),
onTap: controller.clearSingleSelectedEvent, if (tombstoneEvent != null)
child: Builder( ChatAppBarListTile(
builder: (context) { title: tombstoneEvent.parsedTombstoneContent.body,
if (controller.timeline == null) { leading: const Padding(
return const Center( padding: EdgeInsets.all(8.0),
child: CircularProgressIndicator.adaptive( child: Icon(Icons.upgrade_outlined),
strokeWidth: 2, ),
), trailing: TextButton(
); onPressed: controller.goToNewRoomAction,
} child: Text(L10n.of(context).goToTheNewRoom),
return ChatEventList( ),
controller: controller, ),
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) : null,
Container( body: DropTarget(
margin: EdgeInsets.only( onDragDone: controller.onDragDone,
bottom: bottomSheetPadding, onDragEntered: controller.onDragEntered,
left: bottomSheetPadding, onDragExited: controller.onDragExited,
right: bottomSheetPadding, child: Stack(
), children: <Widget>[
constraints: const BoxConstraints( if (roomWallpaper != null)
maxWidth: FluffyThemes.columnWidth * 2.5, Opacity(
), opacity: accountConfig.wallpaperOpacity ?? 1,
alignment: Alignment.center, child: MxcImage(
child: Material( uri: roomWallpaper,
clipBehavior: Clip.hardEdge, fit: BoxFit.cover,
color: theme.colorScheme.surfaceContainerHigh, isThumbnail: true,
borderRadius: const BorderRadius.all( width: FluffyThemes.columnWidth * 4,
Radius.circular(24), 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,
);
},
),
), ),
child: controller.room.isAbandonedDMRoom == true ),
? Row( if (controller.room.canSendDefaultMessages &&
mainAxisAlignment: controller.room.membership == Membership.join)
MainAxisAlignment.spaceEvenly, Container(
children: [ margin: EdgeInsets.only(
TextButton.icon( bottom: bottomSheetPadding,
style: TextButton.styleFrom( left: bottomSheetPadding,
padding: const EdgeInsets.all( right: bottomSheetPadding,
16, ),
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: TextButton.icon(
theme.colorScheme.error, style: TextButton.styleFrom(
), padding: const EdgeInsets.all(
icon: const Icon( 16,
Icons.archive_outlined, ),
), ),
onPressed: controller.leaveChat, icon: const Icon(
label: Text( Icons.forum_outlined,
L10n.of(context).leave, ),
), onPressed:
), controller.recreateChat,
TextButton.icon( label: Text(
style: TextButton.styleFrom( L10n.of(context).reopenChat,
padding: const EdgeInsets.all( ),
16,
), ),
), ],
icon: const Icon( )
Icons.forum_outlined, : Column(
), mainAxisSize: MainAxisSize.min,
onPressed: controller.recreateChat, children: [
label: Text( const ConnectionStatusHeader(),
L10n.of(context).reopenChat, ReactionsPicker(controller),
), ReplyDisplay(controller),
ChatInputRow(controller),
ChatEmojiPicker(controller),
],
), ),
], ),
) ),
: Column( ],
mainAxisSize: MainAxisSize.min, ),
children: [ ),
const ConnectionStatusHeader(), if (controller.dragging)
ReactionsPicker(controller), Container(
ReplyDisplay(controller), color:
ChatInputRow(controller), theme.scaffoldBackgroundColor.withOpacity(0.9),
ChatEmojiPicker(controller), 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,
),
),
],
),
),
);
},
),
), ),
); );
} }

@ -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(),
};
}
Loading…
Cancel
Save