From a1c83fb1d8324769b0d364e11baadf8138a3752f Mon Sep 17 00:00:00 2001 From: Airyzz <36567925+Airyzz@users.noreply.github.com> Date: Tue, 4 Jun 2024 10:57:38 +0930 Subject: [PATCH 01/27] initial fix of over building --- commet/lib/client/matrix/matrix_client.dart | 6 -- .../timeline_events/timeline_view_entry.dart | 37 +++++++++ .../lib/ui/molecules/timeline_viewer_2.dart | 75 +++++++++++++++++++ commet/lib/ui/organisms/chat/chat_view.dart | 15 ++-- .../benchmarks/timeline_viewer_benchmark.dart | 5 +- 5 files changed, 123 insertions(+), 15 deletions(-) create mode 100644 commet/lib/ui/molecules/timeline_events/timeline_view_entry.dart create mode 100644 commet/lib/ui/molecules/timeline_viewer_2.dart diff --git a/commet/lib/client/matrix/matrix_client.dart b/commet/lib/client/matrix/matrix_client.dart index dbdef4f3..633534e1 100644 --- a/commet/lib/client/matrix/matrix_client.dart +++ b/commet/lib/client/matrix/matrix_client.dart @@ -65,10 +65,6 @@ class MatrixClient extends Client { : matrix.NativeImplementationsIsolate(compute); MatrixClient({required String identifier}) { - if (preferences.developerMode) { - matrix.Logs().level = matrix.Level.verbose; - } - _id = identifier; _matrixClient = _createMatrixClient(identifier); _matrixClient.onSync.stream.listen(onMatrixClientSync); @@ -257,8 +253,6 @@ class MatrixClient extends Client { legacyDatabaseBuilder: (client) => getLegacyMatrixDatabase(client.clientName), databaseBuilder: (client) => getMatrixDatabase(client.clientName), - logLevel: - BuildConfig.RELEASE ? matrix.Level.warning : matrix.Level.verbose, ); client.onSyncStatus.stream.listen(onSyncStatusChanged); diff --git a/commet/lib/ui/molecules/timeline_events/timeline_view_entry.dart b/commet/lib/ui/molecules/timeline_events/timeline_view_entry.dart new file mode 100644 index 00000000..69ac83e9 --- /dev/null +++ b/commet/lib/ui/molecules/timeline_events/timeline_view_entry.dart @@ -0,0 +1,37 @@ +import 'package:commet/client/client.dart'; +import 'package:commet/diagnostic/benchmark_values.dart'; +import 'package:commet/ui/molecules/timeline_event.dart'; +import 'package:flutter/material.dart'; +import 'package:tiamat/tiamat.dart' as tiamat; + +class TimelineViewEntry extends StatefulWidget { + const TimelineViewEntry( + {required this.timeline, required this.index, super.key}); + final Timeline timeline; + final int index; + + @override + State createState() => _TimelineViewEntryState(); +} + +class _TimelineViewEntryState extends State { + late String eventId; + + @override + void initState() { + var event = widget.timeline.events[widget.index]; + eventId = event.eventId; + // TODO: implement initState + super.initState(); + } + + @override + Widget build(BuildContext context) { + BenchmarkValues.numTimelineEventsBuilt += 1; + print( + "Num times timeline event built: ${BenchmarkValues.numTimelineEventsBuilt}"); + + return TimelineEventView( + event: widget.timeline.events[widget.index], timeline: widget.timeline); + } +} diff --git a/commet/lib/ui/molecules/timeline_viewer_2.dart b/commet/lib/ui/molecules/timeline_viewer_2.dart new file mode 100644 index 00000000..34324f4e --- /dev/null +++ b/commet/lib/ui/molecules/timeline_viewer_2.dart @@ -0,0 +1,75 @@ +import 'package:commet/client/timeline.dart'; +import 'package:commet/ui/molecules/timeline_event.dart'; +import 'package:commet/ui/molecules/timeline_events/timeline_view_entry.dart'; +import 'package:flutter/material.dart'; + +class TimelineViewer2 extends StatefulWidget { + const TimelineViewer2({required this.timeline, super.key}); + final Timeline timeline; + @override + State createState() => _TimelineViewer2State(); +} + +class _TimelineViewer2State extends State { + int numBuilds = 0; + + int recentItemsCount = 0; + int historyItemsCount = 0; + late ScrollController controller; + + @override + void initState() { + recentItemsCount = widget.timeline.events.length; + controller = ScrollController(initialScrollOffset: -9999999999); + super.initState(); + } + + @override + Widget build(BuildContext context) { + const Key centerKey = ValueKey('bottom-sliver-list'); + return Scaffold( + body: CustomScrollView( + controller: controller, + reverse: true, + center: centerKey, + slivers: [ + SliverList( + // Recent Items + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) { + int displayIndex = recentItemsCount - index - 1; + numBuilds += 1; + print("Num Builds: $numBuilds"); + return Container( + alignment: Alignment.center, + color: Colors.blue[200 + index % 4 * 100], + child: TimelineViewEntry( + timeline: widget.timeline, index: displayIndex), + ); + }, + childCount: recentItemsCount, + ), + ), + SliverList( + key: centerKey, + // History Items + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) { + numBuilds += 1; + print("Num Builds: $numBuilds"); + var displayIndex = recentItemsCount + index; + return Container( + alignment: Alignment.center, + color: Colors.red[200 + index % 4 * 100], + child: TimelineViewEntry( + timeline: widget.timeline, index: displayIndex), + ); + }, + childCount: historyItemsCount, + ), + ), + ], + ), + ); + } +} diff --git a/commet/lib/ui/organisms/chat/chat_view.dart b/commet/lib/ui/organisms/chat/chat_view.dart index 4d0c1f35..ad762907 100644 --- a/commet/lib/ui/organisms/chat/chat_view.dart +++ b/commet/lib/ui/organisms/chat/chat_view.dart @@ -3,6 +3,7 @@ import 'package:commet/config/layout_config.dart'; import 'package:commet/ui/molecules/message_input.dart'; import 'package:commet/ui/molecules/read_indicator.dart'; import 'package:commet/ui/molecules/timeline_viewer.dart'; +import 'package:commet/ui/molecules/timeline_viewer_2.dart'; import 'package:commet/ui/organisms/chat/chat.dart'; import 'package:commet/utils/autofill_utils.dart'; import 'package:flutter/material.dart'; @@ -53,14 +54,14 @@ class ChatView extends StatelessWidget { ? const Center( child: CircularProgressIndicator(), ) - : TimelineViewer( + : TimelineViewer2( timeline: state.timeline!, - markAsRead: handleMarkAsRead, - setReplyingEvent: (event) => state.setInteractingEvent(event, - type: EventInteractionType.reply), - setEditingEvent: (event) => state.setInteractingEvent(event, - type: EventInteractionType.edit), - onAddReaction: state.addReaction, + // markAsRead: handleMarkAsRead, + // setReplyingEvent: (event) => state.setInteractingEvent(event, + // type: EventInteractionType.reply), + // setEditingEvent: (event) => state.setInteractingEvent(event, + // type: EventInteractionType.edit), + // onAddReaction: state.addReaction, ); } diff --git a/commet/lib/ui/pages/developer/benchmarks/timeline_viewer_benchmark.dart b/commet/lib/ui/pages/developer/benchmarks/timeline_viewer_benchmark.dart index 7e1e9caa..36d1cfa4 100644 --- a/commet/lib/ui/pages/developer/benchmarks/timeline_viewer_benchmark.dart +++ b/commet/lib/ui/pages/developer/benchmarks/timeline_viewer_benchmark.dart @@ -3,6 +3,7 @@ import 'package:commet/client/matrix/matrix_client.dart'; import 'package:commet/client/matrix/matrix_profile.dart'; import 'package:commet/diagnostic/mocks/matrix_client_component_mocks.dart'; import 'package:commet/ui/molecules/timeline_viewer.dart'; +import 'package:commet/ui/molecules/timeline_viewer_2.dart'; import 'package:commet/ui/pages/developer/benchmarks/benchmark_utils.dart'; import 'package:flutter/material.dart'; @@ -38,10 +39,10 @@ class _BenchmarkTimelineViewerState extends State { onPressed: () => Navigator.of(context).pop(), child: const Icon(Icons.chevron_left), ), - body: TimelineViewer( + body: TimelineViewer2( key: const ValueKey("timeline-viewer-benchmark"), timeline: timeline, - doMessageOverlayMenu: false, + // doMessageOverlayMenu: false, ), ); } From d4bf7f29df86111d968b1169967d02e49d0711e7 Mon Sep 17 00:00:00 2001 From: Airyzz <36567925+Airyzz@users.noreply.github.com> Date: Tue, 4 Jun 2024 12:24:35 +0930 Subject: [PATCH 02/27] slight refactor of new timeline view --- .../room_timeline_widget.dart | 53 +++++ .../room_timeline_widget_view.dart | 224 ++++++++++++++++++ .../timeline_events/timeline_view_entry.dart | 11 +- .../lib/ui/molecules/timeline_viewer_2.dart | 75 ------ commet/lib/ui/organisms/chat/chat_view.dart | 5 +- .../benchmarks/timeline_viewer_benchmark.dart | 5 +- 6 files changed, 289 insertions(+), 84 deletions(-) create mode 100644 commet/lib/ui/molecules/room_timeline_widget/room_timeline_widget.dart create mode 100644 commet/lib/ui/molecules/room_timeline_widget/room_timeline_widget_view.dart delete mode 100644 commet/lib/ui/molecules/timeline_viewer_2.dart diff --git a/commet/lib/ui/molecules/room_timeline_widget/room_timeline_widget.dart b/commet/lib/ui/molecules/room_timeline_widget/room_timeline_widget.dart new file mode 100644 index 00000000..0bc8831b --- /dev/null +++ b/commet/lib/ui/molecules/room_timeline_widget/room_timeline_widget.dart @@ -0,0 +1,53 @@ +import 'package:commet/client/timeline.dart'; +import 'package:commet/debug/log.dart'; +import 'package:commet/ui/molecules/room_timeline_widget/room_timeline_widget_view.dart'; +import 'package:flutter/material.dart'; + +class RoomTimelineWidget extends StatefulWidget { + const RoomTimelineWidget({required this.timeline, super.key}); + final Timeline timeline; + + @override + State createState() => _RoomTimelineWidgetState(); +} + +class _RoomTimelineWidgetState extends State { + Future? loadingHistory; + + GlobalKey timelineViewKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return RoomTimelineWidgetView( + key: timelineViewKey, + timeline: widget.timeline, + onViewScrolled: onViewScrolled, + ); + } + + void onViewScrolled( + {required double offset, required double maxScrollExtent}) { + double loadingThreshold = 500; + var state = timelineViewKey.currentState as RoomTimelineWidgetViewState?; + + // When the history items are empty, the sliver takes up exactly the height of the viewport, so we should use that height instead + if (state?.historyItemsCount == 0) { + var renderBox = + timelineViewKey.currentContext?.findRenderObject() as RenderBox?; + if (renderBox != null) { + loadingThreshold = renderBox.size.height; + } + } + + if (offset > maxScrollExtent - loadingThreshold && loadingHistory == null) { + loadMoreHistory(); + } + } + + void loadMoreHistory() async { + Log.d("Loading more history!"); + loadingHistory = widget.timeline.loadMoreHistory(); + await loadingHistory; + loadingHistory = null; + } +} 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 new file mode 100644 index 00000000..9108729a --- /dev/null +++ b/commet/lib/ui/molecules/room_timeline_widget/room_timeline_widget_view.dart @@ -0,0 +1,224 @@ +import 'dart:async'; + +import 'package:commet/client/timeline.dart'; +import 'package:commet/debug/log.dart'; +import 'package:commet/ui/molecules/timeline_events/timeline_view_entry.dart'; +import 'package:flutter/material.dart'; + +class RoomTimelineWidgetView extends StatefulWidget { + const RoomTimelineWidgetView( + {required this.timeline, + this.markAsRead, + this.onViewScrolled, + super.key}); + final Timeline timeline; + final Function(TimelineEvent event)? markAsRead; + final Function({required double offset, required double maxScrollExtent})? + onViewScrolled; + + @override + State createState() => RoomTimelineWidgetViewState(); +} + +class RoomTimelineWidgetViewState extends State { + int numBuilds = 0; + + int recentItemsCount = 0; + int historyItemsCount = 0; + bool firstFrame = true; + + late ScrollController controller; + late List<(GlobalKey, String)> eventKeys; + bool animatingToBottom = false; + + GlobalKey firstFrameScrollViewKey = GlobalKey(); + GlobalKey scrollViewKey = GlobalKey(); + GlobalKey centerKey = GlobalKey(); + + late List subscriptions; + + bool get attachedToBottom => controller.hasClients + ? controller.offset - controller.positions.first.minScrollExtent < 50 || + animatingToBottom + : true; + + @override + void initState() { + recentItemsCount = widget.timeline.events.length; + + subscriptions = [ + widget.timeline.onEventAdded.stream.listen(onEventAdded), + widget.timeline.onChange.stream.listen(onEventChanged), + widget.timeline.onRemove.stream.listen(onEventRemoved), + ]; + + controller = ScrollController(initialScrollOffset: -999999); + WidgetsBinding.instance.addPostFrameCallback(onAfterFirstFrame); + + eventKeys = List.from( + widget.timeline.events + .map((e) => (GlobalKey(debugLabel: e.eventId), e.eventId)), + growable: true); + super.initState(); + } + + @override + void dispose() { + for (var element in subscriptions) { + element.cancel(); + } + + super.dispose(); + } + + void onEventAdded(int index) { + setState(() {}); + if (index == 0 || index < recentItemsCount) { + recentItemsCount += 1; + } else { + historyItemsCount = widget.timeline.events.length - recentItemsCount; + } + + eventKeys.insert(index, ( + GlobalKey(debugLabel: widget.timeline.events[index].eventId), + widget.timeline.events[index].eventId + )); + + if (index == 0) { + if (attachedToBottom || animatingToBottom) { + WidgetsBinding.instance.addPostFrameCallback((_) { + animateAndSnapToBottom(); + }); + + widget.markAsRead?.call(widget.timeline.events[0]); + } + } + } + + void onEventChanged(int index) { + var event = widget.timeline.events[index]; + var existing = eventKeys[index]; + eventKeys[index] = (existing.$1, event.eventId); + + var key = eventKeys.firstWhere( + (element) => element.$2 == event.eventId, + ); + + assert(event.eventId == key.$2); + + key.$1.currentState?.setState(() {}); + } + + void onEventRemoved(int index) { + var removed = eventKeys.removeAt(index); + assert(widget.timeline.events[index].eventId == removed.$2); + } + + void onAfterFirstFrame(_) { + if (widget.timeline.events.isNotEmpty) { + widget.markAsRead?.call(widget.timeline.events.first); + } + + if (controller.hasClients) { + double extent = controller.position.minScrollExtent; + controller = ScrollController(initialScrollOffset: extent); + controller.addListener(onScroll); + setState(() { + firstFrame = false; + }); + } + } + + void onScroll() { + widget.onViewScrolled?.call( + offset: controller.offset, + maxScrollExtent: controller.position.maxScrollExtent); + } + + void animateAndSnapToBottom() { + controller.position.hold(() {}); + + animatingToBottom = true; + + int lastEvent = recentItemsCount; + + controller + .animateTo(controller.position.minScrollExtent, + duration: const Duration(milliseconds: 5000), + curve: Curves.easeOutExpo) + .then((value) { + if (recentItemsCount == lastEvent) { + controller.jumpTo(controller.position.minScrollExtent); + + animatingToBottom = false; + } + }); + } + + @override + Widget build(BuildContext context) { + return Offstage( + offstage: firstFrame, + child: Scaffold( + body: CustomScrollView( + key: firstFrame ? firstFrameScrollViewKey : scrollViewKey, + controller: controller, + reverse: true, + center: centerKey, + slivers: [ + SliverList( + // Recent Items + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) { + int displayIndex = recentItemsCount - index - 1; + numBuilds += 1; + + Log.d("Num Builds: $numBuilds"); + var key = eventKeys[displayIndex]; + assert( + key.$2 == widget.timeline.events[displayIndex].eventId); + + return Container( + alignment: Alignment.center, + color: Colors.blue[200 + index % 4 * 100]!.withAlpha(30), + child: TimelineViewEntry( + key: key.$1, + timeline: widget.timeline, + index: displayIndex), + ); + }, + childCount: recentItemsCount, + ), + ), + SliverList( + key: centerKey, + // History Items + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) { + numBuilds += 1; + // ignore: avoid_print + Log.d("Num Builds: $numBuilds"); + var displayIndex = recentItemsCount + index; + + var key = eventKeys[displayIndex]; + assert( + key.$2 == widget.timeline.events[displayIndex].eventId); + + return Container( + alignment: Alignment.center, + color: Colors.red[200 + index % 4 * 100]!.withAlpha(30), + child: TimelineViewEntry( + key: key.$1, + timeline: widget.timeline, + index: displayIndex), + ); + }, + childCount: historyItemsCount, + ), + ), + ], + ), + ), + ); + } +} 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 69ac83e9..dc4f59a6 100644 --- a/commet/lib/ui/molecules/timeline_events/timeline_view_entry.dart +++ b/commet/lib/ui/molecules/timeline_events/timeline_view_entry.dart @@ -1,4 +1,5 @@ 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:flutter/material.dart'; @@ -11,10 +12,10 @@ class TimelineViewEntry extends StatefulWidget { final int index; @override - State createState() => _TimelineViewEntryState(); + State createState() => TimelineViewEntryState(); } -class _TimelineViewEntryState extends State { +class TimelineViewEntryState extends State { late String eventId; @override @@ -25,10 +26,14 @@ class _TimelineViewEntryState extends State { super.initState(); } + void update() { + setState(() {}); + } + @override Widget build(BuildContext context) { BenchmarkValues.numTimelineEventsBuilt += 1; - print( + Log.d( "Num times timeline event built: ${BenchmarkValues.numTimelineEventsBuilt}"); return TimelineEventView( diff --git a/commet/lib/ui/molecules/timeline_viewer_2.dart b/commet/lib/ui/molecules/timeline_viewer_2.dart deleted file mode 100644 index 34324f4e..00000000 --- a/commet/lib/ui/molecules/timeline_viewer_2.dart +++ /dev/null @@ -1,75 +0,0 @@ -import 'package:commet/client/timeline.dart'; -import 'package:commet/ui/molecules/timeline_event.dart'; -import 'package:commet/ui/molecules/timeline_events/timeline_view_entry.dart'; -import 'package:flutter/material.dart'; - -class TimelineViewer2 extends StatefulWidget { - const TimelineViewer2({required this.timeline, super.key}); - final Timeline timeline; - @override - State createState() => _TimelineViewer2State(); -} - -class _TimelineViewer2State extends State { - int numBuilds = 0; - - int recentItemsCount = 0; - int historyItemsCount = 0; - late ScrollController controller; - - @override - void initState() { - recentItemsCount = widget.timeline.events.length; - controller = ScrollController(initialScrollOffset: -9999999999); - super.initState(); - } - - @override - Widget build(BuildContext context) { - const Key centerKey = ValueKey('bottom-sliver-list'); - return Scaffold( - body: CustomScrollView( - controller: controller, - reverse: true, - center: centerKey, - slivers: [ - SliverList( - // Recent Items - delegate: SliverChildBuilderDelegate( - (BuildContext context, int index) { - int displayIndex = recentItemsCount - index - 1; - numBuilds += 1; - print("Num Builds: $numBuilds"); - return Container( - alignment: Alignment.center, - color: Colors.blue[200 + index % 4 * 100], - child: TimelineViewEntry( - timeline: widget.timeline, index: displayIndex), - ); - }, - childCount: recentItemsCount, - ), - ), - SliverList( - key: centerKey, - // History Items - delegate: SliverChildBuilderDelegate( - (BuildContext context, int index) { - numBuilds += 1; - print("Num Builds: $numBuilds"); - var displayIndex = recentItemsCount + index; - return Container( - alignment: Alignment.center, - color: Colors.red[200 + index % 4 * 100], - child: TimelineViewEntry( - timeline: widget.timeline, index: displayIndex), - ); - }, - childCount: historyItemsCount, - ), - ), - ], - ), - ); - } -} diff --git a/commet/lib/ui/organisms/chat/chat_view.dart b/commet/lib/ui/organisms/chat/chat_view.dart index ad762907..818997bd 100644 --- a/commet/lib/ui/organisms/chat/chat_view.dart +++ b/commet/lib/ui/organisms/chat/chat_view.dart @@ -2,8 +2,7 @@ import 'package:commet/client/timeline.dart'; import 'package:commet/config/layout_config.dart'; import 'package:commet/ui/molecules/message_input.dart'; import 'package:commet/ui/molecules/read_indicator.dart'; -import 'package:commet/ui/molecules/timeline_viewer.dart'; -import 'package:commet/ui/molecules/timeline_viewer_2.dart'; +import 'package:commet/ui/molecules/room_timeline_widget/room_timeline_widget.dart'; import 'package:commet/ui/organisms/chat/chat.dart'; import 'package:commet/utils/autofill_utils.dart'; import 'package:flutter/material.dart'; @@ -54,7 +53,7 @@ class ChatView extends StatelessWidget { ? const Center( child: CircularProgressIndicator(), ) - : TimelineViewer2( + : RoomTimelineWidget( timeline: state.timeline!, // markAsRead: handleMarkAsRead, // setReplyingEvent: (event) => state.setInteractingEvent(event, diff --git a/commet/lib/ui/pages/developer/benchmarks/timeline_viewer_benchmark.dart b/commet/lib/ui/pages/developer/benchmarks/timeline_viewer_benchmark.dart index 36d1cfa4..607af6fe 100644 --- a/commet/lib/ui/pages/developer/benchmarks/timeline_viewer_benchmark.dart +++ b/commet/lib/ui/pages/developer/benchmarks/timeline_viewer_benchmark.dart @@ -2,8 +2,7 @@ import 'package:commet/client/client.dart'; import 'package:commet/client/matrix/matrix_client.dart'; import 'package:commet/client/matrix/matrix_profile.dart'; import 'package:commet/diagnostic/mocks/matrix_client_component_mocks.dart'; -import 'package:commet/ui/molecules/timeline_viewer.dart'; -import 'package:commet/ui/molecules/timeline_viewer_2.dart'; +import 'package:commet/ui/molecules/room_timeline_widget/room_timeline_widget_view.dart'; import 'package:commet/ui/pages/developer/benchmarks/benchmark_utils.dart'; import 'package:flutter/material.dart'; @@ -39,7 +38,7 @@ class _BenchmarkTimelineViewerState extends State { onPressed: () => Navigator.of(context).pop(), child: const Icon(Icons.chevron_left), ), - body: TimelineViewer2( + body: RoomTimelineWidgetView( key: const ValueKey("timeline-viewer-benchmark"), timeline: timeline, // doMessageOverlayMenu: false, From a002c2f2273c20a8ddbf1d7b52d548b24a66da63 Mon Sep 17 00:00:00 2001 From: Airyzz <36567925+Airyzz@users.noreply.github.com> Date: Tue, 4 Jun 2024 22:41:28 +0930 Subject: [PATCH 03/27] more work on reimplementation --- .../android/firebase_push_notifier.dart | 2 +- .../android/unified_push_notifier.dart | 2 +- commet/lib/client/matrix/matrix_room.dart | 26 ++-- .../client/matrix/matrix_timeline_event.dart | 6 +- commet/lib/client/room.dart | 2 +- commet/lib/client/timeline.dart | 2 +- .../background_service_task_notification.dart | 2 +- commet/lib/ui/atoms/emoji_reaction.dart | 2 +- commet/lib/ui/molecules/read_indicator.dart | 2 +- .../room_timeline_widget_view.dart | 82 ++++++++--- commet/lib/ui/molecules/timeline_event.dart | 8 +- .../timeline_event_view_attachments.dart | 23 +++ .../events/timeline_event_view_message.dart | 134 ++++++++++++++++++ .../events/timeline_event_view_reactions.dart | 70 +++++++++ .../events/timeline_event_view_reply.dart | 134 ++++++++++++++++++ .../timeline_event_layout_message.dart | 90 ++++++++++++ .../timeline_event_layout.dart | 3 + .../timeline_events/timeline_view_entry.dart | 68 +++++++-- commet/lib/ui/organisms/chat/chat_view.dart | 3 +- .../home_screen/home_screen_view.dart | 4 +- .../space_summary/space_summary_view.dart | 2 +- commet/lib/ui/pages/main/main_page.dart | 18 +-- 22 files changed, 611 insertions(+), 74 deletions(-) create mode 100644 commet/lib/ui/molecules/timeline_events/events/timeline_event_view_attachments.dart create mode 100644 commet/lib/ui/molecules/timeline_events/events/timeline_event_view_message.dart create mode 100644 commet/lib/ui/molecules/timeline_events/events/timeline_event_view_reactions.dart create mode 100644 commet/lib/ui/molecules/timeline_events/events/timeline_event_view_reply.dart create mode 100644 commet/lib/ui/molecules/timeline_events/layouts/timeline_event_layout_message.dart create mode 100644 commet/lib/ui/molecules/timeline_events/timeline_event_layout.dart 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; From db044128fbb4c7d282dffded216d8502b9b69036 Mon Sep 17 00:00:00 2001 From: Airyzz <36567925+Airyzz@users.noreply.github.com> Date: Wed, 5 Jun 2024 06:46:57 +0930 Subject: [PATCH 04/27] Update timeline_event_menu.dart --- .../lib/ui/molecules/timeline_events/timeline_event_menu.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commet/lib/ui/molecules/timeline_events/timeline_event_menu.dart b/commet/lib/ui/molecules/timeline_events/timeline_event_menu.dart index 4b8c208f..c9bb2f82 100644 --- a/commet/lib/ui/molecules/timeline_events/timeline_event_menu.dart +++ b/commet/lib/ui/molecules/timeline_events/timeline_event_menu.dart @@ -90,7 +90,7 @@ class TimelineEventMenu { ), if (canDeleteEvent) TimelineEventMenuEntry( - name: CommonStrings.promptAddReaction, + name: CommonStrings.promptDelete, icon: Icons.delete, action: (BuildContext context) => { AdaptiveDialog.confirmation(context).then((value) { From 2afd82c07f0a9edada99facf1735c38838a33ece Mon Sep 17 00:00:00 2001 From: Airyzz <36567925+Airyzz@users.noreply.github.com> Date: Wed, 5 Jun 2024 13:13:41 +0930 Subject: [PATCH 05/27] Reimplement message menu --- commet/lib/ui/molecules/emoji_picker.dart | 4 +- .../message_popup_menu.dart | 111 ---- .../message_popup_menu_view_dialog.dart | 200 ------- .../message_popup_menu_view_overlay.dart | 200 ------- .../room_timeline_overlay.dart | 196 +++++++ .../room_timeline_widget.dart | 10 +- .../room_timeline_widget_view.dart | 251 +++++--- .../timeline_event_layout.dart | 8 + .../timeline_events/timeline_event_menu.dart | 156 +++++ .../timeline_event_menu_dialog.dart | 101 ++++ .../timeline_events/timeline_view_entry.dart | 91 ++- commet/lib/ui/molecules/timeline_viewer.dart | 535 ------------------ commet/lib/ui/organisms/chat/chat_view.dart | 8 +- 13 files changed, 722 insertions(+), 1149 deletions(-) delete mode 100644 commet/lib/ui/molecules/message_popup_menu/message_popup_menu.dart delete mode 100644 commet/lib/ui/molecules/message_popup_menu/message_popup_menu_view_dialog.dart delete mode 100644 commet/lib/ui/molecules/message_popup_menu/message_popup_menu_view_overlay.dart create mode 100644 commet/lib/ui/molecules/room_timeline_widget/room_timeline_overlay.dart create mode 100644 commet/lib/ui/molecules/timeline_events/timeline_event_menu.dart create mode 100644 commet/lib/ui/molecules/timeline_events/timeline_event_menu_dialog.dart delete mode 100644 commet/lib/ui/molecules/timeline_viewer.dart diff --git a/commet/lib/ui/molecules/emoji_picker.dart b/commet/lib/ui/molecules/emoji_picker.dart index 16a17b62..8229f3a3 100644 --- a/commet/lib/ui/molecules/emoji_picker.dart +++ b/commet/lib/ui/molecules/emoji_picker.dart @@ -16,6 +16,7 @@ class EmojiPicker extends StatelessWidget { this.onlyEmoji = false, this.onlyStickers = false, this.staggered = false, + this.preferredTooltipDirection = AxisDirection.right, this.packListAxis = Axis.vertical}); final void Function(Emoticon emoticon)? onEmoticonPressed; final List packs; @@ -25,6 +26,7 @@ class EmojiPicker extends StatelessWidget { final bool staggered; final bool onlyStickers; final bool onlyEmoji; + final AxisDirection preferredTooltipDirection; final ItemScrollController itemScrollController = ItemScrollController(); @@ -112,7 +114,7 @@ class EmojiPicker extends StatelessWidget { return SizedBox( child: tiamat.Tooltip( text: packs[index].displayName, - preferredDirection: AxisDirection.right, + preferredDirection: preferredTooltipDirection, child: ImageButton( size: packButtonSize, iconSize: packButtonSize - 8, diff --git a/commet/lib/ui/molecules/message_popup_menu/message_popup_menu.dart b/commet/lib/ui/molecules/message_popup_menu/message_popup_menu.dart deleted file mode 100644 index 8c98202f..00000000 --- a/commet/lib/ui/molecules/message_popup_menu/message_popup_menu.dart +++ /dev/null @@ -1,111 +0,0 @@ -import 'dart:async'; - -import 'package:commet/client/components/emoticon/emoticon.dart'; -import 'package:commet/client/components/emoticon/emoticon_component.dart'; -import 'package:commet/client/timeline.dart'; -import 'package:commet/ui/atoms/code_block.dart'; -import 'package:commet/ui/molecules/message_popup_menu/message_popup_menu_view_overlay.dart'; -import 'package:commet/ui/molecules/message_popup_menu/message_popup_menu_view_dialog.dart'; -import 'package:commet/ui/navigation/adaptive_dialog.dart'; -import 'package:commet/utils/download_utils.dart'; -import 'package:flutter/material.dart'; -import '../../../client/client.dart'; -import 'package:flutter/services.dart' as services; - -class MessagePopupMenu extends StatefulWidget { - final TimelineEvent event; - final Timeline timeline; - final bool isEditable; - final bool isDeletable; - final Stream? onMessageChanged; - final bool asDialog; - final bool canSaveAttachment; - const MessagePopupMenu(this.event, this.timeline, - {super.key, - this.setEditingEvent, - this.onMessageChanged, - this.setReplyingEvent, - this.isDeletable = false, - this.addReaction, - this.onPopupStateChanged, - this.canSaveAttachment = false, - this.asDialog = false, - this.isEditable = false}); - - final Function(TimelineEvent? event)? setReplyingEvent; - final Function(TimelineEvent? event)? setEditingEvent; - final Function(TimelineEvent event, Emoticon emoticon)? addReaction; - final Function(bool state)? onPopupStateChanged; - - @override - State createState() => MessagePopupMenuState(); -} - -class MessagePopupMenuState extends State { - bool get isEditable => widget.isEditable; - bool get isDeletable => widget.isDeletable; - bool get canSaveAttachment => widget.canSaveAttachment; - Timeline get timeline => widget.timeline; - TimelineEvent get event => widget.event; - Stream? get onMessageChanged => widget.onMessageChanged; - RoomEmoticonComponent? emoticons; - @override - void initState() { - emoticons = widget.timeline.room.getComponent(); - super.initState(); - } - - @override - Widget build(BuildContext context) { - if (widget.asDialog) return MessagePopupMenuViewDialog(this); - - return MessagePopupMenuViewOverlay(this); - } - - void deleteEvent() { - AdaptiveDialog.confirmation(context).then((value) { - if (value == true) { - widget.timeline.deleteEvent(event); - } - }); - } - - void setReplyingEvent() { - widget.setReplyingEvent?.call(event); - } - - void setEditingEvent() { - widget.setEditingEvent?.call(event); - } - - void addReaction(Emoticon emoticon) { - widget.addReaction?.call(event, emoticon); - } - - void onPopupStateChanged(bool state) { - widget.onPopupStateChanged?.call(state); - } - - void copyToClipboard() { - services.Clipboard.setData(services.ClipboardData(text: event.body!)); - } - - void showSource(BuildContext context) { - AdaptiveDialog.show( - context, - title: "Source", - builder: (context) { - return SelectionArea( - child: Codeblock(text: event.rawContent, language: "json"), - ); - }, - ); - } - - void saveAttachment() async { - var attachment = widget.event.attachments?.firstOrNull; - if (attachment != null) { - DownloadUtils.downloadAttachment(attachment); - } - } -} diff --git a/commet/lib/ui/molecules/message_popup_menu/message_popup_menu_view_dialog.dart b/commet/lib/ui/molecules/message_popup_menu/message_popup_menu_view_dialog.dart deleted file mode 100644 index 2bdee64b..00000000 --- a/commet/lib/ui/molecules/message_popup_menu/message_popup_menu_view_dialog.dart +++ /dev/null @@ -1,200 +0,0 @@ -import 'package:commet/client/components/push_notification/notification_content.dart'; -import 'package:commet/client/components/push_notification/notification_manager.dart'; -import 'package:commet/client/timeline.dart'; -import 'package:commet/main.dart'; -import 'package:commet/ui/atoms/scaled_safe_area.dart'; -import 'package:commet/ui/molecules/emoji_picker.dart'; -import 'package:commet/ui/molecules/message_popup_menu/message_popup_menu.dart'; -import 'package:commet/ui/molecules/timeline_event.dart'; -import 'package:commet/utils/common_strings.dart'; -import 'package:flutter/material.dart'; -import 'package:tiamat/atoms/seperator.dart'; -import 'package:tiamat/tiamat.dart' as tiamat; - -class MessagePopupMenuViewDialog extends StatelessWidget { - final MessagePopupMenuState state; - - const MessagePopupMenuViewDialog(this.state, {super.key}); - - @override - Widget build(BuildContext context) { - return buildMessageMenu(context, state.event); - } - - Widget buildMessageMenu(BuildContext context, TimelineEvent event) { - return Padding( - padding: const EdgeInsets.fromLTRB(10, 0, 10, 0), - child: ScaledSafeArea( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - IgnorePointer( - ignoring: true, - child: Padding( - padding: const EdgeInsets.fromLTRB(0, 0, 0, 4), - child: ShaderMask( - blendMode: BlendMode.dstIn, - shaderCallback: (bounds) { - return const LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - Colors.white, - Colors.transparent, - ], - stops: [0.80, 1.0], - ).createShader(bounds); - }, - child: ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 100), - child: SingleChildScrollView( - physics: const NeverScrollableScrollPhysics(), - child: SizedBox( - child: TimelineEventView( - hovered: true, - event: event, - timeline: state.timeline), - ), - ), - ), - ), - ), - ), - SizedBox( - height: 50, - child: tiamat.TextButton( - CommonStrings.promptReply, - icon: Icons.reply, - onTap: () { - state.setReplyingEvent(); - Navigator.pop(context); - }, - ), - ), - if (state.emoticons != null) - SizedBox( - height: 50, - child: tiamat.TextButton( - CommonStrings.promptAddReaction, - icon: Icons.add_reaction_rounded, - onTap: () { - Navigator.pop(context); - showReactionMenu(event, context); - }, - ), - ), - if (state.isEditable) - SizedBox( - height: 50, - child: tiamat.TextButton( - CommonStrings.promptEdit, - icon: Icons.edit, - onTap: () { - state.setEditingEvent(); - Navigator.pop(context); - }, - ), - ), - if (state.isDeletable) - SizedBox( - height: 50, - child: tiamat.TextButton( - CommonStrings.promptDelete, - icon: Icons.delete_forever, - onTap: () { - Navigator.pop(context); - state.deleteEvent(); - }, - ), - ), - if (state.canSaveAttachment) - SizedBox( - height: 50, - child: tiamat.TextButton( - CommonStrings.promptDownload, - icon: Icons.download, - onTap: () { - Navigator.pop(context); - state.saveAttachment(); - }, - ), - ), - SizedBox( - height: 50, - child: tiamat.TextButton( - CommonStrings.promptCopy, - icon: Icons.copy, - onTap: () { - state.copyToClipboard(); - Navigator.pop(context); - }, - ), - ), - const Seperator(), - SizedBox( - height: 50, - child: tiamat.TextButton( - "Show Source", - icon: Icons.code, - onTap: () { - Navigator.pop(context); - state.showSource(context); - }, - ), - ), - if (preferences.developerMode) - SizedBox( - height: 50, - child: tiamat.TextButton( - "Send Notification", - icon: Icons.notification_add, - onTap: () async { - var room = state.timeline.room; - var user = - await room.client.getProfile(state.event.senderId); - var content = MessageNotificationContent( - senderName: user!.displayName, - senderImage: user.avatar, - senderId: user.identifier, - roomName: room.displayName, - roomId: room.identifier, - roomImage: await room.getShortcutImage(), - content: event.body ?? "Sent a message", - clientId: room.client.identifier, - eventId: event.eventId, - isDirectMessage: room.isDirectMessage, - ); - - NotificationManager.notify(content, bypassModifiers: true); - }, - ), - ), - ], - ), - ), - ); - } - - void showReactionMenu(TimelineEvent event, BuildContext context) { - showModalBottomSheet( - context: context, - builder: (context) { - return DraggableScrollableSheet( - expand: false, - initialChildSize: 0.6, - minChildSize: 0.6, - builder: (context, scrollController) { - return SizedBox( - height: 700, - child: EmojiPicker(state.emoticons!.availableEmoji, - size: 48, - packButtonSize: 40, onEmoticonPressed: (emoticon) { - state.addReaction(emoticon); - Navigator.pop(context); - })); - }, - ); - }, - ); - } -} diff --git a/commet/lib/ui/molecules/message_popup_menu/message_popup_menu_view_overlay.dart b/commet/lib/ui/molecules/message_popup_menu/message_popup_menu_view_overlay.dart deleted file mode 100644 index 467a0f77..00000000 --- a/commet/lib/ui/molecules/message_popup_menu/message_popup_menu_view_overlay.dart +++ /dev/null @@ -1,200 +0,0 @@ -import 'package:commet/ui/molecules/emoji_picker.dart'; -import 'package:commet/ui/molecules/message_popup_menu/message_popup_menu.dart'; -import 'package:flutter/material.dart'; -import 'package:just_the_tooltip/just_the_tooltip.dart'; -import 'dart:async'; - -import 'package:commet/client/components/emoticon/emoticon.dart'; -import 'package:commet/utils/common_strings.dart'; - -import 'package:flutter/material.dart' as m; -import 'package:tiamat/atoms/context_menu.dart'; -import 'package:tiamat/config/style/theme_extensions.dart'; - -import 'package:tiamat/tiamat.dart' as tiamat; - -class MessagePopupMenuViewOverlay extends StatefulWidget { - final MessagePopupMenuState state; - - const MessagePopupMenuViewOverlay(this.state, {super.key}); - - @override - State createState() => - _MessagePopupMenuViewOverlayState(); -} - -class _MessagePopupMenuViewOverlayState - extends State { - JustTheController controller = JustTheController(); - PageStorageBucket storage = PageStorageBucket(); - StreamSubscription? sub; - - @override - void initState() { - super.initState(); - sub = widget.state.onMessageChanged?.listen(onMessageChanged); - controller.addListener(onTooltipControllerStateChanged); - } - - @override - void dispose() { - sub?.cancel(); - controller.removeListener(onTooltipControllerStateChanged); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return JustTheTooltip( - tailLength: 0, - tailBaseWidth: 0, - isModal: true, - controller: controller, - shadow: const Shadow(color: Colors.transparent), - content: PageStorage( - bucket: storage, - child: DecoratedBox( - decoration: BoxDecoration(boxShadow: [ - BoxShadow( - blurRadius: 10, - spreadRadius: 2, - color: Theme.of(context).shadowColor), - ]), - child: ClipRRect( - borderRadius: BorderRadius.circular(5), - child: Container( - color: Theme.of(context).colorScheme.surface, - child: SizedBox( - width: 300, - height: 300, - child: widget.state.emoticons != null - ? EmojiPicker( - widget.state.emoticons!.availableEmoji, - onEmoticonPressed: onEmoticonPicked, - ) - : Container(), - ), - ), - ), - ), - ), - preferredDirection: AxisDirection.up, - child: buildMenu(context)); - } - - void toggleTooltipMenu() { - if (controller.value == TooltipStatus.isHidden) { - controller.showTooltip(); - } else { - controller.hideTooltip(); - } - } - - Widget buildMenu(BuildContext context) { - return m.Padding( - padding: const EdgeInsets.fromLTRB(20, 16, 20, 0), - child: DecoratedBox( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - color: m.Theme.of(context).colorScheme.surface, - border: Border.all( - color: - m.Theme.of(context).extension()!.surfaceLow2, - width: 1)), - child: m.Padding( - padding: const EdgeInsets.fromLTRB(4, 0, 4, 0), - child: Row( - children: [ - buildMenuEntry(m.Icons.reply, CommonStrings.promptReply, - callback: () { - widget.state.setReplyingEvent(); - }), - buildMenuEntry( - m.Icons.add_reaction, CommonStrings.promptAddReaction, - callback: toggleTooltipMenu), - if (widget.state.isEditable) - buildMenuEntry(m.Icons.edit, CommonStrings.promptEdit, - callback: () { - widget.state.setEditingEvent(); - }), - if (widget.state.isDeletable) - buildMenuEntry(m.Icons.delete, CommonStrings.promptDelete, - callback: () { - widget.state.deleteEvent(); - }), - if (widget.state.canSaveAttachment) - buildMenuEntry(m.Icons.download, CommonStrings.promptDownload, - callback: () { - widget.state.saveAttachment(); - }), - buildMenuEntry(m.Icons.more_vert, CommonStrings.promptOptions, - items: [ - ContextMenuItem( - text: "Show Source", - icon: Icons.code, - onPressed: () => widget.state.showSource(context)) - ]), - ], - ), - ), - ), - ); - } - - Widget buildMenuEntry(IconData icon, String label, - {Function()? callback, List? items}) { - const double size = 32; - var pad = const EdgeInsets.all(2); - return tiamat.Tooltip( - text: label, - child: m.Padding( - padding: const EdgeInsets.fromLTRB(0, 0, 0, 0), - child: ClipRRect( - borderRadius: BorderRadius.circular(size / 2), - child: Material( - color: Colors.transparent, - child: m.SizedBox( - width: size, - height: size, - child: items != null - ? ContextMenu( - modal: true, - items: items, - child: Padding( - padding: pad, - child: Icon( - icon, - size: size / 1.5, - ), - ), - ) - : InkWell( - onTap: callback, - child: Padding( - padding: pad, - child: Icon( - icon, - size: size / 1.5, - )), - ), - ), - ), - ), - ), - ); - } - - void onMessageChanged(int event) { - controller.hideTooltip(); - } - - void onEmoticonPicked(Emoticon emoticon) { - controller.hideTooltip(); - widget.state.addReaction(emoticon); - } - - void onTooltipControllerStateChanged() { - widget.state - .onPopupStateChanged(controller.value == TooltipStatus.isShowing); - } -} diff --git a/commet/lib/ui/molecules/room_timeline_widget/room_timeline_overlay.dart b/commet/lib/ui/molecules/room_timeline_widget/room_timeline_overlay.dart new file mode 100644 index 00000000..2119d3f3 --- /dev/null +++ b/commet/lib/ui/molecules/room_timeline_widget/room_timeline_overlay.dart @@ -0,0 +1,196 @@ +import 'package:commet/main.dart'; +import 'package:commet/ui/molecules/timeline_events/timeline_event_menu.dart'; +import 'package:flutter/material.dart'; +import 'package:just_the_tooltip/just_the_tooltip.dart'; +import 'package:tiamat/atoms/context_menu.dart'; +import 'package:tiamat/atoms/tile.dart'; +import 'package:tiamat/config/style/theme_extensions.dart'; + +import 'package:tiamat/tiamat.dart' as tiamat; + +import 'package:flutter/material.dart' as m; + +class TimelineOverlay extends StatefulWidget { + const TimelineOverlay({required this.link, super.key}); + final LayerLink link; + + @override + State createState() => TimelineOverlayState(); +} + +class TimelineOverlayState extends State { + TimelineEventMenu? currentMenu; + JustTheController controller = JustTheController(); + PageStorageBucket storage = PageStorageBucket(); + + TimelineEventMenuEntry? selectedEntry; + GlobalKey menuKey = GlobalKey(); + + bool? openDownwards; + + final double tooltipHeight = 300; + + @override + Widget build(BuildContext context) { + return Positioned( + right: 0, + top: 0, + child: CompositedTransformFollower( + targetAnchor: Alignment.topRight, + followerAnchor: openDownwards == true + ? Alignment.topRight + : Alignment.bottomRight, + showWhenUnlinked: false, + offset: Offset(-20, openDownwards == true ? -50 : 0), + link: widget.link, + child: MouseRegion( + child: Padding( + padding: const EdgeInsets.fromLTRB(20, 20, 20, 0), + child: buildTooltipMenu(child: buildPrimaryMenu(context)), + )))); + } + + Widget buildTooltipMenu({required Widget child}) { + return Align( + alignment: Alignment.bottomCenter, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.end, + verticalDirection: openDownwards == true + ? VerticalDirection.up + : VerticalDirection.down, + children: [ + if (selectedEntry != null) + Padding( + padding: const EdgeInsets.all(8.0), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Tile.low1( + child: SizedBox( + width: tooltipHeight, + height: tooltipHeight, + child: MouseRegion( + child: selectedEntry!.secondaryMenuBuilder + ?.call(context, clearSelection))), + ), + ), + ), + Flexible(child: child), + ], + ), + ); + } + + Widget buildPrimaryMenu(BuildContext context) { + return MouseRegion( + child: DecoratedBox( + key: menuKey, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: m.Theme.of(context).colorScheme.surface, + border: Border.all( + color: + m.Theme.of(context).extension()!.surfaceLow2, + width: 1)), + child: currentMenu != null + ? Padding( + padding: const EdgeInsets.fromLTRB(4, 0, 4, 0), + child: Row(children: [ + for (var e in currentMenu!.primaryActions) + buildAction( + name: e.name, + icon: e.icon, + onTap: e.secondaryMenuBuilder != null + ? () => togglePopupMenu(e) + : () => e.action?.call(context)), + buildAction( + name: "Options", + icon: m.Icons.more_vert, + contextMenuItems: currentMenu!.secondaryActions + .map((e) => ContextMenuItem( + text: e.name, + icon: e.icon, + onPressed: () => e.action?.call(context))) + .toList()) + ]), + ) + : Container()), + ); + } + + void clearSelection() { + setState(() { + openDownwards = null; + selectedEntry = null; + }); + } + + void togglePopupMenu(TimelineEventMenuEntry entry) { + if (selectedEntry == entry) { + setState(() { + selectedEntry = null; + }); + } else { + var obj = menuKey.currentContext?.findRenderObject() as RenderBox; + var pos = obj.localToGlobal(Offset.zero) * preferences.appScale; + openDownwards = pos.dy < (tooltipHeight + 100); + setState(() { + selectedEntry = entry; + }); + } + } + + void setMenu(TimelineEventMenu menu) { + setState(() { + currentMenu = menu; + selectedEntry = null; + }); + } + + Widget buildAction( + {required String name, + required IconData icon, + Function()? onTap, + List? contextMenuItems}) { + const double size = 30; + var pad = const EdgeInsets.all(2); + return tiamat.Tooltip( + text: name, + child: m.Padding( + padding: const EdgeInsets.fromLTRB(0, 0, 0, 0), + child: ClipRRect( + borderRadius: BorderRadius.circular(size / 2), + child: Material( + color: Colors.transparent, + child: m.SizedBox( + width: size, + height: size, + child: contextMenuItems != null + ? ContextMenu( + modal: true, + items: contextMenuItems, + child: Padding( + padding: pad, + child: Icon( + icon, + size: size / 1.5, + ), + ), + ) + : InkWell( + onTap: onTap, + child: Padding( + padding: pad, + child: Icon( + icon, + size: size / 1.5, + )), + ), + ), + ), + ), + ), + ); + } +} diff --git a/commet/lib/ui/molecules/room_timeline_widget/room_timeline_widget.dart b/commet/lib/ui/molecules/room_timeline_widget/room_timeline_widget.dart index 0bc8831b..ba17f7a9 100644 --- a/commet/lib/ui/molecules/room_timeline_widget/room_timeline_widget.dart +++ b/commet/lib/ui/molecules/room_timeline_widget/room_timeline_widget.dart @@ -4,8 +4,14 @@ import 'package:commet/ui/molecules/room_timeline_widget/room_timeline_widget_vi import 'package:flutter/material.dart'; class RoomTimelineWidget extends StatefulWidget { - const RoomTimelineWidget({required this.timeline, super.key}); + const RoomTimelineWidget( + {required this.timeline, + this.setEditingEvent, + this.setReplyingEvent, + super.key}); final Timeline timeline; + final Function(TimelineEvent? event)? setReplyingEvent; + final Function(TimelineEvent? event)? setEditingEvent; @override State createState() => _RoomTimelineWidgetState(); @@ -22,6 +28,8 @@ class _RoomTimelineWidgetState extends State { key: timelineViewKey, timeline: widget.timeline, onViewScrolled: onViewScrolled, + setReplyingEvent: widget.setReplyingEvent, + setEditingEvent: widget.setEditingEvent, ); } 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 7c226c6e..aaa10c1d 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 @@ -2,9 +2,12 @@ import 'dart:async'; import 'package:commet/client/timeline.dart'; import 'package:commet/config/build_config.dart'; +import 'package:commet/config/layout_config.dart'; import 'package:commet/debug/log.dart'; import 'package:commet/main.dart'; +import 'package:commet/ui/molecules/room_timeline_widget/room_timeline_overlay.dart'; import 'package:commet/ui/molecules/timeline_events/timeline_event_layout.dart'; +import 'package:commet/ui/molecules/timeline_events/timeline_event_menu.dart'; import 'package:commet/ui/molecules/timeline_events/timeline_view_entry.dart'; import 'package:flutter/material.dart'; @@ -13,9 +16,14 @@ class RoomTimelineWidgetView extends StatefulWidget { {required this.timeline, this.markAsRead, this.onViewScrolled, + this.setEditingEvent, + this.setReplyingEvent, super.key}); final Timeline timeline; final Function(TimelineEvent event)? markAsRead; + final Function(TimelineEvent? event)? setReplyingEvent; + final Function(TimelineEvent? event)? setEditingEvent; + final Function({required double offset, required double maxScrollExtent})? onViewScrolled; @@ -38,6 +46,10 @@ class RoomTimelineWidgetViewState extends State { GlobalKey scrollViewKey = GlobalKey(); GlobalKey centerKey = GlobalKey(); GlobalKey recentItemsKey = GlobalKey(); + GlobalKey overlayKey = GlobalKey(); + + LayerLink selectedEventLayerLink = LayerLink(); + SelectableEventViewWidget? selectedEventView; late List subscriptions; @@ -115,7 +127,7 @@ class RoomTimelineWidgetViewState extends State { var state = key.$1.currentState; if (state is TimelineEventViewWidget) { - (state as TimelineViewEntryState).update(index); + (state as TimelineEventViewWidget).update(index); } else { Log.w("Failed to get state"); } @@ -156,7 +168,7 @@ class RoomTimelineWidgetViewState extends State { controller .animateTo(controller.position.minScrollExtent, - duration: const Duration(milliseconds: 5000), + duration: const Duration(milliseconds: 500), curve: Curves.easeOutExpo) .then((value) { if (recentItemsCount == lastEvent) { @@ -167,98 +179,159 @@ class RoomTimelineWidgetViewState extends State { }); } + void eventHovered(String eventId) { + var key = eventKeys.firstWhere( + (element) => element.$2 == eventId, + ); + + assert(eventId == key.$2); + + var state = key.$1.currentState; + + if (state is SelectableEventViewWidget) { + var selectable = state as SelectableEventViewWidget; + + if (selectable != selectedEventView) { + deselectEvent(); + + selectable.select(selectedEventLayerLink); + selectedEventView = selectable; + + var overlayState = overlayKey.currentState as TimelineOverlayState?; + var event = widget.timeline.tryGetEvent(eventId)!; + overlayState?.setMenu(TimelineEventMenu( + timeline: widget.timeline, + event: event, + setEditingEvent: (event) => widget.setEditingEvent?.call(event), + setReplyingEvent: (event) => widget.setReplyingEvent?.call(event), + )); + } + } else { + Log.w("Failed to get selectable state"); + } + } + + void deselectEvent() { + var overlayState = overlayKey.currentState as TimelineOverlayState?; + overlayState?.clearSelection(); + + selectedEventView?.deselect(); + selectedEventView = null; + } + @override Widget build(BuildContext context) { 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( - childCount: recentItemsCount, - addAutomaticKeepAlives: false, - (BuildContext context, int sliverIndex) { - int timelineIndex = recentItemsCount - sliverIndex - 1; - numBuilds += 1; - - var key = eventKeys[timelineIndex]; - assert( - key.$2 == widget.timeline.events[timelineIndex].eventId); - - return Container( - alignment: Alignment.center, - color: preferences.developerMode && BuildConfig.DEBUG - ? Colors.blue[200 + sliverIndex % 4 * 100]! - .withAlpha(30) - : null, - child: TimelineViewEntry( - key: key.$1, - timeline: widget.timeline, - initialIndex: timelineIndex), - ); - }, - 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( - addAutomaticKeepAlives: false, - childCount: historyItemsCount, - (BuildContext context, int sliverIndex) { - numBuilds += 1; - // ignore: avoid_print - Log.d("Num Builds: $numBuilds"); - var timelineIndex = recentItemsCount + sliverIndex; - - var key = eventKeys[timelineIndex]; - assert( - key.$2 == widget.timeline.events[timelineIndex].eventId); - - return Container( - alignment: Alignment.center, - color: preferences.developerMode && BuildConfig.DEBUG - ? Colors.red[200 + sliverIndex % 4 * 100]!.withAlpha(30) - : null, - child: TimelineViewEntry( - key: key.$1, - timeline: widget.timeline, - initialIndex: timelineIndex), - ); - }, - 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; - }, + child: MouseRegion( + // onExit: (_) => deselectEvent(), + child: ClipRect( + child: Stack( + children: [ + Offstage( + offstage: firstFrame, + child: CustomScrollView( + key: firstFrame ? firstFrameScrollViewKey : scrollViewKey, + controller: controller, + reverse: true, + center: centerKey, + slivers: [ + SliverList( + key: recentItemsKey, + // Recent Items + delegate: SliverChildBuilderDelegate( + childCount: recentItemsCount, + addAutomaticKeepAlives: false, + (BuildContext context, int sliverIndex) { + int timelineIndex = + recentItemsCount - sliverIndex - 1; + numBuilds += 1; + + var key = eventKeys[timelineIndex]; + assert(key.$2 == + widget.timeline.events[timelineIndex].eventId); + + return Container( + alignment: Alignment.center, + color: + preferences.developerMode && BuildConfig.DEBUG + ? Colors.blue[200 + sliverIndex % 4 * 100]! + .withAlpha(30) + : null, + child: TimelineViewEntry( + key: key.$1, + timeline: widget.timeline, + onEventHovered: eventHovered, + setEditingEvent: widget.setEditingEvent, + setReplyingEvent: widget.setReplyingEvent, + initialIndex: timelineIndex), + ); + }, + 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( + addAutomaticKeepAlives: false, + childCount: historyItemsCount, + (BuildContext context, int sliverIndex) { + numBuilds += 1; + // ignore: avoid_print + Log.d("Num Builds: $numBuilds"); + var timelineIndex = recentItemsCount + sliverIndex; + + var key = eventKeys[timelineIndex]; + assert(key.$2 == + widget.timeline.events[timelineIndex].eventId); + + return Container( + alignment: Alignment.center, + color: + preferences.developerMode && BuildConfig.DEBUG + ? Colors.red[200 + sliverIndex % 4 * 100]! + .withAlpha(30) + : null, + child: TimelineViewEntry( + key: key.$1, + onEventHovered: eventHovered, + timeline: widget.timeline, + setEditingEvent: widget.setEditingEvent, + setReplyingEvent: widget.setReplyingEvent, + initialIndex: timelineIndex), + ); + }, + 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; + }, + ), + ), + ], + ), ), - ), - ], + if (Layout.desktop) + TimelineOverlay(key: overlayKey, link: selectedEventLayerLink) + ], + ), ), ), ); diff --git a/commet/lib/ui/molecules/timeline_events/timeline_event_layout.dart b/commet/lib/ui/molecules/timeline_events/timeline_event_layout.dart index cd97a4e1..0c293db0 100644 --- a/commet/lib/ui/molecules/timeline_events/timeline_event_layout.dart +++ b/commet/lib/ui/molecules/timeline_events/timeline_event_layout.dart @@ -1,3 +1,11 @@ +import 'package:flutter/widgets.dart'; + abstract class TimelineEventViewWidget { void update(int newIndex); } + +abstract class SelectableEventViewWidget { + void select(LayerLink link); + + void deselect(); +} diff --git a/commet/lib/ui/molecules/timeline_events/timeline_event_menu.dart b/commet/lib/ui/molecules/timeline_events/timeline_event_menu.dart new file mode 100644 index 00000000..85b6eea6 --- /dev/null +++ b/commet/lib/ui/molecules/timeline_events/timeline_event_menu.dart @@ -0,0 +1,156 @@ +import 'package:commet/client/components/emoticon/emoticon_component.dart'; +import 'package:commet/client/components/push_notification/notification_content.dart'; +import 'package:commet/client/components/push_notification/notification_manager.dart'; +import 'package:commet/client/timeline.dart'; +import 'package:commet/main.dart'; +import 'package:commet/ui/atoms/code_block.dart'; +import 'package:commet/ui/molecules/emoji_picker.dart'; +import 'package:commet/ui/navigation/adaptive_dialog.dart'; +import 'package:commet/utils/download_utils.dart'; +import 'package:flutter/material.dart'; + +class TimelineEventMenu { + final Timeline timeline; + final TimelineEvent event; + + late final List primaryActions; + late final List secondaryActions; + + final Function(TimelineEvent event)? setEditingEvent; + final Function(TimelineEvent event)? setReplyingEvent; + final Function()? onActionFinished; + + TimelineEventMenu({ + required this.timeline, + required this.event, + this.setEditingEvent, + this.setReplyingEvent, + this.onActionFinished, + }) { + bool canEditEvent = timeline.room.permissions.canUserEditMessages && + event.senderId == timeline.room.client.self!.identifier && + setEditingEvent != null; + + bool canDeleteEvent = timeline.canDeleteEvent(event); + + bool canReply = setReplyingEvent != null; + + bool canSaveAttachment = event.attachments?.isNotEmpty ?? false; + + var emoticons = timeline.room.getComponent(); + bool canAddReaction = emoticons != null; + + primaryActions = [ + if (canEditEvent) + TimelineEventMenuEntry( + name: "Edit", + icon: Icons.edit, + action: (BuildContext context) { + setEditingEvent?.call(event); + onActionFinished?.call(); + }), + if (canReply) + TimelineEventMenuEntry( + name: "Reply", + icon: Icons.reply, + action: (BuildContext context) { + setReplyingEvent?.call(event); + onActionFinished?.call(); + }), + if (canSaveAttachment) + TimelineEventMenuEntry( + name: "Download", + icon: Icons.download, + action: (BuildContext context) { + var attachment = event.attachments?.firstOrNull; + if (attachment != null) { + DownloadUtils.downloadAttachment(attachment); + } + onActionFinished?.call(); + }), + if (canAddReaction) + TimelineEventMenuEntry( + name: "Add Reaction", + icon: Icons.add_reaction, + secondaryMenuBuilder: (context, dismissSecondaryMenu) { + return EmojiPicker(emoticons.availableEmoji, + preferredTooltipDirection: AxisDirection.left, + onEmoticonPressed: (emote) async { + timeline.room.addReaction(event, emote); + await Future.delayed(const Duration(milliseconds: 100)); + dismissSecondaryMenu(); + }); + }, + ), + if (canDeleteEvent) + TimelineEventMenuEntry( + name: "Delete", + icon: Icons.delete, + action: (BuildContext context) => { + AdaptiveDialog.confirmation(context).then((value) { + if (value == true) { + timeline.deleteEvent(event); + } + onActionFinished?.call(); + }) + }), + ]; + + secondaryActions = [ + TimelineEventMenuEntry( + name: "Show Source", + icon: Icons.code, + action: (BuildContext context) { + AdaptiveDialog.show( + context, + title: "Source", + builder: (context) { + return SelectionArea( + child: Codeblock(text: event.rawContent, language: "json"), + ); + }, + ); + + onActionFinished?.call(); + }), + if (preferences.developerMode) + TimelineEventMenuEntry( + name: "Show Notification", + icon: Icons.notification_add, + action: (BuildContext context) async { + var room = timeline.room; + var user = await room.client.getProfile(event.senderId); + var content = MessageNotificationContent( + senderName: user!.displayName, + senderImage: user.avatar, + senderId: user.identifier, + roomName: room.displayName, + roomId: room.identifier, + roomImage: await room.getShortcutImage(), + content: event.body ?? "Sent a message", + clientId: room.client.identifier, + eventId: event.eventId, + isDirectMessage: room.isDirectMessage, + ); + + NotificationManager.notify(content, bypassModifiers: true); + onActionFinished?.call(); + }), + ]; + } +} + +class TimelineEventMenuEntry { + final String name; + final Function(BuildContext context)? action; + final IconData icon; + + final Widget Function(BuildContext context, Function() dismissMenu)? + secondaryMenuBuilder; + + TimelineEventMenuEntry( + {required this.name, + required this.icon, + this.action, + this.secondaryMenuBuilder}); +} diff --git a/commet/lib/ui/molecules/timeline_events/timeline_event_menu_dialog.dart b/commet/lib/ui/molecules/timeline_events/timeline_event_menu_dialog.dart new file mode 100644 index 00000000..a99b2782 --- /dev/null +++ b/commet/lib/ui/molecules/timeline_events/timeline_event_menu_dialog.dart @@ -0,0 +1,101 @@ +import 'package:commet/client/timeline.dart'; +import 'package:commet/ui/atoms/scaled_safe_area.dart'; +import 'package:commet/ui/molecules/timeline_event.dart'; +import 'package:commet/ui/molecules/timeline_events/timeline_event_menu.dart'; +import 'package:flutter/material.dart'; +import 'package:tiamat/atoms/seperator.dart'; +import 'package:tiamat/tiamat.dart' as tiamat; + +class TimelineEventMenuDialog extends StatelessWidget { + const TimelineEventMenuDialog( + {required this.event, + required this.timeline, + required this.menu, + super.key}); + + final TimelineEvent event; + final Timeline timeline; + + final TimelineEventMenu menu; + + @override + Widget build(BuildContext context) { + return buildMessageMenu(context, event); + } + + Widget buildMessageMenu(BuildContext context, TimelineEvent event) { + return Padding( + padding: const EdgeInsets.fromLTRB(10, 0, 10, 0), + child: ScaledSafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + IgnorePointer( + ignoring: true, + child: Padding( + padding: const EdgeInsets.fromLTRB(0, 0, 0, 4), + child: ShaderMask( + blendMode: BlendMode.dstIn, + shaderCallback: (bounds) { + return const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.white, + Colors.transparent, + ], + stops: [0.80, 1.0], + ).createShader(bounds); + }, + child: ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 100), + child: SingleChildScrollView( + physics: const NeverScrollableScrollPhysics(), + child: SizedBox( + child: TimelineEventView( + hovered: true, event: event, timeline: timeline), + ), + ), + ), + ), + ), + ), + for (var action in menu.primaryActions) + SizedBox( + height: 50, + child: tiamat.TextButton(action.name, + icon: action.icon, onTap: () => doAction(action, context)), + ), + const Seperator(), + for (var action in menu.secondaryActions) + SizedBox( + height: 50, + child: tiamat.TextButton(action.name, + icon: action.icon, onTap: () => doAction(action, context)), + ), + ], + ), + ), + ); + } + + void doAction(TimelineEventMenuEntry entry, BuildContext context) { + if (entry.action != null) { + entry.action?.call(context); + return; + } + + if (entry.secondaryMenuBuilder != null) { + Navigator.of(context).pop(); + + showModalBottomSheet( + context: context, + builder: (context) { + return entry.secondaryMenuBuilder!.call(context, () { + Navigator.of(context).pop(); + }); + }, + ); + } + } +} 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 f6d1028b..cb8668a2 100644 --- a/commet/lib/ui/molecules/timeline_events/timeline_view_entry.dart +++ b/commet/lib/ui/molecules/timeline_events/timeline_view_entry.dart @@ -1,24 +1,33 @@ import 'package:commet/client/client.dart'; +import 'package:commet/config/layout_config.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:commet/ui/molecules/timeline_events/timeline_event_menu.dart'; +import 'package:commet/ui/molecules/timeline_events/timeline_event_menu_dialog.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; class TimelineViewEntry extends StatefulWidget { const TimelineViewEntry( - {required this.timeline, required this.initialIndex, super.key}); + {required this.timeline, + required this.initialIndex, + this.onEventHovered, + this.setEditingEvent, + this.setReplyingEvent, + super.key}); final Timeline timeline; final int initialIndex; + final Function(String eventId)? onEventHovered; + final Function(TimelineEvent? event)? setReplyingEvent; + final Function(TimelineEvent? event)? setEditingEvent; @override State createState() => TimelineViewEntryState(); } class TimelineViewEntryState extends State - implements TimelineEventViewWidget { + implements TimelineEventViewWidget, SelectableEventViewWidget { late String eventId; late EventType eventType; late TimelineEventStatus status; @@ -26,6 +35,9 @@ class TimelineViewEntryState extends State GlobalKey eventKey = GlobalKey(); + bool selected = false; + LayerLink? timelineLayerLink; + @override void initState() { loadState(widget.initialIndex); @@ -63,12 +75,59 @@ class TimelineViewEntryState extends State var event = buildEvent(); - if (event != null) { - return event; + if (Layout.desktop) { + event = MouseRegion( + onEnter: (_) => + widget.onEventHovered?.call(widget.timeline.events[index].eventId), + child: event, + ); + } + + if (Layout.mobile) { + event = InkWell( + onLongPress: () { + var event = widget.timeline.events[index]; + + showModalBottomSheet( + showDragHandle: true, + isScrollControlled: true, + elevation: 0, + context: context, + builder: (context) => TimelineEventMenuDialog( + event: event, + timeline: widget.timeline, + menu: TimelineEventMenu( + timeline: widget.timeline, + event: event, + setEditingEvent: widget.setEditingEvent, + setReplyingEvent: widget.setReplyingEvent, + onActionFinished: () => Navigator.of(context).pop(), + ), + )); + }, + child: event, + ); + } + + if (selected) { + event = Container( + color: Theme.of(context).hoverColor, + child: event, + ); + } + + if (timelineLayerLink != null) { + event = Stack( + alignment: Alignment.topRight, + children: [ + CompositedTransformTarget( + link: timelineLayerLink!, child: const SizedBox()), + event ?? Container() + ], + ); } - return TimelineEventView( - event: widget.timeline.events[index], timeline: widget.timeline); + return event ?? Container(); } Widget? buildEvent() { @@ -85,4 +144,20 @@ class TimelineViewEntryState extends State ); } } + + @override + void deselect() { + setState(() { + selected = false; + timelineLayerLink = null; + }); + } + + @override + void select(LayerLink link) { + setState(() { + selected = true; + timelineLayerLink = link; + }); + } } diff --git a/commet/lib/ui/molecules/timeline_viewer.dart b/commet/lib/ui/molecules/timeline_viewer.dart deleted file mode 100644 index 2b819195..00000000 --- a/commet/lib/ui/molecules/timeline_viewer.dart +++ /dev/null @@ -1,535 +0,0 @@ -import 'dart:async'; -import 'package:commet/config/build_config.dart'; -import 'package:commet/config/layout_config.dart'; -import 'package:commet/main.dart'; -import 'package:commet/ui/molecules/message_popup_menu/message_popup_menu.dart'; -import 'package:commet/ui/molecules/timeline_event.dart'; -import 'package:commet/utils/text_utils.dart'; -import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; -import 'package:tiamat/config/config.dart'; -import '../../client/client.dart'; -import '../../client/components/emoticon/emoticon.dart'; -import 'package:tiamat/tiamat.dart' as tiamat; -/* Note: This aint your mother's timeline viewer... -This file contains some unusual hacks in order to achieve a smoother experience - -Hack #1: Split Timeline -This timeline renders elements as part of two seperate slivers -one for new events, and one for previous events -this allows us to center the scroll view on the 'middle' of the two slivers/ -This prevents the view from jumping around when new items are added / removed from -the top and bottom of the timeline - ---- - -Hack #2: First frame offscreen render -Due to the previous hack, when we initially open the viewer, the recent events are offscreen -because the scrollviewer starts with an offset of 0, and all our recent events are in an offset < 0 -To work around this, on the first frame we render the scroll view offstage, so we can grab the -minScrollExtent of the scroll view - -Then after this frame is rendered, we construct a new scroll controller and set the initial offset -to the minScrollExtent we just grabbed, which places timeline right where we would expect it! - ---- - -I know its weird but its necessary! -*/ - -class TimelineViewer extends StatefulWidget { - const TimelineViewer( - {required this.timeline, - this.markAsRead, - this.setReplyingEvent, - this.onEventDoubleTap, - this.setEditingEvent, - this.onEventLongPress, - this.onAddReaction, - this.onReactionTapped, - this.doMessageOverlayMenu = true, - Key? key}) - : super(key: key); - - final bool doMessageOverlayMenu; - final Timeline timeline; - final Function(TimelineEvent event)? markAsRead; - final Function(TimelineEvent? event)? setReplyingEvent; - final Function(TimelineEvent? event)? setEditingEvent; - final Function(TimelineEvent event)? onEventDoubleTap; - final Function(TimelineEvent event)? onEventLongPress; - final Function(TimelineEvent event, Emoticon emote)? onAddReaction; - final Function(TimelineEvent event, Emoticon emote)? onReactionTapped; - - @override - State createState() => TimelineViewerState(); -} - -class TimelineViewerState extends State - with WidgetsBindingObserver { - Key historyEventsKey = GlobalKey(); - ScrollController controller = ScrollController(initialScrollOffset: -999999); - bool firstFrame = true; - - int recentItemsCount = 0; - int historyItemsCount = 0; - int hoveredIndex = -1; - double height = -1; - bool animatingToBottom = false; - bool get attachedToBottom => controller.hasClients - ? controller.offset - controller.positions.first.minScrollExtent < 50 || - animatingToBottom - : true; - - Future? loadingHistory; - bool toBeDisposed = false; - - bool showJumpToBottom = false; - - StreamController onHoveredMessageChanged = StreamController.broadcast(); - - late StreamSubscription eventAdded; - late StreamSubscription eventChanged; - late StreamSubscription eventRemoved; - final LayerLink messageLayerLink = LayerLink(); - - bool messagePopupIsBeingInteracted = false; - - String get labelJumpToLatest => Intl.message("Jump to latest", - desc: - "Label for the button which jumps the room timeline view to the latest message", - name: "labelJumpToLatest"); - - @override - void initState() { - recentItemsCount = widget.timeline.events.length; - eventAdded = widget.timeline.onEventAdded.stream.listen(onEventAdded); - eventChanged = widget.timeline.onChange.stream.listen(onEventChanged); - eventRemoved = widget.timeline.onRemove.stream.listen(onEventRemoved); - WidgetsBinding.instance.addPostFrameCallback(onAfterFirstFrame); - WidgetsBinding.instance.addObserver(this); - super.initState(); - } - - @override - void didChangeAppLifecycleState(AppLifecycleState state) async { - super.didChangeAppLifecycleState(state); - - if (state == AppLifecycleState.resumed) { - if (attachedToBottom) { - widget.markAsRead?.call(widget.timeline.events.first); - } - } - } - - @override - void dispose() { - eventAdded.cancel(); - eventChanged.cancel(); - eventRemoved.cancel(); - WidgetsBinding.instance.removeObserver(this); - super.dispose(); - } - - Widget buildOverlay() { - if (hoveredIndex == -1) { - return Container(); - } - - var event = widget.timeline.events[hoveredIndex]; - return Positioned( - height: 50, - child: CompositedTransformFollower( - targetAnchor: Alignment.topRight, - followerAnchor: Alignment.topRight, - showWhenUnlinked: false, - offset: const Offset(-20, -48), - link: messageLayerLink, - child: SizedBox(child: buildPopupMenu(event, false)))); - } - - onEventLongPress(TimelineEvent event) { - if (BuildConfig.MOBILE) { - showModalBottomSheet( - showDragHandle: true, - isScrollControlled: true, - elevation: 0, - context: context, - builder: (context) => buildPopupMenu(event, true)); - } - } - - Widget buildPopupMenu(TimelineEvent event, bool asDialog) { - return SingleChildScrollView( - child: MessagePopupMenu( - event, - widget.timeline, - isEditable: canUserEditEvent(event), - asDialog: asDialog, - isDeletable: widget.timeline.canDeleteEvent(event), - setEditingEvent: widget.setEditingEvent, - setReplyingEvent: widget.setReplyingEvent, - canSaveAttachment: event.attachments?.isNotEmpty ?? false, - addReaction: widget.onAddReaction, - onPopupStateChanged: (state) => messagePopupIsBeingInteracted = state, - ), - ); - } - - bool canUserEditEvent(TimelineEvent event) { - if (event.attachments != null) return false; - - return widget.timeline.room.permissions.canUserEditMessages && - event.senderId == widget.timeline.room.client.self!.identifier; - } - - void onAfterFirstFrame(_) { - if (widget.timeline.events.isNotEmpty) { - widget.markAsRead?.call(widget.timeline.events.first); - } - - if (controller.hasClients) { - double extent = controller.position.minScrollExtent; - controller = ScrollController(initialScrollOffset: extent); - controller.addListener(onScroll); - setState(() { - firstFrame = false; - }); - } - } - - void loadMoreHistory() async { - loadingHistory = widget.timeline.loadMoreHistory(); - await loadingHistory; - loadingHistory = null; - } - - bool shouldScrollPositionLoadHistory() { - return controller.offset > controller.position.maxScrollExtent - 500; - } - - void onScroll() { - if (shouldScrollPositionLoadHistory()) { - if (loadingHistory == null) loadMoreHistory(); - } - - setState(() { - showJumpToBottom = !attachedToBottom; - }); - } - - void animateAndSnapToBottom() { - if (toBeDisposed) return; - controller.position.hold(() {}); - - setState(() { - animatingToBottom = true; - showJumpToBottom = false; - }); - int lastEvent = recentItemsCount; - - controller - .animateTo(controller.position.minScrollExtent, - duration: const Duration(milliseconds: 500), - curve: Curves.easeOutExpo) - .then((value) { - if (recentItemsCount == lastEvent) { - controller.jumpTo(controller.position.minScrollExtent); - - setState(() { - animatingToBottom = false; - showJumpToBottom = false; - }); - } - }); - } - - void onEventAdded(int index) { - setState(() {}); - if (index == 0 || index < recentItemsCount) { - recentItemsCount += 1; - } else { - historyItemsCount = widget.timeline.events.length - recentItemsCount; - } - - if (index == 0) { - if (attachedToBottom || animatingToBottom) { - WidgetsBinding.instance.addPostFrameCallback((_) { - animateAndSnapToBottom(); - }); - - widget.markAsRead?.call(widget.timeline.events[0]); - } - } - } - - void onEventChanged(int index) { - setState(() {}); - } - - void onEventRemoved(int index) { - setState(() {}); - } - - void prepareForDisposal() { - toBeDisposed = true; - controller.position.hold(() {}); - } - - @override - Widget build(BuildContext context) { - if (firstFrame) { - return Offstage( - child: buildScrollView(), - ); - } - - return NotificationListener( - onNotification: (SizeChangedLayoutNotification notification) { - var prevHeight = height; - height = MediaQuery.of(context).size.height; - if (prevHeight == -1) return true; - - var diff = prevHeight - height; - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - controller.jumpTo(controller.offset + diff); - }); - - return true; - }, - child: buildScrollView()); - } - - Widget buildScrollView() { - var view = CustomScrollView( - center: historyEventsKey, - reverse: true, - controller: controller, - anchor: 0, - slivers: [ - //Beware, these are in reverse order - SliverList( - delegate: SliverChildBuilderDelegate(buildRecentItem, - childCount: recentItemsCount)), - SliverList( - key: historyEventsKey, - delegate: SliverChildBuilderDelegate(buildHistoryItem, - childCount: historyItemsCount)), - ], - ); - - return ClipRect( - child: Stack( - children: [ - if (Layout.mobile) view, - if (Layout.desktop) SelectionArea(child: view), - if (Layout.desktop) buildOverlay(), - Align( - alignment: Alignment.bottomCenter, - child: AnimatedSlide( - offset: Offset(0, showJumpToBottom ? 0 : 1), - duration: const Duration(milliseconds: 200), - curve: Curves.easeInOutCubic, - child: buildJumpToLatestButton())) - ], - ), - ); - } - - Widget buildJumpToLatestButton() { - var padding = BuildConfig.MOBILE - ? const EdgeInsets.fromLTRB(18, 12, 18, 12) - : const EdgeInsets.fromLTRB(12, 4, 12, 4); - return Padding( - padding: const EdgeInsets.all(8.0), - child: DecoratedBox( - decoration: BoxDecoration( - boxShadow: [ - BoxShadow( - color: Theme.of(context).shadowColor, - blurRadius: 5, - ) - ], - borderRadius: BorderRadius.circular(20), - border: Border.all( - color: Theme.of(context).extension()!.highlight, - width: 1), - color: Theme.of(context).extension()!.surfaceHigh1), - child: ClipRRect( - borderRadius: BorderRadius.circular(20), - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: animateAndSnapToBottom, - child: Padding( - padding: padding, - child: tiamat.Text.labelLow( - labelJumpToLatest, - color: Theme.of(context).colorScheme.onSurface, - ), - ), - ), - ), - ), - )); - } - - Widget? buildHistoryItem(BuildContext context, int index) { - var displayIndex = recentItemsCount + index; - return Container( - color: BuildConfig.DEBUG && preferences.developerMode - ? Colors.red.withAlpha(15) - : null, - child: buildEvent(displayIndex, index)); - } - - Widget? buildRecentItem(BuildContext context, int index) { - int displayIndex = recentItemsCount - index - 1; - return Container( - color: BuildConfig.DEBUG && preferences.developerMode - ? Colors.blue.withAlpha(15) - : null, - child: buildEvent(displayIndex, index)); - } - - Widget buildEvent(int displayIndex, int actualIndex) { - var event = Stack(alignment: Alignment.topRight, children: [ - buildTimelineEvent(displayIndex), - if (displayIndex == hoveredIndex) - CompositedTransformTarget( - link: messageLayerLink, - child: const SizedBox( - height: 1, - width: 1, - ), - ) - ]); - - if (shouldShowDate(displayIndex)) { - return Column( - children: [ - dateTimeMarker(displayIndex), - event, - ], - ); - } - - return event; - } - - Widget dateTimeMarker(int actualIndex) { - var color = Theme.of(context).extension()!.surfaceLow2; - return Padding( - padding: const EdgeInsets.fromLTRB(20, 20, 20, 20), - child: Row( - mainAxisSize: MainAxisSize.max, - children: [ - Expanded( - child: Divider( - height: 1, - color: Theme.of(context).extension()!.surfaceLow2, - ), - ), - Padding( - padding: const EdgeInsets.fromLTRB(12, 0, 12, 0), - child: tiamat.Text.labelLow(TextUtils.timestampToLocalizedTime( - widget.timeline.events[actualIndex].originServerTs, - MediaQuery.of(context).alwaysUse24HourFormat)), - ), - Expanded( - child: Divider( - height: 1, - color: color, - ), - ), - ], - ), - ); - } - - Widget buildTimelineEvent(int index) { - return MouseRegion( - onEnter: (event) { - if (!widget.doMessageOverlayMenu) return; - if (index == hoveredIndex) return; - if (messagePopupIsBeingInteracted) return; - - setState(() { - hoveredIndex = index; - }); - }, - child: Container( - color: hoveredIndex == index - ? Theme.of(context).hoverColor - : Colors.transparent, - child: TimelineEventView( - event: widget.timeline.events[index], - timeline: widget.timeline, - hovered: hoveredIndex == index, - onReactionTapped: (emote) => - widget.onAddReaction?.call(widget.timeline.events[index], emote), - showSender: shouldShowSender(index), - setEditingEvent: () => - widget.setEditingEvent?.call(widget.timeline.events[index]), - setReplyingEvent: () => - widget.setReplyingEvent?.call(widget.timeline.events[index]), - onLongPress: () => onEventLongPress(widget.timeline.events[index]), - useCachedFormat: true, - ), - ), - ); - } - - bool shouldShowDate(int index) { - var offsetIndex = index + 1; - - if (widget.timeline.events.length <= offsetIndex) { - return false; - } - - if ([ - EventType.emote, - EventType.message, - ].contains(widget.timeline.events[index].type) == - false) { - return false; - } - - if (widget.timeline.events[index].originServerTs.toLocal().day != - widget.timeline.events[offsetIndex].originServerTs.toLocal().day) { - return true; - } - - if (widget.timeline.events[index].originServerTs - .difference(widget.timeline.events[offsetIndex].originServerTs) - .inHours > - 2) return true; - - return false; - } - - 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/organisms/chat/chat_view.dart b/commet/lib/ui/organisms/chat/chat_view.dart index e83c4554..2e40b186 100644 --- a/commet/lib/ui/organisms/chat/chat_view.dart +++ b/commet/lib/ui/organisms/chat/chat_view.dart @@ -57,10 +57,10 @@ class ChatView extends StatelessWidget { key: ValueKey("${state.room.identifier}-timeline"), timeline: state.timeline!, // markAsRead: handleMarkAsRead, - // setReplyingEvent: (event) => state.setInteractingEvent(event, - // type: EventInteractionType.reply), - // setEditingEvent: (event) => state.setInteractingEvent(event, - // type: EventInteractionType.edit), + setReplyingEvent: (event) => state.setInteractingEvent(event, + type: EventInteractionType.reply), + setEditingEvent: (event) => state.setInteractingEvent(event, + type: EventInteractionType.edit), // onAddReaction: state.addReaction, ); } From e34086949699fb2b880c01bcaa5d22b525d9f0c6 Mon Sep 17 00:00:00 2001 From: Airyzz <36567925+Airyzz@users.noreply.github.com> Date: Wed, 5 Jun 2024 13:47:16 +0930 Subject: [PATCH 06/27] Reimplement generic timeline events --- commet/lib/ui/atoms/generic_room_event.dart | 54 --- commet/lib/ui/molecules/timeline_event.dart | 413 ------------------ .../events/timeline_event_view_generic.dart | 188 ++++++++ .../events/timeline_event_view_message.dart | 3 +- .../events/timeline_event_view_reply.dart | 2 + .../timeline_events/timeline_event_menu.dart | 22 +- .../timeline_event_menu_dialog.dart | 10 +- .../timeline_events/timeline_view_entry.dart | 25 +- 8 files changed, 233 insertions(+), 484 deletions(-) delete mode 100644 commet/lib/ui/atoms/generic_room_event.dart delete mode 100644 commet/lib/ui/molecules/timeline_event.dart create mode 100644 commet/lib/ui/molecules/timeline_events/events/timeline_event_view_generic.dart diff --git a/commet/lib/ui/atoms/generic_room_event.dart b/commet/lib/ui/atoms/generic_room_event.dart deleted file mode 100644 index a3283029..00000000 --- a/commet/lib/ui/atoms/generic_room_event.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'package:flutter/widgets.dart'; -import 'package:tiamat/atoms/avatar.dart'; -import 'package:tiamat/tiamat.dart' as tiamat; -import 'package:flutter/material.dart' as m; - -class GenericRoomEvent extends StatelessWidget { - const GenericRoomEvent(this.text, {this.icon, this.senderImage, super.key}); - final String text; - final IconData? icon; - final ImageProvider? senderImage; - - @override - Widget build(BuildContext context) { - return m.Material( - color: m.Colors.transparent, - child: Padding( - padding: const EdgeInsets.all(2.0), - child: Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: const EdgeInsets.fromLTRB(20, 8, 20, 8), - child: Row( - children: [ - if (icon != null) - Padding( - padding: const EdgeInsets.fromLTRB(44, 0, 8, 0), - child: Icon( - icon, - size: 20, - ), - ), - if (senderImage != null) - Padding( - padding: const EdgeInsets.fromLTRB(44, 0, 8, 0), - child: Avatar( - image: senderImage, - radius: 10, - ), - ), - Flexible( - child: Row( - children: [ - Flexible(child: tiamat.Text.labelLow(text)), - ], - ), - ) - ], - ), - ), - ), - ), - ); - } -} diff --git a/commet/lib/ui/molecules/timeline_event.dart b/commet/lib/ui/molecules/timeline_event.dart deleted file mode 100644 index 859ee121..00000000 --- a/commet/lib/ui/molecules/timeline_event.dart +++ /dev/null @@ -1,413 +0,0 @@ -import 'package:commet/client/components/url_preview/url_preview_component.dart'; -import 'package:commet/config/build_config.dart'; -import 'package:commet/diagnostic/benchmark_values.dart'; -import 'package:commet/main.dart'; -import 'package:commet/ui/atoms/generic_room_event.dart'; -import 'package:commet/ui/molecules/message.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart' as m; -import 'package:flutter/widgets.dart'; -import 'package:intl/intl.dart'; -import 'package:tiamat/atoms/icon_button.dart'; -import 'package:tiamat/tiamat.dart' as tiamat; - -import '../../client/client.dart'; -import '../../client/components/emoticon/emoticon.dart'; -import '../atoms/message_attachment.dart'; - -class TimelineEventView extends StatefulWidget { - const TimelineEventView( - {required this.event, - required this.timeline, - super.key, - this.onDelete, - this.hovered = false, - this.showSender = true, - this.setReplyingEvent, - this.setEditingEvent, - this.onDoubleTap, - this.onReactionTapped, - this.onLongPress, - this.deleteEvent, - this.canDeleteEvent = false, - this.useCachedFormat = false, - this.debugInfo}); - final TimelineEvent event; - final bool hovered; - final Function? onDelete; - final bool showSender; - final String? debugInfo; - final Timeline timeline; - final bool useCachedFormat; - final Function()? onDoubleTap; - final Function()? onLongPress; - final Function()? setReplyingEvent; - final Function()? setEditingEvent; - final Function()? deleteEvent; - final bool canDeleteEvent; - final Function(Emoticon emote)? onReactionTapped; - - static int timelineEventBuildsCount = 0; - - @override - State createState() => _TimelineEventState(); -} - -class _TimelineEventState extends State { - TimelineEvent? relatedEvent; - - String messagePlaceholderSticker(String user) => - Intl.message("$user sent a sticker", - desc: "Message body for when a user sends a sticker", - args: [user], - name: "messagePlaceholderSticker"); - - String get messageFailedToDecrypt => Intl.message("Failed to decrypt event", - desc: "Placeholde text for when a message fails to decrypt", - name: "messageFailedToDecrypt"); - - String messagePlaceholderUserCreatedRoom(String user) => - Intl.message("$user created the room!", - desc: "Message body for when a user created the room", - args: [user], - name: "messagePlaceholderUserCreatedRoom"); - - String messagePlaceholderUserJoinedRoom(String user) => - Intl.message("$user joined the room!", - desc: "Message body for when a user joins the room", - args: [user], - name: "messagePlaceholderUserJoinedRoom"); - - String messagePlaceholderUserLeftRoom(String user) => - Intl.message("$user left the room", - desc: "Message body for when a user leaves the room", - args: [user], - name: "messagePlaceholderUserLeftRoom"); - - String messagePlaceholderUserUpdatedAvatar(String user) => - Intl.message("$user updated their avatar", - desc: "Message body for when a user updates their avatar", - args: [user], - name: "messagePlaceholderUserUpdatedAvatar"); - - String messagePlaceholderUserUpdatedName(String user) => - Intl.message("$user updated their display name", - desc: "Message body for when a user updates their display name", - args: [user], - name: "messagePlaceholderUserUpdatedName"); - - String messagePlaceholderUserInvited(String sender, String invitedUser) => - Intl.message("$sender invited $invitedUser", - desc: "Message body for when a user invites another user to the room", - args: [sender, invitedUser], - name: "messagePlaceholderUserInvited"); - - String messagePlaceholderUserRejectedInvite(String user) => - Intl.message("$user rejected the invitation", - desc: "Message body for when a user rejected an invitation to a room", - args: [user], - name: "messagePlaceholderUserRejectedInvite"); - - String messageUserEmote(String user, String emote) => - Intl.message("*$user $emote", - desc: "Message to display when a user does a custom emote (/me)", - args: [user, emote], - name: "messageUserEmote"); - - String get errorMessageFailedToSend => Intl.message("Failed to send", - desc: - "Text that is placed below a message when the message fails to send", - name: "errorMessageFailedToSend"); - - String get displayName => widget.timeline.room - .getMemberOrFallback(widget.event.senderId) - .displayName; - - ImageProvider? get avatar => - widget.timeline.room.getMemberOrFallback(widget.event.senderId).avatar; - - Color get color => widget.timeline.room.getColorOfUser(widget.event.senderId); - - Color get replyColor => relatedEvent == null - ? m.Theme.of(context).colorScheme.onPrimary - : widget.timeline.room.getColorOfUser(relatedEvent!.senderId); - - String? get relatedEventDisplayName => relatedEvent == null - ? null - : widget.timeline.room - .getMemberOrFallback(relatedEvent!.senderId) - .displayName; - - UrlPreviewData? urlPreviews; - bool loadingUrlPreviews = false; - - @override - void initState() { - if (widget.event.relatedEventId != null) { - relatedEvent = widget.timeline.tryGetEvent(widget.event.relatedEventId!); - if (relatedEvent == null) { - fetchRelatedEvent(); - } - } - - var component = - widget.timeline.room.client.getComponent(); - if (component?.shouldGetPreviewData(widget.timeline.room, widget.event) == - true) { - getCachedUrlPreview(); - if (urlPreviews == null) { - loadingUrlPreviews = true; - fetchUrlPreviews(); - } - } - - super.initState(); - } - - void fetchRelatedEvent() async { - var event = - await widget.timeline.fetchEventById(widget.event.relatedEventId!); - if (mounted) - setState(() { - relatedEvent = event; - }); - } - - void getCachedUrlPreview() { - var component = - widget.timeline.room.client.getComponent(); - - if (component == null) { - return; - } - - var cached = component.getCachedPreview(widget.timeline.room, widget.event); - urlPreviews = cached; - } - - void fetchUrlPreviews() async { - if (widget.event.links == null) { - return; - } - - var component = - widget.timeline.room.client.getComponent(); - - if (component == null) { - return; - } - - var data = await component.getPreview(widget.timeline.room, widget.event); - - if (data?.image != null) { - if (mounted) { - await precacheImage(data!.image!, context); - } - } - - if (mounted) { - setState(() { - urlPreviews = data; - loadingUrlPreviews = false; - }); - } - } - - @override - Widget build(BuildContext context) { - BenchmarkValues.numTimelineEventsBuilt += 1; - - return eventToWidget(widget.event) ?? Container(); - } - - Widget? eventToWidget(TimelineEvent event) { - if (event.status == TimelineEventStatus.removed) return const SizedBox(); - switch (widget.event.type) { - case EventType.message: - case EventType.sticker: - return Message( - senderName: displayName, - senderColor: color, - senderAvatar: avatar, - sentTimeStamp: widget.event.originServerTs, - showDetailed: widget.hovered, - onDoubleTap: widget.onDoubleTap, - onLongPress: widget.onLongPress, - showSender: widget.showSender, - reactions: widget.event.reactions, - currentUserIdentifier: widget.timeline.room.client.self!.identifier, - replyBody: relatedEvent?.body ?? - (relatedEvent?.type == EventType.sticker - ? messagePlaceholderSticker(displayName) - : relatedEvent?.attachments?.firstOrNull?.name), - replySenderName: relatedEventDisplayName, - replySenderColor: replyColor, - isInReply: widget.event.relatedEventId != null, - edited: widget.event.edited, - onReactionTapped: widget.onReactionTapped, - links: urlPreviews, - loadingUrlPreviews: loadingUrlPreviews, - body: buildBody(), - child: event.status == TimelineEventStatus.error - ? tiamat.Text.error(errorMessageFailedToSend) - : null, - ); - case EventType.encrypted: - return Message( - senderName: displayName, - senderColor: color, - senderAvatar: avatar, - showSender: widget.showSender, - body: tiamat.Text.error(messageFailedToDecrypt), - currentUserIdentifier: widget.timeline.room.client.self!.identifier, - sentTimeStamp: widget.event.originServerTs); - case EventType.roomCreated: - return GenericRoomEvent(messagePlaceholderUserCreatedRoom(displayName), - icon: m.Icons.room_preferences_outlined); - case EventType.memberJoined: - return GenericRoomEvent(messagePlaceholderUserJoinedRoom(displayName), - icon: m.Icons.waving_hand_rounded); - case EventType.memberLeft: - return GenericRoomEvent(messagePlaceholderUserLeftRoom(displayName), - icon: m.Icons.subdirectory_arrow_left_rounded); - case EventType.memberAvatar: - return GenericRoomEvent( - messagePlaceholderUserUpdatedAvatar(displayName), - icon: m.Icons.person); - case EventType.memberDisplayName: - return GenericRoomEvent(messagePlaceholderUserUpdatedName(displayName), - icon: m.Icons.edit); - case EventType.memberInvited: - return GenericRoomEvent( - messagePlaceholderUserInvited(displayName, event.stateKey!), - icon: m.Icons.person_add); - case EventType.memberInvitationRejected: - return GenericRoomEvent( - messagePlaceholderUserRejectedInvite(displayName), - icon: m.Icons.subdirectory_arrow_left_rounded); - case EventType.emote: - return GenericRoomEvent( - messageUserEmote(displayName, event.body ?? ""), - senderImage: avatar, - ); - default: - break; - } - - if (BuildConfig.DEBUG && preferences.developerMode) { - return m.Padding( - padding: const EdgeInsets.all(8.0), - child: Placeholder( - child: event.source != null - ? tiamat.Text.tiny(event.source!) - : const Placeholder( - fallbackHeight: 20, - )), - ); - } - return null; - } - - Widget buildMenuEntry(IconData icon, String label, Function()? callback) { - const double size = 32; - return tiamat.Tooltip( - text: label, - child: m.Padding( - padding: const EdgeInsets.fromLTRB(0, 0, 0, 0), - child: m.SizedBox( - width: size, - height: size, - child: IconButton( - size: 20, - icon: icon, - onPressed: () => callback?.call(), - ), - ), - ), - ); - } - - bool canUserEditEvent() { - return widget.timeline.room.permissions.canUserEditMessages && - widget.event.senderId == widget.timeline.room.client.self!.identifier; - } - - Widget buildBody() { - BenchmarkValues.numTimelineMessageBodyBuilt += 1; - switch (widget.event.type) { - case EventType.message: - return buildMessageBody(); - case EventType.sticker: - return buildStickerBody(); - default: - return const Placeholder( - fallbackHeight: 50, - ); - } - } - - Widget buildMessageBody() { - return m.Material( - color: m.Colors.transparent, - child: Column( - children: [ - buildMessageText(), - if (widget.event.attachments != null) buildMessageAttachments() - ], - ), - ); - } - - Widget buildMessageAttachments() { - return Wrap( - children: widget.event.attachments! - .map((e) => Padding( - padding: const EdgeInsets.fromLTRB(0, 8, 8, 8), - child: MessageAttachment( - e, - ), - )) - .toList(), - ); - } - - Widget buildMessageText() { - if (widget.event.bodyFormat != null) { - var formatted = widget.useCachedFormat - ? widget.event.formattedContent - : widget.event.buildFormattedContent(); - - // if the cache didnt have anything lets just build new content. This should really never happen though - formatted ??= widget.event.buildFormattedContent(); - - return formatted!; - } - - if (widget.event.body != null) - return tiamat.Text.body("${widget.event.body}\n"); - - return const SizedBox(); - } - - Widget buildStickerBody() { - return m.Material( - color: m.Colors.transparent, - child: Column( - children: [ - if (widget.event.attachments != null) - Wrap( - children: widget.event.attachments! - .map((e) => Padding( - padding: const EdgeInsets.fromLTRB(0, 8, 8, 8), - child: MessageAttachment( - e, - ignorePointer: widget.event.type == EventType.sticker, - ), - )) - .toList(), - ), - ], - ), - ); - } -} diff --git a/commet/lib/ui/molecules/timeline_events/events/timeline_event_view_generic.dart b/commet/lib/ui/molecules/timeline_events/events/timeline_event_view_generic.dart new file mode 100644 index 00000000..7050eda8 --- /dev/null +++ b/commet/lib/ui/molecules/timeline_events/events/timeline_event_view_generic.dart @@ -0,0 +1,188 @@ +import 'package:commet/client/timeline.dart'; +import 'package:commet/ui/molecules/timeline_events/timeline_event_layout.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +import 'package:flutter/material.dart' as m; + +import 'package:tiamat/tiamat.dart' as tiamat; + +import 'package:tiamat/atoms/avatar.dart'; + +class TimelineEventViewGeneric extends StatefulWidget { + const TimelineEventViewGeneric( + {required this.timeline, required this.initialIndex, super.key}); + final Timeline timeline; + final int initialIndex; + @override + State createState() => + _TimelineEventViewGenericState(); +} + +class _TimelineEventViewGenericState extends State + implements TimelineEventViewWidget { + late String? text; + late IconData? icon; + late ImageProvider? senderAvatar; + + String messagePlaceholderSticker(String user) => + Intl.message("$user sent a sticker", + desc: "Message body for when a user sends a sticker", + args: [user], + name: "messagePlaceholderSticker"); + + String get messageFailedToDecrypt => Intl.message("Failed to decrypt event", + desc: "Placeholde text for when a message fails to decrypt", + name: "messageFailedToDecrypt"); + + String messagePlaceholderUserCreatedRoom(String user) => + Intl.message("$user created the room!", + desc: "Message body for when a user created the room", + args: [user], + name: "messagePlaceholderUserCreatedRoom"); + + String messagePlaceholderUserJoinedRoom(String user) => + Intl.message("$user joined the room!", + desc: "Message body for when a user joins the room", + args: [user], + name: "messagePlaceholderUserJoinedRoom"); + + String messagePlaceholderUserLeftRoom(String user) => + Intl.message("$user left the room", + desc: "Message body for when a user leaves the room", + args: [user], + name: "messagePlaceholderUserLeftRoom"); + + String messagePlaceholderUserUpdatedAvatar(String user) => + Intl.message("$user updated their avatar", + desc: "Message body for when a user updates their avatar", + args: [user], + name: "messagePlaceholderUserUpdatedAvatar"); + + String messagePlaceholderUserUpdatedName(String user) => + Intl.message("$user updated their display name", + desc: "Message body for when a user updates their display name", + args: [user], + name: "messagePlaceholderUserUpdatedName"); + + String messagePlaceholderUserInvited(String sender, String invitedUser) => + Intl.message("$sender invited $invitedUser", + desc: "Message body for when a user invites another user to the room", + args: [sender, invitedUser], + name: "messagePlaceholderUserInvited"); + + String messagePlaceholderUserRejectedInvite(String user) => + Intl.message("$user rejected the invitation", + desc: "Message body for when a user rejected an invitation to a room", + args: [user], + name: "messagePlaceholderUserRejectedInvite"); + + String messageUserEmote(String user, String emote) => + Intl.message("*$user $emote", + desc: "Message to display when a user does a custom emote (/me)", + args: [user, emote], + name: "messageUserEmote"); + + String get errorMessageFailedToSend => Intl.message("Failed to send", + desc: + "Text that is placed below a message when the message fails to send", + name: "errorMessageFailedToSend"); + + @override + void initState() { + setStateFromindex(widget.initialIndex); + super.initState(); + } + + @override + Widget build(BuildContext context) { + if (text == null) { + return Container(); + } + + return m.Material( + color: m.Colors.transparent, + child: Padding( + padding: const EdgeInsets.all(2.0), + child: Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.fromLTRB(20, 8, 20, 8), + child: Row( + children: [ + if (icon != null) + Padding( + padding: const EdgeInsets.fromLTRB(44, 0, 8, 0), + child: Icon( + icon, + size: 20, + ), + ), + if (senderAvatar != null) + Padding( + padding: const EdgeInsets.fromLTRB(44, 0, 8, 0), + child: Avatar( + image: senderAvatar, + radius: 10, + ), + ), + Flexible( + child: Row( + children: [ + Flexible(child: tiamat.Text.labelLow(text!)), + ], + ), + ) + ], + ), + ), + ), + ), + ); + } + + @override + void update(int newIndex) { + setStateFromindex(newIndex); + } + + void setStateFromindex(int index) { + var event = widget.timeline.events[index]; + var sender = widget.timeline.room.getMemberOrFallback(event.senderId); + var displayName = sender.displayName; + + if ([EventType.emote].contains(event.type)) { + senderAvatar = sender.avatar; + } else { + senderAvatar = null; + } + + text = switch (event.type) { + EventType.roomCreated => messagePlaceholderUserCreatedRoom(displayName), + EventType.memberJoined => messagePlaceholderUserJoinedRoom(displayName), + EventType.memberLeft => messagePlaceholderUserLeftRoom(displayName), + EventType.memberAvatar => + messagePlaceholderUserUpdatedAvatar(displayName), + EventType.memberDisplayName => + messagePlaceholderUserUpdatedName(displayName), + EventType.memberInvited => + messagePlaceholderUserInvited(displayName, event.stateKey!), + EventType.memberInvitationRejected => + messagePlaceholderUserRejectedInvite(displayName), + EventType.emote => messageUserEmote(displayName, event.body ?? ""), + _ => "$displayName: ${event.body}" + }; + + icon = switch (event.type) { + EventType.roomCreated => m.Icons.room_preferences_outlined, + EventType.memberJoined => m.Icons.waving_hand_rounded, + EventType.memberLeft => m.Icons.subdirectory_arrow_left_rounded, + EventType.memberAvatar => m.Icons.person, + EventType.memberDisplayName => m.Icons.edit, + EventType.memberInvited => m.Icons.person_add, + EventType.memberInvitationRejected => + m.Icons.subdirectory_arrow_left_rounded, + _ => null + }; + } +} 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 index cd900949..cbbda920 100644 --- 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 @@ -97,7 +97,8 @@ class _TimelineEventViewMessageState extends State showSender = shouldShowSender(eventIndex); if (event.bodyFormat != null) { - formattedContent = event.formattedContent!; + formattedContent = + Container(key: GlobalKey(), child: event.buildFormattedContent()!); } hasReactions = event.reactions != null && event.reactions!.isNotEmpty; 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 index 39f44348..76d1156d 100644 --- 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 @@ -1,4 +1,5 @@ import 'package:commet/client/timeline.dart'; +import 'package:commet/diagnostic/benchmark_values.dart'; import 'package:flutter/material.dart'; import 'package:tiamat/tiamat.dart' as tiamat; import 'package:flutter/material.dart' as material; @@ -58,6 +59,7 @@ class _TimelineEventViewReplyState extends State { @override Widget build(BuildContext context) { + BenchmarkValues.numTimelineReplyBodyBuilt += 1; return IntrinsicHeight( child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( diff --git a/commet/lib/ui/molecules/timeline_events/timeline_event_menu.dart b/commet/lib/ui/molecules/timeline_events/timeline_event_menu.dart index 85b6eea6..9c369b0b 100644 --- a/commet/lib/ui/molecules/timeline_events/timeline_event_menu.dart +++ b/commet/lib/ui/molecules/timeline_events/timeline_event_menu.dart @@ -6,6 +6,7 @@ import 'package:commet/main.dart'; import 'package:commet/ui/atoms/code_block.dart'; import 'package:commet/ui/molecules/emoji_picker.dart'; import 'package:commet/ui/navigation/adaptive_dialog.dart'; +import 'package:commet/utils/common_strings.dart'; import 'package:commet/utils/download_utils.dart'; import 'package:flutter/material.dart'; @@ -33,17 +34,21 @@ class TimelineEventMenu { bool canDeleteEvent = timeline.canDeleteEvent(event); - bool canReply = setReplyingEvent != null; + bool canReply = [EventType.message, EventType.emote, EventType.sticker] + .contains(event.type) && + setReplyingEvent != null; bool canSaveAttachment = event.attachments?.isNotEmpty ?? false; var emoticons = timeline.room.getComponent(); - bool canAddReaction = emoticons != null; + bool canAddReaction = + [EventType.message, EventType.sticker].contains(event.type) && + emoticons != null; primaryActions = [ if (canEditEvent) TimelineEventMenuEntry( - name: "Edit", + name: CommonStrings.promptEdit, icon: Icons.edit, action: (BuildContext context) { setEditingEvent?.call(event); @@ -51,7 +56,7 @@ class TimelineEventMenu { }), if (canReply) TimelineEventMenuEntry( - name: "Reply", + name: CommonStrings.promptReply, icon: Icons.reply, action: (BuildContext context) { setReplyingEvent?.call(event); @@ -59,7 +64,7 @@ class TimelineEventMenu { }), if (canSaveAttachment) TimelineEventMenuEntry( - name: "Download", + name: CommonStrings.promptDownload, icon: Icons.download, action: (BuildContext context) { var attachment = event.attachments?.firstOrNull; @@ -70,7 +75,7 @@ class TimelineEventMenu { }), if (canAddReaction) TimelineEventMenuEntry( - name: "Add Reaction", + name: CommonStrings.promptAddReaction, icon: Icons.add_reaction, secondaryMenuBuilder: (context, dismissSecondaryMenu) { return EmojiPicker(emoticons.availableEmoji, @@ -84,7 +89,7 @@ class TimelineEventMenu { ), if (canDeleteEvent) TimelineEventMenuEntry( - name: "Delete", + name: CommonStrings.promptAddReaction, icon: Icons.delete, action: (BuildContext context) => { AdaptiveDialog.confirmation(context).then((value) { @@ -101,6 +106,7 @@ class TimelineEventMenu { name: "Show Source", icon: Icons.code, action: (BuildContext context) { + onActionFinished?.call(); AdaptiveDialog.show( context, title: "Source", @@ -110,8 +116,6 @@ class TimelineEventMenu { ); }, ); - - onActionFinished?.call(); }), if (preferences.developerMode) TimelineEventMenuEntry( diff --git a/commet/lib/ui/molecules/timeline_events/timeline_event_menu_dialog.dart b/commet/lib/ui/molecules/timeline_events/timeline_event_menu_dialog.dart index a99b2782..4c636c2e 100644 --- a/commet/lib/ui/molecules/timeline_events/timeline_event_menu_dialog.dart +++ b/commet/lib/ui/molecules/timeline_events/timeline_event_menu_dialog.dart @@ -1,7 +1,7 @@ import 'package:commet/client/timeline.dart'; import 'package:commet/ui/atoms/scaled_safe_area.dart'; -import 'package:commet/ui/molecules/timeline_event.dart'; import 'package:commet/ui/molecules/timeline_events/timeline_event_menu.dart'; +import 'package:commet/ui/molecules/timeline_events/timeline_view_entry.dart'; import 'package:flutter/material.dart'; import 'package:tiamat/atoms/seperator.dart'; import 'package:tiamat/tiamat.dart' as tiamat; @@ -52,8 +52,10 @@ class TimelineEventMenuDialog extends StatelessWidget { child: SingleChildScrollView( physics: const NeverScrollableScrollPhysics(), child: SizedBox( - child: TimelineEventView( - hovered: true, event: event, timeline: timeline), + child: TimelineViewEntry( + timeline: timeline, + initialIndex: timeline.events.indexOf(event), + ), ), ), ), @@ -86,8 +88,6 @@ class TimelineEventMenuDialog extends StatelessWidget { } if (entry.secondaryMenuBuilder != null) { - Navigator.of(context).pop(); - showModalBottomSheet( context: context, builder: (context) { 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 cb8668a2..a3f28432 100644 --- a/commet/lib/ui/molecules/timeline_events/timeline_view_entry.dart +++ b/commet/lib/ui/molecules/timeline_events/timeline_view_entry.dart @@ -2,6 +2,8 @@ import 'package:commet/client/client.dart'; import 'package:commet/config/layout_config.dart'; import 'package:commet/debug/log.dart'; import 'package:commet/diagnostic/benchmark_values.dart'; +import 'package:commet/main.dart'; +import 'package:commet/ui/molecules/timeline_events/events/timeline_event_view_generic.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:commet/ui/molecules/timeline_events/timeline_event_menu.dart'; @@ -138,10 +140,29 @@ class TimelineViewEntryState extends State key: eventKey, timeline: widget.timeline, initialIndex: widget.initialIndex); - default: - return Container( + case EventType.roomCreated: + case EventType.memberJoined: + case EventType.memberLeft: + case EventType.memberAvatar: + case EventType.memberDisplayName: + case EventType.memberInvited: + case EventType.memberInvitationRejected: + case EventType.emote: + return TimelineEventViewGeneric( + timeline: widget.timeline, + initialIndex: widget.initialIndex, key: eventKey, ); + default: + return preferences.developerMode + ? TimelineEventViewGeneric( + timeline: widget.timeline, + initialIndex: widget.initialIndex, + key: eventKey, + ) + : Container( + key: eventKey, + ); } } From fa8c44aebc8b5ad5389ffc5b22656dc94af5e93f Mon Sep 17 00:00:00 2001 From: Airyzz <36567925+Airyzz@users.noreply.github.com> Date: Wed, 5 Jun 2024 15:15:34 +0930 Subject: [PATCH 07/27] show timestamps --- .../events/timeline_event_view_message.dart | 26 +++++++++++++++++++ .../timeline_event_layout_message.dart | 12 ++++++++- .../timeline_events/timeline_event_menu.dart | 5 ++-- .../timeline_event_menu_dialog.dart | 1 + .../timeline_events/timeline_view_entry.dart | 3 +++ 5 files changed, 44 insertions(+), 3 deletions(-) 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 index cbbda920..50ee7e56 100644 --- 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 @@ -8,16 +8,20 @@ import 'package:commet/ui/molecules/timeline_events/timeline_event_layout.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; +import 'package:intl/intl.dart' as intl; + class TimelineEventViewMessage extends StatefulWidget { const TimelineEventViewMessage( {super.key, required this.timeline, this.showSender = true, + this.detailed = false, required this.initialIndex}); final Timeline timeline; final int initialIndex; final bool showSender; + final bool detailed; @override State createState() => @@ -38,6 +42,7 @@ class _TimelineEventViewMessageState extends State bool isInResponse = false; bool showSender = false; late String currentUserIdentifier; + late DateTime sentTime; int index = 0; @@ -56,6 +61,7 @@ class _TimelineEventViewMessageState extends State senderAvatar: senderAvatar, showSender: showSender, formattedContent: formattedContent, + timestamp: timestampToString(sentTime), attachments: attachments != null ? TimelineEventViewAttachments(attachments: attachments!) : null, @@ -106,6 +112,26 @@ class _TimelineEventViewMessageState extends State attachments = event.attachments; isInResponse = event.relatedEventId != null && event.relationshipType == EventRelationshipType.reply; + + sentTime = event.originServerTs; + } + + String timestampToString(DateTime time) { + var use24 = MediaQuery.of(context).alwaysUse24HourFormat; + + if (widget.detailed) { + if (use24) { + return intl.DateFormat.yMMMMd().add_Hms().format(time.toLocal()); + } else { + return intl.DateFormat.yMMMMd().add_jms().format(time.toLocal()); + } + } else { + if (use24) { + return intl.DateFormat.Hm().format(time.toLocal()); + } else { + return intl.DateFormat.jm().format(time.toLocal()); + } + } } bool shouldShowSender(int index) { 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 index 313a56a7..b2f59f62 100644 --- 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 @@ -15,6 +15,7 @@ class TimelineEventLayoutMessage extends StatelessWidget { this.attachments, this.inResponseTo, this.reactions, + this.timestamp, this.avatarSize = 32, this.showSender = true}); final String senderName; @@ -25,6 +26,7 @@ class TimelineEventLayoutMessage extends StatelessWidget { final Widget? inResponseTo; final Widget? reactions; final bool showSender; + final String? timestamp; final double avatarSize; @@ -48,7 +50,15 @@ class TimelineEventLayoutMessage extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (showSender) name(), + if (showSender) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + name(), + if (timestamp != null) + tiamat.Text.labelLow(timestamp!), + ], + ), if (formattedContent != null) RepaintBoundary(child: formattedContent!), if (attachments != null) attachments!, diff --git a/commet/lib/ui/molecules/timeline_events/timeline_event_menu.dart b/commet/lib/ui/molecules/timeline_events/timeline_event_menu.dart index 9c369b0b..4b8c208f 100644 --- a/commet/lib/ui/molecules/timeline_events/timeline_event_menu.dart +++ b/commet/lib/ui/molecules/timeline_events/timeline_event_menu.dart @@ -28,7 +28,8 @@ class TimelineEventMenu { this.setReplyingEvent, this.onActionFinished, }) { - bool canEditEvent = timeline.room.permissions.canUserEditMessages && + bool canEditEvent = event.type == EventType.message && + timeline.room.permissions.canUserEditMessages && event.senderId == timeline.room.client.self!.identifier && setEditingEvent != null; @@ -117,7 +118,7 @@ class TimelineEventMenu { }, ); }), - if (preferences.developerMode) + if (preferences.developerMode && event.type == EventType.message) TimelineEventMenuEntry( name: "Show Notification", icon: Icons.notification_add, diff --git a/commet/lib/ui/molecules/timeline_events/timeline_event_menu_dialog.dart b/commet/lib/ui/molecules/timeline_events/timeline_event_menu_dialog.dart index 4c636c2e..0c4356c2 100644 --- a/commet/lib/ui/molecules/timeline_events/timeline_event_menu_dialog.dart +++ b/commet/lib/ui/molecules/timeline_events/timeline_event_menu_dialog.dart @@ -55,6 +55,7 @@ class TimelineEventMenuDialog extends StatelessWidget { child: TimelineViewEntry( timeline: timeline, initialIndex: timeline.events.indexOf(event), + showDetailed: true, ), ), ), 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 a3f28432..b7e5c755 100644 --- a/commet/lib/ui/molecules/timeline_events/timeline_view_entry.dart +++ b/commet/lib/ui/molecules/timeline_events/timeline_view_entry.dart @@ -17,12 +17,14 @@ class TimelineViewEntry extends StatefulWidget { this.onEventHovered, this.setEditingEvent, this.setReplyingEvent, + this.showDetailed = false, super.key}); final Timeline timeline; final int initialIndex; final Function(String eventId)? onEventHovered; final Function(TimelineEvent? event)? setReplyingEvent; final Function(TimelineEvent? event)? setEditingEvent; + final bool showDetailed; @override State createState() => TimelineViewEntryState(); @@ -139,6 +141,7 @@ class TimelineViewEntryState extends State return TimelineEventViewMessage( key: eventKey, timeline: widget.timeline, + detailed: widget.showDetailed || selected, initialIndex: widget.initialIndex); case EventType.roomCreated: case EventType.memberJoined: From 1eb8f2328529784a0384fcc82d59bbf7d97fc9a4 Mon Sep 17 00:00:00 2001 From: Airyzz <36567925+Airyzz@users.noreply.github.com> Date: Wed, 5 Jun 2024 15:20:36 +0930 Subject: [PATCH 08/27] Handle encrypted events --- .../events/timeline_event_view_generic.dart | 4 ---- .../events/timeline_event_view_message.dart | 10 ++++++++++ .../molecules/timeline_events/timeline_view_entry.dart | 1 + 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/commet/lib/ui/molecules/timeline_events/events/timeline_event_view_generic.dart b/commet/lib/ui/molecules/timeline_events/events/timeline_event_view_generic.dart index 7050eda8..37fbf873 100644 --- a/commet/lib/ui/molecules/timeline_events/events/timeline_event_view_generic.dart +++ b/commet/lib/ui/molecules/timeline_events/events/timeline_event_view_generic.dart @@ -31,10 +31,6 @@ class _TimelineEventViewGenericState extends State args: [user], name: "messagePlaceholderSticker"); - String get messageFailedToDecrypt => Intl.message("Failed to decrypt event", - desc: "Placeholde text for when a message fails to decrypt", - name: "messageFailedToDecrypt"); - String messagePlaceholderUserCreatedRoom(String user) => Intl.message("$user created the room!", desc: "Message body for when a user created the room", 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 index 50ee7e56..4b2f8545 100644 --- 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 @@ -8,7 +8,9 @@ import 'package:commet/ui/molecules/timeline_events/timeline_event_layout.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; +import 'package:tiamat/tiamat.dart' as tiamat; import 'package:intl/intl.dart' as intl; +import 'package:intl/intl.dart'; class TimelineEventViewMessage extends StatefulWidget { const TimelineEventViewMessage( @@ -33,6 +35,10 @@ class _TimelineEventViewMessageState extends State late String senderName; late Color senderColor; + String get messageFailedToDecrypt => Intl.message("Failed to decrypt event", + desc: "Placeholde text for when a message fails to decrypt", + name: "messageFailedToDecrypt"); + GlobalKey reactionsKey = GlobalKey(); Widget? formattedContent; @@ -102,6 +108,10 @@ class _TimelineEventViewMessageState extends State showSender = shouldShowSender(eventIndex); + if (event.type == EventType.encrypted) { + formattedContent = tiamat.Text.error(messageFailedToDecrypt); + } + if (event.bodyFormat != null) { formattedContent = Container(key: GlobalKey(), child: event.buildFormattedContent()!); 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 b7e5c755..ea52a204 100644 --- a/commet/lib/ui/molecules/timeline_events/timeline_view_entry.dart +++ b/commet/lib/ui/molecules/timeline_events/timeline_view_entry.dart @@ -138,6 +138,7 @@ class TimelineViewEntryState extends State switch (eventType) { case EventType.message: case EventType.sticker: + case EventType.encrypted: return TimelineEventViewMessage( key: eventKey, timeline: widget.timeline, From 00f2a9f210e783133c227383eff4756ca6dbc5cb Mon Sep 17 00:00:00 2001 From: Airyzz <36567925+Airyzz@users.noreply.github.com> Date: Wed, 5 Jun 2024 15:28:54 +0930 Subject: [PATCH 09/27] Add date time marker --- .../timeline_event_date_time_marker.dart | 39 +++++++++++++++++ .../timeline_events/timeline_view_entry.dart | 43 +++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 commet/lib/ui/molecules/timeline_events/timeline_event_date_time_marker.dart diff --git a/commet/lib/ui/molecules/timeline_events/timeline_event_date_time_marker.dart b/commet/lib/ui/molecules/timeline_events/timeline_event_date_time_marker.dart new file mode 100644 index 00000000..3d4918e2 --- /dev/null +++ b/commet/lib/ui/molecules/timeline_events/timeline_event_date_time_marker.dart @@ -0,0 +1,39 @@ +import 'package:commet/utils/text_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:tiamat/config/style/theme_extensions.dart'; +import 'package:tiamat/tiamat.dart' as tiamat; + +class TimelineEventDateTimeMarker extends StatelessWidget { + const TimelineEventDateTimeMarker({required this.time, super.key}); + final DateTime time; + + @override + Widget build(BuildContext context) { + var color = Theme.of(context).extension()!.surfaceLow2; + return Padding( + padding: const EdgeInsets.fromLTRB(20, 20, 20, 20), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Expanded( + child: Divider( + height: 1, + color: Theme.of(context).extension()!.surfaceLow2, + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(12, 0, 12, 0), + child: tiamat.Text.labelLow(TextUtils.timestampToLocalizedTime( + time, MediaQuery.of(context).alwaysUse24HourFormat)), + ), + Expanded( + child: Divider( + height: 1, + color: color, + ), + ), + ], + ), + ); + } +} 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 ea52a204..43a7ac5c 100644 --- a/commet/lib/ui/molecules/timeline_events/timeline_view_entry.dart +++ b/commet/lib/ui/molecules/timeline_events/timeline_view_entry.dart @@ -5,6 +5,7 @@ import 'package:commet/diagnostic/benchmark_values.dart'; import 'package:commet/main.dart'; import 'package:commet/ui/molecules/timeline_events/events/timeline_event_view_generic.dart'; import 'package:commet/ui/molecules/timeline_events/events/timeline_event_view_message.dart'; +import 'package:commet/ui/molecules/timeline_events/timeline_event_date_time_marker.dart'; import 'package:commet/ui/molecules/timeline_events/timeline_event_layout.dart'; import 'package:commet/ui/molecules/timeline_events/timeline_event_menu.dart'; import 'package:commet/ui/molecules/timeline_events/timeline_event_menu_dialog.dart'; @@ -42,6 +43,9 @@ class TimelineViewEntryState extends State bool selected = false; LayerLink? timelineLayerLink; + late DateTime time; + bool showDate = false; + @override void initState() { loadState(widget.initialIndex); @@ -54,6 +58,36 @@ class TimelineViewEntryState extends State eventType = event.type; status = event.status; index = eventIndex; + time = event.originServerTs; + showDate = shouldEventShowDate(eventIndex); + } + + bool shouldEventShowDate(int index) { + var offsetIndex = index + 1; + + if (widget.timeline.events.length <= offsetIndex) { + return false; + } + + if ([ + EventType.emote, + EventType.message, + ].contains(widget.timeline.events[index].type) == + false) { + return false; + } + + if (widget.timeline.events[index].originServerTs.toLocal().day != + widget.timeline.events[offsetIndex].originServerTs.toLocal().day) { + return true; + } + + if (widget.timeline.events[index].originServerTs + .difference(widget.timeline.events[offsetIndex].originServerTs) + .inHours > + 2) return true; + + return false; } @override @@ -131,6 +165,15 @@ class TimelineViewEntryState extends State ); } + if (showDate) { + event = Column( + children: [ + TimelineEventDateTimeMarker(time: time), + event ?? Container() + ], + ); + } + return event ?? Container(); } From dc1a023890f35642573bfb9314905ca33ef4089c Mon Sep 17 00:00:00 2001 From: Airyzz <36567925+Airyzz@users.noreply.github.com> Date: Wed, 5 Jun 2024 15:48:46 +0930 Subject: [PATCH 10/27] Reimplement url previews --- .../events/timeline_event_view_message.dart | 21 ++++- .../timeline_event_view_url_previews.dart | 77 +++++++++++++++++++ .../timeline_event_layout_message.dart | 3 + .../timeline_event_menu_dialog.dart | 1 + .../timeline_events/timeline_view_entry.dart | 3 + 5 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 commet/lib/ui/molecules/timeline_events/events/timeline_event_view_url_previews.dart 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 index 4b2f8545..ae9c1aa9 100644 --- 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 @@ -1,8 +1,10 @@ import 'package:commet/client/attachment.dart'; import 'package:commet/client/client.dart'; +import 'package:commet/client/components/url_preview/url_preview_component.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/events/timeline_event_view_url_previews.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'; @@ -16,13 +18,13 @@ class TimelineEventViewMessage extends StatefulWidget { const TimelineEventViewMessage( {super.key, required this.timeline, - this.showSender = true, + this.overrideShowSender = false, this.detailed = false, required this.initialIndex}); final Timeline timeline; final int initialIndex; - final bool showSender; + final bool overrideShowSender; final bool detailed; @override @@ -50,11 +52,16 @@ class _TimelineEventViewMessageState extends State late String currentUserIdentifier; late DateTime sentTime; + UrlPreviewComponent? previewComponent; + bool hasLinks = false; + int index = 0; @override void initState() { currentUserIdentifier = widget.timeline.client.self!.identifier; + previewComponent = + widget.timeline.room.client.getComponent(); loadEventState(widget.initialIndex); super.initState(); } @@ -81,6 +88,12 @@ class _TimelineEventViewMessageState extends State ? TimelineEventViewReactions( key: reactionsKey, timeline: widget.timeline, initialIndex: index) : null, + urlPreviews: previewComponent != null && hasLinks + ? TimelineEventViewUrlPreviews( + initialIndex: index, + timeline: widget.timeline, + component: previewComponent!) + : null, ); } @@ -124,6 +137,8 @@ class _TimelineEventViewMessageState extends State event.relationshipType == EventRelationshipType.reply; sentTime = event.originServerTs; + + hasLinks = event.links?.isNotEmpty == true; } String timestampToString(DateTime time) { @@ -145,6 +160,8 @@ class _TimelineEventViewMessageState extends State } bool shouldShowSender(int index) { + if (widget.overrideShowSender) return true; + if (widget.timeline.events.length <= index + 1) { return true; } diff --git a/commet/lib/ui/molecules/timeline_events/events/timeline_event_view_url_previews.dart b/commet/lib/ui/molecules/timeline_events/events/timeline_event_view_url_previews.dart new file mode 100644 index 00000000..f3be9393 --- /dev/null +++ b/commet/lib/ui/molecules/timeline_events/events/timeline_event_view_url_previews.dart @@ -0,0 +1,77 @@ +import 'package:commet/client/components/url_preview/url_preview_component.dart'; +import 'package:commet/client/timeline.dart'; +import 'package:commet/diagnostic/benchmark_values.dart'; +import 'package:commet/ui/molecules/timeline_events/timeline_event_layout.dart'; +import 'package:commet/ui/molecules/url_preview_widget.dart'; +import 'package:commet/utils/link_utils.dart'; +import 'package:flutter/material.dart'; + +class TimelineEventViewUrlPreviews extends StatefulWidget { + const TimelineEventViewUrlPreviews( + {required this.initialIndex, + required this.timeline, + required this.component, + super.key}); + + final int initialIndex; + final Timeline timeline; + final UrlPreviewComponent component; + + @override + State createState() => + _TimelineEventViewUrlPreviewsState(); +} + +class _TimelineEventViewUrlPreviewsState + extends State + implements TimelineEventViewWidget { + UrlPreviewData? data; + + @override + Widget build(BuildContext context) { + BenchmarkValues.numTimelineUrlPreviewBuilt += 1; + return Padding( + padding: const EdgeInsets.fromLTRB(0, 2, 40, 2), + child: UrlPreviewWidget( + data, + onTap: () { + LinkUtils.open(data!.uri); + }, + ), + ); + } + + @override + void update(int newIndex) { + setStateFromIndex(newIndex); + } + + @override + void initState() { + setStateFromIndex(widget.initialIndex); + super.initState(); + } + + void setStateFromIndex(int index) { + var event = widget.timeline.events[index]; + var cachedData = + widget.component.getCachedPreview(widget.timeline.room, event); + + if (cachedData != null) { + setState(() { + data = cachedData; + }); + } else { + widget.component.getPreview(widget.timeline.room, event).then( + (value) async { + if (value?.image != null) { + await precacheImage(value!.image!, context); + } + setState(() { + data = value; + }); + }, + ); + } + } +} 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 index b2f59f62..d4dc03a3 100644 --- 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 @@ -16,6 +16,7 @@ class TimelineEventLayoutMessage extends StatelessWidget { this.inResponseTo, this.reactions, this.timestamp, + this.urlPreviews, this.avatarSize = 32, this.showSender = true}); final String senderName; @@ -25,6 +26,7 @@ class TimelineEventLayoutMessage extends StatelessWidget { final Widget? attachments; final Widget? inResponseTo; final Widget? reactions; + final Widget? urlPreviews; final bool showSender; final String? timestamp; @@ -62,6 +64,7 @@ class TimelineEventLayoutMessage extends StatelessWidget { if (formattedContent != null) RepaintBoundary(child: formattedContent!), if (attachments != null) attachments!, + if (urlPreviews != null) urlPreviews!, if (reactions != null) Padding( padding: const EdgeInsets.fromLTRB(0, 4, 0, 0), diff --git a/commet/lib/ui/molecules/timeline_events/timeline_event_menu_dialog.dart b/commet/lib/ui/molecules/timeline_events/timeline_event_menu_dialog.dart index 0c4356c2..a7609e56 100644 --- a/commet/lib/ui/molecules/timeline_events/timeline_event_menu_dialog.dart +++ b/commet/lib/ui/molecules/timeline_events/timeline_event_menu_dialog.dart @@ -54,6 +54,7 @@ class TimelineEventMenuDialog extends StatelessWidget { child: SizedBox( child: TimelineViewEntry( timeline: timeline, + overrideShowSender: true, initialIndex: timeline.events.indexOf(event), showDetailed: true, ), 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 43a7ac5c..426b9998 100644 --- a/commet/lib/ui/molecules/timeline_events/timeline_view_entry.dart +++ b/commet/lib/ui/molecules/timeline_events/timeline_view_entry.dart @@ -19,6 +19,7 @@ class TimelineViewEntry extends StatefulWidget { this.setEditingEvent, this.setReplyingEvent, this.showDetailed = false, + this.overrideShowSender = false, super.key}); final Timeline timeline; final int initialIndex; @@ -26,6 +27,7 @@ class TimelineViewEntry extends StatefulWidget { final Function(TimelineEvent? event)? setReplyingEvent; final Function(TimelineEvent? event)? setEditingEvent; final bool showDetailed; + final bool overrideShowSender; @override State createState() => TimelineViewEntryState(); @@ -186,6 +188,7 @@ class TimelineViewEntryState extends State key: eventKey, timeline: widget.timeline, detailed: widget.showDetailed || selected, + overrideShowSender: widget.overrideShowSender, initialIndex: widget.initialIndex); case EventType.roomCreated: case EventType.memberJoined: From 3bcde777195504bd0bed76297ede38ee94bb5327 Mon Sep 17 00:00:00 2001 From: Airyzz <36567925+Airyzz@users.noreply.github.com> Date: Wed, 5 Jun 2024 15:55:48 +0930 Subject: [PATCH 11/27] Refresh url preview when message changes --- .../events/timeline_event_view_message.dart | 8 ++++++-- .../events/timeline_event_view_url_previews.dart | 14 +++++++++----- commet/lib/ui/molecules/url_preview_widget.dart | 6 ++++++ 3 files changed, 21 insertions(+), 7 deletions(-) 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 index ae9c1aa9..c1310f73 100644 --- 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 @@ -7,6 +7,7 @@ import 'package:commet/ui/molecules/timeline_events/events/timeline_event_view_r import 'package:commet/ui/molecules/timeline_events/events/timeline_event_view_url_previews.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/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; @@ -42,6 +43,7 @@ class _TimelineEventViewMessageState extends State name: "messageFailedToDecrypt"); GlobalKey reactionsKey = GlobalKey(); + GlobalKey urlPreviewsKey = GlobalKey(); Widget? formattedContent; ImageProvider? senderAvatar; @@ -92,7 +94,9 @@ class _TimelineEventViewMessageState extends State ? TimelineEventViewUrlPreviews( initialIndex: index, timeline: widget.timeline, - component: previewComponent!) + component: previewComponent!, + key: urlPreviewsKey, + ) : null, ); } @@ -103,7 +107,7 @@ class _TimelineEventViewMessageState extends State loadEventState(newIndex); }); - for (var key in [reactionsKey]) { + for (var key in [reactionsKey, urlPreviewsKey]) { if (key.currentState is TimelineEventViewWidget) { (key.currentState as TimelineEventViewWidget).update(newIndex); } diff --git a/commet/lib/ui/molecules/timeline_events/events/timeline_event_view_url_previews.dart b/commet/lib/ui/molecules/timeline_events/events/timeline_event_view_url_previews.dart index f3be9393..35bf307e 100644 --- a/commet/lib/ui/molecules/timeline_events/events/timeline_event_view_url_previews.dart +++ b/commet/lib/ui/molecules/timeline_events/events/timeline_event_view_url_previews.dart @@ -26,6 +26,7 @@ class _TimelineEventViewUrlPreviewsState extends State implements TimelineEventViewWidget { UrlPreviewData? data; + GlobalKey key = GlobalKey(); @override Widget build(BuildContext context) { @@ -33,6 +34,7 @@ class _TimelineEventViewUrlPreviewsState return Padding( padding: const EdgeInsets.fromLTRB(0, 2, 40, 2), child: UrlPreviewWidget( + key: key, data, onTap: () { LinkUtils.open(data!.uri); @@ -57,11 +59,12 @@ class _TimelineEventViewUrlPreviewsState var cachedData = widget.component.getCachedPreview(widget.timeline.room, event); - if (cachedData != null) { - setState(() { - data = cachedData; - }); - } else { + setState(() { + data = cachedData; + key = GlobalKey(); + }); + + if (cachedData == null) { widget.component.getPreview(widget.timeline.room, event).then( (value) async { if (value?.image != null) { @@ -69,6 +72,7 @@ class _TimelineEventViewUrlPreviewsState } setState(() { data = value; + key = GlobalKey(); }); }, ); diff --git a/commet/lib/ui/molecules/url_preview_widget.dart b/commet/lib/ui/molecules/url_preview_widget.dart index 5d90cef8..5d4138d3 100644 --- a/commet/lib/ui/molecules/url_preview_widget.dart +++ b/commet/lib/ui/molecules/url_preview_widget.dart @@ -31,6 +31,12 @@ class _UrlPreviewWidgetState extends State { super.initState(); } + @override + void didChangeDependencies() { + setState(() {}); + super.didChangeDependencies(); + } + @override Widget build(BuildContext context) { return ClipRRect( From deab62cba0523b10a5e7dcfb961625cf57e56d4b Mon Sep 17 00:00:00 2001 From: Airyzz <36567925+Airyzz@users.noreply.github.com> Date: Wed, 5 Jun 2024 15:58:15 +0930 Subject: [PATCH 12/27] Add edited marker --- .../events/timeline_event_view_message.dart | 4 ++++ .../layouts/timeline_event_layout_message.dart | 8 ++++++++ 2 files changed, 12 insertions(+) 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 index c1310f73..4b29f915 100644 --- 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 @@ -59,6 +59,8 @@ class _TimelineEventViewMessageState extends State int index = 0; + late bool edited; + @override void initState() { currentUserIdentifier = widget.timeline.client.self!.identifier; @@ -77,6 +79,7 @@ class _TimelineEventViewMessageState extends State showSender: showSender, formattedContent: formattedContent, timestamp: timestampToString(sentTime), + edited: edited, attachments: attachments != null ? TimelineEventViewAttachments(attachments: attachments!) : null, @@ -125,6 +128,7 @@ class _TimelineEventViewMessageState extends State showSender = shouldShowSender(eventIndex); + edited = event.edited; if (event.type == EventType.encrypted) { formattedContent = tiamat.Text.error(messageFailedToDecrypt); } 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 index d4dc03a3..d85845c1 100644 --- 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 @@ -3,6 +3,7 @@ import 'package:commet/diagnostic/benchmark_values.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; +import 'package:intl/intl.dart'; import 'package:tiamat/tiamat.dart' as tiamat; class TimelineEventLayoutMessage extends StatelessWidget { @@ -17,6 +18,7 @@ class TimelineEventLayoutMessage extends StatelessWidget { this.reactions, this.timestamp, this.urlPreviews, + this.edited = false, this.avatarSize = 32, this.showSender = true}); final String senderName; @@ -28,10 +30,15 @@ class TimelineEventLayoutMessage extends StatelessWidget { final Widget? reactions; final Widget? urlPreviews; final bool showSender; + final bool edited; final String? timestamp; final double avatarSize; + String get messageEditedMarker => Intl.message("(Edited)", + name: "messageEditedMarker", + desc: "Short text to mark that a message has been edited"); + @override Widget build(BuildContext context) { BenchmarkValues.numTimelineMessageBodyBuilt += 1; @@ -63,6 +70,7 @@ class TimelineEventLayoutMessage extends StatelessWidget { ), if (formattedContent != null) RepaintBoundary(child: formattedContent!), + if (edited) tiamat.Text.labelLow(messageEditedMarker), if (attachments != null) attachments!, if (urlPreviews != null) urlPreviews!, if (reactions != null) From 73f4376b3e03c59214a923adfdc35abd31283072 Mon Sep 17 00:00:00 2001 From: Airyz <36567925+Airyzz@users.noreply.github.com> Date: Wed, 5 Jun 2024 16:06:30 +0930 Subject: [PATCH 13/27] Update timeline_event_view_url_previews.dart --- .../events/timeline_event_view_url_previews.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/commet/lib/ui/molecules/timeline_events/events/timeline_event_view_url_previews.dart b/commet/lib/ui/molecules/timeline_events/events/timeline_event_view_url_previews.dart index 35bf307e..621bf6a7 100644 --- a/commet/lib/ui/molecules/timeline_events/events/timeline_event_view_url_previews.dart +++ b/commet/lib/ui/molecules/timeline_events/events/timeline_event_view_url_previews.dart @@ -67,8 +67,9 @@ class _TimelineEventViewUrlPreviewsState if (cachedData == null) { widget.component.getPreview(widget.timeline.room, event).then( (value) async { - if (value?.image != null) { - await precacheImage(value!.image!, context); + final image = value?.image; + if (image != null) { + await precacheImage(image, context); } setState(() { data = value; From 994de471411827adf1ff2debaac24e202cdcf418 Mon Sep 17 00:00:00 2001 From: Airyzz <36567925+Airyzz@users.noreply.github.com> Date: Wed, 5 Jun 2024 06:54:59 +0930 Subject: [PATCH 14/27] Update timeline_event_view_url_previews.dart --- .../timeline_event_view_url_previews.dart | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/commet/lib/ui/molecules/timeline_events/events/timeline_event_view_url_previews.dart b/commet/lib/ui/molecules/timeline_events/events/timeline_event_view_url_previews.dart index 621bf6a7..c1f526d9 100644 --- a/commet/lib/ui/molecules/timeline_events/events/timeline_event_view_url_previews.dart +++ b/commet/lib/ui/molecules/timeline_events/events/timeline_event_view_url_previews.dart @@ -67,14 +67,19 @@ class _TimelineEventViewUrlPreviewsState if (cachedData == null) { widget.component.getPreview(widget.timeline.room, event).then( (value) async { - final image = value?.image; - if (image != null) { - await precacheImage(image, context); + if (mounted) { + final image = value?.image; + if (image != null) { + if (context.mounted) { + await precacheImage(image, context); + } + } + + setState(() { + data = value; + key = GlobalKey(); + }); } - setState(() { - data = value; - key = GlobalKey(); - }); }, ); } From 5d310e3e630db333885008c93849fd323335de1b Mon Sep 17 00:00:00 2001 From: Airyzz <36567925+Airyzz@users.noreply.github.com> Date: Wed, 5 Jun 2024 07:02:41 +0930 Subject: [PATCH 15/27] Fix getting wrong event in menu --- .../timeline_event_menu_dialog.dart | 12 ++++--- .../timeline_events/timeline_view_entry.dart | 35 +++++++++++-------- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/commet/lib/ui/molecules/timeline_events/timeline_event_menu_dialog.dart b/commet/lib/ui/molecules/timeline_events/timeline_event_menu_dialog.dart index a7609e56..c07c11b1 100644 --- a/commet/lib/ui/molecules/timeline_events/timeline_event_menu_dialog.dart +++ b/commet/lib/ui/molecules/timeline_events/timeline_event_menu_dialog.dart @@ -83,21 +83,23 @@ class TimelineEventMenuDialog extends StatelessWidget { ); } - void doAction(TimelineEventMenuEntry entry, BuildContext context) { + void doAction(TimelineEventMenuEntry entry, BuildContext context) async { if (entry.action != null) { entry.action?.call(context); return; } if (entry.secondaryMenuBuilder != null) { - showModalBottomSheet( + await showModalBottomSheet( context: context, - builder: (context) { - return entry.secondaryMenuBuilder!.call(context, () { - Navigator.of(context).pop(); + builder: (newContext) { + return entry.secondaryMenuBuilder!.call(newContext, () { + Navigator.of(newContext).pop(); }); }, ); + + if (context.mounted) Navigator.of(context).pop(); } } } 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 426b9998..d547901c 100644 --- a/commet/lib/ui/molecules/timeline_events/timeline_view_entry.dart +++ b/commet/lib/ui/molecules/timeline_events/timeline_view_entry.dart @@ -38,6 +38,9 @@ class TimelineViewEntryState extends State late String eventId; late EventType eventType; late TimelineEventStatus status; + + // Note that this index is only reliable on builds - if an item is inserted in to the list, this index will be out of sync until its updated. + // If you need to get the event which this widget represents, use the ID late int index; GlobalKey eventKey = GlobalKey(); @@ -113,20 +116,22 @@ class TimelineViewEntryState extends State if (status == TimelineEventStatus.removed) return Container(); - var event = buildEvent(); + var result = buildEvent(); if (Layout.desktop) { - event = MouseRegion( - onEnter: (_) => - widget.onEventHovered?.call(widget.timeline.events[index].eventId), - child: event, + result = MouseRegion( + onEnter: (_) => widget.onEventHovered?.call(eventId), + child: result, ); } if (Layout.mobile) { - event = InkWell( + result = InkWell( onLongPress: () { - var event = widget.timeline.events[index]; + var event = widget.timeline.tryGetEvent(eventId); + if (event == null) { + return; + } showModalBottomSheet( showDragHandle: true, @@ -145,38 +150,38 @@ class TimelineViewEntryState extends State ), )); }, - child: event, + child: result, ); } if (selected) { - event = Container( + result = Container( color: Theme.of(context).hoverColor, - child: event, + child: result, ); } if (timelineLayerLink != null) { - event = Stack( + result = Stack( alignment: Alignment.topRight, children: [ CompositedTransformTarget( link: timelineLayerLink!, child: const SizedBox()), - event ?? Container() + result ?? Container() ], ); } if (showDate) { - event = Column( + result = Column( children: [ TimelineEventDateTimeMarker(time: time), - event ?? Container() + result ?? Container() ], ); } - return event ?? Container(); + return result ?? Container(); } Widget? buildEvent() { From 8fe9342ff8115f536bcf6434b459edd8eb432868 Mon Sep 17 00:00:00 2001 From: Airyzz <36567925+Airyzz@users.noreply.github.com> Date: Wed, 5 Jun 2024 19:08:16 +0930 Subject: [PATCH 16/27] Add jump to latest button --- .../room_timeline_overlay.dart | 74 ++++++++++++++----- .../room_timeline_overlay_button.dart | 51 +++++++++++++ .../room_timeline_widget_view.dart | 13 +++- 3 files changed, 119 insertions(+), 19 deletions(-) create mode 100644 commet/lib/ui/molecules/room_timeline_widget/room_timeline_overlay_button.dart diff --git a/commet/lib/ui/molecules/room_timeline_widget/room_timeline_overlay.dart b/commet/lib/ui/molecules/room_timeline_widget/room_timeline_overlay.dart index 2119d3f3..e70ae4a4 100644 --- a/commet/lib/ui/molecules/room_timeline_widget/room_timeline_overlay.dart +++ b/commet/lib/ui/molecules/room_timeline_widget/room_timeline_overlay.dart @@ -1,6 +1,8 @@ import 'package:commet/main.dart'; +import 'package:commet/ui/molecules/room_timeline_widget/room_timeline_overlay_button.dart'; import 'package:commet/ui/molecules/timeline_events/timeline_event_menu.dart'; import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; import 'package:just_the_tooltip/just_the_tooltip.dart'; import 'package:tiamat/atoms/context_menu.dart'; import 'package:tiamat/atoms/tile.dart'; @@ -11,8 +13,14 @@ import 'package:tiamat/tiamat.dart' as tiamat; import 'package:flutter/material.dart' as m; class TimelineOverlay extends StatefulWidget { - const TimelineOverlay({required this.link, super.key}); + const TimelineOverlay( + {required this.link, + this.showMessageMenu = true, + this.jumpToLatest, + super.key}); final LayerLink link; + final bool showMessageMenu; + final Function()? jumpToLatest; @override State createState() => TimelineOverlayState(); @@ -30,24 +38,48 @@ class TimelineOverlayState extends State { final double tooltipHeight = 300; + bool isAttatchedToBottom = true; + + String get labelJumpToLatest => Intl.message("Jump to latest", + desc: + "Label for the button which jumps the room timeline view to the latest message", + name: "labelJumpToLatest"); + @override Widget build(BuildContext context) { - return Positioned( - right: 0, - top: 0, - child: CompositedTransformFollower( - targetAnchor: Alignment.topRight, - followerAnchor: openDownwards == true - ? Alignment.topRight - : Alignment.bottomRight, - showWhenUnlinked: false, - offset: Offset(-20, openDownwards == true ? -50 : 0), - link: widget.link, - child: MouseRegion( - child: Padding( - padding: const EdgeInsets.fromLTRB(20, 20, 20, 0), - child: buildTooltipMenu(child: buildPrimaryMenu(context)), - )))); + return Stack( + children: [ + if (widget.showMessageMenu) + Positioned( + right: 0, + top: 0, + child: CompositedTransformFollower( + targetAnchor: Alignment.topRight, + followerAnchor: openDownwards == true + ? Alignment.topRight + : Alignment.bottomRight, + showWhenUnlinked: false, + offset: Offset(-20, openDownwards == true ? -50 : 0), + link: widget.link, + child: MouseRegion( + child: Padding( + padding: const EdgeInsets.fromLTRB(20, 20, 20, 0), + child: buildTooltipMenu(child: buildPrimaryMenu(context)), + )))), + Align( + alignment: Alignment.bottomCenter, + child: AnimatedSlide( + offset: Offset(0, !isAttatchedToBottom ? 0 : 1), + duration: const Duration(milliseconds: 200), + curve: Curves.easeInOutCubic, + child: RoomTimelineOverlayButton( + text: labelJumpToLatest, + onTap: widget.jumpToLatest, + ), + ), + ) + ], + ); } Widget buildTooltipMenu({required Widget child}) { @@ -148,6 +180,14 @@ class TimelineOverlayState extends State { }); } + void setAttatchedToBottom(bool value) { + if (value != isAttatchedToBottom) { + setState(() { + isAttatchedToBottom = value; + }); + } + } + Widget buildAction( {required String name, required IconData icon, diff --git a/commet/lib/ui/molecules/room_timeline_widget/room_timeline_overlay_button.dart b/commet/lib/ui/molecules/room_timeline_widget/room_timeline_overlay_button.dart new file mode 100644 index 00000000..88751235 --- /dev/null +++ b/commet/lib/ui/molecules/room_timeline_widget/room_timeline_overlay_button.dart @@ -0,0 +1,51 @@ +import 'package:commet/config/layout_config.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:tiamat/config/style/theme_extensions.dart'; +import 'package:tiamat/tiamat.dart' as tiamat; + +class RoomTimelineOverlayButton extends StatelessWidget { + const RoomTimelineOverlayButton( + {this.onTap, this.text = "Hello, World!", super.key}); + final void Function()? onTap; + final String text; + + @override + Widget build(BuildContext context) { + var padding = Layout.mobile + ? const EdgeInsets.fromLTRB(18, 12, 18, 12) + : const EdgeInsets.fromLTRB(12, 4, 12, 4); + return Padding( + padding: const EdgeInsets.all(8.0), + child: DecoratedBox( + decoration: BoxDecoration( + boxShadow: [ + BoxShadow( + color: Theme.of(context).shadowColor, + blurRadius: 5, + ) + ], + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: Theme.of(context).extension()!.highlight, + width: 1), + color: Theme.of(context).extension()!.surfaceHigh1), + child: ClipRRect( + borderRadius: BorderRadius.circular(20), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: onTap, + child: Padding( + padding: padding, + child: tiamat.Text.labelLow( + text, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ), + ), + ), + )); + } +} 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 aaa10c1d..4c963079 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 @@ -157,11 +157,17 @@ class RoomTimelineWidgetViewState extends State { widget.onViewScrolled?.call( offset: controller.offset, maxScrollExtent: controller.position.maxScrollExtent); + + var overlayState = overlayKey.currentState as TimelineOverlayState?; + overlayState?.setAttatchedToBottom(attachedToBottom); } void animateAndSnapToBottom() { controller.position.hold(() {}); + var overlayState = overlayKey.currentState as TimelineOverlayState?; + overlayState?.setAttatchedToBottom(attachedToBottom); + animatingToBottom = true; int lastEvent = recentItemsCount; @@ -328,8 +334,11 @@ class RoomTimelineWidgetViewState extends State { ], ), ), - if (Layout.desktop) - TimelineOverlay(key: overlayKey, link: selectedEventLayerLink) + TimelineOverlay( + key: overlayKey, + showMessageMenu: Layout.desktop, + jumpToLatest: animateAndSnapToBottom, + link: selectedEventLayerLink) ], ), ), From f7233dc177b7a3df7cf4b1289d844c5998dd1a0b Mon Sep 17 00:00:00 2001 From: Airyzz <36567925+Airyzz@users.noreply.github.com> Date: Wed, 5 Jun 2024 19:10:14 +0930 Subject: [PATCH 17/27] dont show time seperator on single event --- .../timeline_events/timeline_event_menu_dialog.dart | 2 +- .../timeline_events/timeline_view_entry.dart | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/commet/lib/ui/molecules/timeline_events/timeline_event_menu_dialog.dart b/commet/lib/ui/molecules/timeline_events/timeline_event_menu_dialog.dart index c07c11b1..7bc37cf9 100644 --- a/commet/lib/ui/molecules/timeline_events/timeline_event_menu_dialog.dart +++ b/commet/lib/ui/molecules/timeline_events/timeline_event_menu_dialog.dart @@ -54,7 +54,7 @@ class TimelineEventMenuDialog extends StatelessWidget { child: SizedBox( child: TimelineViewEntry( timeline: timeline, - overrideShowSender: true, + singleEvent: true, initialIndex: timeline.events.indexOf(event), showDetailed: true, ), 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 d547901c..4806e9d2 100644 --- a/commet/lib/ui/molecules/timeline_events/timeline_view_entry.dart +++ b/commet/lib/ui/molecules/timeline_events/timeline_view_entry.dart @@ -19,7 +19,7 @@ class TimelineViewEntry extends StatefulWidget { this.setEditingEvent, this.setReplyingEvent, this.showDetailed = false, - this.overrideShowSender = false, + this.singleEvent = false, super.key}); final Timeline timeline; final int initialIndex; @@ -27,7 +27,9 @@ class TimelineViewEntry extends StatefulWidget { final Function(TimelineEvent? event)? setReplyingEvent; final Function(TimelineEvent? event)? setEditingEvent; final bool showDetailed; - final bool overrideShowSender; + + // Should be true if we are showing this event on its own, and not as part of a timeline + final bool singleEvent; @override State createState() => TimelineViewEntryState(); @@ -68,6 +70,10 @@ class TimelineViewEntryState extends State } bool shouldEventShowDate(int index) { + if (widget.singleEvent) { + return false; + } + var offsetIndex = index + 1; if (widget.timeline.events.length <= offsetIndex) { @@ -193,7 +199,7 @@ class TimelineViewEntryState extends State key: eventKey, timeline: widget.timeline, detailed: widget.showDetailed || selected, - overrideShowSender: widget.overrideShowSender, + overrideShowSender: widget.singleEvent, initialIndex: widget.initialIndex); case EventType.roomCreated: case EventType.memberJoined: From d9d8209241c939d4b75fab6453d5a6ffbc33e751 Mon Sep 17 00:00:00 2001 From: Airyzz <36567925+Airyzz@users.noreply.github.com> Date: Wed, 5 Jun 2024 19:18:32 +0930 Subject: [PATCH 18/27] Handle read receipts --- .../room_timeline_overlay.dart | 2 +- .../room_timeline_widget.dart | 9 +++++++++ .../room_timeline_widget_view.dart | 16 ++++++++++++++-- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/commet/lib/ui/molecules/room_timeline_widget/room_timeline_overlay.dart b/commet/lib/ui/molecules/room_timeline_widget/room_timeline_overlay.dart index e70ae4a4..c25807bc 100644 --- a/commet/lib/ui/molecules/room_timeline_widget/room_timeline_overlay.dart +++ b/commet/lib/ui/molecules/room_timeline_widget/room_timeline_overlay.dart @@ -180,7 +180,7 @@ class TimelineOverlayState extends State { }); } - void setAttatchedToBottom(bool value) { + void setAttachedToBottom(bool value) { if (value != isAttatchedToBottom) { setState(() { isAttatchedToBottom = value; diff --git a/commet/lib/ui/molecules/room_timeline_widget/room_timeline_widget.dart b/commet/lib/ui/molecules/room_timeline_widget/room_timeline_widget.dart index ba17f7a9..9e2501e0 100644 --- a/commet/lib/ui/molecules/room_timeline_widget/room_timeline_widget.dart +++ b/commet/lib/ui/molecules/room_timeline_widget/room_timeline_widget.dart @@ -28,6 +28,7 @@ class _RoomTimelineWidgetState extends State { key: timelineViewKey, timeline: widget.timeline, onViewScrolled: onViewScrolled, + onAttachedToBottom: onAttachedToBottom, setReplyingEvent: widget.setReplyingEvent, setEditingEvent: widget.setEditingEvent, ); @@ -50,6 +51,14 @@ class _RoomTimelineWidgetState extends State { if (offset > maxScrollExtent - loadingThreshold && loadingHistory == null) { loadMoreHistory(); } + + if (state?.attachedToBottom == true) {} + } + + void onAttachedToBottom() { + if (widget.timeline.events.isNotEmpty) { + widget.timeline.markAsRead(widget.timeline.events.first); + } } void loadMoreHistory() async { 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 4c963079..3c7c0727 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 @@ -18,11 +18,13 @@ class RoomTimelineWidgetView extends StatefulWidget { this.onViewScrolled, this.setEditingEvent, this.setReplyingEvent, + this.onAttachedToBottom, super.key}); final Timeline timeline; final Function(TimelineEvent event)? markAsRead; final Function(TimelineEvent? event)? setReplyingEvent; final Function(TimelineEvent? event)? setEditingEvent; + final Function()? onAttachedToBottom; final Function({required double offset, required double maxScrollExtent})? onViewScrolled; @@ -53,6 +55,8 @@ class RoomTimelineWidgetViewState extends State { late List subscriptions; + bool wasLastScrollAttachedToBottom = false; + bool get attachedToBottom => controller.hasClients ? controller.offset - controller.positions.first.minScrollExtent < 50 || animatingToBottom @@ -147,6 +151,7 @@ class RoomTimelineWidgetViewState extends State { double extent = controller.position.minScrollExtent; controller = ScrollController(initialScrollOffset: extent); controller.addListener(onScroll); + widget.onAttachedToBottom?.call(); setState(() { firstFrame = false; }); @@ -159,14 +164,21 @@ class RoomTimelineWidgetViewState extends State { maxScrollExtent: controller.position.maxScrollExtent); var overlayState = overlayKey.currentState as TimelineOverlayState?; - overlayState?.setAttatchedToBottom(attachedToBottom); + overlayState?.setAttachedToBottom(attachedToBottom); + + if (wasLastScrollAttachedToBottom == false && attachedToBottom) { + widget.onAttachedToBottom?.call(); + } + + wasLastScrollAttachedToBottom = attachedToBottom; } void animateAndSnapToBottom() { controller.position.hold(() {}); var overlayState = overlayKey.currentState as TimelineOverlayState?; - overlayState?.setAttatchedToBottom(attachedToBottom); + overlayState?.setAttachedToBottom(attachedToBottom); + widget.onAttachedToBottom?.call(); animatingToBottom = true; From 45bfd582effb41c058384c814b5cd8be58cb9695 Mon Sep 17 00:00:00 2001 From: Airyz <36567925+Airyzz@users.noreply.github.com> Date: Wed, 5 Jun 2024 20:04:55 +0930 Subject: [PATCH 19/27] only show preview if we should actually get one --- .../events/timeline_event_view_message.dart | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) 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 index 4b29f915..bf94afff 100644 --- 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 @@ -55,7 +55,7 @@ class _TimelineEventViewMessageState extends State late DateTime sentTime; UrlPreviewComponent? previewComponent; - bool hasLinks = false; + bool doUrlPreview = false; int index = 0; @@ -93,7 +93,7 @@ class _TimelineEventViewMessageState extends State ? TimelineEventViewReactions( key: reactionsKey, timeline: widget.timeline, initialIndex: index) : null, - urlPreviews: previewComponent != null && hasLinks + urlPreviews: previewComponent != null && doUrlPreview ? TimelineEventViewUrlPreviews( initialIndex: index, timeline: widget.timeline, @@ -146,7 +146,10 @@ class _TimelineEventViewMessageState extends State sentTime = event.originServerTs; - hasLinks = event.links?.isNotEmpty == true; + doUrlPreview = + previewComponent?.shouldGetPreviewData(widget.timeline.room, event) == + true && + event.links?.isNotEmpty == true; } String timestampToString(DateTime time) { From aa56fa98136d35b656a53c78c67c73f29dc70a19 Mon Sep 17 00:00:00 2001 From: Airyzz <36567925+Airyzz@users.noreply.github.com> Date: Fri, 7 Jun 2024 07:31:14 +0930 Subject: [PATCH 20/27] Update timeline_event_view_url_previews.dart --- .../timeline_event_view_url_previews.dart | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/commet/lib/ui/molecules/timeline_events/events/timeline_event_view_url_previews.dart b/commet/lib/ui/molecules/timeline_events/events/timeline_event_view_url_previews.dart index c1f526d9..8e48395d 100644 --- a/commet/lib/ui/molecules/timeline_events/events/timeline_event_view_url_previews.dart +++ b/commet/lib/ui/molecules/timeline_events/events/timeline_event_view_url_previews.dart @@ -26,6 +26,8 @@ class _TimelineEventViewUrlPreviewsState extends State implements TimelineEventViewWidget { UrlPreviewData? data; + bool loading = false; + GlobalKey key = GlobalKey(); @override @@ -33,13 +35,15 @@ class _TimelineEventViewUrlPreviewsState BenchmarkValues.numTimelineUrlPreviewBuilt += 1; return Padding( padding: const EdgeInsets.fromLTRB(0, 2, 40, 2), - child: UrlPreviewWidget( - key: key, - data, - onTap: () { - LinkUtils.open(data!.uri); - }, - ), + child: (loading || data != null) + ? UrlPreviewWidget( + key: key, + data, + onTap: () { + LinkUtils.open(data!.uri); + }, + ) + : Container(), ); } @@ -65,6 +69,9 @@ class _TimelineEventViewUrlPreviewsState }); if (cachedData == null) { + setState(() { + loading = true; + }); widget.component.getPreview(widget.timeline.room, event).then( (value) async { if (mounted) { @@ -76,6 +83,7 @@ class _TimelineEventViewUrlPreviewsState } setState(() { + loading = false; data = value; key = GlobalKey(); }); From 0277e9c87bce8c7c49086fda83a4f3b505d22c84 Mon Sep 17 00:00:00 2001 From: Airyzz <36567925+Airyzz@users.noreply.github.com> Date: Fri, 7 Jun 2024 08:45:15 +0930 Subject: [PATCH 21/27] handle server not supporting url preview --- .../matrix_url_preview_component.dart | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/commet/lib/client/matrix/components/url_preview/matrix_url_preview_component.dart b/commet/lib/client/matrix/components/url_preview/matrix_url_preview_component.dart index 46bdf9ca..28142b8b 100644 --- a/commet/lib/client/matrix/components/url_preview/matrix_url_preview_component.dart +++ b/commet/lib/client/matrix/components/url_preview/matrix_url_preview_component.dart @@ -9,6 +9,8 @@ import 'package:commet/main.dart'; import 'package:flutter/widgets.dart'; import 'package:matrix/matrix.dart' as matrix; import 'package:encrypted_url_preview/encrypted_url_preview.dart'; +import 'package:matrix/matrix_api_lite.dart'; +import 'package:matrix/matrix_api_lite/model/matrix_exception.dart'; class MatrixUrlPreviewComponent implements UrlPreviewComponent { @override @@ -20,6 +22,8 @@ class MatrixUrlPreviewComponent implements UrlPreviewComponent { EncryptedUrlPreview? privatePreviewGetter; + bool? serverSupportsUrlPreview; + void createPrivatePreviewGetter() { privatePreviewGetter = EncryptedUrlPreview( proxyServerUrl: Uri.https("telescope.commet.chat"), @@ -62,7 +66,10 @@ pQIDAQAB return null; } - cache[uri.toString()] = data!; + if (data != null) { + cache[uri.toString()] = data; + } + return data; } @@ -82,6 +89,10 @@ pQIDAQAB return false; } + if (serverSupportsUrlPreview == false) { + return false; + } + return event.links?.isNotEmpty == true; } @@ -137,10 +148,24 @@ pQIDAQAB Future fetchPreviewData( matrix.Client client, Uri url) async { - var response = await client.request( - matrix.RequestType.GET, "/media/v3/preview_url", - query: {"url": url.toString()}); + late Map response; + try { + response = await client.request( + matrix.RequestType.GET, "/media/v3/preview_url", + query: {"url": url.toString()}); + } catch (e, s) { + if (e is MatrixException) { + if (e.error == MatrixError.M_UNRECOGNIZED) { + serverSupportsUrlPreview = false; + } + } + + Log.onError(e, s); + + return null; + } + serverSupportsUrlPreview = true; var title = response['og:title'] as String?; var siteName = response['og:site_name'] as String?; var imageUrl = response['og:image'] as String?; From 375403ba6c35f1c7bea15075ded7806b211f605b Mon Sep 17 00:00:00 2001 From: Airyzz <36567925+Airyzz@users.noreply.github.com> Date: Fri, 7 Jun 2024 08:48:30 +0930 Subject: [PATCH 22/27] Update room_timeline_widget_view.dart I think this fixes #259 --- .../room_timeline_widget_view.dart | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 3c7c0727..36c7f882 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 @@ -92,7 +92,10 @@ class RoomTimelineWidgetViewState extends State { } void onEventAdded(int index) { - setState(() {}); + eventKeys.insert(index, ( + GlobalKey(debugLabel: widget.timeline.events[index].eventId), + widget.timeline.events[index].eventId + )); if (index == 0 || index < recentItemsCount) { recentItemsCount += 1; @@ -100,11 +103,6 @@ class RoomTimelineWidgetViewState extends State { historyItemsCount = widget.timeline.events.length - recentItemsCount; } - eventKeys.insert(index, ( - GlobalKey(debugLabel: widget.timeline.events[index].eventId), - widget.timeline.events[index].eventId - )); - if (index == 0) { if (attachedToBottom || animatingToBottom) { WidgetsBinding.instance.addPostFrameCallback((_) { @@ -114,6 +112,8 @@ class RoomTimelineWidgetViewState extends State { widget.markAsRead?.call(widget.timeline.events[0]); } } + + setState(() {}); } void onEventChanged(int index) { From 0ae192d3b4244482b853b40da67a7b4c3877492b Mon Sep 17 00:00:00 2001 From: Airyzz <36567925+Airyzz@users.noreply.github.com> Date: Fri, 7 Jun 2024 08:57:57 +0930 Subject: [PATCH 23/27] Update room_timeline_widget.dart --- .../room_timeline_widget.dart | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/commet/lib/ui/molecules/room_timeline_widget/room_timeline_widget.dart b/commet/lib/ui/molecules/room_timeline_widget/room_timeline_widget.dart index 9e2501e0..9ae2e768 100644 --- a/commet/lib/ui/molecules/room_timeline_widget/room_timeline_widget.dart +++ b/commet/lib/ui/molecules/room_timeline_widget/room_timeline_widget.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:commet/client/timeline.dart'; import 'package:commet/debug/log.dart'; import 'package:commet/ui/molecules/room_timeline_widget/room_timeline_widget_view.dart'; @@ -22,6 +24,30 @@ class _RoomTimelineWidgetState extends State { GlobalKey timelineViewKey = GlobalKey(); + StreamSubscription? sub; + + @override + void initState() { + sub = widget.timeline.onEventAdded.stream.listen(onEventReceived); + + super.initState(); + } + + @override + void dispose() { + sub?.cancel(); + super.dispose(); + } + + void onEventReceived(int index) { + if (index == 0) { + var state = timelineViewKey.currentState as RoomTimelineWidgetViewState?; + if (state?.attachedToBottom == true) { + widget.timeline.markAsRead(widget.timeline.events[index]); + } + } + } + @override Widget build(BuildContext context) { return RoomTimelineWidgetView( From 2f79e18f5363eb25d98739e6abb56c49b9e1162f Mon Sep 17 00:00:00 2001 From: Airyzz <36567925+Airyzz@users.noreply.github.com> Date: Fri, 7 Jun 2024 09:31:07 +0930 Subject: [PATCH 24/27] Only show image in url for supported image types --- .../url_preview/matrix_url_preview_component.dart | 15 ++++++++++++++- commet/lib/utils/mime.dart | 1 + 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/commet/lib/client/matrix/components/url_preview/matrix_url_preview_component.dart b/commet/lib/client/matrix/components/url_preview/matrix_url_preview_component.dart index 28142b8b..6c1d8f9b 100644 --- a/commet/lib/client/matrix/components/url_preview/matrix_url_preview_component.dart +++ b/commet/lib/client/matrix/components/url_preview/matrix_url_preview_component.dart @@ -6,6 +6,7 @@ import 'package:commet/client/room.dart'; import 'package:commet/client/timeline.dart'; import 'package:commet/debug/log.dart'; import 'package:commet/main.dart'; +import 'package:commet/utils/mime.dart'; import 'package:flutter/widgets.dart'; import 'package:matrix/matrix.dart' as matrix; import 'package:encrypted_url_preview/encrypted_url_preview.dart'; @@ -171,11 +172,23 @@ pQIDAQAB var imageUrl = response['og:image'] as String?; var description = response['og:description'] as String?; + var type = response["og:image:type"] as String?; + if (type != null) { + if (Mime.displayableTypes.contains(type) == false) { + imageUrl = null; + } + } + ImageProvider? image; if (imageUrl != null) { var imageUri = Uri.parse(imageUrl); if (imageUri.scheme == "mxc") { - image = MatrixMxcImage(imageUri, client, doThumbnail: false); + try { + image = MatrixMxcImage(imageUri, client, doThumbnail: false); + } catch (exception, stack) { + Log.onError(exception, stack); + Log.w("Failed to get mxc image"); + } } } diff --git a/commet/lib/utils/mime.dart b/commet/lib/utils/mime.dart index 7f95fb5c..dcbd36a3 100644 --- a/commet/lib/utils/mime.dart +++ b/commet/lib/utils/mime.dart @@ -54,6 +54,7 @@ class Mime { static String? lookupType(String filepath, {Uint8List? data}) { var resolver = mime.MimeTypeResolver(); resolver.addMagicNumber([0x42, 0x4d], "image/bmp"); + resolver.addMagicNumber([0x3c, 0x73, 0x76, 0x67], "image/svg+xml"); // ' Date: Fri, 7 Jun 2024 09:32:34 +0930 Subject: [PATCH 25/27] clean up some logs --- .../ui/molecules/room_timeline_widget/room_timeline_widget.dart | 1 - .../room_timeline_widget/room_timeline_widget_view.dart | 2 -- .../timeline_events/layouts/timeline_event_layout_message.dart | 2 -- .../lib/ui/molecules/timeline_events/timeline_view_entry.dart | 2 -- 4 files changed, 7 deletions(-) diff --git a/commet/lib/ui/molecules/room_timeline_widget/room_timeline_widget.dart b/commet/lib/ui/molecules/room_timeline_widget/room_timeline_widget.dart index 9ae2e768..b0b29b1b 100644 --- a/commet/lib/ui/molecules/room_timeline_widget/room_timeline_widget.dart +++ b/commet/lib/ui/molecules/room_timeline_widget/room_timeline_widget.dart @@ -88,7 +88,6 @@ class _RoomTimelineWidgetState extends State { } void loadMoreHistory() async { - Log.d("Loading more history!"); loadingHistory = widget.timeline.loadMoreHistory(); await loadingHistory; loadingHistory = null; 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 36c7f882..0b124b96 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 @@ -117,7 +117,6 @@ 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); @@ -307,7 +306,6 @@ class RoomTimelineWidgetViewState extends State { (BuildContext context, int sliverIndex) { numBuilds += 1; // ignore: avoid_print - Log.d("Num Builds: $numBuilds"); var timelineIndex = recentItemsCount + sliverIndex; var key = eventKeys[timelineIndex]; 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 index d85845c1..57852edf 100644 --- 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 @@ -42,8 +42,6 @@ class TimelineEventLayoutMessage extends StatelessWidget { @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( 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 4806e9d2..9ea75307 100644 --- a/commet/lib/ui/molecules/timeline_events/timeline_view_entry.dart +++ b/commet/lib/ui/molecules/timeline_events/timeline_view_entry.dart @@ -117,8 +117,6 @@ class TimelineViewEntryState extends State @override Widget build(BuildContext context) { BenchmarkValues.numTimelineEventsBuilt += 1; - Log.d( - "Num times timeline event built: ${BenchmarkValues.numTimelineEventsBuilt} ($eventId)"); if (status == TimelineEventStatus.removed) return Container(); From 824363fa71ca4e63c7a815108123a4ff0018ae5f Mon Sep 17 00:00:00 2001 From: Airyzz <36567925+Airyzz@users.noreply.github.com> Date: Fri, 7 Jun 2024 09:34:24 +0930 Subject: [PATCH 26/27] Update matrix_client.dart --- commet/lib/client/matrix/matrix_client.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/commet/lib/client/matrix/matrix_client.dart b/commet/lib/client/matrix/matrix_client.dart index 633534e1..b00230fb 100644 --- a/commet/lib/client/matrix/matrix_client.dart +++ b/commet/lib/client/matrix/matrix_client.dart @@ -227,7 +227,6 @@ class MatrixClient extends Client { } void onMatrixClientSync(matrix.SyncUpdate update) { - Log.d("On Matrix Sync!"); _onSync.add(null); _updateRoomslist(); _updateSpacesList(); From e5ebf6e7628be5356a318887f2d1be8a5795fdd7 Mon Sep 17 00:00:00 2001 From: Airyzz <36567925+Airyzz@users.noreply.github.com> Date: Fri, 7 Jun 2024 09:36:54 +0930 Subject: [PATCH 27/27] fix warnings --- .../components/url_preview/matrix_url_preview_component.dart | 1 - .../room_timeline_widget/room_timeline_overlay_button.dart | 1 - .../ui/molecules/room_timeline_widget/room_timeline_widget.dart | 1 - .../timeline_events/layouts/timeline_event_layout_message.dart | 1 - 4 files changed, 4 deletions(-) diff --git a/commet/lib/client/matrix/components/url_preview/matrix_url_preview_component.dart b/commet/lib/client/matrix/components/url_preview/matrix_url_preview_component.dart index 6c1d8f9b..e908b13d 100644 --- a/commet/lib/client/matrix/components/url_preview/matrix_url_preview_component.dart +++ b/commet/lib/client/matrix/components/url_preview/matrix_url_preview_component.dart @@ -11,7 +11,6 @@ import 'package:flutter/widgets.dart'; import 'package:matrix/matrix.dart' as matrix; import 'package:encrypted_url_preview/encrypted_url_preview.dart'; import 'package:matrix/matrix_api_lite.dart'; -import 'package:matrix/matrix_api_lite/model/matrix_exception.dart'; class MatrixUrlPreviewComponent implements UrlPreviewComponent { @override diff --git a/commet/lib/ui/molecules/room_timeline_widget/room_timeline_overlay_button.dart b/commet/lib/ui/molecules/room_timeline_widget/room_timeline_overlay_button.dart index 88751235..b6bf0655 100644 --- a/commet/lib/ui/molecules/room_timeline_widget/room_timeline_overlay_button.dart +++ b/commet/lib/ui/molecules/room_timeline_widget/room_timeline_overlay_button.dart @@ -1,5 +1,4 @@ import 'package:commet/config/layout_config.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:tiamat/config/style/theme_extensions.dart'; import 'package:tiamat/tiamat.dart' as tiamat; diff --git a/commet/lib/ui/molecules/room_timeline_widget/room_timeline_widget.dart b/commet/lib/ui/molecules/room_timeline_widget/room_timeline_widget.dart index b0b29b1b..0d69b0c3 100644 --- a/commet/lib/ui/molecules/room_timeline_widget/room_timeline_widget.dart +++ b/commet/lib/ui/molecules/room_timeline_widget/room_timeline_widget.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:commet/client/timeline.dart'; -import 'package:commet/debug/log.dart'; import 'package:commet/ui/molecules/room_timeline_widget/room_timeline_widget_view.dart'; import 'package:flutter/material.dart'; 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 index 57852edf..19191298 100644 --- 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 @@ -1,4 +1,3 @@ -import 'package:commet/debug/log.dart'; import 'package:commet/diagnostic/benchmark_values.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart';