Skip to content

Commit 3e7bf4c

Browse files
committed
profile: display user's local time
1 parent d04f896 commit 3e7bf4c

File tree

1 file changed

+72
-1
lines changed

1 file changed

+72
-1
lines changed

lib/widgets/profile.dart

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
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/model.dart';
69
import '../generated/l10n/zulip_localizations.dart';
10+
import '../model/binding.dart';
711
import '../model/content.dart';
812
import '../model/narrow.dart';
913
import 'app_bar.dart';
@@ -78,7 +82,11 @@ class ProfilePage extends StatelessWidget {
7882
style: _TextStyles.primaryFieldText),
7983
// TODO(#197) render user status
8084
// TODO(#196) render active status
81-
// TODO(#292) render user local time
85+
DefaultTextStyle.merge(
86+
textAlign: TextAlign.center,
87+
style: _TextStyles.primaryFieldText,
88+
child: UserLocalTimeText(user: user)
89+
),
8290

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

0 commit comments

Comments
 (0)