diff --git a/gomuks b/gomuks index a65299d..ba82b6e 160000 --- a/gomuks +++ b/gomuks @@ -1 +1 @@ -Subproject commit a65299d327e3a533e6c476b0450b09be91fbb827 +Subproject commit ba82b6e31c87c99766a8366a72a9f07d643b323f diff --git a/lib/controllers/client.dart b/lib/controllers/client.dart index 9a6cf67..78670ea 100644 --- a/lib/controllers/client.dart +++ b/lib/controllers/client.dart @@ -8,7 +8,6 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:ffi/ffi.dart"; import "package:flutter/cupertino.dart"; import "package:intl/intl.dart"; -import "package:nexus/controllers/settings.dart"; import "package:nexus/helpers/extensions/gomuks_buffer.dart"; import "package:nexus/models/capabilities.dart"; import "package:nexus/models/content/message.dart"; @@ -107,50 +106,39 @@ class ClientController extends AsyncNotifier { return handle; } - /// Calls gomuks with [data] as JSON, returning its decoded response. Future callGomuksMethod( Map data, - void Function( - int handle, - GomuksOwnedBuffer data, - Pointer> callback, - ) - call, + FutureOr Function(int handle, GomuksBorrowedBuffer data) + callback, ) async { - final handle = await future; - final buffer = data.toGomuksOwnedBuffer(); - - final completer = Completer(); - late final NativeCallable callable; - callable = .listener((GomuksResponse response) { - callable.close(); - try { - final json = response.buf.toJsonAndFree(); - - if (response.command.cast().toDartString() == "error") { - throw GomuksException(json.toString()); - } - - completer.complete(json); - } catch (error, stackTrace) { - completer.completeError(error, stackTrace); - } - }); + final bufferPointer = data.toGomuksBufferPtr(); try { - call(handle, buffer, callable.nativeFunction); - } catch (_) { - // gomuks never got the call, so it won't respond or free the buffer - callable.close(); - GomuksFreeBuffer(buffer); - rethrow; - } + final handle = await future; - return completer.future; + final response = await Isolate.run( + () => callback(handle, bufferPointer.ref), + ); + + final json = response.buf.toJsonAndFree(); + + if (response.command.cast().toDartString() == "error") { + throw GomuksException(json.toString()); + } + + return json; + } finally { + calloc + ..free(bufferPointer.ref.base) + ..free(bufferPointer); + } } Future handlePush(Map data) async { - final response = await callGomuksMethod(data, GomuksHandlePushAsync); + final response = await callGomuksMethod( + data, + (handle, data) async => GomuksHandlePush(handle, data), + ); return response?["notification"] == null ? null @@ -160,12 +148,11 @@ class ClientController extends AsyncNotifier { Future _sendCommand( String command, [ Map data = const {}, - ]) => callGomuksMethod(data, (handle, data, callback) { + ]) => callGomuksMethod(data, (handle, data) { final commandPointer = command.toNativeUtf8().cast(); try { - GomuksSubmitCommandAsync(handle, commandPointer, data, callback); + return GomuksSubmitCommand(handle, commandPointer, data); } finally { - // gomuks copies the command name before returning calloc.free(commandPointer); } }); @@ -345,14 +332,9 @@ class ClientController extends AsyncNotifier { if (event == null) return; - // Private until settings have loaded, in case the user turned sending off - final sendPublic = - ref.read(SettingsController.provider).value?.readReceipts == - .showAndSend; - await _sendCommand("mark_read", { "room_id": room.metadata!.id, - "receipt_type": sendPublic ? "m.read" : "m.read.private", + "receipt_type": "m.read", "event_id": event.eventId, }); } diff --git a/lib/controllers/read_receipts.dart b/lib/controllers/read_receipts.dart deleted file mode 100644 index 15f76aa..0000000 --- a/lib/controllers/read_receipts.dart +++ /dev/null @@ -1,90 +0,0 @@ -import "package:collection/collection.dart"; -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/controllers/client_state.dart"; -import "package:nexus/controllers/room_chat.dart"; -import "package:nexus/controllers/rooms.dart"; -import "package:nexus/controllers/settings.dart"; -import "package:nexus/helpers/extensions/is_shown_as_message.dart"; -import "package:nexus/models/read_receipt.dart"; - -/// Other users' read receipts in a chat, by the event ID of the message to -/// show them under: the nearest message at or before what they've read. -class ReadReceiptsController(final ChatParams chat) - extends Notifier>> { - @override - IMap> build() { - final shouldShow = ref.watch( - SettingsController.provider.select( - (settings) => settings.value?.readReceipts != .neither, - ), - ); - if (!shouldShow) return const .empty(); - - final userId = ref.watch( - ClientStateController.provider.select((state) => state?.userId), - ); - // The chat's own timeline, so receipts follow what's actually shown - final timeline = ref.watch( - RoomChatController.provider(chat) - .select((roomChat) => roomChat.value?.timeline), - ); - final room = ref.watch( - RoomsController.provider.select( - (rooms) => switch (rooms[chat.roomId]) { - final room? => (events: room.events, receipts: room.receipts), - null => null, - }, - ), - ); - if (userId == null || timeline == null || room == null) { - return const .empty(); - } - - final events = timeline.map((rowId) => room.events[rowId]).nonNulls; - final indexes = IMap.fromEntries( - events.indexed.map((entry) => MapEntry(entry.$2.eventId, entry.$1)), - ); - final messages = events.indexed - .where((entry) => entry.$2.isShownAsMessage) - .toIList(); - final messageIndexes = messages.map((entry) => entry.$1).toList(); - - return room.receipts.values.flattened - .where( - (receipt) => - receipt.userId != userId && - // Receipts in threads are for the thread, not the timeline - receipt.thread == "main", - ) - .map( - (receipt) => switch (indexes[receipt.eventId]) { - final index? => ( - receipt: receipt, - message: lowerBound(messageIndexes, index + 1) - 1, - ), - null => null, - }, - ) - .nonNulls - .where((entry) => entry.message >= 0) - .groupListsBy((entry) => messages[entry.message].$2.eventId) - .map( - (eventId, entries) => MapEntry( - eventId, - entries - .map((entry) => entry.receipt) - .sorted((a, b) => b.timestamp.compareTo(a.timestamp)) - .toIList(), - ), - ) - .toIMap(); - } - - static final provider = NotifierProvider.family - .autoDispose< - ReadReceiptsController, - IMap>, - ChatParams - >(ReadReceiptsController.new); -} diff --git a/lib/controllers/room_chat.dart b/lib/controllers/room_chat.dart index b8b688d..4ed6462 100644 --- a/lib/controllers/room_chat.dart +++ b/lib/controllers/room_chat.dart @@ -156,7 +156,6 @@ class RoomChatController(final ChatParams info) valueMapper: (event) => event, ), hasMore: response.hasMore, - receipts: response.receipts, timeline: IMap.fromIterable( response.events, keyMapper: (event) => event.timelineRowId, diff --git a/lib/controllers/rooms.dart b/lib/controllers/rooms.dart index f846718..8b740c6 100644 --- a/lib/controllers/rooms.dart +++ b/lib/controllers/rooms.dart @@ -2,7 +2,6 @@ import "package:collection/collection.dart"; import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/models/event.dart"; -import "package:nexus/models/read_receipt.dart"; import "package:nexus/models/room.dart"; class RoomsController extends Notifier> { @@ -34,26 +33,6 @@ class RoomsController extends Notifier> { .new(), ); - static IMap> mergeReceipts( - IMap> existing, - IMap> incoming, - ) => incoming.isEmpty - ? existing - : existing.values.flattened - .followedBy(incoming.values.flattened) - .groupListsBy((receipt) => (receipt.userId, receipt.thread)) - .values - .map( - (receipts) => receipts.reduce( - (newest, receipt) => newest.timestamp.isAfter(receipt.timestamp) - ? newest - : receipt, - ), - ) - .groupListsBy((receipt) => receipt.eventId) - .map((eventId, receipts) => MapEntry(eventId, receipts.toIList())) - .toIMap(); - /// Adds events to their rooms, without changing the rooms' timelines. void storeEvents(Iterable events) => update( .new( @@ -117,11 +96,15 @@ class RoomsController extends Notifier> { timeline: incoming.reset ? incoming.timeline : existing.timeline.addAll(incoming.timeline), - receipts: mergeReceipts(existing.receipts, incoming.receipts), + receipts: incoming.receipts.entries.fold( + existing.receipts, + (receiptAcc, event) => receiptAcc.add( + event.key, + (receiptAcc[event.key] ?? .new()).addAll(event.value), + ), + ), ) ?? - incoming.copyWith( - receipts: mergeReceipts(const .empty(), incoming.receipts), - ), + incoming, ); }); diff --git a/lib/controllers/settings_sections.dart b/lib/controllers/settings_sections.dart index 9a437d8..596fc95 100644 --- a/lib/controllers/settings_sections.dart +++ b/lib/controllers/settings_sections.dart @@ -19,9 +19,8 @@ import "package:nexus/controllers/unified_push_distributors.dart"; import "package:nexus/helpers/extensions/distributor_name.dart"; import "package:nexus/models/account_data.dart"; import "package:nexus/models/room_order.dart"; -import "package:nexus/models/settings.dart"; import "package:nexus/models/settings_category.dart"; -import "package:nexus/models/show_and_send.dart"; +import "package:nexus/models/typing_indicators.dart"; import "package:nexus/main.dart"; import "package:nexus/widgets/settings/dialog_list_tile.dart"; import "package:nexus/widgets/reset_encryption_sheet.dart"; @@ -33,30 +32,6 @@ class SettingsSectionsController Future>> build() async { final settings = await ref.watch(SettingsController.provider.future); - /// A tile for a [ShowAndSend] setting, saving the settings from [update]. - Widget showAndSendTile( - String title, - String description, - IconData icon, { - required ShowAndSend value, - required Settings Function(ShowAndSend value) update, - }) => DialogListTile( - icon: Icon(icon), - title: title, - subtitle: Text(description), - value: value, - options: ShowAndSend.values, - getName: (option) => switch (option) { - .showAndSend => "Show and send", - .show => "Only show", - .neither => "Neither", - }, - onChanged: (value) => ref - .read(SettingsController.provider.notifier) - .set(update(value)) - .onError(showError), - ); - return .new({ "General": .new([ .new( @@ -217,25 +192,23 @@ class SettingsSectionsController title: "Typing Indicators", description: "Show when others are typing, and let others see when you're typing.", icon: Icons.keyboard, - builder: (title, description, icon) => showAndSendTile( - title, - description, - icon, - value: settings.typingIndicators, - update: (value) => settings.copyWith(typingIndicators: value), - ), - ), - .new( - title: "Read Receipts", - description: "Show how far others have read, and let others see what you've read. When not sent, read receipts are still sent privately, so your other devices know what you've read.", - icon: Icons.done_all, - builder: (title, description, icon) => showAndSendTile( - title, - description, - icon, - value: settings.readReceipts, - update: (value) => settings.copyWith(readReceipts: value), - ), + builder: (title, description, icon) => + DialogListTile( + icon: Icon(icon), + title: title, + subtitle: Text(description), + value: settings.typingIndicators, + options: TypingIndicators.values, + getName: (option) => switch (option) { + .showAndSend => "Show and send", + .show => "Only show", + .neither => "Neither", + }, + onChanged: (value) => ref + .read(SettingsController.provider.notifier) + .set(settings.copyWith(typingIndicators: value)) + .onError(showError), + ), ), ]), ), diff --git a/lib/helpers/extensions/gomuks_buffer.dart b/lib/helpers/extensions/gomuks_buffer.dart index 57425be..10a273a 100644 --- a/lib/helpers/extensions/gomuks_buffer.dart +++ b/lib/helpers/extensions/gomuks_buffer.dart @@ -19,21 +19,19 @@ extension GomuksOwnedBufferToX on GomuksOwnedBuffer { } extension JsonToGomuksBuffer on Map { - GomuksOwnedBuffer toGomuksOwnedBuffer() { - final bytes = utf8.encode(json.encode(this)); - final buffer = GomuksAllocBuffer(bytes.length); - buffer.base.asTypedList(bytes.length).setAll(0, bytes); - return buffer; - } - Pointer toGomuksBufferPtr() { - final bytes = utf8.encode(json.encode(this)); + final jsonString = json.encode(this); + final bytes = utf8.encode(jsonString); final dataPtr = calloc(bytes.length); dataPtr.asTypedList(bytes.length).setAll(0, bytes); - return calloc() - ..ref.base = dataPtr - ..ref.length = bytes.length; + final ptr = calloc(); + + ptr.ref + ..base = dataPtr + ..length = bytes.length; + + return ptr; } } diff --git a/lib/helpers/extensions/is_shown_as_message.dart b/lib/helpers/extensions/is_shown_as_message.dart deleted file mode 100644 index a78810b..0000000 --- a/lib/helpers/extensions/is_shown_as_message.dart +++ /dev/null @@ -1,15 +0,0 @@ -import "package:nexus/models/content/encrypted.dart"; -import "package:nexus/models/content/message.dart"; -import "package:nexus/models/content/sticker.dart"; -import "package:nexus/models/event.dart"; - -extension IsShownAsMessage on Event { - /// Whether this is shown in the timeline as a message, with MessageRenderer. - bool get isShownAsMessage => - redactedBy == null && - relationType != "m.replace" && - switch (content) { - MessageContent() || EncryptedContent() || StickerContent() => true, - _ => false, - }; -} diff --git a/lib/models/paginate.dart b/lib/models/paginate.dart index b7c6bf8..dc6eede 100644 --- a/lib/models/paginate.dart +++ b/lib/models/paginate.dart @@ -1,7 +1,6 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:freezed_annotation/freezed_annotation.dart"; import "package:nexus/models/event.dart"; -import "package:nexus/models/read_receipt.dart"; part "paginate.freezed.dart"; part "paginate.g.dart"; @@ -11,9 +10,6 @@ part "paginate.g.dart"; class const Paginate({ required final IList events, required final IList relatedEvents, - - /// Read receipts for the events, by event ID. - final IMap> receipts = const .empty(), required final bool hasMore, }) with _$Paginate { Map toJson() => _$PaginateToJson(this); diff --git a/lib/models/read_receipt.dart b/lib/models/read_receipt.dart index 4d62635..ab4edd8 100644 --- a/lib/models/read_receipt.dart +++ b/lib/models/read_receipt.dart @@ -13,11 +13,6 @@ class const ReadReceipt({ required final String eventId, @EpochDateTimeConverter() required final DateTime timestamp, }) with _$ReadReceipt { - String get thread => switch (threadId) { - final threadId? when threadId.isNotEmpty => threadId, - _ => "main", - }; - Map toJson() => _$ReadReceiptToJson(this); factory ReadReceipt.fromJson(Map json) => diff --git a/lib/models/settings.dart b/lib/models/settings.dart index 7ea61d9..e1e1bbc 100644 --- a/lib/models/settings.dart +++ b/lib/models/settings.dart @@ -1,7 +1,7 @@ import "package:material_ui/material_ui.dart"; import "package:freezed_annotation/freezed_annotation.dart"; import "package:nexus/models/room_order.dart"; -import "package:nexus/models/show_and_send.dart"; +import "package:nexus/models/typing_indicators.dart"; part "settings.freezed.dart"; part "settings.g.dart"; @@ -17,11 +17,7 @@ class const Settings({ final bool swipeToReply = false, final bool hideMembershipEvents = false, final RoomOrder roomOrder = .unreadFirst, - final ShowAndSend typingIndicators = .showAndSend, - - /// When not sending, read receipts are still sent privately, so that the - /// user's other devices know what has been read. - final ShowAndSend readReceipts = .showAndSend, + final TypingIndicators typingIndicators = .showAndSend, }) with _$Settings { Map toJson() => _$SettingsToJson(this); diff --git a/lib/models/show_and_send.dart b/lib/models/show_and_send.dart deleted file mode 100644 index f9663cc..0000000 --- a/lib/models/show_and_send.dart +++ /dev/null @@ -1,2 +0,0 @@ -/// Whether to show something from others, and send it to them, e.g. typing indicators or read receipts. -enum ShowAndSend { showAndSend, show, neither } diff --git a/lib/models/typing_indicators.dart b/lib/models/typing_indicators.dart new file mode 100644 index 0000000..28df4db --- /dev/null +++ b/lib/models/typing_indicators.dart @@ -0,0 +1 @@ +enum TypingIndicators { showAndSend, show, neither } diff --git a/lib/widgets/avatar_ring.dart b/lib/widgets/avatar_ring.dart deleted file mode 100644 index f410978..0000000 --- a/lib/widgets/avatar_ring.dart +++ /dev/null @@ -1,39 +0,0 @@ -import "package:material_ui/material_ui.dart"; - -class const AvatarRing({ - required final String tooltip, - required final VoidCallback onTap, - required final Widget child, - final double dimension = 24, - super.key, -}) extends StatelessWidget { - static const ringWidth = 1.5; - - @override - Widget build(BuildContext context) => Tooltip( - message: tooltip, - child: Container( - padding: .all(ringWidth), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - shape: .circle, - ), - child: ClipOval( - child: SizedBox.square( - dimension: dimension, - child: Stack( - fit: .expand, - children: [ - child, - // Over the child, so the ripple shows on top of avatars - Material( - type: .transparency, - child: InkWell(onTap: onTap), - ), - ], - ), - ), - ), - ), - ); -} diff --git a/lib/widgets/read_receipts_row.dart b/lib/widgets/read_receipts_row.dart deleted file mode 100644 index e3f206f..0000000 --- a/lib/widgets/read_receipts_row.dart +++ /dev/null @@ -1,112 +0,0 @@ -import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:material_ui/material_ui.dart"; -import "package:nexus/controllers/read_receipts.dart"; -import "package:nexus/controllers/room_chat.dart"; -import "package:nexus/controllers/user.dart"; -import "package:nexus/helpers/extensions/get_localpart.dart"; -import "package:nexus/helpers/extensions/show_adaptive_sheet.dart"; -import "package:nexus/helpers/extensions/show_user_popover.dart"; -import "package:nexus/widgets/avatar_or_hash.dart"; -import "package:nexus/widgets/avatar_ring.dart"; -import "package:nexus/widgets/read_receipts_sheet.dart"; - -class const ReadReceiptsRow( - final String eventId, { - required final ChatParams chat, - super.key, -}) extends ConsumerWidget { - static const avatarSize = 24.0; - static const overlap = 6.0; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final theme = Theme.of(context); - final receipts = ref.watch( - ReadReceiptsController.provider(chat) - .select((receipts) => receipts[eventId] ?? const .empty()), - ); - - return AnimatedSize( - duration: Durations.medium2, - curve: Curves.easeInOutCubicEmphasized, - alignment: .topRight, - child: receipts.isEmpty - ? const SizedBox(width: .infinity) - : Padding( - // Clear of the scrollbar - padding: .only(right: 16, bottom: 2), - child: LayoutBuilder( - builder: (context, constraints) { - const width = avatarSize + AvatarRing.ringWidth * 2; - final fits = - (constraints.maxWidth - width) ~/ (width - overlap) + 1; - - // If they don't all fit, the last spot shows how many more there are - final shown = receipts.length <= fits - ? receipts.length - : fits - 1; - - return Align( - alignment: .centerRight, - child: Wrap( - spacing: -overlap, - children: [ - ...receipts - .take(shown) - .map( - (receipt) => Consumer( - builder: (context, ref, _) { - final member = ref - .watch( - UserController.provider( - .new( - roomId: chat.roomId, - userId: receipt.userId, - ), - ), - ) - .value; - final name = - member?.displayName ?? - receipt.userId.localpart; - - return AvatarRing( - tooltip: name, - onTap: () => context.showUserPopover( - receipt.userId, - roomId: chat.roomId, - ), - child: AvatarOrHash( - member?.avatarUrl, - name, - dimension: avatarSize, - ), - ); - }, - ), - ), - if (receipts.length > shown) - AvatarRing( - tooltip: "Show everyone who has read up to here", - onTap: () => context.showAdaptiveSheet( - (_) => ReadReceiptsSheet(eventId, chat: chat), - ), - child: ColoredBox( - color: theme.colorScheme.surfaceContainerHighest, - child: Center( - child: Text( - "+${receipts.length - shown}", - style: theme.textTheme.labelSmall, - ), - ), - ), - ), - ], - ), - ); - }, - ), - ), - ); - } -} diff --git a/lib/widgets/read_receipts_sheet.dart b/lib/widgets/read_receipts_sheet.dart deleted file mode 100644 index 60793c8..0000000 --- a/lib/widgets/read_receipts_sheet.dart +++ /dev/null @@ -1,55 +0,0 @@ -import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:material_ui/material_ui.dart"; -import "package:nexus/controllers/read_receipts.dart"; -import "package:nexus/controllers/room_chat.dart"; -import "package:nexus/controllers/user.dart"; -import "package:nexus/helpers/extensions/get_localpart.dart"; -import "package:nexus/helpers/extensions/show_user_popover.dart"; -import "package:nexus/widgets/avatar_or_hash.dart"; -import "package:nexus/widgets/sheet_layout.dart"; - -/// Everyone who has read up to an event, kept up to date while open. -class const ReadReceiptsSheet( - final String eventId, { - required final ChatParams chat, - super.key, -}) extends ConsumerWidget { - @override - Widget build(BuildContext context, WidgetRef ref) => SheetLayout( - title: Text("Read by"), - actions: [ - TextButton(onPressed: Navigator.of(context).pop, child: Text("Close")), - ], - children: ref - .watch( - ReadReceiptsController.provider(chat) - .select((receipts) => receipts[eventId] ?? const .empty()), - ) - .map( - (receipt) => Consumer( - builder: (context, ref, _) { - final member = ref - .watch( - UserController.provider( - .new(roomId: chat.roomId, userId: receipt.userId), - ), - ) - .value; - final name = member?.displayName ?? receipt.userId.localpart; - - return ListTile( - contentPadding: .zero, - leading: AvatarOrHash(member?.avatarUrl, name, dimension: 40), - title: Text(name, overflow: .ellipsis), - subtitle: Text(receipt.userId, overflow: .ellipsis), - onTap: () => context.showUserPopover( - receipt.userId, - roomId: chat.roomId, - ), - ); - }, - ), - ) - .toList(), - ); -} diff --git a/lib/widgets/renderers/event.dart b/lib/widgets/renderers/event.dart index 47750fa..d1be9dd 100644 --- a/lib/widgets/renderers/event.dart +++ b/lib/widgets/renderers/event.dart @@ -7,26 +7,27 @@ import "package:nexus/controllers/event.dart"; import "package:nexus/controllers/room_chat.dart"; import "package:nexus/controllers/rooms.dart"; import "package:nexus/controllers/settings.dart"; -import "package:nexus/helpers/extensions/is_shown_as_message.dart"; import "package:nexus/helpers/extensions/show_adaptive_menu.dart"; import "package:nexus/helpers/hooks/chat_scroll.dart"; import "package:nexus/models/content/avatar.dart"; import "package:nexus/models/content/canonical_alias.dart"; import "package:nexus/models/content/content.dart"; import "package:nexus/models/content/create.dart"; +import "package:nexus/models/content/encrypted.dart"; import "package:nexus/models/content/history_visibility.dart"; import "package:nexus/models/content/join_rules.dart"; import "package:nexus/models/content/membership.dart"; +import "package:nexus/models/content/message.dart"; import "package:nexus/models/content/pinned_events.dart"; import "package:nexus/models/content/power_levels.dart"; import "package:nexus/models/content/server_acl.dart"; +import "package:nexus/models/content/sticker.dart"; import "package:nexus/models/content/topic.dart"; import "package:nexus/models/event.dart"; import "package:nexus/widgets/error_dialog.dart"; import "package:nexus/widgets/lazy_loading/message_displayname.dart"; import "package:nexus/widgets/renderers/message.dart"; import "package:nexus/widgets/reaction_row.dart"; -import "package:nexus/widgets/read_receipts_row.dart"; import "package:nexus/widgets/renderers/membership.dart"; import "package:nexus/widgets/renderers/generic_event.dart"; import "package:nexus/widgets/swipe_to_reply.dart"; @@ -43,10 +44,6 @@ class const EventRenderer( /// If set, events can be swiped, calling this final VoidCallback? onSwipe, - - /// If set, shows who in this chat has read up to this event, e.g. in the - /// timeline. - final ChatParams? readReceiptsChat, super.key, }) extends HookConsumerWidget { @override @@ -99,7 +96,9 @@ class const EventRenderer( } final isGrouped = - (previousEvent?.isShownAsMessage ?? false) && + previousEvent?.content is MessageContent && + previousEvent?.redactedBy == null && + previousEvent?.relationType != "m.replace" && event.sender == previousEvent?.sender && event.pmp?.id == previousEvent?.pmp?.id; @@ -112,26 +111,7 @@ class const EventRenderer( TextSpan(text: text), ]); - final child = event.isShownAsMessage - ? MessageRenderer( - event, - onTapReply: jumpToEvent == null - ? null - : () async { - final replyEvent = await ref.read( - EventController.provider( - .new(roomId: roomId, eventId: event.replyTo!), - ).future, - ); - if (replyEvent != null) { - await jumpToEvent!(replyEvent.rowId); - } - }, - isGrouped: isGrouped, - maxLines: maxLines, - textOnly: textOnly, - ) - : event.redactedBy != null || event.relationType == "m.replace" + final child = event.redactedBy != null || event.relationType == "m.replace" ? null : switch (event.content) { Content(:final parseError?, :final stackTrace) => Row( @@ -143,6 +123,27 @@ class const EventRenderer( ], ), + MessageContent() || + EncryptedContent() || + StickerContent() => MessageRenderer( + event, + onTapReply: jumpToEvent == null + ? null + : () async { + final replyEvent = await ref.read( + EventController.provider( + .new(roomId: roomId, eventId: event.replyTo!), + ).future, + ); + if (replyEvent != null) { + await jumpToEvent!(replyEvent.rowId); + } + }, + isGrouped: isGrouped, + maxLines: maxLines, + textOnly: textOnly, + ), + MembershipContent content => switch (event.previousContent) { MembershipContent(:final status) => status == content.status @@ -284,10 +285,6 @@ class const EventRenderer( ...[ if (child is! MessageRenderer) ReactionRow(event), - // Outside the message itself, so it doesn't move when swiped - if (readReceiptsChat case final chat? when event.isShownAsMessage) - ReadReceiptsRow(event.eventId, chat: chat), - if (event.sendError != null && event.sendError != "not sent") Padding( padding: .only(bottom: 4), diff --git a/lib/widgets/room_chat/chat_timeline.dart b/lib/widgets/room_chat/chat_timeline.dart index c949344..2de327c 100644 --- a/lib/widgets/room_chat/chat_timeline.dart +++ b/lib/widgets/room_chat/chat_timeline.dart @@ -1,6 +1,5 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:material_ui/material_ui.dart"; -import "package:nexus/controllers/room_chat.dart"; import "package:nexus/helpers/hooks/chat_scroll.dart"; import "package:nexus/models/event.dart"; import "package:nexus/widgets/renderers/event.dart"; @@ -8,7 +7,7 @@ import "package:nexus/widgets/highlight_wrapper.dart"; import "package:super_sliver_list/super_sliver_list.dart"; class const ChatTimeline({ - required final ChatParams chat, + required final String roomId, required final ChatScroll scroll, required final JumpToEvent jumpToEvent, required final IList Function(Event) getEventOptions, @@ -28,11 +27,10 @@ class const ChatTimeline({ EventRenderer( eventRowId, previousEventRowId: previousEventRowId, - roomId: chat.roomId, + roomId: roomId, jumpToEvent: jumpToEvent, getEventOptions: getEventOptions, onSwipe: onSwipe == null ? null : () => onSwipe!(eventRowId), - readReceiptsChat: chat, ), key: key, isHighlighted: highlightedEvent == eventRowId, diff --git a/lib/widgets/room_chat/room_chat.dart b/lib/widgets/room_chat/room_chat.dart index dd231f2..7e807da 100644 --- a/lib/widgets/room_chat/room_chat.dart +++ b/lib/widgets/room_chat/room_chat.dart @@ -85,11 +85,10 @@ final class const RoomChat({ ), ); - final ChatParams chat = ( + final controllerProvider = RoomChatController.provider(( roomId: roomId, contextualEvent: contextualEvent.value, - ); - final controllerProvider = RoomChatController.provider(chat); + )); final notifier = ref.watch(controllerProvider.notifier); final client = ref.read(ClientController.provider.notifier); @@ -247,7 +246,7 @@ final class const RoomChat({ AsyncLoading(value: _?) || AsyncData(value: _) => ChatTimeline( scroll: scroll, - chat: chat, + roomId: roomId, jumpToEvent: jumpToEvent, getEventOptions: getEventOptions, onSwipe: swipeToReply && canReply