diff --git a/mobile/lib/features/channels/channel_detail_page.dart b/mobile/lib/features/channels/channel_detail_page.dart index 745b0f3950..162f1fd804 100644 --- a/mobile/lib/features/channels/channel_detail_page.dart +++ b/mobile/lib/features/channels/channel_detail_page.dart @@ -675,12 +675,16 @@ class _ResolvedChannelDetailPage extends HookConsumerWidget { ? dmHeader?.presenceLabel : headerMemberLabel; - final timelineMessages = resolvedChannel.isForum - ? [] - : formatTimeline( - messagesState.value ?? const [], - currentPubkey: currentPubkey, - ); + // The page also rebuilds for read state, presence, roster and layout + // changes. Format only when the events themselves change: the notifier + // publishes a new list for every event change. + final events = messagesState.value ?? const []; + final timelineMessages = useMemoized( + () => resolvedChannel.isForum + ? [] + : formatTimeline(events, currentPubkey: currentPubkey), + [events, currentPubkey, resolvedChannel.isForum], + ); final timelineEntries = buildMainTimelineEntries( // End events still update the original card through allMessages. timelineMessages diff --git a/mobile/test/features/channels/channel_detail_page_test/loading_review_tests.dart b/mobile/test/features/channels/channel_detail_page_test/loading_review_tests.dart index 1c71be9ee6..e69e4005e7 100644 --- a/mobile/test/features/channels/channel_detail_page_test/loading_review_tests.dart +++ b/mobile/test/features/channels/channel_detail_page_test/loading_review_tests.dart @@ -446,24 +446,43 @@ void _loadingReviewTests() { ); } - testWidgets('loaded page rebuild formats the timeline once', (tester) async { + testWidgets('loaded page formats the timeline only when its events change', ( + tester, + ) async { var transforms = 0; debugOnFormatTimeline = () => transforms++; addTearDown(() => debugOnFormatTimeline = null); + final first = _textMsg( + id: 'one', + pubkey: 'alice', + content: 'Loaded message', + ); + final messages = _FakeMessagesNotifier([first]); await tester.pumpWidget( - _buildTestable( - messages: [ - _textMsg(id: 'one', pubkey: 'alice', content: 'Loaded message'), - ], - ), + _buildTestable(messages: [first], messagesNotifier: messages), ); await tester.pumpAndSettle(); + // Read state, presence and layout changes rebuild the page; formatting + // every event again for each of them stalls large channels. for (var i = 0; i < 3; i++) { transforms = 0; tester.element(find.byType(ChannelDetailPage)).markNeedsBuild(); await tester.pump(); - expect(transforms, 1); + expect(transforms, 0); } + transforms = 0; + messages.setMessages([ + first, + _textMsg( + id: 'two', + pubkey: 'alice', + createdAt: first.createdAt + 1, + content: 'Live message', + ), + ]); + await tester.pump(); + expect(transforms, 1); + expect(find.text('Live message', findRichText: true), findsOneWidget); }); testWidgets('reconnect keeps cached media in place', (tester) async {