Skip to content

Commit a2db566

Browse files
sirpengignprice
authored andcommitted
msglist: Format dates in recipient headers
Show relative time labels such as "Today" or "Yesterday", and dates far in the past (or somehow in the future) have their year attached. Fixes: #411
1 parent ae2f3c9 commit a2db566

File tree

3 files changed

+74
-6
lines changed

3 files changed

+74
-6
lines changed

assets/l10n/app_en.arb

+8
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,14 @@
364364
"@errorMarkAsReadFailedTitle": {
365365
"description": "Error title when mark as read action failed."
366366
},
367+
"today": "Today",
368+
"@today": {
369+
"description": "Term to use to reference the current day."
370+
},
371+
"yesterday": "Yesterday",
372+
"@yesterday": {
373+
"description": "Term to use to reference the previous day."
374+
},
367375
"userRoleOwner": "Owner",
368376
"@userRoleOwner": {
369377
"description": "Label for UserRole.owner"

lib/widgets/message_list.dart

+40-3
Original file line numberDiff line numberDiff line change
@@ -710,6 +710,7 @@ class RecipientHeaderDate extends StatelessWidget {
710710

711711
@override
712712
Widget build(BuildContext context) {
713+
final zulipLocalizations = ZulipLocalizations.of(context);
713714
return Padding(
714715
padding: const EdgeInsets.fromLTRB(10, 0, 16, 0),
715716
child: Text(
@@ -725,12 +726,48 @@ class RecipientHeaderDate extends StatelessWidget {
725726
// https://developer.mozilla.org/en-US/docs/Web/CSS/font-variant-caps#all-small-caps
726727
fontFeatures: const [FontFeature.enable('c2sc'), FontFeature.enable('smcp')],
727728
).merge(weightVariableTextStyle(context)),
728-
_kRecipientHeaderDateFormat.format(
729-
DateTime.fromMillisecondsSinceEpoch(message.timestamp * 1000))));
729+
formatHeaderDate(
730+
zulipLocalizations,
731+
DateTime.fromMillisecondsSinceEpoch(message.timestamp * 1000),
732+
now: DateTime.now())));
730733
}
731734
}
732735

733-
final _kRecipientHeaderDateFormat = DateFormat('y-MM-dd', 'en_US'); // TODO(#278)
736+
@visibleForTesting
737+
String formatHeaderDate(
738+
ZulipLocalizations zulipLocalizations,
739+
DateTime dateTime, {
740+
required DateTime now,
741+
}) {
742+
assert(!dateTime.isUtc && !now.isUtc,
743+
'`dateTime` and `now` need to be in local time.');
744+
745+
if (dateTime.year == now.year &&
746+
dateTime.month == now.month &&
747+
dateTime.day == now.day) {
748+
return zulipLocalizations.today;
749+
}
750+
751+
final yesterday = now
752+
.copyWith(hour: 12, minute: 0, second: 0, millisecond: 0, microsecond: 0)
753+
.add(const Duration(days: -1));
754+
if (dateTime.year == yesterday.year &&
755+
dateTime.month == yesterday.month &&
756+
dateTime.day == yesterday.day) {
757+
return zulipLocalizations.yesterday;
758+
}
759+
760+
// If it is Dec 1 and you see a label that says `Dec 2`
761+
// it could be misinterpreted as Dec 2 of the previous
762+
// year. For times in the future, those still on the
763+
// current day will show as today (handled above) and
764+
// any dates beyond that show up with the year.
765+
if (dateTime.year == now.year && dateTime.isBefore(now)) {
766+
return DateFormat.MMMd().format(dateTime);
767+
} else {
768+
return DateFormat.yMMMd().format(dateTime);
769+
}
770+
}
734771

735772
/// A Zulip message, showing the sender's name and avatar if specified.
736773
class MessageWithPossibleSender extends StatelessWidget {

test/widgets/message_list_test.dart

+26-3
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,7 @@ void main() {
366366
testWidgets('show dates', (tester) async {
367367
await setupMessageListPage(tester, messages: [
368368
eg.streamMessage(timestamp: 1671409088),
369-
eg.dmMessage(timestamp: 1692755322, from: eg.selfUser, to: []),
369+
eg.dmMessage(timestamp: 1661219322, from: eg.selfUser, to: []),
370370
]);
371371
// We show the dates in the user's timezone. Dart's standard library
372372
// doesn't give us a way to control which timezone is used — only to
@@ -377,11 +377,34 @@ void main() {
377377
// https://github.com/dart-lang/sdk/issues/28985 (about DateTime.now, not timezone)
378378
// https://github.com/dart-lang/sdk/issues/44928 (about the Dart implementation's own internal tests)
379379
// For this test, just accept outputs corresponding to any possible timezone.
380-
tester.widget(find.textContaining(RegExp("2022-12-1[89]")));
381-
tester.widget(find.textContaining(RegExp("2023-08-2[23]")));
380+
tester.widget(find.textContaining(RegExp("Dec 1[89], 2022")));
381+
tester.widget(find.textContaining(RegExp("Aug 2[23], 2022")));
382382
});
383383
});
384384

385+
group('formatHeaderDate', () {
386+
final zulipLocalizations = GlobalLocalizations.zulipLocalizations;
387+
final now = DateTime.parse("2023-01-10 12:00");
388+
final testCases = [
389+
("2023-01-10 12:00", zulipLocalizations.today),
390+
("2023-01-10 00:00", zulipLocalizations.today),
391+
("2023-01-10 23:59", zulipLocalizations.today),
392+
("2023-01-09 23:59", zulipLocalizations.yesterday),
393+
("2023-01-09 00:00", zulipLocalizations.yesterday),
394+
("2023-01-08 00:00", "Jan 8"),
395+
("2022-12-31 00:00", "Dec 31, 2022"),
396+
// Future times
397+
("2023-01-10 19:00", zulipLocalizations.today),
398+
("2023-01-11 00:00", "Jan 11, 2023"),
399+
];
400+
for (final (dateTime, expected) in testCases) {
401+
test('$dateTime returns $expected', () {
402+
check(formatHeaderDate(zulipLocalizations, DateTime.parse(dateTime), now: now))
403+
.equals(expected);
404+
});
405+
}
406+
});
407+
385408
group('MessageWithPossibleSender', () {
386409
testWidgets('Updates avatar on RealmUserUpdateEvent', (tester) async {
387410
addTearDown(testBinding.reset);

0 commit comments

Comments
 (0)