diff --git a/assets/l10n/app_en.arb b/assets/l10n/app_en.arb index 8d4cb1d2209..8a005225d84 100644 --- a/assets/l10n/app_en.arb +++ b/assets/l10n/app_en.arb @@ -364,6 +364,14 @@ "@errorMarkAsReadFailedTitle": { "description": "Error title when mark as read action failed." }, + "today": "Today", + "@today": { + "description": "Term to use to reference the current day." + }, + "yesterday": "Yesterday", + "@yesterday": { + "description": "Term to use to reference the previous day." + }, "userRoleOwner": "Owner", "@userRoleOwner": { "description": "Label for UserRole.owner" diff --git a/lib/widgets/message_list.dart b/lib/widgets/message_list.dart index 254b55ac218..c7bf75bf567 100644 --- a/lib/widgets/message_list.dart +++ b/lib/widgets/message_list.dart @@ -710,6 +710,7 @@ class RecipientHeaderDate extends StatelessWidget { @override Widget build(BuildContext context) { + final zulipLocalizations = ZulipLocalizations.of(context); return Padding( padding: const EdgeInsets.fromLTRB(10, 0, 16, 0), child: Text( @@ -725,12 +726,47 @@ class RecipientHeaderDate extends StatelessWidget { // https://developer.mozilla.org/en-US/docs/Web/CSS/font-variant-caps#all-small-caps fontFeatures: const [FontFeature.enable('c2sc'), FontFeature.enable('smcp')], ).merge(weightVariableTextStyle(context)), - _kRecipientHeaderDateFormat.format( - DateTime.fromMillisecondsSinceEpoch(message.timestamp * 1000)))); + formatHeaderDate( + zulipLocalizations, + DateTime.fromMillisecondsSinceEpoch(message.timestamp * 1000), + now: DateTime.now()))); } } -final _kRecipientHeaderDateFormat = DateFormat('y-MM-dd', 'en_US'); // TODO(#278) +@visibleForTesting +String formatHeaderDate( + ZulipLocalizations zulipLocalizations, + DateTime dateTime, { + required DateTime now, +}) { + assert(!dateTime.isUtc && !dateTime.isUtc, + '`dateTime` and `now` need to be in local time.'); + if (dateTime.year == now.year && + dateTime.month == now.month && + dateTime.day == now.day) { + return zulipLocalizations.today; + } + + final yesterday = now + .copyWith(hour: 12, minute: 0, second: 0, millisecond: 0, microsecond: 0) + .add(const Duration(days: -1)); + if (dateTime.year == yesterday.year && + dateTime.month == yesterday.month && + dateTime.day == yesterday.day) { + return zulipLocalizations.yesterday; + } + + // If it is Dec 1 and you see a label that says `Dec 2` + // it could be misinterpreted as Dec 2 of the previous + // year. For times in the future, those still on the + // current day will show as today (handled above) and + // any dates beyond that show up with the year. + if (dateTime.year == now.year && dateTime.isBefore(now)) { + return DateFormat.MMMd().format(dateTime); + } else { + return DateFormat.yMMMd().format(dateTime); + } +} /// A Zulip message, showing the sender's name and avatar if specified. class MessageWithPossibleSender extends StatelessWidget { diff --git a/test/widgets/message_list_test.dart b/test/widgets/message_list_test.dart index 7696383e82d..fd8fc1385e1 100644 --- a/test/widgets/message_list_test.dart +++ b/test/widgets/message_list_test.dart @@ -366,7 +366,7 @@ void main() { testWidgets('show dates', (tester) async { await setupMessageListPage(tester, messages: [ eg.streamMessage(timestamp: 1671409088), - eg.dmMessage(timestamp: 1692755322, from: eg.selfUser, to: []), + eg.dmMessage(timestamp: 1661219322, from: eg.selfUser, to: []), ]); // We show the dates in the user's timezone. Dart's standard library // doesn't give us a way to control which timezone is used — only to @@ -377,11 +377,34 @@ void main() { // https://github.com/dart-lang/sdk/issues/28985 (about DateTime.now, not timezone) // https://github.com/dart-lang/sdk/issues/44928 (about the Dart implementation's own internal tests) // For this test, just accept outputs corresponding to any possible timezone. - tester.widget(find.textContaining(RegExp("2022-12-1[89]"))); - tester.widget(find.textContaining(RegExp("2023-08-2[23]"))); + tester.widget(find.textContaining(RegExp("Dec 1[89], 2022"))); + tester.widget(find.textContaining(RegExp("Aug 2[23], 2022"))); }); }); + group('formatHeaderDate', () { + final zulipLocalizations = GlobalLocalizations.zulipLocalizations; + final now = DateTime(2023, 1, 10, 12); + final testCases = [ + ("2023-01-10 12:00", zulipLocalizations.today), + ("2023-01-10 00:00", zulipLocalizations.today), + ("2023-01-10 23:59", zulipLocalizations.today), + ("2023-01-09 23:59", zulipLocalizations.yesterday), + ("2023-01-09 00:00", zulipLocalizations.yesterday), + ("2023-01-08 00:00", "Jan 8"), + ("2022-12-31 00:00", "Dec 31, 2022"), + // Future times + ("2023-01-10 19:00", zulipLocalizations.today), + ("2023-01-11 00:00", "Jan 11, 2023"), + ]; + for (final (dateTime, expected) in testCases) { + test('$dateTime returns $expected', () { + check(formatHeaderDate(zulipLocalizations, DateTime.parse(dateTime), now: now)) + .equals(expected); + }); + } + }); + group('MessageWithPossibleSender', () { testWidgets('Updates avatar on RealmUserUpdateEvent', (tester) async { addTearDown(testBinding.reset);