diff --git a/.github/workflows/versions.env b/.github/workflows/versions.env index 71fae1563..48b9dbee1 100644 --- a/.github/workflows/versions.env +++ b/.github/workflows/versions.env @@ -1,2 +1,2 @@ -FLUTTER_VERSION=3.38.1 +FLUTTER_VERSION=3.38.3 JAVA_VERSION=17 diff --git a/analysis_options.yaml b/analysis_options.yaml index 26cfe8535..30c0fa95e 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -13,6 +13,7 @@ analyzer: errors: todo: ignore use_build_context_synchronously: ignore + deprecated_member_use: ignore exclude: - lib/generated_plugin_registrant.dart - lib/l10n/*.dart diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index 42f2405b2..5dda2ae41 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -47,6 +47,8 @@ abstract class AppConfig { 'https://fluffy.chat/faq/#how_to_use_end_to_end_encryption'; static const String startChatTutorial = 'https://fluffy.chat/faq/#how_do_i_find_other_users'; + static const String howDoIGetStickersTutorial = + 'https://fluffy.chat/faq/#how_do_i_get_stickers'; static const String appId = 'im.fluffychat.FluffyChat'; // #Pangea // static const String appOpenUrlScheme = 'im.fluffychat'; diff --git a/lib/config/routes.dart b/lib/config/routes.dart index dc596eb44..8a8463ae0 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -10,6 +10,7 @@ import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pages/archive/archive.dart'; +import 'package:fluffychat/pages/bootstrap/bootstrap_dialog.dart'; import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pages/chat_access_settings/chat_access_settings_controller.dart'; import 'package:fluffychat/pages/chat_details/chat_details.dart'; @@ -246,6 +247,17 @@ abstract class AppRoutes { ), ), // Pangea# + GoRoute( + path: '/backup', + redirect: loggedOutRedirect, + pageBuilder: (context, state) => defaultPageBuilder( + context, + state, + BootstrapDialog( + wipe: state.uri.queryParameters['wipe'] == 'true', + ), + ), + ), ShellRoute( // Never use a transition on the shell route. Changing the PageBuilder // here based on a MediaQuery causes the child to briefly be rendered diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index d642b929a..a4b4f45d7 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -502,7 +502,7 @@ "type": "String", "placeholders": {} }, - "chatBackupDescription": "Your old messages are secured with a recovery key. Please make sure you don't lose it.", + "chatBackupDescription": "Your messages are secured with a recovery key. Please make sure you don't lose it.", "@chatBackupDescription": { "type": "String", "placeholders": {} @@ -3461,6 +3461,10 @@ "newStickerPack": "New sticker pack", "stickerPackName": "Sticker pack name", "attribution": "Attribution", + "skipChatBackup": "Skip chat backup", + "skipChatBackupWarning": "Are you sure? Without enabling the chat backup you may lose access to your messages if you switch your device.", + "loadingMessages": "Loading messages", + "setupChatBackup": "Set up chat backup", "ignore": "Block", "ignoredUsers": "Blocked users", "writeAMessageLangCodes": "Type in {l1} or {l2}...", diff --git a/lib/pages/bootstrap/bootstrap_dialog.dart b/lib/pages/bootstrap/bootstrap_dialog.dart index a303c3530..3d8a2c2ba 100644 --- a/lib/pages/bootstrap/bootstrap_dialog.dart +++ b/lib/pages/bootstrap/bootstrap_dialog.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:go_router/go_router.dart'; import 'package:matrix/encryption.dart'; import 'package:matrix/matrix.dart'; @@ -10,26 +11,21 @@ import 'package:fluffychat/utils/error_reporter.dart'; import 'package:fluffychat/utils/fluffy_share.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/utils/sync_status_localization.dart'; import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; -import '../../utils/adaptive_bottom_sheet.dart'; +import 'package:fluffychat/widgets/layouts/login_scaffold.dart'; +import 'package:fluffychat/widgets/matrix.dart'; import '../key_verification/key_verification_dialog.dart'; class BootstrapDialog extends StatefulWidget { final bool wipe; - final Client client; const BootstrapDialog({ super.key, this.wipe = false, - required this.client, }); - Future show(BuildContext context) => showAdaptiveBottomSheet( - context: context, - builder: (context) => this, - ); - @override BootstrapDialogState createState() => BootstrapDialogState(); } @@ -38,7 +34,7 @@ class BootstrapDialogState extends State { final TextEditingController _recoveryKeyTextEditingController = TextEditingController(); - late Bootstrap bootstrap; + Bootstrap? bootstrap; String? _recoveryKeyInputError; @@ -54,7 +50,7 @@ class BootstrapDialogState extends State { bool? _wipe; String get _secureStorageKey => - 'ssss_recovery_key_${bootstrap.client.userID}'; + 'ssss_recovery_key_${bootstrap!.client.userID}'; bool get _supportsSecureStorage => PlatformInfos.isMobile || PlatformInfos.isDesktop; @@ -69,18 +65,42 @@ class BootstrapDialogState extends State { return L10n.of(context).storeSecurlyOnThisDevice; } + late final Client client; + @override void initState() { - _createBootstrap(widget.wipe); super.initState(); + client = Matrix.of(context).client; + _createBootstrap(widget.wipe); } + void _cancelAction() async { + final consent = await showOkCancelAlertDialog( + context: context, + title: L10n.of(context).skipChatBackup, + message: L10n.of(context).skipChatBackupWarning, + okLabel: L10n.of(context).skip, + isDestructive: true, + ); + if (consent != OkCancelResult.ok) return; + if (!mounted) return; + _goBackAction(false); + } + + void _goBackAction(bool success) => + context.canPop() ? context.pop(success) : context.go('/rooms'); + void _createBootstrap(bool wipe) async { + await client.roomsLoading; + await client.accountDataLoading; + await client.userDeviceKeysLoading; + while (client.prevBatch == null) { + await client.onSync.stream.first; + } _wipe = wipe; titleText = null; _recoveryKeyStored = false; - bootstrap = - widget.client.encryption!.bootstrap(onUpdate: (_) => setState(() {})); + bootstrap = client.encryption!.bootstrap(onUpdate: (_) => setState(() {})); final key = await const FlutterSecureStorage().read(key: _secureStorageKey); if (key == null) return; _recoveryKeyTextEditingController.text = key; @@ -89,22 +109,45 @@ class BootstrapDialogState extends State { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final bootstrap = this.bootstrap; + if (bootstrap == null) { + return LoginScaffold( + appBar: AppBar( + centerTitle: true, + leading: CloseButton(onPressed: _cancelAction), + title: Text(L10n.of(context).loadingMessages), + ), + body: Center( + child: StreamBuilder( + stream: client.onSyncStatus.stream, + builder: (context, snapshot) { + final status = snapshot.data; + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator.adaptive(value: status?.progress), + if (status != null) Text(status.calcLocalizedString(context)), + ], + ); + }, + ), + ), + ); + } + _wipe ??= widget.wipe; final buttons = []; - Widget body = const CircularProgressIndicator.adaptive(); + Widget body = const Center(child: CircularProgressIndicator.adaptive()); titleText = L10n.of(context).loadingPleaseWait; if (bootstrap.newSsssKey?.recoveryKey != null && _recoveryKeyStored == false) { final key = bootstrap.newSsssKey!.recoveryKey; titleText = L10n.of(context).recoveryKey; - return Scaffold( + return LoginScaffold( appBar: AppBar( centerTitle: true, - leading: IconButton( - icon: const Icon(Icons.close), - onPressed: Navigator.of(context).pop, - ), + leading: CloseButton(onPressed: _cancelAction), title: Text(L10n.of(context).recoveryKey), ), body: Center( @@ -220,14 +263,11 @@ class BootstrapDialogState extends State { break; case BootstrapState.openExistingSsss: _recoveryKeyStored = true; - return Scaffold( + return LoginScaffold( appBar: AppBar( centerTitle: true, - leading: IconButton( - icon: const Icon(Icons.close), - onPressed: Navigator.of(context).pop, - ), - title: Text(L10n.of(context).chatBackup), + leading: CloseButton(onPressed: _cancelAction), + title: Text(L10n.of(context).setupChatBackup), ), body: Center( child: ConstrainedBox( @@ -373,16 +413,14 @@ class BootstrapDialogState extends State { context: context, delay: false, future: () async { - await widget.client.updateUserDeviceKeys(); - return widget.client - .userDeviceKeys[widget.client.userID!]! + await client.updateUserDeviceKeys(); + return client.userDeviceKeys[client.userID!]! .startVerification(); }, ); if (req.error != null) return; await KeyVerificationDialog(request: req.result!) .show(context); - Navigator.of(context, rootNavigator: false).pop(); }, ), const SizedBox(height: 16), @@ -446,8 +484,7 @@ class BootstrapDialogState extends State { body = const Icon(Icons.error_outline, color: Colors.red, size: 80); buttons.add( ElevatedButton( - onPressed: () => - Navigator.of(context, rootNavigator: false).pop(false), + onPressed: () => _goBackAction(false), child: Text(L10n.of(context).close), ), ); @@ -472,8 +509,7 @@ class BootstrapDialogState extends State { ); buttons.add( ElevatedButton( - onPressed: () => - Navigator.of(context, rootNavigator: false).pop(false), + onPressed: () => _goBackAction(false), child: Text(L10n.of(context).close), ), ); @@ -481,14 +517,9 @@ class BootstrapDialogState extends State { } } - return Scaffold( + return LoginScaffold( appBar: AppBar( - leading: Center( - child: CloseButton( - onPressed: () => - Navigator.of(context, rootNavigator: false).pop(true), - ), - ), + leading: CloseButton(onPressed: _cancelAction), title: Text(titleText ?? L10n.of(context).loadingPleaseWait), ), body: Center( diff --git a/lib/pages/chat/chat_event_list.dart b/lib/pages/chat/chat_event_list.dart index 94a04cbf0..090e11f9f 100644 --- a/lib/pages/chat/chat_event_list.dart +++ b/lib/pages/chat/chat_event_list.dart @@ -1,6 +1,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:collection/collection.dart'; import 'package:scroll_to_index/scroll_to_index.dart'; import 'package:fluffychat/config/themes.dart'; @@ -108,26 +109,33 @@ class ChatEventList extends StatelessWidget { // if (i == events.length + 1) { if (i == events.length + 2) { // Pangea# - if (timeline.isRequestingHistory) { - return const Center( - child: CircularProgressIndicator.adaptive(strokeWidth: 2), - ); - } - if (timeline.canRequestHistory && - controller.activeThreadId == null) { + if (controller.activeThreadId == null) { return Builder( builder: (context) { - // #Pangea - // WidgetsBinding.instance - // .addPostFrameCallback(controller.requestHistory); - WidgetsBinding.instance.addPostFrameCallback( - (_) => controller.requestHistory(), + final visibleIndex = timeline.events.lastIndexWhere( + (event) => + !event.isCollapsedState && event.isVisibleInGui, ); - // Pangea# + if (visibleIndex > timeline.events.length - 50) { + // #Pangea + // WidgetsBinding.instance + // .addPostFrameCallback(controller.requestHistory); + WidgetsBinding.instance.addPostFrameCallback( + (_) => controller.requestHistory(), + ); + // Pangea# + } return Center( - child: IconButton( - onPressed: controller.requestHistory, - icon: const Icon(Icons.refresh_outlined), + child: AnimatedSwitcher( + duration: FluffyThemes.animationDuration, + child: timeline.canRequestHistory + ? IconButton( + onPressed: controller.requestHistory, + icon: const Icon(Icons.refresh_outlined), + ) + : const CircularProgressIndicator.adaptive( + strokeWidth: 2, + ), ), ); }, diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index d29f68469..757fea66a 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -82,9 +82,7 @@ class MessageContent extends StatelessWidget { // } // final client = Matrix.of(context).client; // if (client.isUnknownSession && client.encryption!.crossSigning.enabled) { - // final success = await BootstrapDialog( - // client: Matrix.of(context).client, - // ).show(context); + // final success = await context.push('/backup'); // if (success != true) return; // } // event.requestKey(); @@ -159,7 +157,8 @@ class MessageContent extends StatelessWidget { case MessageTypes.Image: case MessageTypes.Sticker: if (event.redacted) continue textmessage; - const maxSize = 256.0; + final maxSize = + event.messageType == MessageTypes.Sticker ? 128.0 : 256.0; final w = event.content .tryGetMap('info') ?.tryGet('w'); diff --git a/lib/pages/chat/sticker_picker_dialog.dart b/lib/pages/chat/sticker_picker_dialog.dart index ff2a701f3..4ad721d7a 100644 --- a/lib/pages/chat/sticker_picker_dialog.dart +++ b/lib/pages/chat/sticker_picker_dialog.dart @@ -66,7 +66,9 @@ class StickerPickerDialogState extends State { GridView.builder( itemCount: imageKeys.length, gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 128, + maxCrossAxisExtent: 84, + mainAxisSpacing: 8.0, + crossAxisSpacing: 8.0, ), shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), @@ -142,7 +144,7 @@ class StickerPickerDialogState extends State { // OutlinedButton.icon( // onPressed: () => UrlLauncher( // context, - // 'https://matrix.to/#/#fluffychat-stickers:janian.de', + // AppConfig.howDoIGetStickersTutorial, // ).launchUrl(), // icon: const Icon(Icons.explore_outlined), // label: Text(L10n.of(context).discover), diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index 22cab966f..6ab641a34 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -1039,17 +1039,6 @@ class ChatListController extends State setState(() { waitForFirstSync = true; }); - - // Display first login bootstrap if enabled - // #Pangea - // if (client.encryption?.keyManager.enabled == true) { - // if (await client.encryption?.keyManager.isCached() == false || - // await client.encryption?.crossSigning.isCached() == false || - // client.isUnknownSession && !mounted) { - // await BootstrapDialog(client: client).show(context); - // } - // } - // Pangea# } // #Pangea diff --git a/lib/pages/settings/settings.dart b/lib/pages/settings/settings.dart index 1fd0d5625..af7cd3b06 100644 --- a/lib/pages/settings/settings.dart +++ b/lib/pages/settings/settings.dart @@ -204,9 +204,7 @@ class SettingsController extends State { // ); // return; // } - // await BootstrapDialog( - // client: Matrix.of(context).client, - // ).show(context); + // await context.push('/backup'); // checkBootstrap(); // Pangea# } diff --git a/lib/pages/settings_security/settings_security.dart b/lib/pages/settings_security/settings_security.dart index c84a144cb..c1708e9f5 100644 --- a/lib/pages/settings_security/settings_security.dart +++ b/lib/pages/settings_security/settings_security.dart @@ -10,7 +10,6 @@ import 'package:fluffychat/widgets/adaptive_dialogs/show_text_input_dialog.dart' import 'package:fluffychat/widgets/app_lock.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; -import '../bootstrap/bootstrap_dialog.dart'; import 'settings_security_view.dart'; class SettingsSecurity extends StatefulWidget { @@ -145,12 +144,6 @@ class SettingsSecurityController extends State { } } - void showBootstrapDialog(BuildContext context) async { - await BootstrapDialog( - client: Matrix.of(context).client, - ).show(context); - } - Future dehydrateAction() => Matrix.of(context).dehydrateAction(context); void changeShareKeysWith(ShareKeysWith? shareKeysWith) async { diff --git a/lib/utils/file_description.dart b/lib/utils/file_description.dart index 639d11632..54f72e159 100644 --- a/lib/utils/file_description.dart +++ b/lib/utils/file_description.dart @@ -12,11 +12,16 @@ extension FileDescriptionExtension on Event { return null; } final formattedBody = content.tryGet('formatted_body'); - if (formattedBody != null) return formattedBody; + if (formattedBody != null && formattedBody.isNotEmpty) return formattedBody; final filename = content.tryGet('filename'); final body = content.tryGet('body'); - if (filename != body && body != null && filename != null) return body; + if (filename != body && + body != null && + filename != null && + body.isNotEmpty) { + return body; + } return null; } } diff --git a/lib/widgets/matrix.dart b/lib/widgets/matrix.dart index 3c07dfdc4..4fe1b17cf 100644 --- a/lib/widgets/matrix.dart +++ b/lib/widgets/matrix.dart @@ -215,7 +215,7 @@ class MatrixState extends State with WidgetsBindingObserver { _registerSubs(_loginClientCandidate!.clientName); _loginClientCandidate = null; // #Pangea - // FluffyChatApp.router.go('/rooms'); + // FluffyChatApp.router.go('/backup'); final isL2Set = await pangeaController.userController.isUserL2Set; FluffyChatApp.router.go( isL2Set ? '/rooms' : '/registration/create', @@ -443,7 +443,7 @@ class MatrixState extends State with WidgetsBindingObserver { FluffyChatApp.router.go('/home'); } // FluffyChatApp.router - // .go(state == LoginState.loggedIn ? '/rooms' : '/home'); + // .go(state == LoginState.loggedIn ? '/backup' : '/home'); // Pangea# } }); diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 5e31606a6..3e928cef2 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -191,7 +191,7 @@ SPEC CHECKSUMS: dynamic_color: cb7c2a300ee67ed3bd96c3e852df3af0300bf610 emoji_picker_flutter: 51ca408e289d84d1e460016b2a28721ec754fcf7 file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a - file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31 + file_selector_macos: 9e9e068e90ebee155097d00e89ae91edb2374db7 flutter_local_notifications: 4bf37a31afde695b56091b4ae3e4d9c7a7e6cda0 flutter_new_badger: 6fe9bf7e42793a164032c21f164c0ad9985cd0f2 flutter_secure_storage_macos: 7f45e30f838cf2659862a4e4e3ee1c347c2b3b54