From 56e40cebcbc2c8baa75bcb362c2dff9650642caa Mon Sep 17 00:00:00 2001 From: Airyzz <36567925+Airyzz@users.noreply.github.com> Date: Sat, 11 Jan 2025 19:27:17 +1030 Subject: [PATCH] add support for pinned events --- .../client/components/component_registry.dart | 2 + .../pinned_messages_component.dart | 11 +++ .../matrix_pinned_messages_component.dart | 31 +++++++ commet/lib/client/matrix/matrix_room.dart | 3 + commet/lib/client/matrix/matrix_timeline.dart | 2 + ...matrix_timeline_event_pinned_messages.dart | 38 ++++++++ .../timeline_events/timeline_event_menu.dart | 11 +++ .../room_pinned_messages_widget.dart | 86 +++++++++++++++++++ .../room_quick_access_menu.dart | 9 ++ .../room_side_panel/room_side_panel.dart | 38 ++++++-- commet/lib/utils/event_bus.dart | 3 + commet/pubspec.lock | 2 +- 12 files changed, 229 insertions(+), 7 deletions(-) create mode 100644 commet/lib/client/components/pinned_messages/pinned_messages_component.dart create mode 100644 commet/lib/client/matrix/components/pinned_messages/matrix_pinned_messages_component.dart create mode 100644 commet/lib/client/matrix/timeline_events/matrix_timeline_event_pinned_messages.dart create mode 100644 commet/lib/ui/organisms/room_pinned_messages/room_pinned_messages_widget.dart diff --git a/commet/lib/client/components/component_registry.dart b/commet/lib/client/components/component_registry.dart index fb1e8bbc..00d62a97 100644 --- a/commet/lib/client/components/component_registry.dart +++ b/commet/lib/client/components/component_registry.dart @@ -10,6 +10,7 @@ import 'package:commet/client/matrix/components/emoticon/matrix_space_emoticon_c import 'package:commet/client/matrix/components/event_search/matrix_event_search_component.dart'; import 'package:commet/client/matrix/components/gif/matrix_gif_component.dart'; import 'package:commet/client/matrix/components/invitation/matrix_invitation_component.dart'; +import 'package:commet/client/matrix/components/pinned_messages/matrix_pinned_messages_component.dart'; import 'package:commet/client/matrix/components/push_notifications/matrix_push_notification_component.dart'; import 'package:commet/client/matrix/components/read_receipts/matrix_read_receipt_component.dart'; import 'package:commet/client/matrix/components/threads/matrix_threads_component.dart'; @@ -42,6 +43,7 @@ class ComponentRegistry { MatrixGifComponent(client, room), MatrixReadReceiptComponent(client, room), MatrixTypingIndicatorsComponent(client, room), + MatrixPinnedMessagesComponent(client, room), ]; } diff --git a/commet/lib/client/components/pinned_messages/pinned_messages_component.dart b/commet/lib/client/components/pinned_messages/pinned_messages_component.dart new file mode 100644 index 00000000..e7b0f4c8 --- /dev/null +++ b/commet/lib/client/components/pinned_messages/pinned_messages_component.dart @@ -0,0 +1,11 @@ +import 'package:commet/client/client.dart'; +import 'package:commet/client/components/room_component.dart'; + +abstract class PinnedMessagesComponent + implements RoomComponent { + List getPinnedMessages(); + + Future pinMessage(String eventId); + + bool get canPinMessages; +} diff --git a/commet/lib/client/matrix/components/pinned_messages/matrix_pinned_messages_component.dart b/commet/lib/client/matrix/components/pinned_messages/matrix_pinned_messages_component.dart new file mode 100644 index 00000000..e8fc4696 --- /dev/null +++ b/commet/lib/client/matrix/components/pinned_messages/matrix_pinned_messages_component.dart @@ -0,0 +1,31 @@ +import 'package:commet/client/components/pinned_messages/pinned_messages_component.dart'; +import 'package:commet/client/matrix/matrix_client.dart'; +import 'package:commet/client/matrix/matrix_room.dart'; +import 'package:matrix/matrix_api_lite/model/event_types.dart'; + +class MatrixPinnedMessagesComponent + extends PinnedMessagesComponent { + @override + MatrixClient client; + + @override + MatrixRoom room; + + MatrixPinnedMessagesComponent(this.client, this.room); + + @override + bool get canPinMessages => + room.matrixRoom.canChangeStateEvent(EventTypes.RoomPinnedEvents); + + @override + List getPinnedMessages() { + return room.matrixRoom.pinnedEventIds.reversed.toList(); + } + + @override + Future pinMessage(String eventId) async { + var pins = room.matrixRoom.pinnedEventIds.toList(); + pins.add(eventId); + await room.matrixRoom.setPinnedEvents(pins); + } +} diff --git a/commet/lib/client/matrix/matrix_room.dart b/commet/lib/client/matrix/matrix_room.dart index 6144bc31..015ad375 100644 --- a/commet/lib/client/matrix/matrix_room.dart +++ b/commet/lib/client/matrix/matrix_room.dart @@ -23,6 +23,7 @@ import 'package:commet/client/matrix/timeline_events/matrix_timeline_event_emote import 'package:commet/client/matrix/timeline_events/matrix_timeline_event_encrypted.dart'; import 'package:commet/client/matrix/timeline_events/matrix_timeline_event_membership.dart'; import 'package:commet/client/matrix/timeline_events/matrix_timeline_event_message.dart'; +import 'package:commet/client/matrix/timeline_events/matrix_timeline_event_pinned_messages.dart'; import 'package:commet/client/matrix/timeline_events/matrix_timeline_event_redaction.dart'; import 'package:commet/client/matrix/timeline_events/matrix_timeline_event_sticker.dart'; import 'package:commet/client/matrix/timeline_events/matrix_timeline_event_unknown.dart'; @@ -439,6 +440,8 @@ class MatrixRoom extends Room { MatrixTimelineEventMembership(event, client: c), matrix.EventTypes.Redaction => MatrixTimelineEventRedaction(event, client: c), + matrix.EventTypes.RoomPinnedEvents => + MatrixTimelineEventPinnedMessages(event, client: c), _ => null }; diff --git a/commet/lib/client/matrix/matrix_timeline.dart b/commet/lib/client/matrix/matrix_timeline.dart index ac541075..bcb48c75 100644 --- a/commet/lib/client/matrix/matrix_timeline.dart +++ b/commet/lib/client/matrix/matrix_timeline.dart @@ -50,6 +50,8 @@ class MatrixTimeline extends Timeline { onRemove: onEventRemoved, eventContextId: contextEventId); + _matrixRoom.postLoad(); + // This could maybe make load times realllly slow if we have a ton of stuff in the cache? // Might be better to only convert as many as we would need to display immediately and then convert the rest on demand convertAllTimelineEvents(); diff --git a/commet/lib/client/matrix/timeline_events/matrix_timeline_event_pinned_messages.dart b/commet/lib/client/matrix/timeline_events/matrix_timeline_event_pinned_messages.dart new file mode 100644 index 00000000..e24564f1 --- /dev/null +++ b/commet/lib/client/matrix/timeline_events/matrix_timeline_event_pinned_messages.dart @@ -0,0 +1,38 @@ +import 'package:commet/client/matrix/timeline_events/matrix_timeline_event.dart'; +import 'package:commet/client/timeline.dart'; +import 'package:commet/client/timeline_events/timeline_event_generic.dart'; +import 'package:flutter/material.dart'; + +class MatrixTimelineEventPinnedMessages extends MatrixTimelineEvent + implements TimelineEventGeneric { + MatrixTimelineEventPinnedMessages(super.event, {required super.client}); + + bool isNewEventPinned() { + if (event.prevContent?.containsKey('pinned') == true) { + var prevList = event.prevContent!['pinned'] as List; + var currList = event.content['pinned'] as List; + + return currList.length > prevList.length; + } else { + return true; + } + } + + @override + IconData get icon => Icons.push_pin; + + @override + String get plainTextBody => getBody(); + + @override + bool get showSenderAvatar => false; + + @override + String getBody({Timeline? timeline}) { + if (isNewEventPinned()) { + return "Message pinned!"; + } else { + return "Message unpinned!"; + } + } +} 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 ffa61393..862a6a49 100644 --- a/commet/lib/ui/molecules/timeline_events/timeline_event_menu.dart +++ b/commet/lib/ui/molecules/timeline_events/timeline_event_menu.dart @@ -1,5 +1,6 @@ import 'package:commet/client/components/direct_messages/direct_message_component.dart'; import 'package:commet/client/components/emoticon/emoticon_component.dart'; +import 'package:commet/client/components/pinned_messages/pinned_messages_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'; @@ -63,6 +64,9 @@ class TimelineEventMenu { bool canCopy = event is TimelineEventMessage; + var pins = timeline.room.getComponent(); + bool canPin = pins?.canPinMessages == true; + primaryActions = [ if (canEditEvent) TimelineEventMenuEntry( @@ -134,6 +138,13 @@ class TimelineEventMenu { ]; secondaryActions = [ + if (canPin) + TimelineEventMenuEntry( + name: "Pin Message", + icon: Icons.push_pin, + action: (context) { + pins!.pinMessage(event.eventId); + }), if (canCopy) TimelineEventMenuEntry( name: CommonStrings.promptCopy, diff --git a/commet/lib/ui/organisms/room_pinned_messages/room_pinned_messages_widget.dart b/commet/lib/ui/organisms/room_pinned_messages/room_pinned_messages_widget.dart new file mode 100644 index 00000000..0f920b8e --- /dev/null +++ b/commet/lib/ui/organisms/room_pinned_messages/room_pinned_messages_widget.dart @@ -0,0 +1,86 @@ +import 'package:commet/client/components/pinned_messages/pinned_messages_component.dart'; +import 'package:commet/client/room.dart'; +import 'package:commet/client/timeline_events/timeline_event.dart'; +import 'package:commet/ui/molecules/timeline_events/timeline_event_view_single.dart'; +import 'package:flutter/material.dart'; +import 'package:tiamat/tiamat.dart' as tiamat; + +class RoomPinnedMessagesWidget extends StatefulWidget { + const RoomPinnedMessagesWidget( + {required this.room, this.onEventClicked, super.key}); + final Room room; + final void Function(String eventId)? onEventClicked; + + @override + State createState() => + _RoomPinnedMessagesWidgetState(); +} + +class _RoomPinnedMessagesWidgetState extends State { + List? events; + + @override + void initState() { + super.initState(); + + loadPinnedMessages(); + } + + @override + Widget build(BuildContext context) { + if (events == null) { + return const tiamat.Tile( + child: Center(child: CircularProgressIndicator())); + } + + if (events!.isEmpty) { + return tiamat.Tile( + child: Center( + child: tiamat.Text.label("No messages have been pinned!"))); + } + + return tiamat.Tile.low( + child: Column( + children: [ + Flexible( + child: ListView.builder( + itemCount: events!.length, + itemBuilder: (context, index) { + var data = events![index]; + return Padding( + padding: const EdgeInsets.fromLTRB(4, 2, 4, 2), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Material( + borderRadius: BorderRadius.circular(8), + child: InkWell( + onTap: () => + widget.onEventClicked?.call(data.eventId), + child: Padding( + padding: const EdgeInsets.all(4.0), + child: TimelineEventViewSingle( + room: widget.room, event: data), + ), + )), + ), + ); + }, + ), + ), + ], + ), + ); + } + + void loadPinnedMessages() async { + final comp = widget.room.getComponent()!; + var eventIds = comp.getPinnedMessages(); + + var allEvents = await Future.wait( + eventIds.map((id) => widget.room.getEvent(id))); + + setState(() { + events = List.from(allEvents.where((e) => e != null)); + }); + } +} diff --git a/commet/lib/ui/organisms/room_quick_access_menu/room_quick_access_menu.dart b/commet/lib/ui/organisms/room_quick_access_menu/room_quick_access_menu.dart index 6791a4e1..9aef7796 100644 --- a/commet/lib/ui/organisms/room_quick_access_menu/room_quick_access_menu.dart +++ b/commet/lib/ui/organisms/room_quick_access_menu/room_quick_access_menu.dart @@ -1,6 +1,7 @@ import 'package:commet/client/client.dart'; import 'package:commet/client/components/event_search/event_search_component.dart'; import 'package:commet/client/components/invitation/invitation_component.dart'; +import 'package:commet/client/components/pinned_messages/pinned_messages_component.dart'; import 'package:commet/ui/navigation/adaptive_dialog.dart'; import 'package:commet/ui/organisms/invitation_view/send_invitation.dart'; import 'package:commet/utils/event_bus.dart'; @@ -16,6 +17,9 @@ class RoomQuickAccessMenu { final invitation = room.client.getComponent(); + final bool supportsPinnedMessages = + room.getComponent() != null; + actions = [ if (invitation != null) RoomQuickAccessMenuEntry( @@ -24,6 +28,11 @@ class RoomQuickAccessMenu { builder: (context) => SendInvitationWidget(room, invitation), title: "Invite"), icon: Icons.person_add), + if (supportsPinnedMessages) + RoomQuickAccessMenuEntry( + name: "Pinned Messages", + action: (context) => EventBus.openPinnedMessages.add(null), + icon: Icons.push_pin), if (canSearch) RoomQuickAccessMenuEntry( name: "Search", diff --git a/commet/lib/ui/organisms/room_side_panel/room_side_panel.dart b/commet/lib/ui/organisms/room_side_panel/room_side_panel.dart index 5a299b0d..41f30b9d 100644 --- a/commet/lib/ui/organisms/room_side_panel/room_side_panel.dart +++ b/commet/lib/ui/organisms/room_side_panel/room_side_panel.dart @@ -5,6 +5,7 @@ import 'package:commet/ui/atoms/scaled_safe_area.dart'; import 'package:commet/ui/organisms/chat/chat.dart'; import 'package:commet/ui/organisms/room_event_search/room_event_search_widget.dart'; import 'package:commet/ui/organisms/room_members_list/room_members_list.dart'; +import 'package:commet/ui/organisms/room_pinned_messages/room_pinned_messages_widget.dart'; import 'package:commet/ui/organisms/room_quick_access_menu/room_quick_access_menu_mobile.dart'; import 'package:commet/ui/pages/main/main_page.dart'; import 'package:commet/utils/event_bus.dart'; @@ -12,11 +13,7 @@ import 'package:flutter/material.dart'; import 'package:tiamat/atoms/tile.dart'; import 'package:tiamat/tiamat.dart' as tiamat; -enum SidePanelState { - defaultView, - thread, - search, -} +enum SidePanelState { defaultView, thread, search, pinnedMessages } class RoomSidePanel extends StatefulWidget { const RoomSidePanel({required this.state, this.builder, super.key}); @@ -43,6 +40,7 @@ class _RoomSidePanelState extends State { EventBus.openThread.stream.listen(onOpenThreadSignal), EventBus.closeThread.stream.listen(onCloseThreadSignal), EventBus.startSearch.stream.listen(onStartSearch), + EventBus.openPinnedMessages.stream.listen(onShowPinnedMessages), ]; super.initState(); } @@ -104,6 +102,8 @@ class _RoomSidePanelState extends State { return buildThread(); case SidePanelState.search: return buildSearch(); + case SidePanelState.pinnedMessages: + return buildPinnedMessages(); } } @@ -197,7 +197,33 @@ class _RoomSidePanelState extends State { void onStartSearch(void event) { setState(() { - state = SidePanelState.search; + if (state == SidePanelState.search) { + state = SidePanelState.defaultView; + } else { + state = SidePanelState.search; + } + }); + } + + void onShowPinnedMessages(void event) { + setState(() { + if (state == SidePanelState.pinnedMessages) { + state = SidePanelState.defaultView; + } else { + state = SidePanelState.pinnedMessages; + } }); } + + Widget buildPinnedMessages() { + return SizedBox( + width: Layout.desktop ? 300 : null, + child: RoomPinnedMessagesWidget( + room: widget.state.currentRoom!, + onEventClicked: (eventId) { + EventBus.jumpToEvent.add(eventId); + EventBus.focusTimeline.add(null); + }, + )); + } } diff --git a/commet/lib/utils/event_bus.dart b/commet/lib/utils/event_bus.dart index 4cf07c4d..5bd03607 100644 --- a/commet/lib/utils/event_bus.dart +++ b/commet/lib/utils/event_bus.dart @@ -28,6 +28,9 @@ class EventBus { static StreamController startSearch = StreamController.broadcast(); + static StreamController openPinnedMessages = + StreamController.broadcast(); + static StreamController jumpToEvent = StreamController.broadcast(); static StreamController focusTimeline = StreamController.broadcast(); diff --git a/commet/pubspec.lock b/commet/pubspec.lock index cd39afbd..7631840d 100644 --- a/commet/pubspec.lock +++ b/commet/pubspec.lock @@ -925,7 +925,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: "59b86f5b420df8a45671d6c1db08242a9fe206f3" + resolved-ref: e2ba813dfb8541e9309316373dcf74993a34559f url: "https://github.com/commetchat/matrix-dart-sdk-drift-db.git" source: git version: "0.0.1"