Share thread calendar cache with sticky date index

Signed-off-by: Tom Brow <tomb@block.xyz>
This commit is contained in:
Tom Brow
2026-10-18 13:34:18 -07:00
committed by Tom Brow
parent ad4b674b16
commit 85f14c4e1e
4 changed files with 81 additions and 4 deletions

View File

@@ -313,7 +313,9 @@ class ThreadDetailPage extends HookConsumerWidget {
const headIndex = 0;
int indexForReply(int chronologicalIndex) => chronologicalIndex + 1;
final tailAnchorIndex = replies.length + 1;
final calendarDays = ref.watch(timelineCalendarDaysFactoryProvider)();
final stickyDateIndex = _ThreadStickyDateIndex(
calendarDays: calendarDays,
head: liveHead,
replies: replies,
);
@@ -923,6 +925,7 @@ class ThreadDetailPage extends HookConsumerWidget {
children: [
Expanded(
child: _ThreadMessageList(
calendarDays: calendarDays,
headHeight: headHeight.value ?? 0,
onHeadHeightChanged: (height) {
if (context.mounted) headHeight.value = height;

View File

@@ -1,6 +1,7 @@
part of '../thread_detail_page.dart';
class _ThreadMessageList extends HookConsumerWidget {
class _ThreadMessageList extends HookWidget {
final TimelineCalendarDays calendarDays;
final double headHeight;
final ValueChanged<double> onHeadHeightChanged;
final LaidOutViewport viewport;
@@ -30,6 +31,7 @@ class _ThreadMessageList extends HookConsumerWidget {
final Map<String, List<TimelineMessage>> childrenByParent;
const _ThreadMessageList({
required this.calendarDays,
required this.headHeight,
required this.onHeadHeightChanged,
required this.viewport,
@@ -144,8 +146,7 @@ class _ThreadMessageList extends HookConsumerWidget {
}
@override
Widget build(BuildContext context, WidgetRef ref) {
final calendarDays = ref.watch(timelineCalendarDaysFactoryProvider)();
Widget build(BuildContext context) {
final headLink = useMemoized(LayerLink.new);
final loadingHeadLink = useMemoized(LayerLink.new);
final headScrollPosition = useRef<ScrollPosition?>(null);

View File

@@ -22,6 +22,7 @@ class _ThreadStickyDateIndex {
final int messageCount;
_ThreadStickyDateIndex({
required TimelineCalendarDays calendarDays,
required TimelineMessage head,
required List<TimelineMessage> replies,
}) : messageCount = replies.length + 1 {
@@ -32,7 +33,10 @@ class _ThreadStickyDateIndex {
final message = messages[index];
final startsDay =
index == 0 ||
!isSameDay(messages[index - 1].createdAt, message.createdAt);
!calendarDays.isSameDay(
messages[index - 1].createdAt,
message.createdAt,
);
if (startsDay) {
activeDayTimestamp = message.createdAt;
activeDayStartIndex = index;

View File

@@ -100,6 +100,75 @@ void timelineCalendarBuilderTests() {
}
});
testWidgets('thread sticky index and visible rows share all conversions', (
tester,
) async {
final conversions = <Map<int, int>>[];
final root = _textMsg(
id: threadIdThreadDashRoot,
pubkey: 'alice',
content: 'Root',
createdAt: 1000,
);
final replies = [
for (var i = 1; i <= 60; i++)
_textMsg(
id: 'calendar-long-$i',
pubkey: 'bob',
content: 'Reply $i',
createdAt: 1000 + i * 60,
extraTags: const [
['e', threadIdThreadDashRoot, '', 'reply'],
],
),
];
await tester.pumpWidget(
_buildTestable(
messages: [root],
threadReplies: {threadIdThreadDashRoot: replies},
calendarDaysFactory: () {
final counts = <int, int>{};
conversions.add(counts);
return TimelineCalendarDays.withConverter((timestamp) {
counts.update(timestamp, (count) => count + 1, ifAbsent: () => 1);
return DateTime.fromMillisecondsSinceEpoch(timestamp * 1000);
});
},
),
);
await tester.pumpAndSettle();
// Discard channel caches before mounting the thread.
conversions.clear();
final head = formatTimeline([root]).single;
Navigator.of(tester.element(find.byType(ChannelDetailPage))).push(
MaterialPageRoute<void>(
builder: (_) => ThreadDetailPage(
threadHead: head,
allMessages: [head],
channelId: _channelId,
currentPubkey: 'self',
isMember: true,
isArchived: false,
),
),
);
await tester.pumpAndSettle();
final threadCaches = conversions
.where((counts) => counts.containsKey(replies.last.createdAt))
.toList();
expect(threadCaches, isNotEmpty);
for (final counts in threadCaches) {
expect(
counts.keys,
containsAll([
root.createdAt,
...replies.map((reply) => reply.createdAt),
]),
);
expect(counts.values, everyElement(1));
}
});
testWidgets('thread shows day dividers when replies cross days', (
tester,
) async {