From 17903aa5d2c894b1548bc59ef21ae9ad730a495a Mon Sep 17 00:00:00 2001 From: Feichtmeier Date: Sat, 11 Jan 2025 22:59:01 +0100 Subject: [PATCH] feat: show device list, improve verification of other devices --- .../authentication/authentication_model.dart | 4 ++ lib/chat/bootstrap/view/bootstrap_page.dart | 4 +- .../view/key_verification_dialog.dart | 18 ++++--- lib/chat/settings/settings_dialog.dart | 42 +++++++++++++-- lib/chat/settings/settings_model.dart | 14 +++++ lib/chat/timeline_model.dart | 2 +- .../chat_master/chat_master_detail_page.dart | 54 ++++++++++--------- lib/common/date_time_x.dart | 4 +- lib/constants.dart | 2 +- lib/l10n/app_de.arb | 18 +++---- lib/l10n/app_en.arb | 18 +++---- lib/l10n/app_sv.arb | 20 +++---- 12 files changed, 132 insertions(+), 68 deletions(-) diff --git a/lib/chat/authentication/authentication_model.dart b/lib/chat/authentication/authentication_model.dart index dbe3112..61a2f58 100644 --- a/lib/chat/authentication/authentication_model.dart +++ b/lib/chat/authentication/authentication_model.dart @@ -1,7 +1,10 @@ +import 'dart:io'; + import 'package:matrix/matrix.dart'; import 'package:safe_change_notifier/safe_change_notifier.dart'; import '../../common/logging.dart'; +import '../../constants.dart'; class AuthenticationModel extends SafeChangeNotifier { AuthenticationModel({required Client client}) : _client = client; @@ -38,6 +41,7 @@ class AuthenticationModel extends SafeChangeNotifier { LoginType.mLoginPassword, password: password, identifier: AuthenticationUserIdentifier(user: username), + initialDeviceDisplayName: '$kAppTitle ${Platform.operatingSystem}', ); await _client.firstSyncReceived; await _client.roomsLoading; diff --git a/lib/chat/bootstrap/view/bootstrap_page.dart b/lib/chat/bootstrap/view/bootstrap_page.dart index a6cce63..3541d29 100644 --- a/lib/chat/bootstrap/view/bootstrap_page.dart +++ b/lib/chat/bootstrap/view/bootstrap_page.dart @@ -79,7 +79,7 @@ class BootstrapPage extends StatelessWidget with WatchItMixin { minLines: 2, maxLines: 4, readOnly: true, - style: const TextStyle(fontFamily: 'RobotoMono'), + style: const TextStyle(fontFamily: 'UbuntuMono'), controller: TextEditingController(text: key), decoration: const InputDecoration( contentPadding: EdgeInsets.all(16), @@ -292,7 +292,7 @@ class _OpenExistingSSSSPageState extends State { readOnly: recoveryKeyInputLoading, autofillHints: recoveryKeyInputLoading ? null : [AutofillHints.password], - style: const TextStyle(fontFamily: 'RobotoMono'), + style: const TextStyle(fontFamily: 'UbuntuMono'), decoration: InputDecoration( prefixIcon: const Icon(YaruIcons.key), labelText: l10n.recoveryKey, diff --git a/lib/chat/bootstrap/view/key_verification_dialog.dart b/lib/chat/bootstrap/view/key_verification_dialog.dart index b3835dd..9e19138 100644 --- a/lib/chat/bootstrap/view/key_verification_dialog.dart +++ b/lib/chat/bootstrap/view/key_verification_dialog.dart @@ -24,10 +24,12 @@ class KeyVerificationDialog extends StatefulWidget { ); final KeyVerification request; + final bool verifyOther; const KeyVerificationDialog({ super.key, required this.request, + this.verifyOther = false, }); @override @@ -329,12 +331,16 @@ class KeyVerificationPageState extends State { context, rootNavigator: false, ).pop(); - Navigator.of(context).pushAndRemoveUntil( - MaterialPageRoute( - builder: (_) => const ChatMasterDetailPage(), - ), - (route) => false, - ); + if (!widget.verifyOther) { + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute( + builder: (_) => const ChatMasterDetailPage( + checkBootstrap: false, + ), + ), + (route) => false, + ); + } } }, ), diff --git a/lib/chat/settings/settings_dialog.dart b/lib/chat/settings/settings_dialog.dart index e491825..7b3d8d8 100644 --- a/lib/chat/settings/settings_dialog.dart +++ b/lib/chat/settings/settings_dialog.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import 'package:watch_it/watch_it.dart'; import 'package:yaru/yaru.dart'; +import '../../common/date_time_x.dart'; +import '../../common/view/build_context_x.dart'; import '../../common/view/snackbars.dart'; import '../../common/view/ui_constants.dart'; import '../../l10n/l10n.dart'; @@ -26,10 +28,12 @@ class _SettingsDialogState extends State { super.initState(); _displayNameController = TextEditingController(); _idController = TextEditingController(text: di().myUserId); - di().getMyProfile().then((v) { - _displayNameController.text = v?.displayName ?? ''; - _idController.text = v?.userId ?? ''; - }); + di() + ..getMyProfile().then((v) { + _displayNameController.text = v?.displayName ?? ''; + _idController.text = v?.userId ?? ''; + }) + ..getDevices(); } @override @@ -42,6 +46,7 @@ class _SettingsDialogState extends State { @override Widget build(BuildContext context) { final l10n = context.l10n; + final settingsModel = di(); watchFuture( (SettingsModel m) => m.getMyProfile(), initialValue: di().myProfile, @@ -53,6 +58,8 @@ class _SettingsDialogState extends State { preserveState: false, ).data; + final devices = watchPropertyValue((SettingsModel m) => m.devices); + return AlertDialog( titlePadding: EdgeInsets.zero, title: YaruDialogTitleBar( @@ -115,6 +122,33 @@ class _SettingsDialogState extends State { ], ), ), + YaruSection( + headline: Text(l10n.devices), + child: Column( + children: devices + .map( + (d) => YaruTile( + trailing: d.deviceId != settingsModel.myDeviceId + ? IconButton( + onPressed: () => + settingsModel.deleteDevice(d.deviceId), + icon: Icon( + YaruIcons.trash, + color: context.colorScheme.error, + ), + ) + : null, + subtitle: Text( + DateTime.fromMillisecondsSinceEpoch( + d.lastSeenTs ?? 0, + ).formatAndLocalize(l10n, simple: true), + ), + title: SelectableText(d.displayName ?? d.deviceId), + ), + ) + .toList(), + ), + ), ], ), ), diff --git a/lib/chat/settings/settings_model.dart b/lib/chat/settings/settings_model.dart index 131cef0..1f237f4 100644 --- a/lib/chat/settings/settings_model.dart +++ b/lib/chat/settings/settings_model.dart @@ -30,6 +30,20 @@ class SettingsModel extends SafeChangeNotifier { return _myProfile; } + String? get myDeviceId => _client.deviceID; + List _devices = []; + List get devices => _devices; + Future getDevices() async { + _devices = await _client.getDevices() ?? []; + notifyListeners(); + } + + // TODO: authenticate for some devices + Future deleteDevice(String id) async { + await _client.deleteDevice(id); + await getDevices(); + } + bool _attachingAvatar = false; bool get attachingAvatar => _attachingAvatar; void setAttachingAvatar(bool value) { diff --git a/lib/chat/timeline_model.dart b/lib/chat/timeline_model.dart index 651cdc7..89cc912 100644 --- a/lib/chat/timeline_model.dart +++ b/lib/chat/timeline_model.dart @@ -26,10 +26,10 @@ class TimelineModel extends SafeChangeNotifier { return; } await timeline.requestHistory(filter: filter, historyCount: historyCount); - await timeline.setReadMarker(); if (notify) { setUpdatingTimeline(false); } + await timeline.setReadMarker(); } bool _timelineSearchActive = false; diff --git a/lib/chat/view/chat_master/chat_master_detail_page.dart b/lib/chat/view/chat_master/chat_master_detail_page.dart index 93f9a4b..8d85fcf 100644 --- a/lib/chat/view/chat_master/chat_master_detail_page.dart +++ b/lib/chat/view/chat_master/chat_master_detail_page.dart @@ -18,7 +18,12 @@ final GlobalKey masterScaffoldKey = GlobalKey(); class ChatMasterDetailPage extends StatefulWidget with WatchItStatefulWidgetMixin { - const ChatMasterDetailPage({super.key}); + const ChatMasterDetailPage({ + super.key, + this.checkBootstrap = true, + }); + + final bool checkBootstrap; @override State createState() => _ChatMasterDetailPageState(); @@ -28,25 +33,27 @@ class _ChatMasterDetailPageState extends State { @override void initState() { super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) { - final bootstrapModel = di(); - bootstrapModel.checkBootstrap().then((isNeeded) { - if (isNeeded) { - bootstrapModel.startBootstrap(wipe: false).then( - (_) { - if (mounted) { - Navigator.of(context).pushAndRemoveUntil( - MaterialPageRoute( - builder: (_) => const BootstrapPage(), - ), - (route) => false, - ); - } - }, - ); - } + if (widget.checkBootstrap) { + WidgetsBinding.instance.addPostFrameCallback((_) { + final bootstrapModel = di(); + bootstrapModel.checkBootstrap().then((isNeeded) { + if (isNeeded) { + bootstrapModel.startBootstrap(wipe: false).then( + (_) { + if (mounted) { + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute( + builder: (_) => const BootstrapPage(), + ), + (route) => false, + ); + } + }, + ); + } + }); }); - }); + } } @override @@ -55,11 +62,10 @@ class _ChatMasterDetailPageState extends State { select: (ChatModel m) => m.onKeyVerificationRequest, handler: (context, newValue, cancel) { if (newValue.hasData) { - showDialog( - context: context, - builder: (context) => - KeyVerificationDialog(request: newValue.data!), - ); + KeyVerificationDialog( + request: newValue.data!, + verifyOther: true, + ).show(context); } }, ); diff --git a/lib/common/date_time_x.dart b/lib/common/date_time_x.dart index ef2e7cc..e9d6076 100644 --- a/lib/common/date_time_x.dart +++ b/lib/common/date_time_x.dart @@ -4,11 +4,11 @@ import 'package:intl/intl.dart'; import '../l10n/l10n.dart'; extension DateTimeX on DateTime { - String formatAndLocalize(AppLocalizations l10n) { + String formatAndLocalize(AppLocalizations l10n, {bool simple = false}) { final now = DateTime.now(); final locale = WidgetsBinding.instance.platformDispatcher.locale; - if (year == now.year && month == now.month) { + if (!simple && year == now.year && month == now.month) { if (day == now.day - 1) { return '${l10n.yesterday}, ${DateFormat.Hm( locale.countryCode, diff --git a/lib/constants.dart b/lib/constants.dart index 032aadf..1f5cea6 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -1,4 +1,4 @@ const kAppName = 'nebuchadnezzar'; -const kOrgName = 'org.feichtmeier'; +const kOrgName = 'feichtmeier.org'; const kAppId = '$kAppName.$kOrgName'; const kAppTitle = 'Nebuchadnezzar'; diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 69e5310..f24768c 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -877,8 +877,8 @@ "type": "text", "placeholders": {} }, - "fluffychat": "FluffyChat", - "@fluffychat": { + "nebuchadnezzar": "Nebuchadnezzar", + "@nebuchadnezzar": { "type": "text", "placeholders": {} }, @@ -1052,7 +1052,7 @@ "type": "text", "placeholders": {} }, - "inviteText": "{username} invited you to FluffyChat.\n1. Visit fluffychat.im and install the app \n2. Sign up or sign in \n3. Open the invite link: \n {link}", + "inviteText": "{username} invited you to Nebuchadnezzar.\n1. Visit https://snapcraft.io/nebuchadnezzar and install the app \n2. Sign up or sign in \n3. Open the invite link: \n {link}", "@inviteText": { "type": "text", "placeholders": { @@ -1220,8 +1220,8 @@ "type": "text", "placeholders": {} }, - "newMessageInFluffyChat": "💬 New message in FluffyChat", - "@newMessageInFluffyChat": { + "newMessageInNebuchadnezzar": "💬 New message in Nebuchadnezzar", + "@newMessageInNebuchadnezzar": { "type": "text", "placeholders": {} }, @@ -1884,7 +1884,7 @@ "type": "text", "placeholders": {} }, - "title": "FluffyChat", + "title": "Nebuchadnezzar", "@title": { "description": "Title for the application", "type": "text", @@ -2217,7 +2217,7 @@ "@emailOrUsername": {}, "indexedDbErrorTitle": "Private mode issues", "@indexedDbErrorTitle": {}, - "indexedDbErrorLong": "The message storage is unfortunately not enabled in private mode by default.\nPlease visit\n - about:config\n - set dom.indexedDB.privateBrowsing.enabled to true\nOtherwise, it is not possible to run FluffyChat.", + "indexedDbErrorLong": "The message storage is unfortunately not enabled in private mode by default.\nPlease visit\n - about:config\n - set dom.indexedDB.privateBrowsing.enabled to true\nOtherwise, it is not possible to run Nebuchadnezzar.", "@indexedDbErrorLong": {}, "switchToAccount": "Switch to account {number}", "@switchToAccount": { @@ -2362,13 +2362,13 @@ "@callingPermissions": {}, "callingAccount": "Calling account", "@callingAccount": {}, - "callingAccountDetails": "Allows FluffyChat to use the native android dialer app.", + "callingAccountDetails": "Allows Nebuchadnezzar to use the native android dialer app.", "@callingAccountDetails": {}, "appearOnTop": "Appear on top", "@appearOnTop": {}, "appearOnTopDetails": "Allows the app to appear on top (not needed if you already have Fluffychat setup as a calling account)", "@appearOnTopDetails": {}, - "otherCallingPermissions": "Microphone, camera and other FluffyChat permissions", + "otherCallingPermissions": "Microphone, camera and other Nebuchadnezzar permissions", "@otherCallingPermissions": {}, "whyIsThisMessageEncrypted": "Why is this message unreadable?", "@whyIsThisMessageEncrypted": {}, diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index b6ea970..7e79240 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -877,8 +877,8 @@ "type": "text", "placeholders": {} }, - "fluffychat": "FluffyChat", - "@fluffychat": { + "nebuchadnezzar": "Nebuchadnezzar", + "@nebuchadnezzar": { "type": "text", "placeholders": {} }, @@ -1052,7 +1052,7 @@ "type": "text", "placeholders": {} }, - "inviteText": "{username} invited you to FluffyChat.\n1. Visit fluffychat.im and install the app \n2. Sign up or sign in \n3. Open the invite link: \n {link}", + "inviteText": "{username} invited you to Nebuchadnezzar.\n1. Visit https://snapcraft.io/nebuchadnezzar and install the app \n2. Sign up or sign in \n3. Open the invite link: \n {link}", "@inviteText": { "type": "text", "placeholders": { @@ -1220,8 +1220,8 @@ "type": "text", "placeholders": {} }, - "newMessageInFluffyChat": "💬 New message in FluffyChat", - "@newMessageInFluffyChat": { + "newMessageInNebuchadnezzar": "💬 New message in Nebuchadnezzar", + "@newMessageInNebuchadnezzar": { "type": "text", "placeholders": {} }, @@ -1884,7 +1884,7 @@ "type": "text", "placeholders": {} }, - "title": "FluffyChat", + "title": "Nebuchadnezzar", "@title": { "description": "Title for the application", "type": "text", @@ -2217,7 +2217,7 @@ "@emailOrUsername": {}, "indexedDbErrorTitle": "Private mode issues", "@indexedDbErrorTitle": {}, - "indexedDbErrorLong": "The message storage is unfortunately not enabled in private mode by default.\nPlease visit\n - about:config\n - set dom.indexedDB.privateBrowsing.enabled to true\nOtherwise, it is not possible to run FluffyChat.", + "indexedDbErrorLong": "The message storage is unfortunately not enabled in private mode by default.\nPlease visit\n - about:config\n - set dom.indexedDB.privateBrowsing.enabled to true\nOtherwise, it is not possible to run Nebuchadnezzar.", "@indexedDbErrorLong": {}, "switchToAccount": "Switch to account {number}", "@switchToAccount": { @@ -2362,13 +2362,13 @@ "@callingPermissions": {}, "callingAccount": "Calling account", "@callingAccount": {}, - "callingAccountDetails": "Allows FluffyChat to use the native android dialer app.", + "callingAccountDetails": "Allows Nebuchadnezzar to use the native android dialer app.", "@callingAccountDetails": {}, "appearOnTop": "Appear on top", "@appearOnTop": {}, "appearOnTopDetails": "Allows the app to appear on top (not needed if you already have Fluffychat setup as a calling account)", "@appearOnTopDetails": {}, - "otherCallingPermissions": "Microphone, camera and other FluffyChat permissions", + "otherCallingPermissions": "Microphone, camera and other Nebuchadnezzar permissions", "@otherCallingPermissions": {}, "whyIsThisMessageEncrypted": "Why is this message unreadable?", "@whyIsThisMessageEncrypted": {}, diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 1bcccaa..89d58b5 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -877,8 +877,8 @@ "type": "text", "placeholders": {} }, - "fluffychat": "FluffyChat", - "@fluffychat": { + "nebuchadnezzar": "Nebuchadnezzar", + "@nebuchadnezzar": { "type": "text", "placeholders": {} }, @@ -1052,7 +1052,7 @@ "type": "text", "placeholders": {} }, - "inviteText": "{username} bjöd in dig till FluffyChat.\n1. Besök fluffychat.im och installera appen\n2. Registrera dig eller logga in \n3. Öppna inbjudningslänken: \n {link}", + "inviteText": "{username} bjöd in dig till Nebuchadnezzar.\n1. Besök https://snapcraft.io/nebuchadnezzar och installera appen\n2. Registrera dig eller logga in \n3. Öppna inbjudningslänken: \n {link}", "@inviteText": { "type": "text", "placeholders": { @@ -1220,8 +1220,8 @@ "type": "text", "placeholders": {} }, - "newMessageInFluffyChat": "💬 Nytt meddelande i FluffyChat", - "@newMessageInFluffyChat": { + "newMessageInNebuchadnezzar": "💬 Nytt meddelande i Nebuchadnezzar", + "@newMessageInNebuchadnezzar": { "type": "text", "placeholders": {} }, @@ -1884,7 +1884,7 @@ "type": "text", "placeholders": {} }, - "title": "FluffyChat", + "title": "Nebuchadnezzar", "@title": { "description": "Titel för applikation", "type": "text", @@ -2217,7 +2217,7 @@ "@emailOrUsername": {}, "indexedDbErrorTitle": "Problem med privat läge", "@indexedDbErrorTitle": {}, - "indexedDbErrorLong": "Meddelandelagringen är tyvärr inte aktiverad i privat läge som standard.\nPVänligen besök\n - about:config\n - ställ in dom.indexedDB.privateBrowsing.enabled till sant\nAnnars går det inte att köra FluffyChat.", + "indexedDbErrorLong": "Meddelandelagringen är tyvärr inte aktiverad i privat läge som standard.\nPVänligen besök\n - about:config\n - ställ in dom.indexedDB.privateBrowsing.enabled till sant\nAnnars går det inte att köra Nebuchadnezzar.", "@indexedDbErrorLong": {}, "switchToAccount": "Ändra till konto {number}", "@switchToAccount": { @@ -2356,19 +2356,19 @@ "@foregroundServiceRunning": {}, "screenSharingTitle": "Skärmdelning", "@screenSharingTitle": {}, - "screenSharingDetail": "Du delar din skärm i FluffyChat", + "screenSharingDetail": "Du delar din skärm i Nebuchadnezzar", "@screenSharingDetail": {}, "callingPermissions": "Ringbehörigheter", "@callingPermissions": {}, "callingAccount": "Ringer konto", "@callingAccount": {}, - "callingAccountDetails": "Tillåter FluffyChat att använda den ursprungliga uppringningsappen för Android.", + "callingAccountDetails": "Tillåter Nebuchadnezzar att använda den ursprungliga uppringningsappen för Android.", "@callingAccountDetails": {}, "appearOnTop": "Visas överst", "@appearOnTop": {}, "appearOnTopDetails": "Tillåter att appen visas överst (behövs inte om du redan har konfigurerat Fluffychat som ett samtalskonto)", "@appearOnTopDetails": {}, - "otherCallingPermissions": "Mikrofon, kamera och andra FluffyChat-behörigheter", + "otherCallingPermissions": "Mikrofon, kamera och andra Nebuchadnezzar-behörigheter", "@otherCallingPermissions": {}, "whyIsThisMessageEncrypted": "Varför är detta meddelande oläsbart?", "@whyIsThisMessageEncrypted": {},