diff --git a/mobile/lib/features/channels/date_formatters.dart b/mobile/lib/features/channels/date_formatters.dart index 60ed7fd573..b165394ed4 100644 --- a/mobile/lib/features/channels/date_formatters.dart +++ b/mobile/lib/features/channels/date_formatters.dart @@ -85,10 +85,21 @@ bool isSameDay(int a, int b) { /// avatars, and sticky headers. Resolve each timestamp only once. Create a new /// instance on each build so a timezone change cannot leave stale cached days. class TimelineCalendarDays { + /// Creates a calendar cache using local-time conversion. + TimelineCalendarDays() : _toLocalDate = _localDate; + + /// Creates a cache with an observable conversion for regression tests. + @visibleForTesting + TimelineCalendarDays.withConverter(this._toLocalDate); + + final DateTime Function(int) _toLocalDate; final _days = {}; + static DateTime _localDate(int unixSeconds) => + DateTime.fromMillisecondsSinceEpoch(unixSeconds * 1000); + (int, int, int) _day(int unixSeconds) => _days.putIfAbsent(unixSeconds, () { - final date = DateTime.fromMillisecondsSinceEpoch(unixSeconds * 1000); + final date = _toLocalDate(unixSeconds); return (date.year, date.month, date.day); }); diff --git a/mobile/test/features/channels/date_formatters_test.dart b/mobile/test/features/channels/date_formatters_test.dart index 329a46eab3..0783fd2902 100644 --- a/mobile/test/features/channels/date_formatters_test.dart +++ b/mobile/test/features/channels/date_formatters_test.dart @@ -5,6 +5,29 @@ import 'package:buzz/features/channels/date_formatters.dart'; int _ts(DateTime local) => local.millisecondsSinceEpoch ~/ 1000; void main() { + test('timeline converts each timestamp only once per cache', () { + final conversions = {}; + DateTime convert(int timestamp) { + conversions.update(timestamp, (count) => count + 1, ifAbsent: () => 1); + return DateTime.fromMillisecondsSinceEpoch(timestamp * 1000); + } + + final first = _ts(DateTime(2026, 10, 18, 10)); + final second = _ts(DateTime(2026, 10, 18, 11)); + final nextDay = _ts(DateTime(2026, 10, 19)); + final days = TimelineCalendarDays.withConverter(convert); + for (var repeat = 0; repeat < 3; repeat++) { + expect(days.isSameDay(first, second), isTrue); + expect(days.isSameDay(second, nextDay), isFalse); + expect(days.isSameDay(first, first), isTrue); + } + expect(conversions, {first: 1, second: 1, nextDay: 1}); + + final nextBuild = TimelineCalendarDays.withConverter(convert); + expect(nextBuild.isSameDay(first, second), isTrue); + expect(conversions, {first: 2, second: 2, nextDay: 1}); + }); + test('timeline calendar grouping matches uncached local-day comparisons', () { final days = TimelineCalendarDays(); final timestamps = [