diff --git a/commet/lib/client/components/push_notification/android/firebase_push_notifier.dart b/commet/lib/client/components/push_notification/android/firebase_push_notifier.dart index 9c0e6161..86d3fc87 100644 --- a/commet/lib/client/components/push_notification/android/firebase_push_notifier.dart +++ b/commet/lib/client/components/push_notification/android/firebase_push_notifier.dart @@ -32,7 +32,7 @@ Future onForegroundMessage(dynamic message) async { var room = client.getRoom(roomId); var event = await room!.getEvent(eventId); - var user = room.getMemberOrFallback(event!.senderId)!; + var user = room.getMemberOrFallback(event!.senderId); Log.i("Dispatching notification"); diff --git a/commet/lib/client/components/push_notification/android/unified_push_notifier.dart b/commet/lib/client/components/push_notification/android/unified_push_notifier.dart index bc280fad..162c3420 100644 --- a/commet/lib/client/components/push_notification/android/unified_push_notifier.dart +++ b/commet/lib/client/components/push_notification/android/unified_push_notifier.dart @@ -99,7 +99,7 @@ class UnifiedPushNotifier implements Notifier { var room = client.getRoom(roomId); var event = await room!.getEvent(eventId); - var user = room.getMemberOrFallback(event!.senderId)!; + var user = room.getMemberOrFallback(event!.senderId); NotificationManager.notify(MessageNotificationContent( senderName: user.displayName, diff --git a/commet/lib/client/matrix/matrix_room.dart b/commet/lib/client/matrix/matrix_room.dart index a849e94d..22db4e41 100644 --- a/commet/lib/client/matrix/matrix_room.dart +++ b/commet/lib/client/matrix/matrix_room.dart @@ -179,10 +179,13 @@ class MatrixRoom extends Room { } } - _onUpdateSubscription = - _matrixRoom.onUpdate.stream.listen(onMatrixRoomUpdate); + _onUpdateSubscription = _matrixRoom.client.onRoomState.stream + .where((event) => event.roomId == _matrixRoom.id) + .listen(onRoomStateUpdated); - _matrixRoom.client.onEvent.stream.listen(onEvent); + _matrixRoom.client.onEvent.stream + .where((event) => event.roomID == _matrixRoom.id) + .listen(onEvent); _permissions = MatrixRoomPermissions(_matrixRoom); } @@ -238,10 +241,6 @@ class MatrixRoom extends Room { var sender = getMemberOrFallback(event.senderId); - if (sender == null) { - return; - } - var notification = MessageNotificationContent( senderName: sender.displayName, senderId: sender.identifier, @@ -371,9 +370,11 @@ class MatrixRoom extends Room { await _matrixRoom.enableEncryption(); } - void onMatrixRoomUpdate(String event) async { + void onRoomStateUpdated(matrix.Event event) async { _displayName = _matrixRoom.getLocalizedDisplayname(); - _onUpdate.add(null); + if (event.type == "m.room.name") { + _onUpdate.add(null); + } } @override @@ -509,7 +510,7 @@ class MatrixRoom extends Room { bool get isMembersListComplete => _matrixRoom.participantListComplete; @override - Member? getMemberOrFallback(String id) { + Member getMemberOrFallback(String id) { return MatrixMember( _matrixRoom.client, _matrixRoom.unsafeGetUserFromMemoryOrFallback(id)); } @@ -522,9 +523,8 @@ class MatrixRoom extends Room { var roles = (state.content["users"] as Map); var ids = roles.keys; - var result = ids - .map((e) => (getMemberOrFallback(e)!, MatrixRole(roles[e]))) - .toList(); + var result = + ids.map((e) => (getMemberOrFallback(e), MatrixRole(roles[e]))).toList(); result.removeWhere((element) => element.$2.rank == 0); diff --git a/commet/lib/client/matrix/matrix_timeline_event.dart b/commet/lib/client/matrix/matrix_timeline_event.dart index 60ea06df..c87fd5f3 100644 --- a/commet/lib/client/matrix/matrix_timeline_event.dart +++ b/commet/lib/client/matrix/matrix_timeline_event.dart @@ -256,7 +256,11 @@ class MatrixTimelineEvent implements TimelineEvent { } @override - Widget buildFormattedContent() { + Widget? buildFormattedContent() { + if (formattedBody == null) { + return null; + } + return MatrixHtmlParser.parse(formattedBody!, event.room.client); } diff --git a/commet/lib/client/room.dart b/commet/lib/client/room.dart index b458b5d0..59d35b90 100644 --- a/commet/lib/client/room.dart +++ b/commet/lib/client/room.dart @@ -142,7 +142,7 @@ abstract class Room { Future getEvent(String eventId); - Member? getMemberOrFallback(String id); + Member getMemberOrFallback(String id); @override bool operator ==(Object other) { diff --git a/commet/lib/client/timeline.dart b/commet/lib/client/timeline.dart index 24469ca4..e1d162fd 100644 --- a/commet/lib/client/timeline.dart +++ b/commet/lib/client/timeline.dart @@ -106,7 +106,7 @@ abstract class TimelineEvent { /// If you want to display the same message twice, use `buildFormattedContent()` to create a new widget Widget? get formattedContent; - Widget buildFormattedContent(); + Widget? buildFormattedContent(); String? get relatedEventId; String? get stateKey; diff --git a/commet/lib/service/background_service_notifications/background_service_task_notification.dart b/commet/lib/service/background_service_notifications/background_service_task_notification.dart index a1eabef3..c3da3ef3 100644 --- a/commet/lib/service/background_service_notifications/background_service_task_notification.dart +++ b/commet/lib/service/background_service_notifications/background_service_task_notification.dart @@ -102,7 +102,7 @@ class BackgroundNotificationsManager { Log.i("Found room: ${room?.displayName}"); var event = await room!.getEvent(eventId); - var user = room.getMemberOrFallback(event!.senderId)!; + var user = room.getMemberOrFallback(event!.senderId); Log.i("Got user: $user ($user)"); Log.i("Got event: ${event.body}"); diff --git a/commet/lib/ui/atoms/emoji_reaction.dart b/commet/lib/ui/atoms/emoji_reaction.dart index fdaaf784..2b3c52fc 100644 --- a/commet/lib/ui/atoms/emoji_reaction.dart +++ b/commet/lib/ui/atoms/emoji_reaction.dart @@ -20,7 +20,7 @@ class EmojiReaction extends StatelessWidget { final int numReactions; final bool highlighted; - BorderRadius get borderRadius => BorderRadius.circular(12); + BorderRadius get borderRadius => BorderRadius.circular(8); @override Widget build(BuildContext context) { diff --git a/commet/lib/ui/molecules/read_indicator.dart b/commet/lib/ui/molecules/read_indicator.dart index bd83779a..2562005c 100644 --- a/commet/lib/ui/molecules/read_indicator.dart +++ b/commet/lib/ui/molecules/read_indicator.dart @@ -86,7 +86,7 @@ class _SingleUserReadIndicatorState extends State { late Member member; @override void initState() { - member = widget.room.getMemberOrFallback(widget.identifier)!; + member = widget.room.getMemberOrFallback(widget.identifier); super.initState(); } diff --git a/commet/lib/ui/molecules/room_timeline_widget/room_timeline_widget_view.dart b/commet/lib/ui/molecules/room_timeline_widget/room_timeline_widget_view.dart index 9108729a..7c226c6e 100644 --- a/commet/lib/ui/molecules/room_timeline_widget/room_timeline_widget_view.dart +++ b/commet/lib/ui/molecules/room_timeline_widget/room_timeline_widget_view.dart @@ -1,7 +1,10 @@ import 'dart:async'; import 'package:commet/client/timeline.dart'; +import 'package:commet/config/build_config.dart'; import 'package:commet/debug/log.dart'; +import 'package:commet/main.dart'; +import 'package:commet/ui/molecules/timeline_events/timeline_event_layout.dart'; import 'package:commet/ui/molecules/timeline_events/timeline_view_entry.dart'; import 'package:flutter/material.dart'; @@ -34,6 +37,7 @@ class RoomTimelineWidgetViewState extends State { GlobalKey firstFrameScrollViewKey = GlobalKey(); GlobalKey scrollViewKey = GlobalKey(); GlobalKey centerKey = GlobalKey(); + GlobalKey recentItemsKey = GlobalKey(); late List subscriptions; @@ -73,6 +77,7 @@ class RoomTimelineWidgetViewState extends State { void onEventAdded(int index) { setState(() {}); + if (index == 0 || index < recentItemsCount) { recentItemsCount += 1; } else { @@ -96,6 +101,7 @@ class RoomTimelineWidgetViewState extends State { } void onEventChanged(int index) { + Log.d("Event changed: $index"); var event = widget.timeline.events[index]; var existing = eventKeys[index]; eventKeys[index] = (existing.$1, event.eventId); @@ -106,7 +112,13 @@ class RoomTimelineWidgetViewState extends State { assert(event.eventId == key.$2); - key.$1.currentState?.setState(() {}); + var state = key.$1.currentState; + + if (state is TimelineEventViewWidget) { + (state as TimelineViewEntryState).update(index); + } else { + Log.w("Failed to get state"); + } } void onEventRemoved(int index) { @@ -157,63 +169,93 @@ class RoomTimelineWidgetViewState extends State { @override Widget build(BuildContext context) { - return Offstage( - offstage: firstFrame, - child: Scaffold( - body: CustomScrollView( + return Material( + color: Colors.transparent, + child: Offstage( + offstage: firstFrame, + child: CustomScrollView( key: firstFrame ? firstFrameScrollViewKey : scrollViewKey, controller: controller, reverse: true, center: centerKey, slivers: [ SliverList( + key: recentItemsKey, // Recent Items delegate: SliverChildBuilderDelegate( - (BuildContext context, int index) { - int displayIndex = recentItemsCount - index - 1; + childCount: recentItemsCount, + addAutomaticKeepAlives: false, + (BuildContext context, int sliverIndex) { + int timelineIndex = recentItemsCount - sliverIndex - 1; numBuilds += 1; - Log.d("Num Builds: $numBuilds"); - var key = eventKeys[displayIndex]; + var key = eventKeys[timelineIndex]; assert( - key.$2 == widget.timeline.events[displayIndex].eventId); + key.$2 == widget.timeline.events[timelineIndex].eventId); return Container( alignment: Alignment.center, - color: Colors.blue[200 + index % 4 * 100]!.withAlpha(30), + color: preferences.developerMode && BuildConfig.DEBUG + ? Colors.blue[200 + sliverIndex % 4 * 100]! + .withAlpha(30) + : null, child: TimelineViewEntry( key: key.$1, timeline: widget.timeline, - index: displayIndex), + initialIndex: timelineIndex), ); }, - childCount: recentItemsCount, + findChildIndexCallback: (key) { + var timelineIndex = + eventKeys.indexWhere((element) => element.$1 == key); + if (timelineIndex == -1) { + Log.w( + "Failed to get timeline index for key: $timelineIndex"); + return null; + } + + return recentItemsCount - timelineIndex - 1; + }, ), ), SliverList( key: centerKey, // History Items delegate: SliverChildBuilderDelegate( - (BuildContext context, int index) { + addAutomaticKeepAlives: false, + childCount: historyItemsCount, + (BuildContext context, int sliverIndex) { numBuilds += 1; // ignore: avoid_print Log.d("Num Builds: $numBuilds"); - var displayIndex = recentItemsCount + index; + var timelineIndex = recentItemsCount + sliverIndex; - var key = eventKeys[displayIndex]; + var key = eventKeys[timelineIndex]; assert( - key.$2 == widget.timeline.events[displayIndex].eventId); + key.$2 == widget.timeline.events[timelineIndex].eventId); return Container( alignment: Alignment.center, - color: Colors.red[200 + index % 4 * 100]!.withAlpha(30), + color: preferences.developerMode && BuildConfig.DEBUG + ? Colors.red[200 + sliverIndex % 4 * 100]!.withAlpha(30) + : null, child: TimelineViewEntry( key: key.$1, timeline: widget.timeline, - index: displayIndex), + initialIndex: timelineIndex), ); }, - childCount: historyItemsCount, + findChildIndexCallback: (key) { + var timelineIndex = + eventKeys.indexWhere((element) => element.$1 == key); + if (timelineIndex == -1) { + Log.w( + "Failed to get timeline index for key: $timelineIndex"); + return null; + } + + return timelineIndex - recentItemsCount; + }, ), ), ], diff --git a/commet/lib/ui/molecules/timeline_event.dart b/commet/lib/ui/molecules/timeline_event.dart index 931c8e43..859ee121 100644 --- a/commet/lib/ui/molecules/timeline_event.dart +++ b/commet/lib/ui/molecules/timeline_event.dart @@ -120,11 +120,11 @@ class _TimelineEventState extends State { name: "errorMessageFailedToSend"); String get displayName => widget.timeline.room - .getMemberOrFallback(widget.event.senderId)! + .getMemberOrFallback(widget.event.senderId) .displayName; ImageProvider? get avatar => - widget.timeline.room.getMemberOrFallback(widget.event.senderId)!.avatar; + widget.timeline.room.getMemberOrFallback(widget.event.senderId).avatar; Color get color => widget.timeline.room.getColorOfUser(widget.event.senderId); @@ -135,7 +135,7 @@ class _TimelineEventState extends State { String? get relatedEventDisplayName => relatedEvent == null ? null : widget.timeline.room - .getMemberOrFallback(relatedEvent!.senderId)! + .getMemberOrFallback(relatedEvent!.senderId) .displayName; UrlPreviewData? urlPreviews; @@ -380,7 +380,7 @@ class _TimelineEventState extends State { // if the cache didnt have anything lets just build new content. This should really never happen though formatted ??= widget.event.buildFormattedContent(); - return formatted; + return formatted!; } if (widget.event.body != null) diff --git a/commet/lib/ui/molecules/timeline_events/events/timeline_event_view_attachments.dart b/commet/lib/ui/molecules/timeline_events/events/timeline_event_view_attachments.dart new file mode 100644 index 00000000..6036a94a --- /dev/null +++ b/commet/lib/ui/molecules/timeline_events/events/timeline_event_view_attachments.dart @@ -0,0 +1,23 @@ +import 'package:commet/client/attachment.dart'; +import 'package:commet/ui/atoms/message_attachment.dart'; +import 'package:flutter/material.dart'; + +class TimelineEventViewAttachments extends StatelessWidget { + const TimelineEventViewAttachments({required this.attachments, super.key}); + final List attachments; + @override + Widget build(BuildContext context) { + return Wrap( + children: attachments + .map((e) => Padding( + padding: const EdgeInsets.fromLTRB(0, 2, 2, 2), + child: RepaintBoundary( + child: MessageAttachment( + e, + ), + ), + )) + .toList(), + ); + } +} diff --git a/commet/lib/ui/molecules/timeline_events/events/timeline_event_view_message.dart b/commet/lib/ui/molecules/timeline_events/events/timeline_event_view_message.dart new file mode 100644 index 00000000..cd900949 --- /dev/null +++ b/commet/lib/ui/molecules/timeline_events/events/timeline_event_view_message.dart @@ -0,0 +1,134 @@ +import 'package:commet/client/attachment.dart'; +import 'package:commet/client/client.dart'; +import 'package:commet/ui/molecules/timeline_events/events/timeline_event_view_attachments.dart'; +import 'package:commet/ui/molecules/timeline_events/events/timeline_event_view_reactions.dart'; +import 'package:commet/ui/molecules/timeline_events/events/timeline_event_view_reply.dart'; +import 'package:commet/ui/molecules/timeline_events/layouts/timeline_event_layout_message.dart'; +import 'package:commet/ui/molecules/timeline_events/timeline_event_layout.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; + +class TimelineEventViewMessage extends StatefulWidget { + const TimelineEventViewMessage( + {super.key, + required this.timeline, + this.showSender = true, + required this.initialIndex}); + + final Timeline timeline; + final int initialIndex; + final bool showSender; + + @override + State createState() => + _TimelineEventViewMessageState(); +} + +class _TimelineEventViewMessageState extends State + implements TimelineEventViewWidget { + late String senderName; + late Color senderColor; + + GlobalKey reactionsKey = GlobalKey(); + + Widget? formattedContent; + ImageProvider? senderAvatar; + List? attachments; + bool hasReactions = false; + bool isInResponse = false; + bool showSender = false; + late String currentUserIdentifier; + + int index = 0; + + @override + void initState() { + currentUserIdentifier = widget.timeline.client.self!.identifier; + loadEventState(widget.initialIndex); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return TimelineEventLayoutMessage( + senderName: senderName, + senderColor: senderColor, + senderAvatar: senderAvatar, + showSender: showSender, + formattedContent: formattedContent, + attachments: attachments != null + ? TimelineEventViewAttachments(attachments: attachments!) + : null, + inResponseTo: isInResponse + ? TimelineEventViewReply( + timeline: widget.timeline, + index: index, + ) + : null, + reactions: hasReactions + ? TimelineEventViewReactions( + key: reactionsKey, timeline: widget.timeline, initialIndex: index) + : null, + ); + } + + @override + void update(int newIndex) { + setState(() { + loadEventState(newIndex); + }); + + for (var key in [reactionsKey]) { + if (key.currentState is TimelineEventViewWidget) { + (key.currentState as TimelineEventViewWidget).update(newIndex); + } + } + } + + void loadEventState(var eventIndex) { + index = eventIndex; + var event = widget.timeline.events[eventIndex]; + var sender = widget.timeline.room.getMemberOrFallback(event.senderId); + + senderName = sender.displayName; + senderAvatar = sender.avatar; + senderColor = sender.defaultColor; + + showSender = shouldShowSender(eventIndex); + + if (event.bodyFormat != null) { + formattedContent = event.formattedContent!; + } + + hasReactions = event.reactions != null && event.reactions!.isNotEmpty; + + attachments = event.attachments; + isInResponse = event.relatedEventId != null && + event.relationshipType == EventRelationshipType.reply; + } + + bool shouldShowSender(int index) { + if (widget.timeline.events.length <= index + 1) { + return true; + } + + if (widget.timeline.events[index].relationshipType == + EventRelationshipType.reply) return true; + + if (![EventType.message, EventType.encrypted] + .contains(widget.timeline.events[index + 1].type)) return true; + + if (widget.timeline.events[index + 1].status == + TimelineEventStatus.removed) { + return true; + } + + if (widget.timeline.events[index].originServerTs + .difference(widget.timeline.events[index + 1].originServerTs) + .inMinutes > + 1) return true; + + return widget.timeline.events[index].senderId != + widget.timeline.events[index + 1].senderId; + } +} diff --git a/commet/lib/ui/molecules/timeline_events/events/timeline_event_view_reactions.dart b/commet/lib/ui/molecules/timeline_events/events/timeline_event_view_reactions.dart new file mode 100644 index 00000000..98d93511 --- /dev/null +++ b/commet/lib/ui/molecules/timeline_events/events/timeline_event_view_reactions.dart @@ -0,0 +1,70 @@ +import 'package:commet/client/components/emoticon/emoticon.dart'; +import 'package:commet/client/timeline.dart'; +import 'package:commet/ui/atoms/emoji_reaction.dart'; +import 'package:commet/ui/molecules/timeline_events/timeline_event_layout.dart'; +import 'package:flutter/material.dart'; + +import 'package:flutter/material.dart' as material; + +class TimelineEventViewReactions extends StatefulWidget { + const TimelineEventViewReactions( + {required this.initialIndex, required this.timeline, super.key}); + + final int initialIndex; + final Timeline timeline; + + @override + State createState() => + _TimelineEventViewReactionsState(); +} + +class _TimelineEventViewReactionsState extends State + implements TimelineEventViewWidget { + late Map> reactions; + + late final String? currentUserIdentifier; + late final TimelineEvent event; + + @override + void initState() { + event = widget.timeline.events[widget.initialIndex]; + currentUserIdentifier = widget.timeline.client.self?.identifier; + setStateFromIndex(widget.initialIndex); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Wrap( + spacing: 3, + runSpacing: 3, + direction: material.Axis.horizontal, + children: reactions.keys.map((key) { + var value = reactions[key]!; + return EmojiReaction( + emoji: key, + onTapped: onReactionTapped, + numReactions: value.length, + highlighted: value.contains(currentUserIdentifier)); + }).toList()); + } + + onReactionTapped(Emoticon emote) { + if (reactions[emote]?.contains(currentUserIdentifier) == true) { + widget.timeline.room.removeReaction(event, emote); + } else { + widget.timeline.room.addReaction(event, emote); + } + } + + @override + void update(int newIndex) { + setStateFromIndex(newIndex); + } + + void setStateFromIndex(int index) { + setState(() { + reactions = widget.timeline.events[index].reactions!; + }); + } +} diff --git a/commet/lib/ui/molecules/timeline_events/events/timeline_event_view_reply.dart b/commet/lib/ui/molecules/timeline_events/events/timeline_event_view_reply.dart new file mode 100644 index 00000000..39f44348 --- /dev/null +++ b/commet/lib/ui/molecules/timeline_events/events/timeline_event_view_reply.dart @@ -0,0 +1,134 @@ +import 'package:commet/client/timeline.dart'; +import 'package:flutter/material.dart'; +import 'package:tiamat/tiamat.dart' as tiamat; +import 'package:flutter/material.dart' as material; + +class TimelineEventViewReply extends StatefulWidget { + const TimelineEventViewReply( + {super.key, + required this.timeline, + required this.index, + this.avatarSize = 32}); + final Timeline timeline; + final int index; + final double avatarSize; + + @override + State createState() => _TimelineEventViewReplyState(); +} + +class _TimelineEventViewReplyState extends State { + String? senderName; + String? body; + Color? senderColor; + + bool loading = false; + + @override + void initState() { + getStateFromIndex(widget.index); + super.initState(); + } + + void getStateFromIndex(int index) { + var event = widget.timeline.events[index]; + var replyEvent = widget.timeline.tryGetEvent(event.relatedEventId!); + + if (replyEvent == null) { + loading = true; + widget.timeline.room.getEvent(event.relatedEventId!).then((value) { + if (mounted && value != null) { + setStateFromEvent(value); + } + }); + } else { + setStateFromEvent(replyEvent); + } + } + + void setStateFromEvent(TimelineEvent event) { + setState(() { + var sender = widget.timeline.room.getMemberOrFallback(event.senderId); + senderName = sender.displayName; + senderColor = sender.defaultColor; + body = event.body ?? ""; + loading = false; + }); + } + + @override + Widget build(BuildContext context) { + return IntrinsicHeight( + child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ + SizedBox( + width: 45, + child: SizedBox.expand( + child: CustomPaint( + painter: ReplyLinePainter2( + pathColor: material.Theme.of(context).colorScheme.secondary, + avatarSize: widget.avatarSize), + ), + ), + ), + Column( + children: [ + tiamat.Text( + senderName ?? "Loading", + color: senderColor, + autoAdjustBrightness: true, + ), + ], + ), + Flexible( + child: Padding( + padding: const EdgeInsets.fromLTRB(8, 0, 8, 0), + child: tiamat.Text( + body ?? "Unknown", + maxLines: 2, + overflow: TextOverflow.ellipsis, + color: material.Theme.of(context).colorScheme.secondary, + ), + ), + ), + ]), + ); + } +} + +class ReplyLinePainter2 extends CustomPainter { + Color pathColor; + double strokeWidth; + double radius; + double padding; + double avatarSize; + ReplyLinePainter2( + {this.pathColor = Colors.white, + this.strokeWidth = 1.5, + this.radius = 5, + this.avatarSize = 32, + this.padding = 4}) { + _paint = Paint() + ..color = pathColor + ..style = PaintingStyle.stroke + ..strokeWidth = strokeWidth + ..strokeCap = StrokeCap.round; + } + + late Paint _paint; + + @override + void paint(Canvas canvas, Size size) { + Path path = Path(); + path.moveTo(avatarSize / 2, size.height - padding); + path.relativeLineTo(0, (-size.height + 11 + padding) + radius); + path.relativeArcToPoint(Offset(radius, -radius), + radius: Radius.circular(radius)); + path.relativeLineTo(size.width - (avatarSize / 2) - radius - padding, 0); + canvas.drawPath(path, _paint); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) { + return true; + } +} diff --git a/commet/lib/ui/molecules/timeline_events/layouts/timeline_event_layout_message.dart b/commet/lib/ui/molecules/timeline_events/layouts/timeline_event_layout_message.dart new file mode 100644 index 00000000..313a56a7 --- /dev/null +++ b/commet/lib/ui/molecules/timeline_events/layouts/timeline_event_layout_message.dart @@ -0,0 +1,90 @@ +import 'package:commet/debug/log.dart'; +import 'package:commet/diagnostic/benchmark_values.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:tiamat/tiamat.dart' as tiamat; + +class TimelineEventLayoutMessage extends StatelessWidget { + const TimelineEventLayoutMessage( + {super.key, + required this.senderName, + required this.senderColor, + this.senderAvatar, + this.formattedContent, + this.attachments, + this.inResponseTo, + this.reactions, + this.avatarSize = 32, + this.showSender = true}); + final String senderName; + final Color senderColor; + final ImageProvider? senderAvatar; + final Widget? formattedContent; + final Widget? attachments; + final Widget? inResponseTo; + final Widget? reactions; + final bool showSender; + + final double avatarSize; + + @override + Widget build(BuildContext context) { + BenchmarkValues.numTimelineMessageBodyBuilt += 1; + Log.d( + "Num times messageevent body built: ${BenchmarkValues.numTimelineMessageBodyBuilt}"); + return Padding( + padding: const EdgeInsets.fromLTRB(16, 2, 8, 2), + child: Column( + children: [ + if (inResponseTo != null) inResponseTo!, + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + avatar(), + Flexible( + child: Padding( + padding: const EdgeInsets.fromLTRB(12, 0, 0, 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (showSender) name(), + if (formattedContent != null) + RepaintBoundary(child: formattedContent!), + if (attachments != null) attachments!, + if (reactions != null) + Padding( + padding: const EdgeInsets.fromLTRB(0, 4, 0, 0), + child: reactions!, + ), + ], + ), + ), + ) + ], + ) + ], + ), + ); + } + + tiamat.Text name() { + return tiamat.Text.name( + senderName, + color: senderColor, + ); + } + + SizedBox avatar() { + return SizedBox( + width: avatarSize, + child: tiamat.Avatar( + radius: avatarSize / 2, + image: senderAvatar, + placeholderText: senderName, + placeholderColor: senderColor, + isPadding: showSender == false, + ), + ); + } +} diff --git a/commet/lib/ui/molecules/timeline_events/timeline_event_layout.dart b/commet/lib/ui/molecules/timeline_events/timeline_event_layout.dart new file mode 100644 index 00000000..cd97a4e1 --- /dev/null +++ b/commet/lib/ui/molecules/timeline_events/timeline_event_layout.dart @@ -0,0 +1,3 @@ +abstract class TimelineEventViewWidget { + void update(int newIndex); +} diff --git a/commet/lib/ui/molecules/timeline_events/timeline_view_entry.dart b/commet/lib/ui/molecules/timeline_events/timeline_view_entry.dart index dc4f59a6..f6d1028b 100644 --- a/commet/lib/ui/molecules/timeline_events/timeline_view_entry.dart +++ b/commet/lib/ui/molecules/timeline_events/timeline_view_entry.dart @@ -2,41 +2,87 @@ import 'package:commet/client/client.dart'; import 'package:commet/debug/log.dart'; import 'package:commet/diagnostic/benchmark_values.dart'; import 'package:commet/ui/molecules/timeline_event.dart'; +import 'package:commet/ui/molecules/timeline_events/events/timeline_event_view_message.dart'; +import 'package:commet/ui/molecules/timeline_events/timeline_event_layout.dart'; import 'package:flutter/material.dart'; -import 'package:tiamat/tiamat.dart' as tiamat; +import 'package:flutter/widgets.dart'; class TimelineViewEntry extends StatefulWidget { const TimelineViewEntry( - {required this.timeline, required this.index, super.key}); + {required this.timeline, required this.initialIndex, super.key}); final Timeline timeline; - final int index; + final int initialIndex; @override State createState() => TimelineViewEntryState(); } -class TimelineViewEntryState extends State { +class TimelineViewEntryState extends State + implements TimelineEventViewWidget { late String eventId; + late EventType eventType; + late TimelineEventStatus status; + late int index; + + GlobalKey eventKey = GlobalKey(); @override void initState() { - var event = widget.timeline.events[widget.index]; - eventId = event.eventId; - // TODO: implement initState + loadState(widget.initialIndex); super.initState(); } - void update() { - setState(() {}); + void loadState(int eventIndex) { + var event = widget.timeline.events[eventIndex]; + eventId = event.eventId; + eventType = event.type; + status = event.status; + index = eventIndex; + } + + @override + void update(int newIndex) { + index = newIndex; + // setState(() { + loadState(newIndex); + + if (eventKey.currentState is TimelineEventViewWidget) { + (eventKey.currentState as TimelineEventViewWidget).update(newIndex); + } else { + Log.w("Failed to get state from event key"); + } } @override Widget build(BuildContext context) { BenchmarkValues.numTimelineEventsBuilt += 1; Log.d( - "Num times timeline event built: ${BenchmarkValues.numTimelineEventsBuilt}"); + "Num times timeline event built: ${BenchmarkValues.numTimelineEventsBuilt} ($eventId)"); + + if (status == TimelineEventStatus.removed) return Container(); + + var event = buildEvent(); + + if (event != null) { + return event; + } return TimelineEventView( - event: widget.timeline.events[widget.index], timeline: widget.timeline); + event: widget.timeline.events[index], timeline: widget.timeline); + } + + Widget? buildEvent() { + switch (eventType) { + case EventType.message: + case EventType.sticker: + return TimelineEventViewMessage( + key: eventKey, + timeline: widget.timeline, + initialIndex: widget.initialIndex); + default: + return Container( + key: eventKey, + ); + } } } diff --git a/commet/lib/ui/organisms/chat/chat_view.dart b/commet/lib/ui/organisms/chat/chat_view.dart index 818997bd..e83c4554 100644 --- a/commet/lib/ui/organisms/chat/chat_view.dart +++ b/commet/lib/ui/organisms/chat/chat_view.dart @@ -30,7 +30,7 @@ class ChatView extends StatelessWidget { String? get relatedEventSenderName => state.interactingEvent == null ? null : state.room - .getMemberOrFallback(state.interactingEvent!.senderId)! + .getMemberOrFallback(state.interactingEvent!.senderId) .displayName; Color? get relatedEventSenderColor => state.interactingEvent == null @@ -54,6 +54,7 @@ class ChatView extends StatelessWidget { child: CircularProgressIndicator(), ) : RoomTimelineWidget( + key: ValueKey("${state.room.identifier}-timeline"), timeline: state.timeline!, // markAsRead: handleMarkAsRead, // setReplyingEvent: (event) => state.setInteractingEvent(event, diff --git a/commet/lib/ui/organisms/home_screen/home_screen_view.dart b/commet/lib/ui/organisms/home_screen/home_screen_view.dart index 5c4ab46e..ecf86b4d 100644 --- a/commet/lib/ui/organisms/home_screen/home_screen_view.dart +++ b/commet/lib/ui/organisms/home_screen/home_screen_view.dart @@ -95,7 +95,7 @@ class HomeScreenView extends StatelessWidget { body: room.lastEvent?.body, recentEventSender: room.lastEvent != null ? room - .getMemberOrFallback(room.lastEvent!.senderId)! + .getMemberOrFallback(room.lastEvent!.senderId) .displayName : null, recentEventSenderColor: room.lastEvent != null @@ -134,7 +134,7 @@ class HomeScreenView extends StatelessWidget { body: room.lastEvent?.body, recentEventSender: room.lastEvent != null ? room - .getMemberOrFallback(room.lastEvent!.senderId)! + .getMemberOrFallback(room.lastEvent!.senderId) .displayName : null, recentEventSenderColor: room.lastEvent != null diff --git a/commet/lib/ui/organisms/space_summary/space_summary_view.dart b/commet/lib/ui/organisms/space_summary/space_summary_view.dart index a96de65a..4ea90e63 100644 --- a/commet/lib/ui/organisms/space_summary/space_summary_view.dart +++ b/commet/lib/ui/organisms/space_summary/space_summary_view.dart @@ -248,7 +248,7 @@ class SpaceSummaryViewState extends State { : null, body: room.lastEvent?.body, recentEventSender: room.lastEvent != null - ? room.getMemberOrFallback(room.lastEvent!.senderId)!.displayName + ? room.getMemberOrFallback(room.lastEvent!.senderId).displayName : null, recentEventSenderColor: room.lastEvent != null ? room.getColorOfUser(room.lastEvent!.senderId) diff --git a/commet/lib/ui/pages/main/main_page.dart b/commet/lib/ui/pages/main/main_page.dart index 1a5b7c12..04fae154 100644 --- a/commet/lib/ui/pages/main/main_page.dart +++ b/commet/lib/ui/pages/main/main_page.dart @@ -3,7 +3,6 @@ import 'package:commet/client/client.dart'; import 'package:commet/client/client_manager.dart'; import 'package:commet/client/profile.dart'; import 'package:commet/config/layout_config.dart'; -import 'package:commet/main.dart'; import 'package:commet/ui/pages/setup/setup_page.dart'; import 'package:commet/utils/event_bus.dart'; import 'package:commet/ui/navigation/navigation_utils.dart'; @@ -69,9 +68,10 @@ class MainPageState extends State { selectRoom(room); } } - backgroundTaskManager.onListUpdate.listen((event) { - setState(() {}); - }); + + // backgroundTaskManager.onListUpdate.listen((event) { + // setState(() {}); + // }); EventBus.openRoom.stream.listen(onOpenRoomSignal); SchedulerBinding.instance.scheduleFrameCallback(onFirstFrame); @@ -119,7 +119,6 @@ class MainPageState extends State { clearRoomSelection(); onSpaceUpdateSubscription?.cancel(); - onSpaceUpdateSubscription = space?.onUpdate.listen(onSpaceUpdated); setState(() { _previousSpace = _currentSpace; _currentSpace = space; @@ -131,7 +130,6 @@ class MainPageState extends State { if (room == currentRoom) return; onRoomUpdateSubscription?.cancel(); - onRoomUpdateSubscription = room.onUpdate.listen(onRoomUpdated); setState(() { _previousRoom = currentRoom; @@ -173,14 +171,6 @@ class MainPageState extends State { }); } - void onSpaceUpdated(void _) { - setState(() {}); - } - - void onRoomUpdated(void _) { - setState(() {}); - } - void onOpenRoomSignal((String, String?) strings) { var roomId = strings.$1; var clientId = strings.$2;