Make RoomChatController only return IList of eventRowId

Fixes #102
This commit is contained in:
Henry Hiles 2026-09-28 12:46:00 -04:00
commit a220e27877
Signed by: Henry-Hiles
SSH key fingerprint: SHA256:VKQUdS31Q90KvX7EkKMHMBpUspcmItAh86a+v7PGiIs
13 changed files with 193 additions and 143 deletions

View file

@ -24,7 +24,12 @@ class const EventPreview(final Event event, {super.key})
runSpacing: 2,
children: [
if (event.content is MessageContent) MessageDisplayname(event),
EventRenderer(event, textOnly: true, maxLines: 1),
EventRenderer(
event.rowId,
roomId: event.roomId,
textOnly: true,
maxLines: 1,
),
],
),
),

View file

@ -1,7 +1,7 @@
import "package:material_ui/material_ui.dart";
import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:nexus/controllers/init_complete.dart";
import "package:nexus/controllers/jump_to_event.dart";
import "package:nexus/controllers/contextual_event.dart";
import "package:nexus/controllers/key.dart";
import "package:nexus/widgets/appbar.dart";
import "package:nexus/widgets/sidebar.dart";
@ -30,7 +30,7 @@ class const ChatPage({super.key}) extends HookConsumerWidget {
child: Consumer(
builder: (context, ref, _) {
final initialHighlight = ref.watch(
JumpToEventController.provider(roomId),
ContextualEventController.provider(roomId),
);
return RoomChat(
key: ValueKey((roomId, initialHighlight)),

View file

@ -5,7 +5,7 @@ import "package:m3e_buttons/m3e_buttons.dart";
import "package:material_ui/material_ui.dart";
import "package:flutter_hooks/flutter_hooks.dart";
import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:nexus/controllers/jump_to_event.dart";
import "package:nexus/controllers/contextual_event.dart";
import "package:nexus/controllers/key.dart";
import "package:nexus/controllers/notifications.dart";
import "package:nexus/controllers/spaces.dart";
@ -168,18 +168,21 @@ class const NotificationsPage({
.set(event.roomId);
ref
.watch(
JumpToEventController.provider(
ContextualEventController.provider(
event.roomId,
).notifier,
)
.set(event.eventId);
.set(event);
if (context.mounted) {
Navigator.of(context).pop();
}
},
child: IgnorePointer(
child: EventRenderer(event),
child: EventRenderer(
event.rowId,
roomId: event.roomId,
),
),
),
isHighlighted: isHighlighted,

View file

@ -10,7 +10,7 @@ import "package:nexus/widgets/renderers/event.dart";
final class const PinnedEventsDrawer(
final String roomId, {
required final IList<PopupMenuEntry> Function(Event event) getEventOptions,
required final Future<void> Function(String eventId) jumpToId,
required final Future<void> Function(Event event) jumpToEvent,
super.key,
}) extends HookConsumerWidget {
@override
@ -62,15 +62,15 @@ final class const PinnedEventsDrawer(
borderRadius: .circular(12),
onTap: () {
Navigator.of(context).pop();
jumpToId(event.eventId);
jumpToEvent(event);
},
child: Padding(
padding: .symmetric(vertical: 4),
child: IgnorePointer(
child: EventRenderer(
event,
event.rowId,
roomId: roomId,
maxLines: 2,
isGrouped: false,
getEventOptions: getEventOptions,
),
),

View file

@ -8,6 +8,7 @@ import "package:nexus/helpers/mxc_image.dart";
import "package:nexus/models/event.dart";
import "package:nexus/widgets/error_dialog.dart";
import "package:nexus/main.dart";
import "package:fast_immutable_collections/fast_immutable_collections.dart";
class const ReactionRow(final Event event, {super.key}) extends ConsumerWidget {
@override
@ -21,20 +22,22 @@ class const ReactionRow(final Event event, {super.key}) extends ConsumerWidget {
.new(roomId: event.roomId, eventRowId: event.rowId),
),
)) {
AsyncData(value: final reactions) ||
AsyncLoading(value: final reactions?) => Wrap(
AsyncData(value: final IMap<String, IList<String>>? reactors) ||
AsyncLoading(value: final reactors) => Wrap(
spacing: 4,
runSpacing: 4,
children: reactions
.where((_, value) => value.isNotEmpty)
children: event.reactions
.where((_, value) => value != 0)
.mapTo(
(reaction, reactors) => HookBuilder(
(reaction, count) => HookBuilder(
builder: (context) {
final enabled = useState(true);
final selected = reactors.contains(clientState!.userId);
final selected =
reactors?[reaction]?.contains(clientState!.userId) ??
false;
return Tooltip(
message: reactors.join(", "),
message: reactors?[reaction]?.join(", ") ?? "",
child: ChoiceChip(
showCheckmark: false,
selected: selected,
@ -53,10 +56,7 @@ class const ReactionRow(final Event event, {super.key}) extends ConsumerWidget {
)
: Text(reaction, overflow: .ellipsis),
),
Text(
reactors.length.toString(),
overflow: .ellipsis,
),
Text(count.toString(), overflow: .ellipsis),
],
),
onSelected: enabled.value
@ -75,7 +75,7 @@ class const ReactionRow(final Event event, {super.key}) extends ConsumerWidget {
.removeReaction(
reaction,
event,
clientState.userId!,
clientState!.userId!,
)
.onError(showError);
} else {
@ -100,8 +100,6 @@ class const ReactionRow(final Event event, {super.key}) extends ConsumerWidget {
error,
stackTrace,
),
_ => SizedBox.shrink(),
},
);
}

View file

@ -3,6 +3,8 @@ import "package:flutter/gestures.dart";
import "package:material_ui/material_ui.dart";
import "package:flutter_hooks/flutter_hooks.dart";
import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:nexus/controllers/event.dart";
import "package:nexus/controllers/rooms.dart";
import "package:nexus/helpers/extensions/show_context_menu.dart";
import "package:nexus/models/content/avatar.dart";
import "package:nexus/models/content/canonical_alias.dart";
@ -28,11 +30,12 @@ import "package:nexus/widgets/renderers/generic_event.dart";
import "package:nexus/widgets/timestamp.dart";
class const EventRenderer(
final Event event, {
final int eventRowId, {
required final String roomId,
final int? previousEventRowId,
final bool textOnly = false,
final bool isGrouped = false,
final int? maxLines,
final VoidCallback? onTapReply,
final Future<void> Function(Event event)? jumpToEvent,
final IList<PopupMenuEntry> Function(Event event)? getEventOptions,
super.key,
}) extends HookConsumerWidget {
@ -44,6 +47,26 @@ class const EventRenderer(
final focusNode = useFocusNode();
useListenable(focusNode);
final (event, previousEvent) = ref.watch(
RoomsController.provider.select(
(value) => (
value[roomId]?.events[eventRowId],
previousEventRowId == null
? null
: value[roomId]?.events[previousEventRowId!],
),
),
);
if (event == null) return SizedBox.shrink();
final isGrouped =
previousEvent?.content is MessageContent &&
previousEvent?.redactedBy == null &&
previousEvent?.relationType != "m.replace" &&
event.sender == previousEvent?.sender &&
event.pmp?.id == previousEvent?.pmp?.id;
final child = event.redactedBy != null || event.relationType == "m.replace"
? null
: switch (event.content) {
@ -60,7 +83,16 @@ class const EventRenderer(
EncryptedContent() ||
StickerContent() => MessageRenderer(
event,
onTapReply: onTapReply,
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);
},
isGrouped: isGrouped,
maxLines: maxLines,
textOnly: textOnly,

View file

@ -1,43 +1,34 @@
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:material_ui/material_ui.dart";
import "package:nexus/helpers/hooks/chat_scroll.dart";
import "package:nexus/models/content/message.dart";
import "package:nexus/models/event.dart";
import "package:nexus/widgets/renderers/event.dart";
import "package:nexus/widgets/highlight_wrapper.dart";
import "package:super_sliver_list/super_sliver_list.dart";
class const ChatTimeline({
required final String roomId,
required final ChatScroll scroll,
required final Future<void> Function(String) jumpToId,
required final Future<void> Function(Event event) jumpToEvent,
required final IList<PopupMenuEntry> Function(Event) getEventOptions,
required final String? highlightedEvent,
required final int? highlightedEvent,
required final double composerHeight,
super.key,
}) extends StatelessWidget {
bool isGrouped(Event event, Event? previousEvent) =>
previousEvent?.content is MessageContent &&
previousEvent?.redactedBy == null &&
previousEvent?.relationType != "m.replace" &&
event.sender == previousEvent?.sender &&
event.pmp?.id == previousEvent?.pmp?.id;
Widget eventRow(
Event event,
Event? previousEvent, {
required Future<void> Function(String) jumpToId,
required IList<PopupMenuEntry> Function(Event) getEventOptions,
required String? highlightedEvent,
int eventRowId,
int? previousEventRowId, {
required Key key,
}) => HighlightWrapper(
EventRenderer(
event,
onTapReply: () => jumpToId(event.replyTo!),
eventRowId,
previousEventRowId: previousEventRowId,
roomId: roomId,
jumpToEvent: jumpToEvent,
getEventOptions: getEventOptions,
isGrouped: isGrouped(event, previousEvent),
),
key: key,
isHighlighted: highlightedEvent == event.eventId,
isHighlighted: highlightedEvent == eventRowId,
);
@override
@ -50,29 +41,23 @@ class const ChatTimeline({
SliverToBoxAdapter(child: SizedBox(height: composerHeight)),
SuperSliverList.builder(
itemCount: scroll.liveItems.length,
itemCount: scroll.liveRows.length,
itemBuilder: (_, index) => eventRow(
scroll.liveItems[index],
scroll.liveRows[index],
index > 0
? scroll.liveItems.getOrNull(index - 1)
: scroll.historyItems.firstOrNull,
jumpToId: jumpToId,
getEventOptions: getEventOptions,
highlightedEvent: highlightedEvent,
key: scroll.keyFor(scroll.liveItems[index].eventId),
? scroll.liveRows.getOrNull(index - 1)
: scroll.historyRows.firstOrNull,
key: scroll.keyFor(scroll.liveRows[index]),
),
),
SuperSliverList.builder(
key: scroll.centerKey,
itemCount: scroll.historyItems.length,
itemCount: scroll.historyRows.length,
itemBuilder: (_, index) => eventRow(
scroll.historyItems[index],
scroll.historyItems.getOrNull(index + 1),
jumpToId: jumpToId,
getEventOptions: getEventOptions,
highlightedEvent: highlightedEvent,
key: scroll.keyFor(scroll.historyItems[index].eventId),
scroll.historyRows[index],
scroll.historyRows.getOrNull(index + 1),
key: scroll.keyFor(scroll.historyRows[index]),
),
),
],

View file

@ -24,15 +24,15 @@ final class const RoomChat({
required final String? roomId,
required final bool isDesktop,
required final bool showMembersByDefault,
final String? initialHighlightedEvent,
final Event? initialHighlightedEvent,
super.key,
}) extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final relatedEvent = useState<Event?>(null);
final relationType = useState(RelationType.reply);
final contextualEvent = useState<String?>(initialHighlightedEvent);
final highlightedEvent = useState<String?>(null);
final contextualEvent = useState<Event?>(initialHighlightedEvent);
final highlightedEvent = useState<int?>(null);
final composerSize = useState<double>(64);
@ -63,7 +63,9 @@ final class const RoomChat({
final controllerProvider = RoomChatController.provider((
roomId,
contextualEvent.value,
contextualEvent.value == null
? null
: (contextualEvent.value!.rowId, contextualEvent.value!.eventId),
));
final notifier = ref.watch(controllerProvider.notifier);
@ -97,12 +99,14 @@ final class const RoomChat({
},
);
Future<void> jumpToId(String eventId) async {
highlightedEvent.value = eventId;
Future<void> jumpToEvent(Event event) async {
highlightedEvent.value = event.rowId;
await scroll.jumpToId(eventId);
await scroll.jumpToEvent(event);
await Future.delayed(.new(milliseconds: 700), () {
if (highlightedEvent.value == eventId) highlightedEvent.value = null;
if (highlightedEvent.value == event.rowId) {
highlightedEvent.value = null;
}
});
}
@ -113,7 +117,7 @@ final class const RoomChat({
if (!context.mounted) return;
if (scroll.scrollController.hasClients) {
jumpToId(initialHighlightedEvent!);
jumpToEvent(initialHighlightedEvent!);
} else {
WidgetsBinding.instance.addPostFrameCallback((_) => check());
}
@ -141,7 +145,7 @@ final class const RoomChat({
endDrawer: PinnedEventsDrawer(
roomId,
getEventOptions: getEventOptions,
jumpToId: jumpToId,
jumpToEvent: jumpToEvent,
),
body: Builder(
builder: (middleContext) => Scaffold(
@ -168,7 +172,8 @@ final class const RoomChat({
padding: .symmetric(horizontal: 4),
child: ChatTimeline(
scroll: scroll,
jumpToId: jumpToId,
roomId: roomId,
jumpToEvent: jumpToEvent,
getEventOptions: getEventOptions,
highlightedEvent: highlightedEvent.value,
composerHeight: composerSize.value,