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

@ -0,0 +1,17 @@
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/models/event.dart";
class ContextualEventController(String? _) extends Notifier<Event?> {
@override
Event? build() => null;
void set(Event? event) => state = event;
@override
bool updateShouldNotify(_, _) => true;
static final provider = NotifierProvider.family
.autoDispose<ContextualEventController, Event?, String?>(
ContextualEventController.new,
);
}

View file

@ -1,16 +0,0 @@
import "package:flutter_riverpod/flutter_riverpod.dart";
class JumpToEventController(String? _) extends Notifier<String?> {
@override
String? build() => null;
void set(String? eventId) => state = eventId;
@override
bool updateShouldNotify(_, _) => true;
static final provider = NotifierProvider.family
.autoDispose<JumpToEventController, String?, String?>(
JumpToEventController.new,
);
}

View file

@ -18,8 +18,9 @@ import "package:nexus/models/requests/send_message.dart";
import "package:nexus/models/room.dart"; import "package:nexus/models/room.dart";
import "package:nexus/models/room_chat.dart"; import "package:nexus/models/room_chat.dart";
class RoomChatController(final (String roomId, String? contextualEvent) info) class RoomChatController(
extends AsyncNotifier<RoomChat?> { final (String roomId, (int rowId, String eventId)? contextualEvent) info,
) extends AsyncNotifier<RoomChat?> {
@override @override
Future<RoomChat?> build() async { Future<RoomChat?> build() async {
final (roomId, eventId) = info; final (roomId, eventId) = info;
@ -37,29 +38,15 @@ class RoomChatController(final (String roomId, String? contextualEvent) info)
final timeline = room.timeline final timeline = room.timeline
.toEntryIList(compare: (a, b) => (a?.key ?? 0).compareTo(b?.key ?? 0)) .toEntryIList(compare: (a, b) => (a?.key ?? 0).compareTo(b?.key ?? 0))
.map((element) => element.value) .map((e) => e.value)
.toIList() .toIList()
.addAll(room.clientSticky) .addAll(room.clientSticky)
.map((entry) {
final foundEvent = entry == null ? null : room.events[entry];
final editedEvent =
foundEvent == null || foundEvent.lastEditRowId == 0
? null
: room.events[foundEvent.lastEditRowId];
return editedEvent == null
? foundEvent
: foundEvent?.copyWith(
content: editedEvent.content,
localContent: editedEvent.localContent,
);
})
.nonNulls .nonNulls
.toIList(); .toIList();
if (info.$2 == null || timeline.contains(info.$2!.$1)) {
if (info.$2 == null || timeline.map((e) => e.eventId).contains(info.$2)) { ref.watch(
ref.watch(RoomsController.provider.select((rooms) => rooms[roomId])); RoomsController.provider.select((rooms) => rooms[roomId]?.timeline),
);
return .new( return .new(
timeline: timeline, timeline: timeline,
@ -68,10 +55,25 @@ class RoomChatController(final (String roomId, String? contextualEvent) info)
); );
} else { } else {
final context = await client.getEventContext( final context = await client.getEventContext(
.new(roomId: roomId, eventId: info.$2!), .new(roomId: roomId, eventId: info.$2!.$2),
);
final events = context.before.add(context.event).addAll(context.after);
ref
.read(RoomsController.provider.notifier)
.update(
.new({
roomId: Room(
events: IMap.fromIterable(
events,
keyMapper: (event) => event.rowId,
),
),
}),
.new(),
); );
return .new( return .new(
timeline: context.before.add(context.event).addAll(context.after), timeline: .new(events.map((element) => element.rowId)),
hasMoreBackward: true, hasMoreBackward: true,
hasMoreForward: true, hasMoreForward: true,
historicalData: .new(start: context.start, end: context.end), historicalData: .new(start: context.start, end: context.end),
@ -153,11 +155,29 @@ class RoomChatController(final (String roomId, String? contextualEvent) info)
), ),
); );
ref
.read(RoomsController.provider.notifier)
.update(
.new({
info.$1: Room(
events: IMap.fromIterable(
paginationResponse.events,
keyMapper: (event) => event.rowId,
),
),
}),
.new(),
);
final eventRowIds = paginationResponse.events
.map((element) => element.rowId)
.toIList();
state = .data( state = .data(
.new( .new(
timeline: direction == .forward timeline: direction == .forward
? chat.timeline.addAll(paginationResponse.events) ? chat.timeline.addAll(eventRowIds)
: paginationResponse.events.addAll(chat.timeline), : eventRowIds.addAll(chat.timeline),
hasMoreForward: hasMoreForward:
direction == .forward && paginationResponse.nextBatch == null direction == .forward && paginationResponse.nextBatch == null
? false ? false
@ -293,7 +313,9 @@ class RoomChatController(final (String roomId, String? contextualEvent) info)
} }
static final provider = AsyncNotifierProvider.family static final provider = AsyncNotifierProvider.family
.autoDispose<RoomChatController, RoomChat?, (String, String?)>( .autoDispose<
RoomChatController.new, RoomChatController,
); RoomChat?,
(String, (int rowId, String eventId)?)
>(RoomChatController.new);
} }

View file

@ -9,33 +9,33 @@ import "package:nexus/models/event.dart";
import "package:nexus/models/room_chat.dart"; import "package:nexus/models/room_chat.dart";
final class ChatScroll({ final class ChatScroll({
required final IList<Event> historyItems, required final IList<int> historyRows,
required final IList<Event> liveItems, required final IList<int> liveRows,
required final GlobalKey centerKey, required final GlobalKey centerKey,
required final ScrollController scrollController, required final ScrollController scrollController,
required final bool atBottom, required final bool atBottom,
required final Future<void> Function(String id) jumpToId, required final Future<void> Function(Event event) jumpToEvent,
required final Future<void> Function() jumpToBottom, required final Future<void> Function() jumpToBottom,
required final GlobalKey Function(String eventId) keyFor, required final GlobalKey Function(int eventRowId) keyFor,
}) { }) {
factory use({ factory use({
required AsyncValue<RoomChat?> controllerData, required AsyncValue<RoomChat?> controllerData,
required Future<void> Function(Direction direction) paginate, required Future<void> Function(Direction direction) paginate,
required Future<void> Function() markRead, required Future<void> Function() markRead,
required ValueNotifier<String?> contextualEvent, required ValueNotifier<Event?> contextualEvent,
}) { }) {
final anchorId = useState<String?>(null); final anchorId = useState<int?>(null);
final itemKeys = useMemoized(() => <String, GlobalKey>{}, []); final itemKeys = useMemoized(() => <int, GlobalKey>{}, []);
GlobalKey keyFor(String eventId) => GlobalKey keyFor(int eventRowId) =>
itemKeys.putIfAbsent(eventId, GlobalKey.new); itemKeys.putIfAbsent(eventRowId, GlobalKey.new);
final scrollController = useScrollController(); final scrollController = useScrollController();
final centerKey = useMemoized(GlobalKey.new); final centerKey = useMemoized(GlobalKey.new);
final atBottom = useState(true); final atBottom = useState(true);
final pendingAnchorTarget = useState<String?>(null); final pendingAnchorTarget = useState<int?>(null);
final anchorMountedCompleter = useRef<Completer<BuildContext>?>(null); final anchorMountedCompleter = useRef<Completer<BuildContext>?>(null);
useEffect(() { useEffect(() {
@ -43,12 +43,12 @@ final class ChatScroll({
if (controllerData case AsyncData(:final value?) if (controllerData case AsyncData(:final value?)
when value.timeline.isNotEmpty) { when value.timeline.isNotEmpty) {
final hasContextualEvent = value.timeline.any( final hasContextualEvent = value.timeline.any(
(event) => event.eventId == contextualEvent.value, (event) => event == contextualEvent.value?.rowId,
); );
anchorId.value = hasContextualEvent anchorId.value = hasContextualEvent
? contextualEvent.value ? contextualEvent.value?.rowId
: value.timeline.last.eventId; : value.timeline.last;
} }
} }
@ -60,9 +60,7 @@ final class ChatScroll({
if (target == null) return null; if (target == null) return null;
final found = final found =
controllerData.value?.timeline.any( controllerData.value?.timeline.any((event) => event == target) ??
(event) => event.eventId == target,
) ??
false; false;
if (found || controllerData is AsyncError) { if (found || controllerData is AsyncError) {
@ -88,7 +86,7 @@ final class ChatScroll({
return null; return null;
}, [controllerData, pendingAnchorTarget.value]); }, [controllerData, pendingAnchorTarget.value]);
final ({IList<Event> history, IList<Event> live}) split = useMemoized(() { final ({IList<int> history, IList<int> live}) split = useMemoized(() {
final items = controllerData.value?.timeline; final items = controllerData.value?.timeline;
final anchor = anchorId.value; final anchor = anchorId.value;
@ -96,7 +94,7 @@ final class ChatScroll({
return (history: const .empty(), live: const .empty()); return (history: const .empty(), live: const .empty());
} }
final anchorIndex = items.indexWhere((item) => item.eventId == anchor); final anchorIndex = items.indexOf(anchor);
if (anchorIndex == -1) { if (anchorIndex == -1) {
return (history: const .empty(), live: items); return (history: const .empty(), live: items);
@ -155,15 +153,15 @@ final class ChatScroll({
); );
return .new( return .new(
historyItems: split.history, historyRows: split.history,
liveItems: split.live, liveRows: split.live,
centerKey: centerKey, centerKey: centerKey,
scrollController: scrollController, scrollController: scrollController,
atBottom: atBottom.value, atBottom: atBottom.value,
jumpToId: (String itemId) async { jumpToEvent: (Event event) async {
if (!scrollController.hasClients) return; if (!scrollController.hasClients) return;
final existing = keyFor(itemId).currentContext; final existing = keyFor(event.rowId).currentContext;
if (existing != null && existing.mounted) { if (existing != null && existing.mounted) {
// Already mounted, just scroll // Already mounted, just scroll
await Scrollable.ensureVisible( await Scrollable.ensureVisible(
@ -175,8 +173,8 @@ final class ChatScroll({
} else { } else {
final completer = Completer<BuildContext>(); final completer = Completer<BuildContext>();
anchorMountedCompleter.value = completer; anchorMountedCompleter.value = completer;
pendingAnchorTarget.value = itemId; pendingAnchorTarget.value = event.rowId;
contextualEvent.value = itemId; contextualEvent.value = event;
final context = await completer.future; final context = await completer.future;
if (!context.mounted) return; if (!context.mounted) return;

View file

@ -8,7 +8,8 @@ part "room_chat.g.dart";
@Freezed(toJson: false, fromJson: false) @Freezed(toJson: false, fromJson: false)
@JsonSerializable() @JsonSerializable()
class const RoomChat({ class const RoomChat({
required final IList<Event> timeline, /// List of eventRowId
required final IList<int> timeline,
required final bool hasMoreForward, required final bool hasMoreForward,
required final bool hasMoreBackward, required final bool hasMoreBackward,
final HistoricalData? historicalData, final HistoricalData? historicalData,

View file

@ -24,7 +24,12 @@ class const EventPreview(final Event event, {super.key})
runSpacing: 2, runSpacing: 2,
children: [ children: [
if (event.content is MessageContent) MessageDisplayname(event), 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:material_ui/material_ui.dart";
import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:nexus/controllers/init_complete.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/controllers/key.dart";
import "package:nexus/widgets/appbar.dart"; import "package:nexus/widgets/appbar.dart";
import "package:nexus/widgets/sidebar.dart"; import "package:nexus/widgets/sidebar.dart";
@ -30,7 +30,7 @@ class const ChatPage({super.key}) extends HookConsumerWidget {
child: Consumer( child: Consumer(
builder: (context, ref, _) { builder: (context, ref, _) {
final initialHighlight = ref.watch( final initialHighlight = ref.watch(
JumpToEventController.provider(roomId), ContextualEventController.provider(roomId),
); );
return RoomChat( return RoomChat(
key: ValueKey((roomId, initialHighlight)), 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:material_ui/material_ui.dart";
import "package:flutter_hooks/flutter_hooks.dart"; import "package:flutter_hooks/flutter_hooks.dart";
import "package:hooks_riverpod/hooks_riverpod.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/key.dart";
import "package:nexus/controllers/notifications.dart"; import "package:nexus/controllers/notifications.dart";
import "package:nexus/controllers/spaces.dart"; import "package:nexus/controllers/spaces.dart";
@ -168,18 +168,21 @@ class const NotificationsPage({
.set(event.roomId); .set(event.roomId);
ref ref
.watch( .watch(
JumpToEventController.provider( ContextualEventController.provider(
event.roomId, event.roomId,
).notifier, ).notifier,
) )
.set(event.eventId); .set(event);
if (context.mounted) { if (context.mounted) {
Navigator.of(context).pop(); Navigator.of(context).pop();
} }
}, },
child: IgnorePointer( child: IgnorePointer(
child: EventRenderer(event), child: EventRenderer(
event.rowId,
roomId: event.roomId,
),
), ),
), ),
isHighlighted: isHighlighted, isHighlighted: isHighlighted,

View file

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

View file

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

View file

@ -3,6 +3,8 @@ import "package:flutter/gestures.dart";
import "package:material_ui/material_ui.dart"; import "package:material_ui/material_ui.dart";
import "package:flutter_hooks/flutter_hooks.dart"; import "package:flutter_hooks/flutter_hooks.dart";
import "package:hooks_riverpod/hooks_riverpod.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/helpers/extensions/show_context_menu.dart";
import "package:nexus/models/content/avatar.dart"; import "package:nexus/models/content/avatar.dart";
import "package:nexus/models/content/canonical_alias.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"; import "package:nexus/widgets/timestamp.dart";
class const EventRenderer( class const EventRenderer(
final Event event, { final int eventRowId, {
required final String roomId,
final int? previousEventRowId,
final bool textOnly = false, final bool textOnly = false,
final bool isGrouped = false,
final int? maxLines, final int? maxLines,
final VoidCallback? onTapReply, final Future<void> Function(Event event)? jumpToEvent,
final IList<PopupMenuEntry> Function(Event event)? getEventOptions, final IList<PopupMenuEntry> Function(Event event)? getEventOptions,
super.key, super.key,
}) extends HookConsumerWidget { }) extends HookConsumerWidget {
@ -44,6 +47,26 @@ class const EventRenderer(
final focusNode = useFocusNode(); final focusNode = useFocusNode();
useListenable(focusNode); 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" final child = event.redactedBy != null || event.relationType == "m.replace"
? null ? null
: switch (event.content) { : switch (event.content) {
@ -60,7 +83,16 @@ class const EventRenderer(
EncryptedContent() || EncryptedContent() ||
StickerContent() => MessageRenderer( StickerContent() => MessageRenderer(
event, 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, isGrouped: isGrouped,
maxLines: maxLines, maxLines: maxLines,
textOnly: textOnly, textOnly: textOnly,

View file

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

View file

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