|
| 1 | +import 'dart:async'; |
1 | 2 | import 'dart:convert';
|
2 | 3 |
|
3 | 4 | import 'package:flutter/material.dart';
|
| 5 | +import 'package:flutter/services.dart'; |
| 6 | +import 'package:timezone/timezone.dart' as tz; |
4 | 7 |
|
5 | 8 | import '../api/model/initial_snapshot.dart';
|
6 | 9 | import '../api/model/model.dart';
|
7 | 10 | import '../generated/l10n/zulip_localizations.dart';
|
| 11 | +import '../model/binding.dart'; |
8 | 12 | import '../model/content.dart';
|
9 | 13 | import '../model/narrow.dart';
|
10 | 14 | import '../model/store.dart';
|
@@ -90,7 +94,11 @@ class ProfilePage extends StatelessWidget {
|
90 | 94 | style: _TextStyles.primaryFieldText),
|
91 | 95 | // TODO(#197) render user status
|
92 | 96 | // 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 | + ), |
94 | 102 |
|
95 | 103 | _ProfileDataTable(profileData: user.profileData),
|
96 | 104 | const SizedBox(height: 16),
|
@@ -307,3 +315,66 @@ class _UserWidget extends StatelessWidget {
|
307 | 315 | ])));
|
308 | 316 | }
|
309 | 317 | }
|
| 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 | +} |
0 commit comments