diff --git a/lib/model/binding.dart b/lib/model/binding.dart index 50477ec01a..efffb07747 100644 --- a/lib/model/binding.dart +++ b/lib/model/binding.dart @@ -118,6 +118,12 @@ abstract class ZulipBinding { /// Outside tests, this just calls the [Stopwatch] constructor. Stopwatch stopwatch(); + /// Provides access to current time. + /// + /// Please refer to this issue: + /// https://github.com/dart-lang/sdk/issues/28985 + DateTime now(); + /// Provides device and operating system information, /// via package:device_info_plus. /// @@ -368,6 +374,9 @@ class LiveZulipBinding extends ZulipBinding { @override Stopwatch stopwatch() => Stopwatch(); + @override + DateTime now() => DateTime.now(); + @override Future get deviceInfo => _deviceInfo; late Future _deviceInfo; diff --git a/lib/model/store.dart b/lib/model/store.dart index 7603c7f452..319a036635 100644 --- a/lib/model/store.dart +++ b/lib/model/store.dart @@ -20,6 +20,7 @@ import '../api/route/realm.dart'; import '../log.dart'; import '../notifications/receive.dart'; import 'autocomplete.dart'; +import 'binding.dart'; import 'database.dart'; import 'emoji.dart'; import 'localizations.dart'; @@ -447,7 +448,7 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, ChannelStore, Mess /// /// To determine if a user is a full member, callers must also check that the /// user's role is at least [UserRole.member]. - bool hasPassedWaitingPeriod(User user, {required DateTime byDate}) { + bool hasPassedWaitingPeriod(User user) { // [User.dateJoined] is in UTC. For logged-in users, the format is: // YYYY-MM-DDTHH:mm+00:00, which includes the timezone offset for UTC. // For logged-out spectators, the format is: YYYY-MM-DD, which doesn't @@ -459,7 +460,8 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, ChannelStore, Mess // See the related discussion: // https://chat.zulip.org/#narrow/channel/412-api-documentation/topic/provide.20an.20explicit.20format.20for.20.60realm_user.2Edate_joined.60/near/1980194 final dateJoined = DateTime.parse(user.dateJoined); - return byDate.difference(dateJoined).inDays >= realmWaitingPeriodThreshold; + final now = ZulipBinding.instance.now(); + return now.difference(dateJoined).inDays >= realmWaitingPeriodThreshold; } //////////////////////////////// @@ -483,7 +485,6 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, ChannelStore, Mess bool hasPostingPermission({ required ZulipStream inChannel, required User user, - required DateTime byDate, }) { final role = user.role; // We let the users with [unknown] role to send the message, then the server @@ -495,7 +496,7 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, ChannelStore, Mess case ChannelPostPolicy.fullMembers: { if (!role.isAtLeast(UserRole.member)) return false; return role == UserRole.member - ? hasPassedWaitingPeriod(user, byDate: byDate) + ? hasPassedWaitingPeriod(user) : true; } case ChannelPostPolicy.moderators: return role.isAtLeast(UserRole.moderator); diff --git a/lib/widgets/compose_box.dart b/lib/widgets/compose_box.dart index 2b1756e4fe..55c7894f73 100644 --- a/lib/widgets/compose_box.dart +++ b/lib/widgets/compose_box.dart @@ -1410,8 +1410,7 @@ class _ComposeBoxState extends State with PerAccountStoreAwareStateM case ChannelNarrow(:final streamId): case TopicNarrow(:final streamId): final channel = store.streams[streamId]; - if (channel == null || !store.hasPostingPermission(inChannel: channel, - user: selfUser, byDate: DateTime.now())) { + if (channel == null || !store.hasPostingPermission(inChannel: channel, user: selfUser)) { return _ErrorBanner(label: ZulipLocalizations.of(context).errorBannerCannotPostInChannelLabel); } diff --git a/lib/widgets/message_list.dart b/lib/widgets/message_list.dart index 06ecff110f..51fb2812c5 100644 --- a/lib/widgets/message_list.dart +++ b/lib/widgets/message_list.dart @@ -7,6 +7,7 @@ import 'package:intl/intl.dart' hide TextDirection; import '../api/model/model.dart'; import '../generated/l10n/zulip_localizations.dart'; +import '../model/binding.dart'; import '../model/message_list.dart'; import '../model/narrow.dart'; import '../model/store.dart'; @@ -1271,19 +1272,19 @@ class DateText extends StatelessWidget { ), formatHeaderDate( zulipLocalizations, - DateTime.fromMillisecondsSinceEpoch(timestamp * 1000), - now: DateTime.now())); + DateTime.fromMillisecondsSinceEpoch(timestamp * 1000))); } } @visibleForTesting String formatHeaderDate( ZulipLocalizations zulipLocalizations, - DateTime dateTime, { - required DateTime now, -}) { - assert(!dateTime.isUtc && !now.isUtc, - '`dateTime` and `now` need to be in local time.'); + DateTime dateTime, +) { + assert(!dateTime.isUtc, + '`dateTime` need to be in local time.'); + + final now = ZulipBinding.instance.now(); if (dateTime.year == now.year && dateTime.month == now.month && diff --git a/test/model/binding.dart b/test/model/binding.dart index 039d6c3787..3102173018 100644 --- a/test/model/binding.dart +++ b/test/model/binding.dart @@ -214,6 +214,9 @@ class TestZulipBinding extends ZulipBinding { @override Stopwatch stopwatch() => clock.stopwatch(); + @override + DateTime now() => clock.now(); + /// The value that `ZulipBinding.instance.deviceInfo` should return. BaseDeviceInfo deviceInfoResult = _defaultDeviceInfoResult; static const _defaultDeviceInfoResult = AndroidDeviceInfo(sdkInt: 33, release: '13'); diff --git a/test/model/store_test.dart b/test/model/store_test.dart index bc393d6d6f..7d3fb35f73 100644 --- a/test/model/store_test.dart +++ b/test/model/store_test.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'package:checks/checks.dart'; +import 'package:clock/clock.dart'; import 'package:fake_async/fake_async.dart'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; @@ -260,9 +261,11 @@ void main() { for (final (String dateJoined, DateTime currentDate, bool hasPassedWaitingPeriod) in testCases) { test('user joined at $dateJoined ${hasPassedWaitingPeriod ? 'has' : "hasn't"} ' 'passed waiting period by $currentDate', () { - final user = eg.user(dateJoined: dateJoined); - check(store.hasPassedWaitingPeriod(user, byDate: currentDate)) - .equals(hasPassedWaitingPeriod); + withClock(Clock.fixed(currentDate), () { + final user = eg.user(dateJoined: dateJoined); + check(store.hasPassedWaitingPeriod(user)) + .equals(hasPassedWaitingPeriod); + }); }); } }); @@ -306,11 +309,10 @@ void main() { test('"${role.name}" user ${canPost ? 'can' : "can't"} post in channel ' 'with "${policy.name}" policy', () { final store = eg.store(); + // we don't use `withClock` because current time is not actually relevant for + // these test cases; for the ones which it is, they're practiced below. final actual = store.hasPostingPermission( - inChannel: eg.stream(channelPostPolicy: policy), user: eg.user(role: role), - // [byDate] is not actually relevant for these test cases; for the - // ones which it is, they're practiced below. - byDate: DateTime.now()); + inChannel: eg.stream(channelPostPolicy: policy), user: eg.user(role: role)); check(actual).equals(canPost); }); } @@ -324,21 +326,23 @@ void main() { role: UserRole.member, dateJoined: dateJoined); test('a "full" member -> can post in the channel', () { - final store = localStore(realmWaitingPeriodThreshold: 3); - final hasPermission = store.hasPostingPermission( - inChannel: eg.stream(channelPostPolicy: ChannelPostPolicy.fullMembers), - user: memberUser(dateJoined: '2024-11-25T10:00+00:00'), - byDate: DateTime.utc(2024, 11, 28, 10, 00)); - check(hasPermission).isTrue(); + withClock(Clock.fixed(DateTime.utc(2024, 11, 28, 10, 00)), () { + final store = localStore(realmWaitingPeriodThreshold: 3); + final hasPermission = store.hasPostingPermission( + inChannel: eg.stream(channelPostPolicy: ChannelPostPolicy.fullMembers), + user: memberUser(dateJoined: '2024-11-25T10:00+00:00')); + check(hasPermission).isTrue(); + }); }); test('not a "full" member -> cannot post in the channel', () { - final store = localStore(realmWaitingPeriodThreshold: 3); - final actual = store.hasPostingPermission( - inChannel: eg.stream(channelPostPolicy: ChannelPostPolicy.fullMembers), - user: memberUser(dateJoined: '2024-11-25T10:00+00:00'), - byDate: DateTime.utc(2024, 11, 28, 09, 59)); - check(actual).isFalse(); + withClock(Clock.fixed(DateTime.utc(2024, 11, 28, 09, 59)), () { + final store = localStore(realmWaitingPeriodThreshold: 3); + final actual = store.hasPostingPermission( + inChannel: eg.stream(channelPostPolicy: ChannelPostPolicy.fullMembers), + user: memberUser(dateJoined: '2024-11-25T10:00+00:00')); + check(actual).isFalse(); + }); }); }); }); diff --git a/test/widgets/message_list_test.dart b/test/widgets/message_list_test.dart index 3f79f8cae6..06f871408f 100644 --- a/test/widgets/message_list_test.dart +++ b/test/widgets/message_list_test.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:checks/checks.dart'; +import 'package:clock/clock.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; @@ -1107,7 +1108,7 @@ void main() { .initNarrow.equals(DmNarrow.withUser(eg.otherUser.userId, selfUserId: eg.selfUser.userId)); await tester.pumpAndSettle(); }); - + testWidgets('does not navigate on tapping recipient header in DmNarrow', (tester) async { final pushedRoutes = >[]; final navObserver = TestNavigatorObserver() @@ -1129,7 +1130,6 @@ void main() { group('formatHeaderDate', () { final zulipLocalizations = GlobalLocalizations.zulipLocalizations; - final now = DateTime.parse("2023-01-10 12:00"); final testCases = [ ("2023-01-10 12:00", zulipLocalizations.today), ("2023-01-10 00:00", zulipLocalizations.today), @@ -1144,8 +1144,10 @@ void main() { ]; for (final (dateTime, expected) in testCases) { test('$dateTime returns $expected', () { - check(formatHeaderDate(zulipLocalizations, DateTime.parse(dateTime), now: now)) - .equals(expected); + withClock(Clock.fixed(DateTime.parse("2023-01-10 12:00")), () { + check(formatHeaderDate(zulipLocalizations, DateTime.parse(dateTime))) + .equals(expected); + }); }); } });