|
| 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'; |
@@ -62,7 +66,11 @@ class ProfilePage extends StatelessWidget { |
62 | 66 | style: _TextStyles.primaryFieldText), |
63 | 67 | // TODO(#197) render user status |
64 | 68 | // TODO(#196) render active status |
65 | | - // TODO(#292) render user local time |
| 69 | + DefaultTextStyle.merge( |
| 70 | + textAlign: TextAlign.center, |
| 71 | + style: _TextStyles.primaryFieldText, |
| 72 | + child: UserLocalTimeText(user: user) |
| 73 | + ), |
66 | 74 |
|
67 | 75 | _ProfileDataTable(profileData: user.profileData), |
68 | 76 | const SizedBox(height: 16), |
@@ -279,3 +287,66 @@ class _UserWidget extends StatelessWidget { |
279 | 287 | ]))); |
280 | 288 | } |
281 | 289 | } |
| 290 | + |
| 291 | +/// The text of current time in [user]'s timezone. |
| 292 | +class UserLocalTimeText extends StatefulWidget { |
| 293 | + const UserLocalTimeText({ |
| 294 | + super.key, |
| 295 | + required this.user, |
| 296 | + }); |
| 297 | + |
| 298 | + final User user; |
| 299 | + |
| 300 | + /// Initialize the timezone database used to know time difference from a timezone string. |
| 301 | + /// |
| 302 | + /// Usually, database initialization is done using `initializeTimeZones`, but it takes >100ms and not asynchronous. |
| 303 | + /// So, we initialize database from the assets file copied from timezone library. |
| 304 | + /// This file is checked up-to-date in `test/widgets/profile_test.dart`. |
| 305 | + static Future<void> initializeTimezonesUsingAssets() async { |
| 306 | + final blob = Uint8List.sublistView(await rootBundle.load('assets/timezone/latest_all.tzf')); |
| 307 | + tz.initializeDatabase(blob); |
| 308 | + } |
| 309 | + |
| 310 | + @override |
| 311 | + State<UserLocalTimeText> createState() => _UserLocalTimeTextState(); |
| 312 | +} |
| 313 | + |
| 314 | +class _UserLocalTimeTextState extends State<UserLocalTimeText> { |
| 315 | + late final Timer _timer; |
| 316 | + final StreamController<DateTime> _streamController = StreamController(); |
| 317 | + Stream<DateTime> get _stream => _streamController.stream; |
| 318 | + |
| 319 | + @override |
| 320 | + void initState() { |
| 321 | + _streamController.add(ZulipBinding.instance.now()); |
| 322 | + _timer = Timer.periodic(const Duration(seconds: 1), (_) { _streamController.add(ZulipBinding.instance.now()); }); |
| 323 | + super.initState(); |
| 324 | + } |
| 325 | + |
| 326 | + @override |
| 327 | + void dispose() { |
| 328 | + _timer.cancel(); |
| 329 | + super.dispose(); |
| 330 | + } |
| 331 | + |
| 332 | + Stream<String> _getDisplayLocalTimeFor(User user, ZulipLocalizations zulipLocalizations) async* { |
| 333 | + if (!tz.timeZoneDatabase.isInitialized) await UserLocalTimeText.initializeTimezonesUsingAssets(); |
| 334 | + |
| 335 | + await for (final DateTime time in _stream) { |
| 336 | + final location = tz.getLocation(user.timezone); |
| 337 | + final localTime = tz.TZDateTime.from(time, location); |
| 338 | + yield zulipLocalizations.userLocalTime(localTime); |
| 339 | + } |
| 340 | + } |
| 341 | + |
| 342 | + @override |
| 343 | + Widget build(BuildContext context) { |
| 344 | + return StreamBuilder( |
| 345 | + stream: _getDisplayLocalTimeFor(widget.user, ZulipLocalizations.of(context)), |
| 346 | + builder: (context, snapshot) { |
| 347 | + if (snapshot.hasError) Error.throwWithStackTrace(snapshot.error!, snapshot.stackTrace!); |
| 348 | + return Text(snapshot.data ?? ''); |
| 349 | + } |
| 350 | + ); |
| 351 | + } |
| 352 | +} |
0 commit comments