Skip to content

Commit 2a87acd

Browse files
committed
squashed commit
1 parent 44df81f commit 2a87acd

26 files changed

+335
-35
lines changed

assets/l10n/app_ar.arb

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,12 @@
77
"wildcardMentionChannelDescription": "إخطار القناة",
88
"wildcardMentionStreamDescription": "إخطار الدفق",
99
"wildcardMentionAllDmDescription": "إخطار المستلمين",
10-
"wildcardMentionTopicDescription": "إخطار الموضوع"
10+
"wildcardMentionTopicDescription": "إخطار الموضوع",
11+
"userLocalTime": "{userTime} الوقت المحلي",
12+
"@userLocalTime": {
13+
"description": "Current time in the user's timezone",
14+
"placeholders": {
15+
"userTime": {"type": "DateTime", "format": "jm"}
16+
}
17+
}
1118
}

assets/l10n/app_en.arb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -824,5 +824,12 @@
824824
"zulipAppTitle": "Zulip",
825825
"@zulipAppTitle": {
826826
"description": "The name of Zulip. This should be either 'Zulip' or a transliteration."
827+
},
828+
"userLocalTime": "{userTime} local time",
829+
"@userLocalTime": {
830+
"description": "Current time in the user's timezone",
831+
"placeholders": {
832+
"userTime": {"type": "DateTime", "format": "jm"}
833+
}
827834
}
828835
}

assets/l10n/app_ja.arb

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,12 @@
1616
"userRoleGuest": "ゲスト",
1717
"@userRoleGuest": {},
1818
"userRoleUnknown": "不明",
19-
"@userRoleUnknown": {}
19+
"@userRoleUnknown": {},
20+
"userLocalTime": "現地時間 {userTime}",
21+
"@userLocalTime": {
22+
"description": "Current time in the user's timezone",
23+
"placeholders": {
24+
"userTime": {"type": "DateTime", "format": "jm"}
25+
}
26+
}
2027
}

assets/l10n/app_pl.arb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -900,5 +900,12 @@
900900
"unpinnedSubscriptionsLabel": "Odpięte",
901901
"@unpinnedSubscriptionsLabel": {
902902
"description": "Label for the list of unpinned subscribed channels."
903+
},
904+
"userLocalTime": "{userTime} czas lokalny",
905+
"@userLocalTime": {
906+
"description": "Current time in the user's timezone",
907+
"placeholders": {
908+
"userTime": {"type": "DateTime", "format": "jm"}
909+
}
903910
}
904911
}

assets/l10n/app_ru.arb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -772,5 +772,12 @@
772772
"errorMessageNotSent": "Сообщение не отправлено",
773773
"@errorMessageNotSent": {
774774
"description": "Error message for compose box when a message could not be sent."
775+
},
776+
"userLocalTime": "{userTime} местное время",
777+
"@userLocalTime": {
778+
"description": "Current time in the user's timezone",
779+
"placeholders": {
780+
"userTime": {"type": "DateTime", "format": "jm"}
781+
}
775782
}
776783
}

assets/timezone/latest_all.tzf

433 KB
Binary file not shown.

lib/generated/l10n/zulip_localizations.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1208,6 +1208,12 @@ abstract class ZulipLocalizations {
12081208
/// In en, this message translates to:
12091209
/// **'Zulip'**
12101210
String get zulipAppTitle;
1211+
1212+
/// Current time in the user's timezone
1213+
///
1214+
/// In en, this message translates to:
1215+
/// **'{userTime} local time'**
1216+
String userLocalTime(DateTime userTime);
12111217
}
12121218

12131219
class _ZulipLocalizationsDelegate extends LocalizationsDelegate<ZulipLocalizations> {

lib/generated/l10n/zulip_localizations_ar.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,4 +643,12 @@ class ZulipLocalizationsAr extends ZulipLocalizations {
643643

644644
@override
645645
String get zulipAppTitle => 'Zulip';
646+
647+
@override
648+
String userLocalTime(DateTime userTime) {
649+
final intl.DateFormat userTimeDateFormat = intl.DateFormat.jm(localeName);
650+
final String userTimeString = userTimeDateFormat.format(userTime);
651+
652+
return '$userTimeString الوقت المحلي';
653+
}
646654
}

lib/generated/l10n/zulip_localizations_en.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,4 +643,12 @@ class ZulipLocalizationsEn extends ZulipLocalizations {
643643

644644
@override
645645
String get zulipAppTitle => 'Zulip';
646+
647+
@override
648+
String userLocalTime(DateTime userTime) {
649+
final intl.DateFormat userTimeDateFormat = intl.DateFormat.jm(localeName);
650+
final String userTimeString = userTimeDateFormat.format(userTime);
651+
652+
return '$userTimeString local time';
653+
}
646654
}

lib/generated/l10n/zulip_localizations_ja.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,4 +643,12 @@ class ZulipLocalizationsJa extends ZulipLocalizations {
643643

644644
@override
645645
String get zulipAppTitle => 'Zulip';
646+
647+
@override
648+
String userLocalTime(DateTime userTime) {
649+
final intl.DateFormat userTimeDateFormat = intl.DateFormat.jm(localeName);
650+
final String userTimeString = userTimeDateFormat.format(userTime);
651+
652+
return '現地時間 $userTimeString';
653+
}
646654
}

lib/generated/l10n/zulip_localizations_nb.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,4 +643,12 @@ class ZulipLocalizationsNb extends ZulipLocalizations {
643643

644644
@override
645645
String get zulipAppTitle => 'Zulip';
646+
647+
@override
648+
String userLocalTime(DateTime userTime) {
649+
final intl.DateFormat userTimeDateFormat = intl.DateFormat.jm(localeName);
650+
final String userTimeString = userTimeDateFormat.format(userTime);
651+
652+
return '$userTimeString local time';
653+
}
646654
}

lib/generated/l10n/zulip_localizations_pl.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,4 +643,12 @@ class ZulipLocalizationsPl extends ZulipLocalizations {
643643

644644
@override
645645
String get zulipAppTitle => 'Zulip';
646+
647+
@override
648+
String userLocalTime(DateTime userTime) {
649+
final intl.DateFormat userTimeDateFormat = intl.DateFormat.jm(localeName);
650+
final String userTimeString = userTimeDateFormat.format(userTime);
651+
652+
return '$userTimeString czas lokalny';
653+
}
646654
}

lib/generated/l10n/zulip_localizations_ru.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,4 +643,12 @@ class ZulipLocalizationsRu extends ZulipLocalizations {
643643

644644
@override
645645
String get zulipAppTitle => 'Zulip';
646+
647+
@override
648+
String userLocalTime(DateTime userTime) {
649+
final intl.DateFormat userTimeDateFormat = intl.DateFormat.jm(localeName);
650+
final String userTimeString = userTimeDateFormat.format(userTime);
651+
652+
return '$userTimeString местное время';
653+
}
646654
}

lib/generated/l10n/zulip_localizations_sk.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,4 +643,12 @@ class ZulipLocalizationsSk extends ZulipLocalizations {
643643

644644
@override
645645
String get zulipAppTitle => 'Zulip';
646+
647+
@override
648+
String userLocalTime(DateTime userTime) {
649+
final intl.DateFormat userTimeDateFormat = intl.DateFormat.jm(localeName);
650+
final String userTimeString = userTimeDateFormat.format(userTime);
651+
652+
return '$userTimeString local time';
653+
}
646654
}

lib/model/binding.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,12 @@ abstract class ZulipBinding {
118118
/// Outside tests, this just calls the [Stopwatch] constructor.
119119
Stopwatch stopwatch();
120120

121+
/// Testable [DateTime.now].
122+
///
123+
/// Please refer to this issue:
124+
/// https://github.com/dart-lang/sdk/issues/28985
125+
DateTime now();
126+
121127
/// Provides device and operating system information,
122128
/// via package:device_info_plus.
123129
///
@@ -368,6 +374,9 @@ class LiveZulipBinding extends ZulipBinding {
368374
@override
369375
Stopwatch stopwatch() => Stopwatch();
370376

377+
@override
378+
DateTime now() => DateTime.now();
379+
371380
@override
372381
Future<BaseDeviceInfo?> get deviceInfo => _deviceInfo;
373382
late Future<BaseDeviceInfo?> _deviceInfo;

lib/model/store.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import '../api/route/realm.dart';
2020
import '../log.dart';
2121
import '../notifications/receive.dart';
2222
import 'autocomplete.dart';
23+
import 'binding.dart';
2324
import 'database.dart';
2425
import 'emoji.dart';
2526
import 'localizations.dart';
@@ -483,7 +484,6 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, ChannelStore, Mess
483484
bool hasPostingPermission({
484485
required ZulipStream inChannel,
485486
required User user,
486-
required DateTime byDate,
487487
}) {
488488
final role = user.role;
489489
// We let the users with [unknown] role to send the message, then the server
@@ -495,7 +495,7 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, ChannelStore, Mess
495495
case ChannelPostPolicy.fullMembers: {
496496
if (!role.isAtLeast(UserRole.member)) return false;
497497
return role == UserRole.member
498-
? hasPassedWaitingPeriod(user, byDate: byDate)
498+
? hasPassedWaitingPeriod(user, byDate: ZulipBinding.instance.now())
499499
: true;
500500
}
501501
case ChannelPostPolicy.moderators: return role.isAtLeast(UserRole.moderator);

lib/widgets/compose_box.dart

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1410,8 +1410,7 @@ class _ComposeBoxState extends State<ComposeBox> with PerAccountStoreAwareStateM
14101410
case ChannelNarrow(:final streamId):
14111411
case TopicNarrow(:final streamId):
14121412
final channel = store.streams[streamId];
1413-
if (channel == null || !store.hasPostingPermission(inChannel: channel,
1414-
user: selfUser, byDate: DateTime.now())) {
1413+
if (channel == null || !store.hasPostingPermission(inChannel: channel, user: selfUser)) {
14151414
return _ErrorBanner(label:
14161415
ZulipLocalizations.of(context).errorBannerCannotPostInChannelLabel);
14171416
}

lib/widgets/message_list.dart

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'package:intl/intl.dart' hide TextDirection;
77

88
import '../api/model/model.dart';
99
import '../generated/l10n/zulip_localizations.dart';
10+
import '../model/binding.dart';
1011
import '../model/message_list.dart';
1112
import '../model/narrow.dart';
1213
import '../model/store.dart';
@@ -1271,19 +1272,19 @@ class DateText extends StatelessWidget {
12711272
),
12721273
formatHeaderDate(
12731274
zulipLocalizations,
1274-
DateTime.fromMillisecondsSinceEpoch(timestamp * 1000),
1275-
now: DateTime.now()));
1275+
DateTime.fromMillisecondsSinceEpoch(timestamp * 1000)));
12761276
}
12771277
}
12781278

12791279
@visibleForTesting
12801280
String formatHeaderDate(
12811281
ZulipLocalizations zulipLocalizations,
1282-
DateTime dateTime, {
1283-
required DateTime now,
1284-
}) {
1285-
assert(!dateTime.isUtc && !now.isUtc,
1286-
'`dateTime` and `now` need to be in local time.');
1282+
DateTime dateTime,
1283+
) {
1284+
assert(!dateTime.isUtc,
1285+
'`dateTime` need to be in local time.');
1286+
1287+
final now = ZulipBinding.instance.now();
12871288

12881289
if (dateTime.year == now.year &&
12891290
dateTime.month == now.month &&

lib/widgets/profile.dart

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1+
import 'dart:async';
12
import 'dart:convert';
23

34
import 'package:flutter/material.dart';
5+
import 'package:flutter/services.dart';
6+
import 'package:timezone/timezone.dart' as tz;
47

58
import '../api/model/initial_snapshot.dart';
69
import '../api/model/model.dart';
710
import '../generated/l10n/zulip_localizations.dart';
11+
import '../model/binding.dart';
812
import '../model/content.dart';
913
import '../model/narrow.dart';
1014
import '../model/store.dart';
@@ -90,7 +94,11 @@ class ProfilePage extends StatelessWidget {
9094
style: _TextStyles.primaryFieldText),
9195
// TODO(#197) render user status
9296
// TODO(#196) render active status
93-
// TODO(#292) render user local time
97+
DefaultTextStyle.merge(
98+
textAlign: TextAlign.center,
99+
style: _TextStyles.primaryFieldText,
100+
child: UserLocalTimeText(user: user)
101+
),
94102

95103
_ProfileDataTable(profileData: user.profileData),
96104
const SizedBox(height: 16),
@@ -307,3 +315,66 @@ class _UserWidget extends StatelessWidget {
307315
])));
308316
}
309317
}
318+
319+
/// The text of current time in [user]'s timezone.
320+
class UserLocalTimeText extends StatefulWidget {
321+
const UserLocalTimeText({
322+
super.key,
323+
required this.user,
324+
});
325+
326+
final User user;
327+
328+
/// Initialize the timezone database used to know time difference from a timezone string.
329+
///
330+
/// Usually, database initialization is done using `initializeTimeZones`, but it takes >100ms and not asynchronous.
331+
/// So, we initialize database from the assets file copied from timezone library.
332+
/// This file is checked up-to-date in `test/widgets/profile_test.dart`.
333+
static Future<void> initializeTimezonesUsingAssets() async {
334+
final blob = Uint8List.sublistView(await rootBundle.load('assets/timezone/latest_all.tzf'));
335+
tz.initializeDatabase(blob);
336+
}
337+
338+
@override
339+
State<UserLocalTimeText> createState() => _UserLocalTimeTextState();
340+
}
341+
342+
class _UserLocalTimeTextState extends State<UserLocalTimeText> {
343+
late final Timer _timer;
344+
final StreamController<DateTime> _streamController = StreamController();
345+
Stream<DateTime> get _stream => _streamController.stream;
346+
347+
@override
348+
void initState() {
349+
_streamController.add(ZulipBinding.instance.now());
350+
_timer = Timer.periodic(const Duration(seconds: 1), (_) { _streamController.add(ZulipBinding.instance.now()); });
351+
super.initState();
352+
}
353+
354+
@override
355+
void dispose() {
356+
_timer.cancel();
357+
super.dispose();
358+
}
359+
360+
Stream<String> _getDisplayLocalTimeFor(User user, ZulipLocalizations zulipLocalizations) async* {
361+
if (!tz.timeZoneDatabase.isInitialized) await UserLocalTimeText.initializeTimezonesUsingAssets();
362+
363+
await for (final DateTime time in _stream) {
364+
final location = tz.getLocation(user.timezone);
365+
final localTime = tz.TZDateTime.from(time, location);
366+
yield zulipLocalizations.userLocalTime(localTime);
367+
}
368+
}
369+
370+
@override
371+
Widget build(BuildContext context) {
372+
return StreamBuilder(
373+
stream: _getDisplayLocalTimeFor(widget.user, ZulipLocalizations.of(context)),
374+
builder: (context, snapshot) {
375+
if (snapshot.hasError) Error.throwWithStackTrace(snapshot.error!, snapshot.stackTrace!);
376+
return Text(snapshot.data ?? '');
377+
}
378+
);
379+
}
380+
}

pubspec.lock

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1104,6 +1104,14 @@ packages:
11041104
url: "https://pub.dev"
11051105
source: hosted
11061106
version: "0.6.8"
1107+
timezone:
1108+
dependency: "direct main"
1109+
description:
1110+
name: timezone
1111+
sha256: ffc9d5f4d1193534ef051f9254063fa53d588609418c84299956c3db9383587d
1112+
url: "https://pub.dev"
1113+
source: hosted
1114+
version: "0.10.0"
11071115
timing:
11081116
dependency: transitive
11091117
description:

pubspec.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ dependencies:
6464
wakelock_plus: ^1.2.8
6565
zulip_plugin:
6666
path: ./packages/zulip_plugin
67+
timezone: ^0.10.0
6768
# Keep list sorted when adding dependencies; it helps prevent merge conflicts.
6869

6970
dependency_overrides:
@@ -114,6 +115,7 @@ flutter:
114115
uses-material-design: true
115116

116117
assets:
118+
- assets/timezone/latest_all.tzf
117119
- assets/Noto_Color_Emoji/LICENSE
118120
- assets/Pygments/AUTHORS.txt
119121
- assets/Pygments/LICENSE.txt

0 commit comments

Comments
 (0)