From 269e73e93ac713cd6c2793e24ef315014cd504c1 Mon Sep 17 00:00:00 2001 From: Grafcube Date: Tue, 24 Mar 2026 17:25:30 +0530 Subject: [PATCH 1/4] Add unread messages marker Add a marker to indicate new messages since the last time the room was read. Some extra features along with this: - Jump to last read when a room is opened - Handle rate limiting and other errors when this happens - Double tap message to reply - Highlight messages replying to you or mentioning you --- commet/lib/client/matrix/matrix_room.dart | 3 + commet/lib/client/matrix/matrix_timeline.dart | 27 +++++++- .../matrix_timeline_event.dart | 6 ++ .../matrix_background_events.dart | 6 ++ .../matrix_background_room.dart | 3 + commet/lib/client/room.dart | 2 + .../timeline_events/timeline_event.dart | 2 + .../room_timeline_widget_view.dart | 65 ++++++++++++------- .../events/timeline_event_view_message.dart | 8 +++ .../events/timeline_event_view_reply.dart | 2 +- .../timeline_event_layout_message.dart | 62 ++++++++++++------ .../timeline_events/timeline_view_entry.dart | 56 +++++++++++++++- commet/lib/ui/organisms/chat/chat.dart | 4 +- 13 files changed, 196 insertions(+), 50 deletions(-) diff --git a/commet/lib/client/matrix/matrix_room.dart b/commet/lib/client/matrix/matrix_room.dart index 6af321e96..883e3ff7e 100644 --- a/commet/lib/client/matrix/matrix_room.dart +++ b/commet/lib/client/matrix/matrix_room.dart @@ -862,6 +862,9 @@ class MatrixRoom extends Room { await tl.setReadMarker(public: public); } + @override + String get lastRead => _matrixRoom.fullyRead; + @override RoomVisibility get visibility { switch (_matrixRoom.joinRules) { diff --git a/commet/lib/client/matrix/matrix_timeline.dart b/commet/lib/client/matrix/matrix_timeline.dart index 1e853a78f..c9692e03a 100644 --- a/commet/lib/client/matrix/matrix_timeline.dart +++ b/commet/lib/client/matrix/matrix_timeline.dart @@ -7,6 +7,7 @@ import 'package:commet/client/matrix/timeline_events/matrix_timeline_event.dart' import 'package:commet/client/timeline_events/timeline_event.dart'; import 'package:commet/client/timeline_events/timeline_event_message.dart'; import 'package:commet/client/timeline_events/timeline_event_sticker.dart'; +import 'package:commet/debug/log.dart'; import '../client.dart'; import 'package:matrix/matrix.dart' as matrix; @@ -117,10 +118,30 @@ class MatrixTimeline extends Timeline { @override Future loadMoreFuture() async { if (canLoadFuture) { - var f = _matrixTimeline?.requestFuture(); - + int waitSec = 4; _loadingStatusChangedController.add(null); - await f; + while (true) { + var f = _matrixTimeline?.requestFuture(); + + try { + await f; + _loadingStatusChangedController.add(null); + break; + } on matrix.MatrixException catch (e) { + if (e.error == matrix.MatrixError.M_LIMIT_EXCEEDED) { + Log.i("Rate limited, waiting $waitSec second(s)..."); + await Future.delayed(Duration(seconds: waitSec)); + waitSec *= 2; + continue; + } else { + Log.e(e); + break; + } + } catch (e) { + Log.e(e); + break; + } + } } } diff --git a/commet/lib/client/matrix/timeline_events/matrix_timeline_event.dart b/commet/lib/client/matrix/timeline_events/matrix_timeline_event.dart index 7948e14a9..c4a7d7c8f 100644 --- a/commet/lib/client/matrix/timeline_events/matrix_timeline_event.dart +++ b/commet/lib/client/matrix/timeline_events/matrix_timeline_event.dart @@ -28,6 +28,12 @@ abstract class MatrixTimelineEvent implements TimelineEvent { String get source => const JsonEncoder.withIndent(' ').convert(event.toJson()); + @override + bool get mentionsRoom => event.mentions.room; + + @override + List get mentions => event.mentions.userIds; + @override TimelineEventStatus get status => switch (event.status) { matrix.EventStatus.error => TimelineEventStatus.error, diff --git a/commet/lib/client/matrix_background/matrix_background_events.dart b/commet/lib/client/matrix_background/matrix_background_events.dart index b8e16a444..9a4bf83f5 100644 --- a/commet/lib/client/matrix_background/matrix_background_events.dart +++ b/commet/lib/client/matrix_background/matrix_background_events.dart @@ -67,6 +67,12 @@ class MatrixBackgroundTimelineEventMessage implements TimelineEventMessage { @override String get source => throw UnimplementedError(); + @override + bool get mentionsRoom => throw UnimplementedError(); + + @override + List get mentions => throw UnimplementedError(); + @override TimelineEventStatus get status => throw UnimplementedError(); diff --git a/commet/lib/client/matrix_background/matrix_background_room.dart b/commet/lib/client/matrix_background/matrix_background_room.dart index 67513dafd..b9f2d2364 100644 --- a/commet/lib/client/matrix_background/matrix_background_room.dart +++ b/commet/lib/client/matrix_background/matrix_background_room.dart @@ -350,6 +350,9 @@ class MatrixBackgroundRoom implements Room { throw UnimplementedError(); } + @override + String get lastRead => throw UnimplementedError(); + @override // TODO: implement visibility RoomVisibility get visibility => throw UnimplementedError(); diff --git a/commet/lib/client/room.dart b/commet/lib/client/room.dart index 5a3173a7b..b205a3264 100644 --- a/commet/lib/client/room.dart +++ b/commet/lib/client/room.dart @@ -242,6 +242,8 @@ abstract class Room { Future markAsRead(); + String get lastRead; + @override bool operator ==(Object other) { if (other is! Room) return false; diff --git a/commet/lib/client/timeline_events/timeline_event.dart b/commet/lib/client/timeline_events/timeline_event.dart index 8fbe01f40..944e5750c 100644 --- a/commet/lib/client/timeline_events/timeline_event.dart +++ b/commet/lib/client/timeline_events/timeline_event.dart @@ -9,6 +9,8 @@ abstract class TimelineEvent { String get senderId; DateTime get originServerTs; String get source; + bool get mentionsRoom; + List get mentions; bool get editable; 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 03c03ffd1..ca3d158fd 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 @@ -89,7 +89,7 @@ class RoomTimelineWidgetViewState extends State { initFromTimeline(widget.timeline); - controller = ScrollController(initialScrollOffset: -999999); + controller = ScrollController(); EventBus.jumpToEvent.stream.listen(jumpToEvent); WidgetsBinding.instance.addPostFrameCallback(onAfterFirstFrame); super.initState(); @@ -218,22 +218,18 @@ class RoomTimelineWidgetViewState extends State { } void onAfterFirstFrame(_) { - if (timeline.events.isNotEmpty) { - widget.markAsRead?.call(timeline.events.first); - } - if (controller.hasClients) { - double extent = controller.position.minScrollExtent; + double extent = controller.position.maxScrollExtent; controller = ScrollController(initialScrollOffset: extent); scrollViewKey = GlobalKey(); controller.addListener(onScroll); - widget.onAttachedToBottom?.call(); setState(() { firstFrame = false; }); } WidgetsBinding.instance.addPostFrameCallback((_) { + jumpToEvent(timeline.room.lastRead, highlight: false); onScroll(); }); } @@ -266,18 +262,24 @@ class RoomTimelineWidgetViewState extends State { if (controller.offset > controller.position.maxScrollExtent - loadingThreshold && !timeline.isLoadingHistory && - timeline.canLoadHistory) { - timeline.loadMoreHistory().then((_) { - WidgetsBinding.instance.addPostFrameCallback((_) => onScroll()); + timeline.canLoadHistory) + setState(() async { + await timeline.loadMoreHistory().then((_) { + WidgetsBinding.instance.addPostFrameCallback((_) => onScroll()); + }); }); - } if (controller.offset < (controller.position.minScrollExtent + loadingThreshold) && !timeline.isLoadingFuture && - timeline.canLoadFuture) { - timeline.loadMoreFuture(); - } + timeline.canLoadFuture) + setState(() async { + await timeline.loadMoreFuture(); + eventKeys = List.from( + timeline.events + .map((e) => (GlobalKey(debugLabel: e.eventId), e.eventId)), + growable: true); + }); } void animateAndSnapToBottom() { @@ -379,6 +381,10 @@ class RoomTimelineWidgetViewState extends State { ), ); })), + // The slivers are split into recent and history events and + // are rendered separately. This prevents the timeline from + // jumping around and allows jumping to specific indices + // with more reliability. SliverList( key: recentItemsKey, // Recent Items @@ -390,7 +396,12 @@ class RoomTimelineWidgetViewState extends State { recentItemsCount - sliverIndex - 1; numBuilds += 1; - var key = eventKeys[timelineIndex]; + var key; + try { + key = eventKeys[timelineIndex]; + } on RangeError { + return Placeholder(); + } assert( key.$2 == timeline.events[timelineIndex].eventId); @@ -439,7 +450,12 @@ class RoomTimelineWidgetViewState extends State { // ignore: avoid_print var timelineIndex = recentItemsCount + sliverIndex; - var key = eventKeys[timelineIndex]; + var key; + try { + key = eventKeys[timelineIndex]; + } on RangeError { + return Placeholder(); + } assert( key.$2 == timeline.events[timelineIndex].eventId); @@ -525,8 +541,8 @@ class RoomTimelineWidgetViewState extends State { ); } - void jumpToEvent(String eventId) async { - if (highlightedEventState?.mounted == true) { + void jumpToEvent(String eventId, {bool highlight = true}) async { + if (highlight && highlightedEventState?.mounted == true) { highlightedEventState!.setHighlighted(false); highlightedEventState = null; } @@ -548,6 +564,7 @@ class RoomTimelineWidgetViewState extends State { setState(() { initFromTimeline(newTimeline); + jumpToEvent(timeline.room.lastRead, highlight: false); }); } @@ -555,7 +572,7 @@ class RoomTimelineWidgetViewState extends State { var key = eventKeys[index].$1; final state = key.currentState; - if (state is TimelineViewEntryState) { + if (highlight && state is TimelineViewEntryState) { state.setHighlighted(true); highlightedEventState = state; } @@ -586,9 +603,11 @@ class RoomTimelineWidgetViewState extends State { setState(() { recentItemsCount = index; - highlightedEventId = timeline.events[index].eventId; - highlightedEventOffstageIndex = index; - highlightedEventOffstageKey = GlobalKey(); + if (highlight) { + highlightedEventId = timeline.events[index].eventId; + highlightedEventOffstageIndex = index; + highlightedEventOffstageKey = GlobalKey(); + } loading = false; }); } @@ -612,7 +631,7 @@ class RoomTimelineWidgetViewState extends State { var index = timeline.events.indexWhere((i) => i.eventId == event); if (index == -1) { - print("Could not find the event in the timeline view"); + Log.w("Could not find the event in the timeline view"); } if (state is TimelineEventViewWidget) { 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 6a9e3d650..821c8c0c5 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 @@ -42,6 +42,7 @@ class TimelineEventViewMessage extends StatefulWidget { this.onReadReceiptsTapped, this.detailed = false, this.previewMedia = false, + this.setReplyingEvent, required this.initialIndex}); final Function(String eventId)? jumpToEvent; @@ -56,6 +57,7 @@ class TimelineEventViewMessage extends StatefulWidget { final bool isThreadTimeline; final bool previewMedia; final Function()? onReadReceiptsTapped; + final Function(TimelineEvent? event)? setReplyingEvent; @override State createState() => @@ -68,6 +70,8 @@ class _TimelineEventViewMessageState extends State late String senderId; late Color senderColor; + late TimelineEvent event; + String get messageFailedToDecrypt => Intl.message("Failed to decrypt event", desc: "Placeholde text for when a message fails to decrypt", name: "messageFailedToDecrypt"); @@ -131,6 +135,9 @@ class _TimelineEventViewMessageState extends State formattedContent: formattedContent, timestamp: timestampToString(sentTime), edited: edited, + isMentioningSelf: event.mentionsRoom || + event.mentions.contains(widget.timeline!.client.self!.identifier), + onDoubleTapMessage: () => widget.setReplyingEvent?.call(event), avatarBuilder: (child) { var room = widget.room ?? widget.timeline?.room; @@ -220,6 +227,7 @@ class _TimelineEventViewMessageState extends State } void loadStateFromEvent(TimelineEvent event) { + this.event = event; showSender = shouldShowSender(index); var room = widget.room ?? widget.timeline?.room; 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 371303b50..15040fd29 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 @@ -111,7 +111,7 @@ class _TimelineEventViewReplyState extends State { text: body ?? "Unknown", style: Theme.of(context) .textTheme - .bodyMedium + .bodySmall ?.copyWith( color: material.Theme.of(context) .colorScheme 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 e55a2bd6b..85d90122e 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,7 +1,5 @@ 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; @@ -22,9 +20,11 @@ class TimelineEventLayoutMessage extends StatelessWidget { this.readReceipts, this.onAvatarTapped, this.edited = false, + this.isMentioningSelf = false, this.avatarSize = 32, this.avatarBuilder, - this.showSender = true}); + this.showSender = true, + this.onDoubleTapMessage}); final String senderName; final Color senderColor; final ImageProvider? senderAvatar; @@ -38,9 +38,11 @@ class TimelineEventLayoutMessage extends StatelessWidget { final Widget? readReceipts; final bool showSender; final bool edited; + final bool isMentioningSelf; final String? timestamp; final Function()? onAvatarTapped; final Widget Function(Widget child)? avatarBuilder; + final Function()? onDoubleTapMessage; final double avatarSize; @@ -51,7 +53,7 @@ class TimelineEventLayoutMessage extends StatelessWidget { @override Widget build(BuildContext context) { BenchmarkValues.numTimelineMessageBodyBuilt += 1; - return Padding( + Widget result = Padding( padding: const EdgeInsets.fromLTRB(16, 2, 8, 2), child: Column( children: [ @@ -81,23 +83,26 @@ class TimelineEventLayoutMessage extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ Flexible( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (formattedContent != null) - RepaintBoundary(child: formattedContent!), - if (edited) - tiamat.Text.labelLow(messageEditedMarker), - if (attachments != null) attachments!, - if (sticker != null) sticker!, - if (urlPreviews != null) urlPreviews!, - if (reactions != null) - Padding( - padding: - const EdgeInsets.fromLTRB(0, 4, 0, 0), - child: reactions!, - ), - ], + child: GestureDetector( + onDoubleTap: onDoubleTapMessage, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (formattedContent != null) + RepaintBoundary(child: formattedContent!), + if (edited) + tiamat.Text.labelLow(messageEditedMarker), + if (attachments != null) attachments!, + if (sticker != null) sticker!, + if (urlPreviews != null) urlPreviews!, + if (reactions != null) + Padding( + padding: + const EdgeInsets.fromLTRB(0, 4, 0, 0), + child: reactions!, + ), + ], + ), ), ), SizedBox( @@ -116,6 +121,21 @@ class TimelineEventLayoutMessage extends StatelessWidget { ], ), ); + + if (isMentioningSelf) + result = Container( + decoration: BoxDecoration( + border: Border( + left: BorderSide( + color: Theme.of(context).colorScheme.tertiary, width: 3)), + color: Theme.of(context).colorScheme.tertiaryContainer), + child: Padding( + padding: const EdgeInsets.fromLTRB(0, 0, 0, 0), + child: result, + ), + ); + + return result; } Widget name() { 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 a87130f8b..0ae6ee445 100644 --- a/commet/lib/ui/molecules/timeline_events/timeline_view_entry.dart +++ b/commet/lib/ui/molecules/timeline_events/timeline_view_entry.dart @@ -24,6 +24,7 @@ import 'package:commet/ui/molecules/timeline_events/timeline_event_menu_dialog.d import 'package:commet/ui/molecules/user_panel.dart'; import 'package:commet/ui/navigation/adaptive_dialog.dart'; import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; import 'package:tiamat/atoms/context_menu.dart'; import 'package:tiamat/tiamat.dart' as tiamat; @@ -46,7 +47,7 @@ class TimelineViewEntry extends StatefulWidget { final Function(String eventId)? onEventHovered; final Function(TimelineEvent? event)? setReplyingEvent; final Function(TimelineEvent? event)? setEditingEvent; - final Function(String eventId)? jumpToEvent; + final Function(String eventId, {bool highlight})? jumpToEvent; final bool showDetailed; final bool isThreadTimeline; final String? highlightedEventId; @@ -92,12 +93,17 @@ class TimelineViewEntryState extends State late DateTime time; bool showDateSeperator = false; + bool showUnreadMarker = false; ThreadsComponent? threads; PollComponent? polls; List readReceipts = []; + String get labelTimelineNewMessagesMarker => Intl.message("New messages", + desc: "Text that is shown below the last read message", + name: "labelTimelineNewMessagesMarker"); + @override void initState() { threads = widget.timeline.room.client.getComponent(); @@ -130,6 +136,7 @@ class TimelineViewEntryState extends State _widgetType = eventToDisplayType(event, polls: polls); showDateSeperator = shouldEventShowDate(eventIndex); + showUnreadMarker = shouldEventShowUnreadMarker(eventIndex); highlighted = event.eventId == widget.highlightedEventId; } @@ -195,6 +202,25 @@ class TimelineViewEntryState extends State return false; } + bool shouldEventShowUnreadMarker(int index) { + final events = widget.timeline.events; + + bool isHidden(event) => + eventToDisplayType(event, polls: polls) == + TimelineEventWidgetDisplayType.hidden; + + if (index == 0 || events.take(index).every(isHidden)) return false; + + final lastReadIndex = + events.indexWhere((e) => e.eventId == widget.timeline.room.lastRead); + + if (lastReadIndex > index) return false; + if (lastReadIndex == index) return true; + if (!isHidden(events[lastReadIndex])) return false; + + return events.getRange(lastReadIndex, index).every(isHidden); + } + @override void update(int newIndex) { index = newIndex; @@ -393,6 +419,34 @@ class TimelineViewEntryState extends State ); } + if (showUnreadMarker) + result = Column(children: [ + result, + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Divider( + color: Colors.red, + thickness: 4.0, + radius: BorderRadiusGeometry.horizontal( + right: Radius.circular(16.0)))), + Padding( + padding: EdgeInsets.symmetric(horizontal: 15), + child: Text(labelTimelineNewMessagesMarker, + style: TextStyle( + color: Colors.red, fontWeight: FontWeight.bold)), + ), + Expanded( + child: Divider( + color: Colors.red, + thickness: 4.0, + radius: BorderRadiusGeometry.horizontal( + left: Radius.circular(16.0)))), + ], + ), + ]); + return result; } diff --git a/commet/lib/ui/organisms/chat/chat.dart b/commet/lib/ui/organisms/chat/chat.dart index f5575d6a0..e4e18fb02 100644 --- a/commet/lib/ui/organisms/chat/chat.dart +++ b/commet/lib/ui/organisms/chat/chat.dart @@ -126,7 +126,9 @@ class ChatState extends State { Future loadThreadTimeline() async { Timeline? timeline = room.timeline; - timeline ??= await room.getTimeline(); + timeline ??= room.lastRead.isNotEmpty + ? await room.getTimeline(contextEventId: room.lastRead) + : await room.getTimeline(); var threadTimeline = await threadsComponent!.getThreadTimeline( roomTimeline: timeline, threadRootEventId: widget.threadId!); From 3725f0c71a7fab81d74bab1effdf8e7b322afe8c Mon Sep 17 00:00:00 2001 From: Grafcube Date: Wed, 25 Mar 2026 19:17:58 +0530 Subject: [PATCH 2/4] Make changes --- .../room_timeline_widget_view.dart | 82 ++++++++++++++++++- .../events/timeline_event_view_message.dart | 16 ++-- .../timeline_events/timeline_view_entry.dart | 61 ++------------ 3 files changed, 94 insertions(+), 65 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 ca3d158fd..5379c03a9 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,6 +1,7 @@ import 'dart:async'; import 'package:commet/client/components/message_effects/message_effect_component.dart'; +import 'package:commet/client/components/polls/poll_component.dart'; import 'package:commet/client/components/read_receipts/read_receipt_component.dart'; import 'package:commet/client/timeline.dart'; import 'package:commet/client/timeline_events/timeline_event.dart'; @@ -14,6 +15,7 @@ import 'package:commet/ui/molecules/timeline_events/timeline_event_menu.dart'; import 'package:commet/ui/molecules/timeline_events/timeline_view_entry.dart'; import 'package:commet/utils/event_bus.dart'; import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; class RoomTimelineWidgetView extends StatefulWidget { const RoomTimelineWidgetView( @@ -83,6 +85,10 @@ class RoomTimelineWidgetViewState extends State { MessageEffectComponent? effects; + String get labelTimelineNewMessagesMarker => Intl.message("New messages", + desc: "Text that is shown below the last read message", + name: "labelTimelineNewMessagesMarker"); + @override void initState() { effects = widget.timeline.client.getComponent(); @@ -352,6 +358,26 @@ class RoomTimelineWidgetViewState extends State { selectedEventView = null; } + bool shouldEventShowUnreadMarker(int index) { + final events = widget.timeline.events; + final polls = widget.timeline.client.getComponent(); + + bool isHidden(event) => + TimelineViewEntryState.eventToDisplayType(event, polls: polls) == + TimelineEventWidgetDisplayType.hidden; + + if (index == 0 || events.take(index).every(isHidden)) return false; + + final lastReadIndex = + events.indexWhere((e) => e.eventId == widget.timeline.room.lastRead); + + if (lastReadIndex > index) return false; + if (lastReadIndex == index) return true; + if (!isHidden(events[lastReadIndex])) return false; + + return events.getRange(lastReadIndex, index).every(isHidden); + } + @override Widget build(BuildContext context) { return Material( @@ -405,7 +431,10 @@ class RoomTimelineWidgetViewState extends State { assert( key.$2 == timeline.events[timelineIndex].eventId); - return Container( + var showUnreadMarker = + shouldEventShowUnreadMarker(timelineIndex); + + Widget result = Container( alignment: Alignment.center, color: preferences.developerMode.value && BuildConfig.DEBUG @@ -420,11 +449,18 @@ class RoomTimelineWidgetViewState extends State { setReplyingEvent: widget.setReplyingEvent, isThreadTimeline: widget.isThreadTimeline, highlightedEventId: highlightedEventId, + overrideShowSender: + showUnreadMarker ? true : null, previewMedia: widget.timeline.room.shouldPreviewMedia, jumpToEvent: jumpToEvent, initialIndex: timelineIndex), ); + + if (showUnreadMarker) + result = newMessagesMarker(result); + + return result; }, findChildIndexCallback: (key) { var timelineIndex = eventKeys @@ -459,7 +495,10 @@ class RoomTimelineWidgetViewState extends State { assert( key.$2 == timeline.events[timelineIndex].eventId); - return Container( + var showUnreadMarker = + shouldEventShowUnreadMarker(timelineIndex); + + Widget result = Container( alignment: Alignment.center, color: preferences.developerMode.value && BuildConfig.DEBUG @@ -474,11 +513,18 @@ class RoomTimelineWidgetViewState extends State { setReplyingEvent: widget.setReplyingEvent, isThreadTimeline: widget.isThreadTimeline, highlightedEventId: highlightedEventId, + overrideShowSender: + showUnreadMarker ? true : null, previewMedia: widget.timeline.room.shouldPreviewMedia, jumpToEvent: jumpToEvent, initialIndex: timelineIndex), ); + + if (showUnreadMarker) + result = newMessagesMarker(result); + + return result; }, findChildIndexCallback: (key) { var timelineIndex = eventKeys @@ -541,6 +587,37 @@ class RoomTimelineWidgetViewState extends State { ); } + Widget newMessagesMarker(Widget child) { + return Column( + children: [ + child, + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Divider( + color: Colors.red, + thickness: 4.0, + radius: BorderRadiusGeometry.horizontal( + right: Radius.circular(16.0)))), + Padding( + padding: EdgeInsets.symmetric(horizontal: 15), + child: Text(labelTimelineNewMessagesMarker, + style: TextStyle( + color: Colors.red, fontWeight: FontWeight.bold)), + ), + Expanded( + child: Divider( + color: Colors.red, + thickness: 4.0, + radius: BorderRadiusGeometry.horizontal( + left: Radius.circular(16.0)))), + ], + ), + ], + ); + } + void jumpToEvent(String eventId, {bool highlight = true}) async { if (highlight && highlightedEventState?.mounted == true) { highlightedEventState!.setHighlighted(false); @@ -564,7 +641,6 @@ class RoomTimelineWidgetViewState extends State { setState(() { initFromTimeline(newTimeline); - jumpToEvent(timeline.room.lastRead, highlight: false); }); } 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 821c8c0c5..07e2e219e 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 @@ -40,9 +40,9 @@ class TimelineEventViewMessage extends StatefulWidget { this.jumpToEvent, this.readReceipts = const [], this.onReadReceiptsTapped, + this.onDoubleTapMessage, this.detailed = false, this.previewMedia = false, - this.setReplyingEvent, required this.initialIndex}); final Function(String eventId)? jumpToEvent; @@ -57,7 +57,7 @@ class TimelineEventViewMessage extends StatefulWidget { final bool isThreadTimeline; final bool previewMedia; final Function()? onReadReceiptsTapped; - final Function(TimelineEvent? event)? setReplyingEvent; + final Function()? onDoubleTapMessage; @override State createState() => @@ -70,7 +70,8 @@ class _TimelineEventViewMessageState extends State late String senderId; late Color senderColor; - late TimelineEvent event; + late bool mentionsRoom; + late List mentions; String get messageFailedToDecrypt => Intl.message("Failed to decrypt event", desc: "Placeholde text for when a message fails to decrypt", @@ -135,9 +136,9 @@ class _TimelineEventViewMessageState extends State formattedContent: formattedContent, timestamp: timestampToString(sentTime), edited: edited, - isMentioningSelf: event.mentionsRoom || - event.mentions.contains(widget.timeline!.client.self!.identifier), - onDoubleTapMessage: () => widget.setReplyingEvent?.call(event), + isMentioningSelf: mentionsRoom || + mentions.contains(widget.timeline!.client.self!.identifier), + onDoubleTapMessage: widget.onDoubleTapMessage, avatarBuilder: (child) { var room = widget.room ?? widget.timeline?.room; @@ -227,7 +228,8 @@ class _TimelineEventViewMessageState extends State } void loadStateFromEvent(TimelineEvent event) { - this.event = event; + mentionsRoom = event.mentionsRoom; + mentions = event.mentions; showSender = shouldShowSender(index); var room = widget.room ?? widget.timeline?.room; 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 0ae6ee445..a170c5c28 100644 --- a/commet/lib/ui/molecules/timeline_events/timeline_view_entry.dart +++ b/commet/lib/ui/molecules/timeline_events/timeline_view_entry.dart @@ -24,7 +24,6 @@ import 'package:commet/ui/molecules/timeline_events/timeline_event_menu_dialog.d import 'package:commet/ui/molecules/user_panel.dart'; import 'package:commet/ui/navigation/adaptive_dialog.dart'; import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; import 'package:tiamat/atoms/context_menu.dart'; import 'package:tiamat/tiamat.dart' as tiamat; @@ -41,6 +40,7 @@ class TimelineViewEntry extends StatefulWidget { this.isThreadTimeline = false, this.previewMedia = false, this.highlightedEventId, + this.overrideShowSender, super.key}); final Timeline timeline; final int initialIndex; @@ -52,6 +52,7 @@ class TimelineViewEntry extends StatefulWidget { final bool isThreadTimeline; final String? highlightedEventId; final bool previewMedia; + 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; @@ -93,17 +94,12 @@ class TimelineViewEntryState extends State late DateTime time; bool showDateSeperator = false; - bool showUnreadMarker = false; ThreadsComponent? threads; PollComponent? polls; List readReceipts = []; - String get labelTimelineNewMessagesMarker => Intl.message("New messages", - desc: "Text that is shown below the last read message", - name: "labelTimelineNewMessagesMarker"); - @override void initState() { threads = widget.timeline.room.client.getComponent(); @@ -136,7 +132,6 @@ class TimelineViewEntryState extends State _widgetType = eventToDisplayType(event, polls: polls); showDateSeperator = shouldEventShowDate(eventIndex); - showUnreadMarker = shouldEventShowUnreadMarker(eventIndex); highlighted = event.eventId == widget.highlightedEventId; } @@ -202,25 +197,6 @@ class TimelineViewEntryState extends State return false; } - bool shouldEventShowUnreadMarker(int index) { - final events = widget.timeline.events; - - bool isHidden(event) => - eventToDisplayType(event, polls: polls) == - TimelineEventWidgetDisplayType.hidden; - - if (index == 0 || events.take(index).every(isHidden)) return false; - - final lastReadIndex = - events.indexWhere((e) => e.eventId == widget.timeline.room.lastRead); - - if (lastReadIndex > index) return false; - if (lastReadIndex == index) return true; - if (!isHidden(events[lastReadIndex])) return false; - - return events.getRange(lastReadIndex, index).every(isHidden); - } - @override void update(int newIndex) { index = newIndex; @@ -419,34 +395,6 @@ class TimelineViewEntryState extends State ); } - if (showUnreadMarker) - result = Column(children: [ - result, - Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: Divider( - color: Colors.red, - thickness: 4.0, - radius: BorderRadiusGeometry.horizontal( - right: Radius.circular(16.0)))), - Padding( - padding: EdgeInsets.symmetric(horizontal: 15), - child: Text(labelTimelineNewMessagesMarker, - style: TextStyle( - color: Colors.red, fontWeight: FontWeight.bold)), - ), - Expanded( - child: Divider( - color: Colors.red, - thickness: 4.0, - radius: BorderRadiusGeometry.horizontal( - left: Radius.circular(16.0)))), - ], - ), - ]); - return result; } @@ -461,6 +409,7 @@ class TimelineViewEntryState extends State return null; } + var event = widget.timeline.tryGetEvent(eventId); if (_widgetType == TimelineEventWidgetDisplayType.message) return TimelineEventViewMessage( key: eventKey, @@ -468,8 +417,10 @@ class TimelineViewEntryState extends State isThreadTimeline: widget.isThreadTimeline, detailed: widget.showDetailed || selected, onReadReceiptsTapped: onReadReceiptsTapped, + onDoubleTapMessage: () => widget.setReplyingEvent?.call(event), readReceipts: readReceipts, - overrideShowSender: widget.singleEvent || showDateSeperator, + overrideShowSender: widget.overrideShowSender ?? + (widget.singleEvent || showDateSeperator), jumpToEvent: widget.jumpToEvent, previewMedia: widget.previewMedia, initialIndex: widget.initialIndex); From 58c000fd3c333f93d278de0b03b80b8c80d271d3 Mon Sep 17 00:00:00 2001 From: Airyzz <36567925+Airyzz@users.noreply.github.com> Date: Mon, 30 Mar 2026 23:09:28 +1030 Subject: [PATCH 3/4] tweak new message indicator logic --- commet/lib/client/timeline.dart | 2 +- .../room_timeline_widget_view.dart | 133 +++++------------- .../timeline_events/timeline_view_entry.dart | 78 +++++++++- commet/lib/ui/organisms/chat/chat.dart | 2 +- 4 files changed, 112 insertions(+), 103 deletions(-) diff --git a/commet/lib/client/timeline.dart b/commet/lib/client/timeline.dart index 3c56c26a5..48e06ab32 100644 --- a/commet/lib/client/timeline.dart +++ b/commet/lib/client/timeline.dart @@ -64,7 +64,7 @@ abstract class Timeline { final Map _eventsDict = {}; late StreamController onEventAdded = StreamController.broadcast(sync: true); - late StreamController onChange = StreamController.broadcast(sync: true); + late StreamController onChange = StreamController.broadcast(sync: false); late StreamController onRemove = StreamController.broadcast(sync: true); late Client client; late Room room; 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 5379c03a9..5ff45c8ca 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,6 @@ import 'dart:async'; import 'package:commet/client/components/message_effects/message_effect_component.dart'; -import 'package:commet/client/components/polls/poll_component.dart'; import 'package:commet/client/components/read_receipts/read_receipt_component.dart'; import 'package:commet/client/timeline.dart'; import 'package:commet/client/timeline_events/timeline_event.dart'; @@ -15,7 +14,6 @@ import 'package:commet/ui/molecules/timeline_events/timeline_event_menu.dart'; import 'package:commet/ui/molecules/timeline_events/timeline_view_entry.dart'; import 'package:commet/utils/event_bus.dart'; import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; class RoomTimelineWidgetView extends StatefulWidget { const RoomTimelineWidgetView( @@ -84,10 +82,7 @@ class RoomTimelineWidgetViewState extends State { bool isLoadingHistory = false; MessageEffectComponent? effects; - - String get labelTimelineNewMessagesMarker => Intl.message("New messages", - desc: "Text that is shown below the last read message", - name: "labelTimelineNewMessagesMarker"); + String? lastReadEventId; @override void initState() { @@ -95,7 +90,7 @@ class RoomTimelineWidgetViewState extends State { initFromTimeline(widget.timeline); - controller = ScrollController(); + controller = ScrollController(initialScrollOffset: -999999); EventBus.jumpToEvent.stream.listen(jumpToEvent); WidgetsBinding.instance.addPostFrameCallback(onAfterFirstFrame); super.initState(); @@ -108,11 +103,24 @@ class RoomTimelineWidgetViewState extends State { } } - isLoadingFuture = false; - isLoadingHistory = false; + String? targetEventId = timeline.room.lastRead; // timeline.contextEventId; this.timeline = timeline; - recentItemsCount = timeline.events.length; + var index = timeline.events.indexWhere((i) => i.eventId == targetEventId); + + if (index > 0) { + lastReadEventId = targetEventId; + + isLoadingFuture = false; + isLoadingHistory = false; + + recentItemsCount = index; + } else { + if (timeline.events.length > 1) { + recentItemsCount = 1; + } + } + var receipts = timeline.room.getComponent(); subscriptions = [ timeline.onEventAdded.stream.listen(onEventAdded), @@ -225,7 +233,16 @@ class RoomTimelineWidgetViewState extends State { void onAfterFirstFrame(_) { if (controller.hasClients) { - double extent = controller.position.maxScrollExtent; + double extent = controller.position.minScrollExtent; + + print("Viewport dimension: ${controller.position.viewportDimension}"); + print("Extent: ${extent}"); + + if (controller.position.viewportDimension < -extent) { + print("Needs to jump to event"); + extent = -controller.position.viewportDimension / 2; + } + controller = ScrollController(initialScrollOffset: extent); scrollViewKey = GlobalKey(); controller.addListener(onScroll); @@ -235,7 +252,6 @@ class RoomTimelineWidgetViewState extends State { } WidgetsBinding.instance.addPostFrameCallback((_) { - jumpToEvent(timeline.room.lastRead, highlight: false); onScroll(); }); } @@ -358,26 +374,6 @@ class RoomTimelineWidgetViewState extends State { selectedEventView = null; } - bool shouldEventShowUnreadMarker(int index) { - final events = widget.timeline.events; - final polls = widget.timeline.client.getComponent(); - - bool isHidden(event) => - TimelineViewEntryState.eventToDisplayType(event, polls: polls) == - TimelineEventWidgetDisplayType.hidden; - - if (index == 0 || events.take(index).every(isHidden)) return false; - - final lastReadIndex = - events.indexWhere((e) => e.eventId == widget.timeline.room.lastRead); - - if (lastReadIndex > index) return false; - if (lastReadIndex == index) return true; - if (!isHidden(events[lastReadIndex])) return false; - - return events.getRange(lastReadIndex, index).every(isHidden); - } - @override Widget build(BuildContext context) { return Material( @@ -423,18 +419,13 @@ class RoomTimelineWidgetViewState extends State { numBuilds += 1; var key; - try { - key = eventKeys[timelineIndex]; - } on RangeError { - return Placeholder(); - } + + key = eventKeys[timelineIndex]; + assert( key.$2 == timeline.events[timelineIndex].eventId); - var showUnreadMarker = - shouldEventShowUnreadMarker(timelineIndex); - - Widget result = Container( + return Container( alignment: Alignment.center, color: preferences.developerMode.value && BuildConfig.DEBUG @@ -449,18 +440,12 @@ class RoomTimelineWidgetViewState extends State { setReplyingEvent: widget.setReplyingEvent, isThreadTimeline: widget.isThreadTimeline, highlightedEventId: highlightedEventId, - overrideShowSender: - showUnreadMarker ? true : null, + lastReadEventId: lastReadEventId, previewMedia: widget.timeline.room.shouldPreviewMedia, jumpToEvent: jumpToEvent, initialIndex: timelineIndex), ); - - if (showUnreadMarker) - result = newMessagesMarker(result); - - return result; }, findChildIndexCallback: (key) { var timelineIndex = eventKeys @@ -487,18 +472,13 @@ class RoomTimelineWidgetViewState extends State { var timelineIndex = recentItemsCount + sliverIndex; var key; - try { - key = eventKeys[timelineIndex]; - } on RangeError { - return Placeholder(); - } + + key = eventKeys[timelineIndex]; + assert( key.$2 == timeline.events[timelineIndex].eventId); - var showUnreadMarker = - shouldEventShowUnreadMarker(timelineIndex); - - Widget result = Container( + return Container( alignment: Alignment.center, color: preferences.developerMode.value && BuildConfig.DEBUG @@ -513,18 +493,12 @@ class RoomTimelineWidgetViewState extends State { setReplyingEvent: widget.setReplyingEvent, isThreadTimeline: widget.isThreadTimeline, highlightedEventId: highlightedEventId, - overrideShowSender: - showUnreadMarker ? true : null, previewMedia: widget.timeline.room.shouldPreviewMedia, + lastReadEventId: lastReadEventId, jumpToEvent: jumpToEvent, initialIndex: timelineIndex), ); - - if (showUnreadMarker) - result = newMessagesMarker(result); - - return result; }, findChildIndexCallback: (key) { var timelineIndex = eventKeys @@ -587,37 +561,6 @@ class RoomTimelineWidgetViewState extends State { ); } - Widget newMessagesMarker(Widget child) { - return Column( - children: [ - child, - Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: Divider( - color: Colors.red, - thickness: 4.0, - radius: BorderRadiusGeometry.horizontal( - right: Radius.circular(16.0)))), - Padding( - padding: EdgeInsets.symmetric(horizontal: 15), - child: Text(labelTimelineNewMessagesMarker, - style: TextStyle( - color: Colors.red, fontWeight: FontWeight.bold)), - ), - Expanded( - child: Divider( - color: Colors.red, - thickness: 4.0, - radius: BorderRadiusGeometry.horizontal( - left: Radius.circular(16.0)))), - ], - ), - ], - ); - } - void jumpToEvent(String eventId, {bool highlight = true}) async { if (highlight && highlightedEventState?.mounted == true) { highlightedEventState!.setHighlighted(false); 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 a170c5c28..a3dd78e01 100644 --- a/commet/lib/ui/molecules/timeline_events/timeline_view_entry.dart +++ b/commet/lib/ui/molecules/timeline_events/timeline_view_entry.dart @@ -24,7 +24,9 @@ import 'package:commet/ui/molecules/timeline_events/timeline_event_menu_dialog.d import 'package:commet/ui/molecules/user_panel.dart'; import 'package:commet/ui/navigation/adaptive_dialog.dart'; import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; import 'package:tiamat/atoms/context_menu.dart'; +import 'package:tiamat/atoms/text.dart'; import 'package:tiamat/tiamat.dart' as tiamat; class TimelineViewEntry extends StatefulWidget { @@ -39,8 +41,8 @@ class TimelineViewEntry extends StatefulWidget { this.singleEvent = false, this.isThreadTimeline = false, this.previewMedia = false, + this.lastReadEventId, this.highlightedEventId, - this.overrideShowSender, super.key}); final Timeline timeline; final int initialIndex; @@ -51,8 +53,8 @@ class TimelineViewEntry extends StatefulWidget { final bool showDetailed; final bool isThreadTimeline; final String? highlightedEventId; + final String? lastReadEventId; final bool previewMedia; - 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; @@ -94,12 +96,17 @@ class TimelineViewEntryState extends State late DateTime time; bool showDateSeperator = false; + bool showUnreadMarker = false; ThreadsComponent? threads; PollComponent? polls; List readReceipts = []; + String get labelTimelineNewMessagesMarker => Intl.message("New messages", + desc: "Text that is shown below the last read message", + name: "labelTimelineNewMessagesMarker"); + @override void initState() { threads = widget.timeline.room.client.getComponent(); @@ -132,6 +139,7 @@ class TimelineViewEntryState extends State _widgetType = eventToDisplayType(event, polls: polls); showDateSeperator = shouldEventShowDate(eventIndex); + showUnreadMarker = shouldEventShowUnreadMarker(eventIndex); highlighted = event.eventId == widget.highlightedEventId; } @@ -197,6 +205,29 @@ class TimelineViewEntryState extends State return false; } + bool shouldEventShowUnreadMarker(int index) { + final events = widget.timeline.events; + + bool isHidden(event) => + eventToDisplayType(event, polls: polls) == + TimelineEventWidgetDisplayType.hidden; + + if (index == 0 || events.take(index).every(isHidden)) return false; + + final lastReadIndex = + events.indexWhere((e) => e.eventId == widget.lastReadEventId); + + if (lastReadIndex == -1) { + return false; + } + + //if (lastReadIndex > index) return false; + if (lastReadIndex == index + 1) return true; + if (!isHidden(events[lastReadIndex])) return false; + + return events.getRange(lastReadIndex, index).every(isHidden); + } + @override void update(int newIndex) { index = newIndex; @@ -395,9 +426,46 @@ class TimelineViewEntryState extends State ); } + if (showUnreadMarker) + result = Column(children: [ + buildNewMessagesMarker(), + result, + ]); + return result; } + Row buildNewMessagesMarker() { + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Divider( + color: ColorScheme.of(context).primaryContainer, + thickness: 2.0, + )), + DecoratedBox( + decoration: BoxDecoration( + color: ColorScheme.of(context).primaryContainer, + borderRadius: BorderRadius.circular(8)), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 15, vertical: 3), + child: tiamat.Text( + labelTimelineNewMessagesMarker, + color: ColorScheme.of(context).onPrimaryContainer, + type: TextType.labelLow, + ), + ), + ), + Expanded( + child: Divider( + color: ColorScheme.of(context).primaryContainer, + thickness: 2.0, + )), + ], + ); + } + Widget? buildEvent() { if (redacted) { return null; @@ -409,7 +477,6 @@ class TimelineViewEntryState extends State return null; } - var event = widget.timeline.tryGetEvent(eventId); if (_widgetType == TimelineEventWidgetDisplayType.message) return TimelineEventViewMessage( key: eventKey, @@ -417,10 +484,9 @@ class TimelineViewEntryState extends State isThreadTimeline: widget.isThreadTimeline, detailed: widget.showDetailed || selected, onReadReceiptsTapped: onReadReceiptsTapped, - onDoubleTapMessage: () => widget.setReplyingEvent?.call(event), readReceipts: readReceipts, - overrideShowSender: widget.overrideShowSender ?? - (widget.singleEvent || showDateSeperator), + overrideShowSender: + widget.singleEvent || showDateSeperator || showUnreadMarker, jumpToEvent: widget.jumpToEvent, previewMedia: widget.previewMedia, initialIndex: widget.initialIndex); diff --git a/commet/lib/ui/organisms/chat/chat.dart b/commet/lib/ui/organisms/chat/chat.dart index e4e18fb02..67820cff9 100644 --- a/commet/lib/ui/organisms/chat/chat.dart +++ b/commet/lib/ui/organisms/chat/chat.dart @@ -118,7 +118,7 @@ class ChatState extends State { } Future loadTimeline() async { - var t = await room.getTimeline(); + var t = await room.getTimeline(contextEventId: room.lastRead); setState(() { _timeline = t; }); From ba94ca44f6d2f1b2b6cc9e1c07ce3c1500f754c5 Mon Sep 17 00:00:00 2001 From: Airyzz <36567925+Airyzz@users.noreply.github.com> Date: Mon, 30 Mar 2026 23:12:18 +1030 Subject: [PATCH 4/4] Update room_timeline_widget_view.dart --- .../room_timeline_widget/room_timeline_widget_view.dart | 4 ---- 1 file changed, 4 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 5ff45c8ca..c3c9f13f6 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 @@ -235,11 +235,7 @@ class RoomTimelineWidgetViewState extends State { if (controller.hasClients) { double extent = controller.position.minScrollExtent; - print("Viewport dimension: ${controller.position.viewportDimension}"); - print("Extent: ${extent}"); - if (controller.position.viewportDimension < -extent) { - print("Needs to jump to event"); extent = -controller.position.viewportDimension / 2; }