From 56ba2341f4497b577303c9a267b1bcbd36ccc638 Mon Sep 17 00:00:00 2001 From: TheOneWithTheBraid Date: Fri, 29 Apr 2022 07:26:01 +0200 Subject: [PATCH] feat: implement session dump Signed-off-by: TheOneWithTheBraid --- assets/l10n/intl_en.arb | 11 +++++ lib/pages/chat_list/chat_list.dart | 19 ++++++++ lib/pages/chat_list/chat_list_body.dart | 17 +++++++ .../homeserver_picker/homeserver_picker.dart | 46 ++++++++++++++++++ .../homeserver_picker_view.dart | 47 +++++++++++++++++++ .../settings_account/settings_account.dart | 37 +++++++++++++++ .../settings_account_view.dart | 8 ++++ lib/utils/tor_stub.dart | 7 +++ pubspec.lock | 9 +++- pubspec.yaml | 7 ++- 10 files changed, 203 insertions(+), 5 deletions(-) create mode 100644 lib/utils/tor_stub.dart diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index a843aab2..d883820d 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -1304,6 +1304,15 @@ "count": {} } }, + "dehydrate": "Export session and wipe device", + "dehydrateWarning": "This action cannot be undone. Ensure you safely store the backup file.", + "dehydrateShare": "This is your private FluffyChat export. Ensure you don't lose it and keep it private.", + "dehydrateTor": "TOR Users: Export session", + "dehydrateTorLong": "For TOR users, it is recommended to export the session before closing the window.", + "hydrateTor": "TOR Users: Import session export", + "hydrateTorLong": "Did you export your session last time on TOR? Quickly import it and continue chatting.", + "hydrate": "Restore from backup file", + "advanced": "Advanced", "loadingPleaseWait": "Loading… Please wait.", "@loadingPleaseWait": { "type": "text", @@ -2752,6 +2761,8 @@ "@experimentalVideoCalls": {}, "emailOrUsername": "Email or username", "@emailOrUsername": {}, + "indexedDbErrorTitle": "Private mode issues", + "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.", "switchToAccount": "Switch to account {number}", "@switchToAccount": { "type": "number", diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index b9210175..ba2de49f 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:io'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart'; @@ -23,6 +24,10 @@ import '../../utils/matrix_sdk_extensions.dart/matrix_file_extension.dart'; import '../../utils/url_launcher.dart'; import '../../widgets/matrix.dart'; import '../bootstrap/bootstrap_dialog.dart'; +import '../settings_account/settings_account.dart'; + +import 'package:fluffychat/utils/tor_stub.dart' + if (dart.library.html) 'package:tor_detector_web/tor_detector_web.dart'; enum SelectMode { normal, share, select } @@ -143,6 +148,8 @@ class ChatListController extends State with TickerProviderStateMixin { isSearching = false; }); + bool isTorBrowser = false; + SpacesEntry get activeSpacesEntry { final id = _activeSpacesEntry; return (id == null || !id.stillValid(context)) ? defaultSpacesEntry : id; @@ -300,6 +307,9 @@ class ChatListController extends State with TickerProviderStateMixin { WidgetsBinding.instance.addPostFrameCallback((_) async { searchServer = await Store().getItem(_serverStoreNamespace); }); + + _checkTorBrowser(); + super.initState(); } @@ -652,6 +662,15 @@ class ChatListController extends State with TickerProviderStateMixin { void _hackyWebRTCFixForWeb() { Matrix.of(context).voipPlugin?.context = context; } + + Future _checkTorBrowser() async { + if (!kIsWeb) return; + final isTor = await TorBrowserDetector.isTorBrowser; + setState(() => isTorBrowser = isTor); + } + + Future dehydrate() => + SettingsAccountController.dehydrateDevice(context); } enum EditBundleAction { addToBundle, removeFromBundle } diff --git a/lib/pages/chat_list/chat_list_body.dart b/lib/pages/chat_list/chat_list_body.dart index f25dee81..f616e585 100644 --- a/lib/pages/chat_list/chat_list_body.dart +++ b/lib/pages/chat_list/chat_list_body.dart @@ -204,6 +204,23 @@ class _ChatListViewBodyState extends State { ), ), ), + AnimatedContainer( + height: widget.controller.isTorBrowser ? 64 : 0, + duration: const Duration(milliseconds: 300), + clipBehavior: Clip.hardEdge, + curve: Curves.bounceInOut, + decoration: const BoxDecoration(), + child: Material( + color: Theme.of(context).colorScheme.surface, + child: ListTile( + leading: const Icon(Icons.vpn_key), + title: Text(L10n.of(context)!.dehydrateTor), + subtitle: Text(L10n.of(context)!.dehydrateTorLong), + trailing: const Icon(Icons.chevron_right_outlined), + onTap: widget.controller.dehydrate, + ), + ), + ), if (widget.controller.isSearchMode) _SearchTitle( title: L10n.of(context)!.chats, diff --git a/lib/pages/homeserver_picker/homeserver_picker.dart b/lib/pages/homeserver_picker/homeserver_picker.dart index c1f7876d..81298791 100644 --- a/lib/pages/homeserver_picker/homeserver_picker.dart +++ b/lib/pages/homeserver_picker/homeserver_picker.dart @@ -1,7 +1,13 @@ import 'dart:async'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:adaptive_dialog/adaptive_dialog.dart'; +import 'package:file_picker_cross/file_picker_cross.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:future_loading_dialog/future_loading_dialog.dart'; +import 'package:hive_flutter/hive_flutter.dart'; import 'package:matrix/matrix.dart'; import 'package:matrix_homeserver_recommendations/matrix_homeserver_recommendations.dart'; import 'package:vrouter/vrouter.dart'; @@ -12,6 +18,9 @@ import 'package:fluffychat/pages/homeserver_picker/homeserver_picker_view.dart'; import 'package:fluffychat/widgets/matrix.dart'; import '../../utils/localized_exception_extension.dart'; +import 'package:fluffychat/utils/tor_stub.dart' + if (dart.library.html) 'package:tor_detector_web/tor_detector_web.dart'; + class HomeserverPicker extends StatefulWidget { const HomeserverPicker({Key? key}) : super(key: key); @@ -33,6 +42,26 @@ class HomeserverPickerController extends State { AppConfig.allowOtherHomeservers && benchmarkResults == null; String searchTerm = ''; + bool isTorBrowser = false; + + Future _checkTorBrowser() async { + if (!kIsWeb) return; + + Hive.openBox('test').then((value) => null).catchError( + (e, s) async { + await showOkAlertDialog( + context: context, + title: L10n.of(context)!.indexedDbErrorTitle, + message: L10n.of(context)!.indexedDbErrorLong, + onWillPop: () async => false); + _checkTorBrowser(); + }, + ); + + final isTor = await TorBrowserDetector.isTorBrowser; + setState(() => isTorBrowser = isTor); + } + void _updateFocus() { if (benchmarkResults == null) _loadHomeserverList(); if (homeserverFocusNode.hasFocus) { @@ -139,6 +168,7 @@ class HomeserverPickerController extends State { @override void initState() { homeserverFocusNode.addListener(_updateFocus); + _checkTorBrowser(); super.initState(); } @@ -147,4 +177,20 @@ class HomeserverPickerController extends State { Matrix.of(context).navigatorContext = context; return HomeserverPickerView(this); } + + Future restoreBackup() async { + await showFutureLoadingDialog( + context: context, + future: () async { + try { + final file = await FilePickerCross.importFromStorage( + fileExtension: '.fluffybackup'); + final client = Matrix.of(context).getLoginClient(); + await client.importDump(file.toString()); + Matrix.of(context).initMatrix(); + } catch (e, s) { + Logs().e('Future error:', e, s); + } + }); + } } diff --git a/lib/pages/homeserver_picker/homeserver_picker_view.dart b/lib/pages/homeserver_picker/homeserver_picker_view.dart index 23938723..f3ae1891 100644 --- a/lib/pages/homeserver_picker/homeserver_picker_view.dart +++ b/lib/pages/homeserver_picker/homeserver_picker_view.dart @@ -24,6 +24,29 @@ class HomeserverPickerView extends StatelessWidget { body: SafeArea( child: Column( children: [ + // display a prominent banner to import session for TOR browser + // users. This feature is just some UX sugar as TOR users are + // usually forced to logout as TOR browser is non-persistent + AnimatedContainer( + height: controller.isTorBrowser ? 64 : 0, + duration: const Duration(milliseconds: 300), + clipBehavior: Clip.hardEdge, + curve: Curves.bounceInOut, + decoration: const BoxDecoration(), + child: Material( + clipBehavior: Clip.hardEdge, + borderRadius: + const BorderRadius.vertical(bottom: Radius.circular(8)), + color: Theme.of(context).colorScheme.surface, + child: ListTile( + leading: const Icon(Icons.vpn_key), + title: Text(L10n.of(context)!.hydrateTor), + subtitle: Text(L10n.of(context)!.hydrateTorLong), + trailing: const Icon(Icons.chevron_right_outlined), + onTap: controller.restoreBackup, + ), + ), + ), Expanded( child: ListView( children: [ @@ -140,6 +163,30 @@ class HomeserverPickerView extends StatelessWidget { ), ), ), + Padding( + padding: const EdgeInsets.all(16), + child: ExpansionTile( + title: Text(L10n.of(context)!.advanced), + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: TextButton( + onPressed: controller.isLoading + ? () {} + : controller.restoreBackup, + style: ElevatedButton.styleFrom( + primary: Colors.white.withAlpha(200), + onPrimary: Colors.black, + shadowColor: Colors.white, + ), + child: controller.isLoading + ? const LinearProgressIndicator() + : Text(L10n.of(context)!.hydrate), + ), + ), + ], + ), + ), ], ), ), diff --git a/lib/pages/settings_account/settings_account.dart b/lib/pages/settings_account/settings_account.dart index 3bf2fa5d..a82369df 100644 --- a/lib/pages/settings_account/settings_account.dart +++ b/lib/pages/settings_account/settings_account.dart @@ -1,8 +1,13 @@ +import 'dart:convert'; +import 'dart:typed_data'; + import 'package:flutter/material.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart'; +import 'package:file_picker_cross/file_picker_cross.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; +import 'package:intl/intl.dart'; import 'package:matrix/matrix.dart'; import 'package:vrouter/vrouter.dart'; @@ -137,4 +142,36 @@ class SettingsAccountController extends State { }); return SettingsAccountView(this); } + + Future dehydrateAction() => dehydrateDevice(context); + + static Future dehydrateDevice(BuildContext context) async { + final response = await showOkCancelAlertDialog( + context: context, + isDestructiveAction: true, + title: L10n.of(context)!.dehydrate, + message: L10n.of(context)!.dehydrateWarning, + ); + if (response != OkCancelResult.ok) { + return; + } + await showFutureLoadingDialog( + context: context, + future: () async { + try { + final export = await Matrix.of(context).client.exportDump(); + final filePickerCross = FilePickerCross( + Uint8List.fromList(const Utf8Codec().encode(export!)), + path: + '/fluffychat-export-${DateFormat(DateFormat.YEAR_MONTH_DAY).format(DateTime.now())}.fluffybackup', + fileExtension: 'fluffybackup'); + await filePickerCross.exportToStorage( + subject: L10n.of(context)!.dehydrateShare, + ); + } catch (e, s) { + Logs().e('Export error', e, s); + } + }, + ); + } } diff --git a/lib/pages/settings_account/settings_account_view.dart b/lib/pages/settings_account/settings_account_view.dart index 267bc07c..cb3fc35b 100644 --- a/lib/pages/settings_account/settings_account_view.dart +++ b/lib/pages/settings_account/settings_account_view.dart @@ -51,6 +51,14 @@ class SettingsAccountView extends StatelessWidget { onTap: controller.logoutAction, ), const Divider(height: 1), + ListTile( + trailing: const Icon(Icons.tap_and_play), + title: Text( + L10n.of(context)!.dehydrate, + style: const TextStyle(color: Colors.red), + ), + onTap: controller.dehydrateAction, + ), ListTile( trailing: const Icon(Icons.delete_outlined), title: Text( diff --git a/lib/utils/tor_stub.dart b/lib/utils/tor_stub.dart new file mode 100644 index 00000000..3223d087 --- /dev/null +++ b/lib/utils/tor_stub.dart @@ -0,0 +1,7 @@ +/// Stub class for [TorBrowserDetector] +/// +/// statically returns false as Tor **browser** can only be detected in a +/// **browser**. +abstract class TorBrowserDetector { + static Future get isTorBrowser => Future.value(false); +} diff --git a/pubspec.lock b/pubspec.lock index 21484873..2f683c27 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -291,7 +291,7 @@ packages: source: hosted version: "1.0.6" dbus: - dependency: "direct overridden" + dependency: transitive description: name: dbus url: "https://pub.dartlang.org" @@ -1701,6 +1701,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.0" + tor_detector_web: + dependency: "direct main" + description: + name: tor_detector_web + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" tuple: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index dd2d4b1e..c26454a8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -58,7 +58,7 @@ dependencies: localstorage: ^4.0.0+1 lottie: ^1.2.2 matrix: ^0.10.3 - matrix_homeserver_recommendations: ^0.2.0 + matrix_homeserver_recommendations: ^0.2.1 matrix_link_text: ^1.0.2 native_imaging: git: https://gitlab.com/famedly/company/frontend/libraries/native_imaging.git @@ -67,7 +67,7 @@ dependencies: pin_code_text_field: ^1.8.0 provider: ^6.0.2 punycode: ^1.0.0 - qr_code_scanner: ^0.7.0 + qr_code_scanner: ^1.0.0 qr_flutter: ^4.0.0 receive_sharing_intent: ^1.4.5 record: ^4.1.1 @@ -77,6 +77,7 @@ dependencies: shared_preferences: ^2.0.13 slugify: ^2.0.0 swipe_to_action: ^0.2.0 + tor_detector_web: ^1.1.0 uni_links: ^0.5.1 unifiedpush: ^4.0.0 universal_html: ^2.0.8 @@ -126,14 +127,12 @@ flutter: dependency_overrides: # Necessary for webRTC on web. # Fix for stream fallback for unsupported browsers: - # https://github.com/fluttercommunity/plus_plugins/pull/746 # Upstream pull request: https://github.com/fluttercommunity/plus_plugins/pull/746 connectivity_plus_web: git: url: https://github.com/TheOneWithTheBraid/plus_plugins.git ref: a04401cb48abe92d138c0e9288b360739994a9e9 path: packages/connectivity_plus/connectivity_plus_web - dbus: ^0.7.1 geolocator_android: hosted: name: geolocator_android