|
| 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/model.dart'; |
6 | 9 | import '../generated/l10n/zulip_localizations.dart'; |
| 10 | +import '../model/binding.dart'; |
7 | 11 | import '../model/content.dart'; |
8 | 12 | import '../model/narrow.dart'; |
9 | 13 | import 'app_bar.dart'; |
@@ -78,7 +82,11 @@ class ProfilePage extends StatelessWidget { |
78 | 82 | style: _TextStyles.primaryFieldText), |
79 | 83 | // TODO(#197) render user status |
80 | 84 | // 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 | + ), |
82 | 90 |
|
83 | 91 | _ProfileDataTable(profileData: user.profileData), |
84 | 92 | const SizedBox(height: 16), |
@@ -295,3 +303,66 @@ class _UserWidget extends StatelessWidget { |
295 | 303 | ]))); |
296 | 304 | } |
297 | 305 | } |
| 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