feat: Implement room themes

pull/1432/head
Krille 4 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: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<Object>(
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<Object>(
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: <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(
titleSpacing: 0,
title: ChatAppBarTitle(controller),
actions: _appBarActions(context),
bottom: PreferredSize(
preferredSize: Size.fromHeight(appbarBottomHeight),
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,
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: <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,
);
},
),
),
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,
),
),
],
),
),
);
},
),
),
);
},
),
);
},
),
);
}

@ -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