diff --git a/lib/account/account_key_value.dart b/lib/account/account_key_value.dart index ec9d5e6991..10028e5285 100644 --- a/lib/account/account_key_value.dart +++ b/lib/account/account_key_value.dart @@ -1,12 +1,8 @@ import '../utils/extension/extension.dart'; import '../utils/hive_key_values.dart'; -class AccountKeyValue extends HiveKeyValue { - AccountKeyValue._() : super(_hiveAccount); - - static AccountKeyValue? _instance; - - static AccountKeyValue get instance => _instance ??= AccountKeyValue._(); +class AccountKeyValue extends HiveKeyValue { + AccountKeyValue() : super(_hiveAccount); static const _hiveAccount = 'account_box'; static const _hasSyncCircle = 'has_sync_circle'; @@ -65,10 +61,4 @@ class AccountKeyValue extends HiveKeyValue { } box.put(_keyRecentUsedEmoji, recentUsedEmoji); } - - @override - Future delete() { - _recentUsedEmoji = null; - return super.delete(); - } } diff --git a/lib/account/account_server.dart b/lib/account/account_server.dart index 89b563a68f..b256faa0d3 100644 --- a/lib/account/account_server.dart +++ b/lib/account/account_server.dart @@ -7,6 +7,7 @@ import 'package:cross_file/cross_file.dart'; import 'package:dio/dio.dart'; import 'package:drift/drift.dart'; import 'package:flutter/services.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:mixin_bot_sdk_dart/mixin_bot_sdk_dart.dart'; import 'package:rxdart/rxdart.dart'; import 'package:stream_channel/isolate_channel.dart'; @@ -27,15 +28,14 @@ import '../db/extension/job.dart'; import '../db/mixin_database.dart' as db; import '../enum/encrypt_category.dart'; import '../enum/message_category.dart'; -import '../ui/provider/account_server_provider.dart'; -import '../ui/provider/multi_auth_provider.dart'; +import '../ui/provider/account/account_server_provider.dart'; +import '../ui/provider/account/multi_auth_provider.dart'; +import '../ui/provider/hive_key_value_provider.dart'; import '../ui/provider/setting_provider.dart'; import '../utils/app_lifecycle.dart'; import '../utils/attachment/attachment_util.dart'; -import '../utils/attachment/download_key_value.dart'; import '../utils/extension/extension.dart'; import '../utils/file.dart'; -import '../utils/hive_key_values.dart'; import '../utils/logger.dart'; import '../utils/mixin_api_client.dart'; import '../utils/proxy.dart'; @@ -46,33 +46,35 @@ import '../workers/isolate_event.dart'; import '../workers/message_worker_isolate.dart'; import 'account_key_value.dart'; import 'send_message_helper.dart'; -import 'show_pin_message_key_value.dart'; class AccountServer { AccountServer({ required this.multiAuthNotifier, - required this.settingChangeNotifier, required this.database, required this.currentConversationId, - this.userAgent, - this.deviceId, + required this.ref, + required this.hiveKeyValues, }); static String? sid; - set language(String language) => - client.dio.options.headers['Accept-Language'] = language; - final MultiAuthStateNotifier multiAuthNotifier; - final SettingChangeNotifier settingChangeNotifier; final Database database; final GetCurrentConversationId currentConversationId; + + final HiveKeyValues hiveKeyValues; + + PrivacyKeyValue get privacyKeyValue => hiveKeyValues.privacyKeyValue; + + AccountKeyValue get accountKeyValue => hiveKeyValues.accountKeyValue; + Timer? checkSignalKeyTimer; - bool get _loginByPhoneNumber => - AccountKeyValue.instance.primarySessionId == null; + bool get loginByPhoneNumber => accountKeyValue.primarySessionId == null; String? userAgent; String? deviceId; + SignalDatabase? signalDatabase; + final Ref ref; Future initServer( String userId, @@ -81,6 +83,9 @@ class AccountServer { String privateKey, ) async { if (sid == sessionId) return; + + i('AccountServer init: $identityNumber'); + sid = sessionId; this.userId = userId; @@ -88,27 +93,30 @@ class AccountServer { this.identityNumber = identityNumber; this.privateKey = privateKey; - await initKeyValues(identityNumber); - await _initClient(); - checkSignalKeyTimer = Timer.periodic(const Duration(days: 1), (timer) { + signalDatabase = await SignalDatabase.connect( + identityNumber: identityNumber, + fromMainIsolate: true, + ); + checkSignalKeyTimer = + Timer.periodic(const Duration(days: 1), (timer) async { i('refreshSignalKeys periodic'); - checkSignalKey(client); + await checkSignalKey(client, signalDatabase!, database.cryptoKeyValue); }); try { await checkSignalKeys(); - } catch (e, s) { - w('$e, $s'); + } catch (error, stacktrace) { + e('checkSignalKeys failed: $error $stacktrace'); await signOutAndClear(); - multiAuthNotifier.signOut(); + multiAuthNotifier.signOut(userId); rethrow; } unawaited(_start()); - DownloadKeyValue.instance.messageIds.forEach((messageId) { + hiveKeyValues.downloadKeyValue.messageIds.forEach((messageId) { attachmentUtil.downloadAttachment(messageId: messageId); }); appActiveListener.addListener(onActive); @@ -129,7 +137,7 @@ class AccountServer { } } await signOutAndClear(); - multiAuthNotifier.signOut(); + multiAuthNotifier.signOut(userId); } } @@ -144,7 +152,7 @@ class AccountServer { userId: userId, sessionId: sessionId, privateKey: privateKey, - loginByPhoneNumber: _loginByPhoneNumber, + loginByPhoneNumber: loginByPhoneNumber, interceptors: [ InterceptorsWrapper( onError: ( @@ -156,9 +164,15 @@ class AccountServer { }, ), ], - )..configProxySetting(database.settingProperties); - - attachmentUtil = AttachmentUtil.init(client, database, identityNumber); + )..configProxySetting(ref.read(settingProvider)); + + attachmentUtil = AttachmentUtil.init( + client, + database, + identityNumber, + hiveKeyValues.downloadKeyValue, + ref.read(settingProvider), + ); _sendMessageHelper = SendMessageHelper(database, attachmentUtil, addSendingJob); @@ -207,17 +221,18 @@ class AccountServer { sessionId: sessionId, privateKey: privateKey, mixinDocumentDirectory: mixinDocumentsDirectory.path, - primarySessionId: AccountKeyValue.instance.primarySessionId, - loginByPhoneNumber: _loginByPhoneNumber, + primarySessionId: accountKeyValue.primarySessionId, + loginByPhoneNumber: loginByPhoneNumber, rootIsolateToken: ServicesBinding.rootIsolateToken!, ), errorsAreFatal: false, onExit: exitReceivePort.sendPort, onError: errorReceivePort.sendPort, + debugName: 'message_process_isolate_$identityNumber', ); jobSubscribers ..add(exitReceivePort.listen((message) { - w('worker isolate service exited. $message'); + w('worker isolate service exited($identityNumber). $message'); _connectedStateBehaviorSubject.add(ConnectedState.disconnected); })) ..add(errorReceivePort.listen((error) { @@ -241,6 +256,7 @@ class AccountServer { case WorkerIsolateEventType.onIsolateReady: d('message process service ready'); case WorkerIsolateEventType.onBlazeConnectStateChanged: + d('blaze connect state changed: ${event.argument}'); _connectedStateBehaviorSubject.add(event.argument as ConnectedState); case WorkerIsolateEventType.onApiRequestedError: _onClientRequestError(event.argument as DioException); @@ -249,7 +265,7 @@ class AccountServer { _onAttachmentDownloadRequest(request); case WorkerIsolateEventType.showPinMessage: final conversationId = event.argument as String; - unawaited(ShowPinMessageKeyValue.instance.show(conversationId)); + unawaited(hiveKeyValues.showPinMessageKeyValue.show(conversationId)); } } @@ -258,12 +274,13 @@ class AccountServer { AttachmentRequest request, ) async { bool needDownload(String category) { + final settings = ref.read(settingProvider); if (category.isImage) { - return settingChangeNotifier.photoAutoDownload; + return settings.photoAutoDownload; } else if (category.isVideo) { - return settingChangeNotifier.videoAutoDownload; + return settings.videoAutoDownload; } else if (category.isData) { - return settingChangeNotifier.fileAutoDownload; + return settings.fileAutoDownload; } return true; } @@ -301,16 +318,22 @@ class AccountServer { Future signOutAndClear() async { _sendEventToWorkerIsolate(MainIsolateEventType.exit); - await client.accountApi.logout(LogoutRequest(sessionId)); + try { + await client.accountApi.logout(LogoutRequest(sessionId)); + } catch (error, stacktrace) { + e('signOutAndClear logout error: $error $stacktrace'); + } await Future.wait(jobSubscribers.map((s) => s.cancel())); jobSubscribers.clear(); - await clearKeyValues(); + await hiveKeyValues.clearAll(); + await database.cryptoKeyValue.clear(); try { - await SignalDatabase.get.clear(); - } catch (_) { - // ignore closed database error + await signalDatabase?.clear(); + await signalDatabase?.close(); + } catch (error, stacktrace) { + e('signOutAndClear signalDatabase error: $error $stacktrace'); } try { @@ -693,7 +716,7 @@ class AccountServer { List messageIds, Map messageExpireAt, ) async { - final primarySessionId = AccountKeyValue.instance.primarySessionId; + final primarySessionId = accountKeyValue.primarySessionId; if (primarySessionId == null) { return; } @@ -709,6 +732,7 @@ class AccountServer { } Future stop() async { + i('stop account server'); appActiveListener.removeListener(onActive); checkSignalKeyTimer?.cancel(); _sendEventToWorkerIsolate(MainIsolateEventType.exit); @@ -745,18 +769,18 @@ class AccountServer { } Future checkSignalKeys() async { - final hasPushSignalKeys = PrivacyKeyValue.instance.hasPushSignalKeys; + final hasPushSignalKeys = privacyKeyValue.hasPushSignalKeys; if (hasPushSignalKeys) { - unawaited(checkSignalKey(client)); + unawaited( + checkSignalKey(client, signalDatabase!, database.cryptoKeyValue)); } else { - await refreshSignalKeys(client); - PrivacyKeyValue.instance.hasPushSignalKeys = true; + await refreshSignalKeys(client, signalDatabase!, database.cryptoKeyValue); + privacyKeyValue.hasPushSignalKeys = true; } } Future refreshSticker({bool force = false}) async { - final refreshStickerLastTime = - AccountKeyValue.instance.refreshStickerLastTime; + final refreshStickerLastTime = accountKeyValue.refreshStickerLastTime; final now = DateTime.now().millisecondsSinceEpoch; if (!force && now - refreshStickerLastTime < hours24) { return; @@ -797,16 +821,16 @@ class AccountServer { } if (hasNewAlbum) { - AccountKeyValue.instance.hasNewAlbum = true; + accountKeyValue.hasNewAlbum = true; } - AccountKeyValue.instance.refreshStickerLastTime = now; + accountKeyValue.refreshStickerLastTime = now; } final refreshUserIdSet = {}; Future initCircles() async { - final hasSyncCircle = AccountKeyValue.instance.hasSyncCircle; + final hasSyncCircle = accountKeyValue.hasSyncCircle; if (hasSyncCircle) { return; } @@ -821,16 +845,16 @@ class AccountServer { await handleCircle(circle); }); - AccountKeyValue.instance.hasSyncCircle = true; + accountKeyValue.hasSyncCircle = true; } Future _cleanupQuoteContent() async { - final clean = AccountKeyValue.instance.alreadyCleanupQuoteContent; + final clean = accountKeyValue.alreadyCleanupQuoteContent; if (clean) { return; } await database.jobDao.insert(createCleanupQuoteContentJob()); - AccountKeyValue.instance.alreadyCleanupQuoteContent = true; + accountKeyValue.alreadyCleanupQuoteContent = true; } Future checkMigration() async { @@ -1386,13 +1410,15 @@ class AccountServer { Future pinMessage({ required String conversationId, required List pinMessageMinimals, - }) => - _sendMessageHelper.sendPinMessage( - conversationId: conversationId, - senderId: userId, - pinMessageMinimals: pinMessageMinimals, - pin: true, - ); + }) async { + await _sendMessageHelper.sendPinMessage( + conversationId: conversationId, + senderId: userId, + pinMessageMinimals: pinMessageMinimals, + pin: true, + ); + unawaited(hiveKeyValues.showPinMessageKeyValue.show(conversationId)); + } Future unpinMessage({ required String conversationId, diff --git a/lib/account/notification_service.dart b/lib/account/notification_service.dart index 0d80acf7c9..a4ed6de964 100644 --- a/lib/account/notification_service.dart +++ b/lib/account/notification_service.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:flutter/widgets.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:mixin_bot_sdk_dart/mixin_bot_sdk_dart.dart'; import 'package:stream_transform/stream_transform.dart'; @@ -9,9 +10,9 @@ import '../db/database_event_bus.dart'; import '../db/extension/conversation.dart'; import '../enum/message_category.dart'; import '../generated/l10n.dart'; - import '../ui/provider/conversation_provider.dart'; import '../ui/provider/mention_cache_provider.dart'; +import '../ui/provider/setting_provider.dart'; import '../ui/provider/slide_category_provider.dart'; import '../utils/app_lifecycle.dart'; import '../utils/extension/extension.dart'; @@ -22,12 +23,15 @@ import '../utils/message_optimize.dart'; import '../utils/reg_exp_utils.dart'; import '../widgets/message/item/pin_message.dart'; import '../widgets/message/item/system_message.dart'; +import 'account_server.dart'; const _keyConversationId = 'conversationId'; class NotificationService { NotificationService({ required BuildContext context, + required AccountServer accountServer, + required WidgetRef ref, }) { streamSubscriptions ..add(DataBaseEventBus.instance.notificationMessageStream @@ -41,31 +45,28 @@ class NotificationService { })) ..add(DataBaseEventBus.instance.notificationMessageStream .where((event) => event.type != MessageCategory.messageRecall) - .where((event) => event.senderId != context.accountServer.userId) + .where((event) => event.senderId != accountServer.userId) .where((event) => event.createdAt != null && event.createdAt! .isAfter(DateTime.now().subtract(const Duration(minutes: 2)))) .where((event) { if (isAppActive) { - final conversationState = - context.providerContainer.read(conversationProvider); + final conversationState = ref.read(conversationProvider); return event.conversationId != (conversationState?.conversationId ?? conversationState?.conversation?.conversationId); } return true; }) - .asyncMapBuffer((event) => context.database.messageDao + .asyncMapBuffer((event) => accountServer.database.messageDao .notificationMessage(event.map((e) => e.messageId).toList()) .get()) .expand((event) => event) .asyncWhere((event) async { - final account = context.account!; - bool mentionedCurrentUser() => mentionNumberRegExp .allMatchesAndSort(event.content ?? '') - .any((element) => element[1] == account.identityNumber); + .any((element) => element[1] == accountServer.identityNumber); // mention current user if (event.type.isText && mentionedCurrentUser()) return true; @@ -75,7 +76,7 @@ class NotificationService { final json = await jsonDecodeWithIsolate(event.quoteContent ?? '') ?? {}; // ignore: avoid_dynamic_calls - return json['user_id'] == account.userId; + return json['user_id'] == accountServer.userId; } catch (_) { // json decode failed return false; @@ -97,16 +98,15 @@ class NotificationService { ); String? body; - if (context.settingChangeNotifier.messagePreview) { - final mentionCache = - context.providerContainer.read(mentionCacheProvider); + if (ref.read(settingProvider).messagePreview) { + final mentionCache = ref.read(mentionCacheProvider); if (event.type == MessageCategory.systemConversation) { body = generateSystemText( actionName: event.actionName, participantUserId: event.participantUserId, senderId: event.senderId, - currentUserId: context.accountServer.userId, + currentUserId: accountServer.userId, participantFullName: event.participantFullName, senderFullName: event.senderFullName, expireIn: int.tryParse(event.content ?? '0'), @@ -174,7 +174,7 @@ class NotificationService { (event) { i('select notification $event'); - context.providerContainer + ref .read(slideCategoryStateProvider.notifier) .switchToChatsIfSettings(); diff --git a/lib/account/scam_warning_key_value.dart b/lib/account/scam_warning_key_value.dart index 513e175b5c..f458ccf37a 100644 --- a/lib/account/scam_warning_key_value.dart +++ b/lib/account/scam_warning_key_value.dart @@ -3,14 +3,9 @@ import 'package:rxdart/rxdart.dart'; import '../utils/hive_key_values.dart'; class ScamWarningKeyValue extends HiveKeyValue { - ScamWarningKeyValue._() : super(_hiveName); + ScamWarningKeyValue() : super(_hiveName); static const _hiveName = 'scam_warning_key_value'; - static ScamWarningKeyValue? _instance; - - static ScamWarningKeyValue get instance => - _instance ??= ScamWarningKeyValue._(); - bool _isShow(String userId) => box.get(userId, defaultValue: null).isShow; Future dismiss(String userId) => diff --git a/lib/account/security_key_value.dart b/lib/account/security_key_value.dart deleted file mode 100644 index a338f96979..0000000000 --- a/lib/account/security_key_value.dart +++ /dev/null @@ -1,94 +0,0 @@ -import 'package:mixin_logger/mixin_logger.dart'; -import 'package:rxdart/rxdart.dart'; - -import '../utils/hive_key_values.dart'; - -class SecurityKeyValue extends HiveKeyValue { - SecurityKeyValue._() : super(_hiveSecurity); - - static SecurityKeyValue? _instance; - - static SecurityKeyValue get instance => _instance ??= SecurityKeyValue._(); - - static const _hiveSecurity = 'security_box'; - static const _passcode = 'passcode'; - static const _biometric = 'biometric'; - static const _lockDuration = 'lockDuration'; - - String? get passcode { - try { - return box.get(_passcode) as String?; - } catch (e, s) { - i('[SecurityKeyValue] passcode error: $e, $s'); - return null; - } - } - - set passcode(String? value) { - if (value != null && value.length != 6) { - throw ArgumentError('Passcode must be 6 digits'); - } - box.put(_passcode, value); - if (value == null) { - lockDuration = null; - biometric = false; - } - } - - bool get biometric { - try { - return box.get(_biometric, defaultValue: false) as bool; - } catch (e, s) { - i('[SecurityKeyValue] biometric error: $e, $s'); - return false; - } - } - - set biometric(bool value) => box.put(_biometric, value); - - bool get hasPasscode => passcode != null; - - // must be return non-null value - Duration? get lockDuration { - dynamic minutes; - try { - minutes = box.get(_lockDuration); - } catch (_) {} - if (minutes == null) return const Duration(minutes: 1); - return Duration(minutes: minutes as int); - } - - set lockDuration(Duration? value) => box.put(_lockDuration, value?.inMinutes); - - Stream watchHasPasscode() { - try { - return box - .watch(key: _passcode) - .map((event) => event.value != null) - .startWith(passcode != null) - .onErrorReturn(false); - } catch (e, s) { - i('[SecurityKeyValue] watchHasPasscode error: $e, $s'); - return Stream.value(false); - } - } - - Stream watchLockDuration() => - box.watch(key: _lockDuration).map((event) { - final minutes = event.value; - if (minutes == null) return const Duration(minutes: 1); - return Duration(minutes: minutes as int); - }); - - Stream watchBiometric() { - try { - return box - .watch(key: _biometric) - .map((event) => (event.value ?? false) as bool) - .onErrorReturn(false); - } catch (e, s) { - i('[SecurityKeyValue] watchBiometric error: $e, $s'); - return Stream.value(false); - } - } -} diff --git a/lib/account/send_message_helper.dart b/lib/account/send_message_helper.dart index 499098cb31..865370098e 100644 --- a/lib/account/send_message_helper.dart +++ b/lib/account/send_message_helper.dart @@ -30,7 +30,6 @@ import '../utils/logger.dart'; import '../utils/reg_exp_utils.dart'; import '../widgets/cache_image.dart'; import '../widgets/message/send_message_dialog/attachment_extra.dart'; -import 'show_pin_message_key_value.dart'; const jpegMimeType = 'image/jpeg'; const gifMimeType = 'image/gif'; @@ -1187,7 +1186,6 @@ class SendMessageHelper { cleanDraft: false, ); }); - unawaited(ShowPinMessageKeyValue.instance.show(conversationId)); } else { await _pinMessageDao .deleteByIds(pinMessageMinimals.map((e) => e.messageId).toList()); diff --git a/lib/account/session_key_value.dart b/lib/account/session_key_value.dart index adcd36ce63..df367f4eb5 100644 --- a/lib/account/session_key_value.dart +++ b/lib/account/session_key_value.dart @@ -9,11 +9,7 @@ import '../utils/hive_key_values.dart'; import '../utils/logger.dart'; class SessionKeyValue extends HiveKeyValue { - SessionKeyValue._() : super('session_box'); - - static SessionKeyValue? _instance; - - static SessionKeyValue get instance => _instance ??= SessionKeyValue._(); + SessionKeyValue() : super('session_box'); static const _keyPinToken = 'pinToken'; static const _keyPinIterator = 'pinIterator'; @@ -36,30 +32,30 @@ List decryptPinToken(String serverPublicKey, ed.PrivateKey privateKey) { return calculateAgreement(bytes, private); } -String? encryptPin(String code) { - assert(code.isNotEmpty, 'code is empty'); - final iterator = SessionKeyValue.instance.pinIterator; - final pinToken = SessionKeyValue.instance.pinToken; +extension EncryptPin on SessionKeyValue { + String? encryptPin(String code) { + assert(code.isNotEmpty, 'code is empty'); - if (pinToken == null) { - e('pinToken is null'); - return null; - } + if (pinToken == null) { + e('pinToken is null'); + return null; + } - d('pinToken: $pinToken'); + d('pinToken: $pinToken'); - final pinBytes = Uint8List.fromList(utf8.encode(code)); - final timeBytes = Uint8List(8); - final iteratorBytes = Uint8List(8); - final nowSec = DateTime.now().millisecondsSinceEpoch ~/ 1000; - timeBytes.buffer.asByteData().setUint64(0, nowSec, Endian.little); - iteratorBytes.buffer.asByteData().setUint64(0, iterator, Endian.little); + final pinBytes = Uint8List.fromList(utf8.encode(code)); + final timeBytes = Uint8List(8); + final iteratorBytes = Uint8List(8); + final nowSec = DateTime.now().millisecondsSinceEpoch ~/ 1000; + timeBytes.buffer.asByteData().setUint64(0, nowSec, Endian.little); + iteratorBytes.buffer.asByteData().setUint64(0, pinIterator, Endian.little); - // pin+time+iterator - final plaintext = Uint8List.fromList(pinBytes + timeBytes + iteratorBytes); - final ciphertext = aesEncrypt(base64Decode(pinToken), plaintext); + // pin+time+iterator + final plaintext = Uint8List.fromList(pinBytes + timeBytes + iteratorBytes); + final ciphertext = aesEncrypt(base64Decode(pinToken!), plaintext); - SessionKeyValue.instance.pinIterator = iterator + 1; + pinIterator = pinIterator + 1; - return base64Encode(ciphertext); + return base64Encode(ciphertext); + } } diff --git a/lib/account/show_pin_message_key_value.dart b/lib/account/show_pin_message_key_value.dart index e976ce0f83..d5c964f4fe 100644 --- a/lib/account/show_pin_message_key_value.dart +++ b/lib/account/show_pin_message_key_value.dart @@ -3,15 +3,10 @@ import 'package:rxdart/rxdart.dart'; import '../utils/hive_key_values.dart'; class ShowPinMessageKeyValue extends HiveKeyValue { - ShowPinMessageKeyValue._() : super(_hiveName); + ShowPinMessageKeyValue() : super(_hiveName); static const _hiveName = 'show_pin_message_box'; - static ShowPinMessageKeyValue? _instance; - - static ShowPinMessageKeyValue get instance => - _instance ??= ShowPinMessageKeyValue._(); - Future show(String conversationId) => box.put(conversationId, true); bool isShow(String conversationId) => diff --git a/lib/app.dart b/lib/app.dart index 12a69007e1..ccab187add 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -10,24 +10,25 @@ import 'package:hooks_riverpod/hooks_riverpod.dart' hide Consumer, FutureProvider, Provider; import 'package:provider/provider.dart'; -import 'account/account_key_value.dart'; import 'account/notification_service.dart'; import 'constants/brightness_theme_data.dart'; import 'constants/resources.dart'; import 'generated/l10n.dart'; - import 'ui/home/bloc/conversation_list_bloc.dart'; import 'ui/home/conversation/conversation_page.dart'; import 'ui/home/home.dart'; import 'ui/landing/landing.dart'; import 'ui/landing/landing_failed.dart'; -import 'ui/provider/account_server_provider.dart'; +import 'ui/landing/landing_initialize.dart'; +import 'ui/provider/account/account_server_provider.dart'; +import 'ui/provider/account/multi_auth_provider.dart'; import 'ui/provider/database_provider.dart'; +import 'ui/provider/hive_key_value_provider.dart'; import 'ui/provider/mention_cache_provider.dart'; -import 'ui/provider/multi_auth_provider.dart'; import 'ui/provider/setting_provider.dart'; import 'ui/provider/slide_category_provider.dart'; import 'utils/extension/extension.dart'; +import 'utils/hook.dart'; import 'utils/logger.dart'; import 'utils/platform.dart'; import 'utils/system/system_fonts.dart'; @@ -52,13 +53,22 @@ class App extends HookConsumerWidget { precacheImage( const AssetImage(Resources.assetsImagesChatBackgroundPng), context); + final initialized = useMemoizedFuture( + () => ref.read(multiAuthStateNotifierProvider.notifier).initialized, + null, + ); + + if (initialized.connectionState == ConnectionState.waiting) { + return const _App(home: AppInitializingPage()); + } + final authState = ref.watch(authProvider); Widget child; if (authState == null) { child = const _App(home: LandingPage()); } else { - child = _LoginApp(authState: authState); + child = const _LoginApp(); } return FocusHelper(child: child); @@ -66,16 +76,15 @@ class App extends HookConsumerWidget { } class _LoginApp extends HookConsumerWidget { - const _LoginApp({required this.authState}); - - final AuthState authState; + const _LoginApp(); @override Widget build(BuildContext context, WidgetRef ref) { final database = ref.watch(databaseProvider); + final accountServer = ref.watch(accountServerProvider); - if (database.isLoading) { - return const _App(home: LandingPage()); + if (database.isLoading || accountServer.isLoading) { + return const _App(home: AppInitializingPage()); } if (database.hasError) { var error = database.error; @@ -121,6 +130,7 @@ class _Providers extends HookConsumerWidget { return MultiBlocProvider( providers: [ BlocProvider( + key: ValueKey(accountServer), create: (BuildContext context) => ConversationListBloc( ref.read(slideCategoryStateProvider.notifier), accountServer.database, @@ -129,7 +139,11 @@ class _Providers extends HookConsumerWidget { ), ], child: Provider( - create: (BuildContext context) => NotificationService(context: context), + create: (BuildContext context) => NotificationService( + context: context, + accountServer: accountServer, + ref: ref, + ), lazy: false, dispose: (_, notificationService) => notificationService.close(), child: PortalProviders(child: app), @@ -175,17 +189,15 @@ class _App extends HookConsumerWidget { ), useMaterial3: true, ).withFallbackFonts(), - themeMode: ref.watch(settingProvider).themeMode, + themeMode: + ref.watch(settingProvider.select((value) => value.themeMode)), builder: (context, child) { - try { - context.accountServer.language = - Localizations.localeOf(context).languageCode; - } catch (_) {} final mediaQueryData = MediaQuery.of(context); return BrightnessObserver( lightThemeData: lightBrightnessThemeData, darkThemeData: darkBrightnessThemeData, - forceBrightness: ref.watch(settingProvider).brightness, + forceBrightness: ref + .watch(settingProvider.select((value) => value.brightness)), child: MediaQuery( data: mediaQueryData.copyWith( // Different linux distro change the value, e.g. 1.2 @@ -233,17 +245,22 @@ class _Home extends HookConsumerWidget { final currentDeviceId = await getDeviceId(); if (currentDeviceId == 'unknown') return; - final deviceId = AccountKeyValue.instance.deviceId; - + final accountKeyValue = + await ref.read(currentAccountKeyValueProvider.future); + if (accountKeyValue == null) { + w('checkDeviceId error: accountKeyValue is null'); + return; + } + final deviceId = accountKeyValue.deviceId; if (deviceId == null) { - await AccountKeyValue.instance.setDeviceId(currentDeviceId); + await accountKeyValue.setDeviceId(currentDeviceId); return; } if (deviceId.toLowerCase() != currentDeviceId.toLowerCase()) { final multiAuthCubit = context.multiAuthChangeNotifier; await accountServer.signOutAndClear(); - multiAuthCubit.signOut(); + multiAuthCubit.signOut(context.accountServer.userId); } } catch (e) { w('checkDeviceId error: $e'); diff --git a/lib/blaze/blaze.dart b/lib/blaze/blaze.dart index 350ce42b42..4480427da1 100644 --- a/lib/blaze/blaze.dart +++ b/lib/blaze/blaze.dart @@ -12,6 +12,7 @@ import '../constants/constants.dart'; import '../db/database.dart'; import '../db/extension/job.dart'; import '../db/mixin_database.dart'; +import '../ui/provider/setting_provider.dart'; import '../utils/extension/extension.dart'; import '../utils/logger.dart'; import '../utils/proxy.dart'; @@ -42,9 +43,10 @@ class Blaze { this.userAgent, this.ackJob, this.floodJob, + this.settingKeyValue, ) { - database.settingProperties.addListener(_onProxySettingChanged); - proxyConfig = database.settingProperties.activatedProxy; + settingKeyValue.addListener(_onProxySettingChanged); + proxyConfig = settingKeyValue.activatedProxy; } final String userId; @@ -56,6 +58,7 @@ class Blaze { final FloodJob floodJob; final String? userAgent; + final AppSettingKeyValue settingKeyValue; ProxyConfig? proxyConfig; @@ -330,8 +333,8 @@ class Blaze { await client.accountApi.getMe(); i('http ping'); await connect(); - } catch (e) { - w('ws ping error: $e'); + } catch (e, s) { + w('ws ping error: $e $s'); if (e is MixinApiError && e.error != null && e.error is MixinError && @@ -347,7 +350,7 @@ class Blaze { } void _onProxySettingChanged() { - final url = database.settingProperties.activatedProxy; + final url = settingKeyValue.activatedProxy; if (url == proxyConfig) { return; } @@ -357,7 +360,7 @@ class Blaze { } void dispose() { - database.settingProperties.removeListener(_onProxySettingChanged); + settingKeyValue.removeListener(_onProxySettingChanged); _disconnect(); _connectedStateBehaviorSubject.close(); } diff --git a/lib/constants/constants.dart b/lib/constants/constants.dart index 90cd1d67f8..e54f6ce165 100644 --- a/lib/constants/constants.dart +++ b/lib/constants/constants.dart @@ -69,3 +69,8 @@ const kRecaptchaKey = ''; const hCaptchaKey = ''; const kDbFileName = 'mixin'; + +const kHttpLogLevel = String.fromEnvironment( + 'MIXIN_HTTP_LOG_LEVEL', + defaultValue: 'all', +); diff --git a/lib/crypto/crypto_key_value.dart b/lib/crypto/crypto_key_value.dart index 0a9e299e05..d229b9c541 100644 --- a/lib/crypto/crypto_key_value.dart +++ b/lib/crypto/crypto_key_value.dart @@ -1,42 +1,51 @@ -// ignore: implementation_imports -import 'package:libsignal_protocol_dart/src/util/medium.dart'; +import 'dart:io'; -import '../utils/crypto_util.dart'; -import '../utils/hive_key_values.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:hive/hive.dart'; +import 'package:path/path.dart' as p; -class CryptoKeyValue extends HiveKeyValue { - CryptoKeyValue._() : super(_hiveCrypto); +import '../utils/db/user_crypto_key_value.dart'; +import '../utils/file.dart'; - static CryptoKeyValue? _instance; - - static CryptoKeyValue get instance => _instance ??= CryptoKeyValue._(); +class CryptoKeyValue { + CryptoKeyValue(); static const _hiveCrypto = 'crypto_box'; - static const _localRegistrationId = 'local_registration_id'; static const _nextPreKeyId = 'next_pre_key_id'; static const _nextSignedPreKeyId = 'next_signed_pre_key_id'; static const _activeSignedPreKeyId = 'active_signed_pre_key_id'; - int get localRegistrationId => - box.get(_localRegistrationId, defaultValue: 0)!; - - set localRegistrationId(int registrationId) => - box.put(_localRegistrationId, registrationId); - - int get nextPreKeyId => - box.get(_nextPreKeyId, defaultValue: generateRandomInt(maxValue))!; - - set nextPreKeyId(int preKeyId) => box.put(_nextPreKeyId, preKeyId); - - int get nextSignedPreKeyId => - box.get(_nextSignedPreKeyId, defaultValue: generateRandomInt(maxValue))!; - - set nextSignedPreKeyId(int preKeyId) => - box.put(_nextSignedPreKeyId, preKeyId); - - int get activeSignedPreKeyId => - box.get(_activeSignedPreKeyId, defaultValue: -1)!; - - set activeSignedPreKeyId(int preKeyId) => - box.put(_activeSignedPreKeyId, preKeyId); + Future migrateToNewCryptoKeyValue( + HiveInterface hive, + String identityNumber, + UserCryptoKeyValue cryptoKeyValue, + ) async { + final directory = Directory( + p.join(mixinDocumentsDirectory.path, identityNumber, _hiveCrypto)); + WidgetsFlutterBinding.ensureInitialized(); + if (!kIsWeb) { + hive.init(directory.absolute.path); + } + final exist = await hive.boxExists(_hiveCrypto); + if (!exist) { + return; + } + final box = await hive.openBox(_hiveCrypto); + final nextPreKeyId = box.get(_nextPreKeyId, defaultValue: null) as int?; + final nextSignedPreKeyId = + box.get(_nextSignedPreKeyId, defaultValue: null) as int?; + final activeSignedPreKeyId = + box.get(_activeSignedPreKeyId, defaultValue: null) as int?; + if (nextPreKeyId != null) { + await cryptoKeyValue.setNextPreKeyId(nextPreKeyId); + } + if (nextSignedPreKeyId != null) { + await cryptoKeyValue.setNextSignedPreKeyId(nextSignedPreKeyId); + } + if (activeSignedPreKeyId != null) { + await cryptoKeyValue.setActiveSignedPreKeyId(activeSignedPreKeyId); + } + await box.deleteFromDisk(); + } } diff --git a/lib/crypto/privacy_key_value.dart b/lib/crypto/privacy_key_value.dart index 12d46cfae2..a17d12a83c 100644 --- a/lib/crypto/privacy_key_value.dart +++ b/lib/crypto/privacy_key_value.dart @@ -1,11 +1,7 @@ import '../utils/hive_key_values.dart'; class PrivacyKeyValue extends HiveKeyValue { - PrivacyKeyValue._() : super(_hivePrivacy); - - static PrivacyKeyValue? _instance; - - static PrivacyKeyValue get instance => _instance ??= PrivacyKeyValue._(); + PrivacyKeyValue() : super(_hivePrivacy); static const _hivePrivacy = 'privacy_box'; static const _hasSyncSession = 'has_sync_session'; diff --git a/lib/crypto/signal/identity_key_util.dart b/lib/crypto/signal/identity_key_util.dart index cc48e7721e..ce9cf960e8 100644 --- a/lib/crypto/signal/identity_key_util.dart +++ b/lib/crypto/signal/identity_key_util.dart @@ -4,11 +4,11 @@ import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'identity_extension.dart'; import 'signal_database.dart'; -Future generateSignalDatabaseIdentityKeyPair( +Future generateSignalDatabaseIdentityKeyPair( SignalDatabase db, List? privateKey, + int registrationId, ) async { - final registrationId = generateRegistrationId(false); final identityKeyPair = privateKey == null ? generateIdentityKeyPair() : generateIdentityKeyPairFromPrivate(privateKey); @@ -19,7 +19,6 @@ Future generateSignalDatabaseIdentityKeyPair( privateKey: Value(identityKeyPair.getPrivateKey().serialize()), timestamp: DateTime.now().millisecondsSinceEpoch); await db.identityDao.insert(identity); - return registrationId; } Future getIdentityKeyPair(SignalDatabase db) async => diff --git a/lib/crypto/signal/pre_key_util.dart b/lib/crypto/signal/pre_key_util.dart index 60cd4e9d34..fe2d995e5a 100644 --- a/lib/crypto/signal/pre_key_util.dart +++ b/lib/crypto/signal/pre_key_util.dart @@ -3,36 +3,40 @@ import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; // ignore: implementation_imports import 'package:libsignal_protocol_dart/src/util/key_helper.dart' as helper; -import '../crypto_key_value.dart'; +import '../../utils/db/user_crypto_key_value.dart'; import 'signal_database.dart'; import 'storage/mixin_prekey_store.dart'; const batchSize = 700; -Future> generatePreKeys() async { - final preKeyStore = MixinPreKeyStore(SignalDatabase.get); - final preKeyIdOffset = CryptoKeyValue.instance.nextPreKeyId; +Future> generatePreKeys( + SignalDatabase database, UserCryptoKeyValue cryptoKeyValue) async { + final preKeyStore = MixinPreKeyStore(database); + final preKeyIdOffset = await cryptoKeyValue.getNextPreKeyId(); final records = helper.generatePreKeys(preKeyIdOffset, batchSize); final preKeys = []; for (final r in records) { preKeys.add(PrekeysCompanion.insert(prekeyId: r.id, record: r.serialize())); } await preKeyStore.storePreKeyList(preKeys); - CryptoKeyValue.instance.nextPreKeyId = - (preKeyIdOffset + batchSize + 1) % maxValue; + await cryptoKeyValue + .setNextPreKeyId((preKeyIdOffset + batchSize + 1) % maxValue); return records; } Future generateSignedPreKey( - IdentityKeyPair identityKeyPair, bool active) async { - final signedPreKeyStore = MixinPreKeyStore(SignalDatabase.get); - final signedPreKeyId = CryptoKeyValue.instance.nextSignedPreKeyId; + IdentityKeyPair identityKeyPair, + bool active, + SignalDatabase database, + UserCryptoKeyValue cryptoKeyValue) async { + final signedPreKeyStore = MixinPreKeyStore(database); + final signedPreKeyId = await cryptoKeyValue.getNextSignedPreKeyId(); final record = helper.generateSignedPreKey(identityKeyPair, signedPreKeyId); await signedPreKeyStore.storeSignedPreKey(signedPreKeyId, record); - CryptoKeyValue.instance.nextSignedPreKeyId = (signedPreKeyId + 1) % maxValue; + await cryptoKeyValue.setNextSignedPreKeyId((signedPreKeyId + 1) % maxValue); if (active) { - CryptoKeyValue.instance.activeSignedPreKeyId = signedPreKeyId; + await cryptoKeyValue.setActiveSignedPreKeyId(signedPreKeyId); } return record; } diff --git a/lib/crypto/signal/signal_database.dart b/lib/crypto/signal/signal_database.dart index 360f0ca8e5..af7c274a4c 100644 --- a/lib/crypto/signal/signal_database.dart +++ b/lib/crypto/signal/signal_database.dart @@ -1,10 +1,9 @@ -import 'dart:io'; +import 'dart:async'; import 'package:drift/drift.dart'; -import 'package:drift/native.dart'; -import 'package:path/path.dart' as p; -import '../../utils/file.dart'; +import '../../db/util/open_database.dart'; +import '../../utils/logger.dart'; import 'dao/identity_dao.dart'; import 'dao/pre_key_dao.dart'; import 'dao/ratchet_sender_key_dao.dart'; @@ -31,11 +30,20 @@ part 'signal_database.g.dart'; RatchetSenderKeyDao, ]) class SignalDatabase extends _$SignalDatabase { - SignalDatabase._() : super(_openConnection()); + SignalDatabase._(super.e); - static SignalDatabase? _instance; - - static SignalDatabase get get => _instance ??= SignalDatabase._(); + static Future connect({ + required String identityNumber, + required bool fromMainIsolate, + }) async { + final executor = await openQueryExecutor( + identityNumber: identityNumber, + dbName: 'signal', + fromMainIsolate: fromMainIsolate, + readCount: 0, + ); + return SignalDatabase._(executor); + } @override int get schemaVersion => 1; @@ -50,15 +58,10 @@ class SignalDatabase extends _$SignalDatabase { }); Future clear() => transaction(() async { + i('clear signal database'); await customStatement('PRAGMA wal_checkpoint(FULL)'); for (final table in allTables) { await delete(table).go(); } }); } - -LazyDatabase _openConnection() => LazyDatabase(() { - final dbFolder = mixinDocumentsDirectory; - final file = File(p.join(dbFolder.path, 'signal.db')); - return NativeDatabase(file); - }); diff --git a/lib/crypto/signal/signal_key_util.dart b/lib/crypto/signal/signal_key_util.dart index 7cb93f4a15..5233dfc614 100644 --- a/lib/crypto/signal/signal_key_util.dart +++ b/lib/crypto/signal/signal_key_util.dart @@ -2,6 +2,7 @@ import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart' hide generatePreKeys, generateSignedPreKey; import 'package:mixin_bot_sdk_dart/mixin_bot_sdk_dart.dart'; +import '../../utils/db/user_crypto_key_value.dart'; import 'identity_key_util.dart'; import 'pre_key_util.dart'; import 'signal_database.dart'; @@ -9,27 +10,31 @@ import 'signal_key_request.dart'; const int preKeyMinNum = 500; -Future checkSignalKey(Client client) async { +Future checkSignalKey(Client client, SignalDatabase signalDatabase, + UserCryptoKeyValue cryptoKeyValue) async { final response = await client.accountApi.getSignalKeyCount(); final availableKeyCount = response.data.preKeyCount; if (availableKeyCount > preKeyMinNum) { return; } - await refreshSignalKeys(client); + await refreshSignalKeys(client, signalDatabase, cryptoKeyValue); } -Future> refreshSignalKeys(Client client) async { - final keys = await generateKeys(); +Future> refreshSignalKeys(Client client, + SignalDatabase signalDatabase, UserCryptoKeyValue cryptoKeyValue) async { + final keys = await generateKeys(signalDatabase, cryptoKeyValue); return client.accountApi.pushSignalKeys(keys.toJson()); } -Future generateKeys() async { - final identityKeyPair = await getIdentityKeyPair(SignalDatabase.get); +Future generateKeys( + SignalDatabase signalDatabase, UserCryptoKeyValue cryptoKeyValue) async { + final identityKeyPair = await getIdentityKeyPair(signalDatabase); if (identityKeyPair == null) { throw InvalidKeyException('Local identity key pair is null!'); } - final oneTimePreKeys = await generatePreKeys(); - final signedPreKeyRecord = await generateSignedPreKey(identityKeyPair, false); + final oneTimePreKeys = await generatePreKeys(signalDatabase, cryptoKeyValue); + final signedPreKeyRecord = await generateSignedPreKey( + identityKeyPair, false, signalDatabase, cryptoKeyValue); return SignalKeyRequest.from( identityKeyPair.getPublicKey(), signedPreKeyRecord, preKeyRecords: oneTimePreKeys); diff --git a/lib/crypto/signal/signal_protocol.dart b/lib/crypto/signal/signal_protocol.dart index 2549d75a14..05b32f72be 100644 --- a/lib/crypto/signal/signal_protocol.dart +++ b/lib/crypto/signal/signal_protocol.dart @@ -24,22 +24,31 @@ import 'storage/mixin_session_store.dart'; import 'storage/mixin_signal_protocol_store.dart'; class SignalProtocol { - SignalProtocol(this._accountId); + SignalProtocol(this._accountId, this.db); static const int defaultDeviceId = 1; final String _accountId; - late SignalDatabase db; + final SignalDatabase db; late MixinSignalProtocolStore mixinSignalProtocolStore; late MixinSenderKeyStore senderKeyStore; - static Future initSignal(List? private) => - generateSignalDatabaseIdentityKeyPair(SignalDatabase.get, private); + static Future initSignal( + String identityNumber, int registrationId, List? private) async { + final db = await SignalDatabase.connect( + identityNumber: identityNumber, + fromMainIsolate: true, + ); + try { + await generateSignalDatabaseIdentityKeyPair(db, private, registrationId); + } finally { + await db.close(); + } + } void init() { - db = SignalDatabase.get; final preKeyStore = MixinPreKeyStore(db); final signedPreKeyStore = MixinPreKeyStore(db); final identityKeyStore = MixinIdentityKeyStore(db, _accountId); diff --git a/lib/db/app/app_database.dart b/lib/db/app/app_database.dart new file mode 100644 index 0000000000..4136e96e8b --- /dev/null +++ b/lib/db/app/app_database.dart @@ -0,0 +1,45 @@ +import 'dart:io'; + +import 'package:drift/drift.dart'; +import 'package:path/path.dart' as p; + +import '../../enum/property_group.dart'; +import '../../ui/provider/setting_provider.dart'; +import '../../utils/file.dart'; +import '../util/open_database.dart'; +import 'converter/app_property_group_converter.dart'; +import 'dao/app_key_value_dao.dart'; + +part 'app_database.g.dart'; + +/// The database for application, shared by all users. +@DriftDatabase( + include: {'drift/app.drift'}, + daos: [ + AppKeyValueDao, + ], +) +class AppDatabase extends _$AppDatabase { + AppDatabase(super.e); + + factory AppDatabase.connect({ + bool fromMainIsolate = false, + }) { + final dbFilePath = p.join(mixinDocumentsDirectory.path, 'app.db'); + return AppDatabase(LazyDatabase(() async { + final queryExecutor = await createOrConnectDriftIsolate( + portName: 'one_mixin_drift_app', + debugName: 'isolate_drift_app', + fromMainIsolate: fromMainIsolate, + dbFile: File(dbFilePath), + ); + return queryExecutor.connect(); + })); + } + + late final AppSettingKeyValue settingKeyValue = + AppSettingKeyValue(appKeyValueDao); + + @override + int get schemaVersion => 1; +} diff --git a/lib/db/app/app_database.g.dart b/lib/db/app/app_database.g.dart new file mode 100644 index 0000000000..22a8987afc --- /dev/null +++ b/lib/db/app/app_database.g.dart @@ -0,0 +1,246 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'app_database.dart'; + +// ignore_for_file: type=lint +class Properties extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Properties(this.attachedDatabase, [this._alias]); + static const VerificationMeta _keyMeta = const VerificationMeta('key'); + late final GeneratedColumn key = GeneratedColumn( + 'key', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL'); + static const VerificationMeta _groupMeta = const VerificationMeta('group'); + late final GeneratedColumnWithTypeConverter group = + GeneratedColumn('group', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL') + .withConverter(Properties.$convertergroup); + static const VerificationMeta _valueMeta = const VerificationMeta('value'); + late final GeneratedColumn value = GeneratedColumn( + 'value', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL'); + @override + List get $columns => [key, group, value]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'properties'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('key')) { + context.handle( + _keyMeta, key.isAcceptableOrUnknown(data['key']!, _keyMeta)); + } else if (isInserting) { + context.missing(_keyMeta); + } + context.handle(_groupMeta, const VerificationResult.success()); + if (data.containsKey('value')) { + context.handle( + _valueMeta, value.isAcceptableOrUnknown(data['value']!, _valueMeta)); + } else if (isInserting) { + context.missing(_valueMeta); + } + return context; + } + + @override + Set get $primaryKey => {key, group}; + @override + Propertie map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return Propertie( + key: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}key'])!, + group: Properties.$convertergroup.fromSql(attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}group'])!), + value: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}value'])!, + ); + } + + @override + Properties createAlias(String alias) { + return Properties(attachedDatabase, alias); + } + + static TypeConverter $convertergroup = + const AppPropertyGroupConverter(); + @override + List get customConstraints => const ['PRIMARY KEY("key", "group")']; + @override + bool get dontWriteConstraints => true; +} + +class Propertie extends DataClass implements Insertable { + final String key; + final AppPropertyGroup group; + final String value; + const Propertie( + {required this.key, required this.group, required this.value}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['key'] = Variable(key); + { + final converter = Properties.$convertergroup; + map['group'] = Variable(converter.toSql(group)); + } + map['value'] = Variable(value); + return map; + } + + PropertiesCompanion toCompanion(bool nullToAbsent) { + return PropertiesCompanion( + key: Value(key), + group: Value(group), + value: Value(value), + ); + } + + factory Propertie.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return Propertie( + key: serializer.fromJson(json['key']), + group: serializer.fromJson(json['group']), + value: serializer.fromJson(json['value']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'key': serializer.toJson(key), + 'group': serializer.toJson(group), + 'value': serializer.toJson(value), + }; + } + + Propertie copyWith({String? key, AppPropertyGroup? group, String? value}) => + Propertie( + key: key ?? this.key, + group: group ?? this.group, + value: value ?? this.value, + ); + @override + String toString() { + return (StringBuffer('Propertie(') + ..write('key: $key, ') + ..write('group: $group, ') + ..write('value: $value') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(key, group, value); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is Propertie && + other.key == this.key && + other.group == this.group && + other.value == this.value); +} + +class PropertiesCompanion extends UpdateCompanion { + final Value key; + final Value group; + final Value value; + final Value rowid; + const PropertiesCompanion({ + this.key = const Value.absent(), + this.group = const Value.absent(), + this.value = const Value.absent(), + this.rowid = const Value.absent(), + }); + PropertiesCompanion.insert({ + required String key, + required AppPropertyGroup group, + required String value, + this.rowid = const Value.absent(), + }) : key = Value(key), + group = Value(group), + value = Value(value); + static Insertable custom({ + Expression? key, + Expression? group, + Expression? value, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (key != null) 'key': key, + if (group != null) 'group': group, + if (value != null) 'value': value, + if (rowid != null) 'rowid': rowid, + }); + } + + PropertiesCompanion copyWith( + {Value? key, + Value? group, + Value? value, + Value? rowid}) { + return PropertiesCompanion( + key: key ?? this.key, + group: group ?? this.group, + value: value ?? this.value, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (key.present) { + map['key'] = Variable(key.value); + } + if (group.present) { + final converter = Properties.$convertergroup; + + map['group'] = Variable(converter.toSql(group.value)); + } + if (value.present) { + map['value'] = Variable(value.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PropertiesCompanion(') + ..write('key: $key, ') + ..write('group: $group, ') + ..write('value: $value, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +abstract class _$AppDatabase extends GeneratedDatabase { + _$AppDatabase(QueryExecutor e) : super(e); + late final Properties properties = Properties(this); + late final AppKeyValueDao appKeyValueDao = + AppKeyValueDao(this as AppDatabase); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [properties]; +} diff --git a/lib/db/app/converter/app_property_group_converter.dart b/lib/db/app/converter/app_property_group_converter.dart new file mode 100644 index 0000000000..2dab4f645b --- /dev/null +++ b/lib/db/app/converter/app_property_group_converter.dart @@ -0,0 +1,15 @@ +import 'package:drift/drift.dart'; + +import '../../../enum/property_group.dart'; + +class AppPropertyGroupConverter + extends TypeConverter { + const AppPropertyGroupConverter(); + + @override + AppPropertyGroup fromSql(String fromDb) => + AppPropertyGroup.values.byName(fromDb); + + @override + String toSql(AppPropertyGroup value) => value.name; +} diff --git a/lib/db/app/dao/app_key_value_dao.dart b/lib/db/app/dao/app_key_value_dao.dart new file mode 100644 index 0000000000..600c5d9474 --- /dev/null +++ b/lib/db/app/dao/app_key_value_dao.dart @@ -0,0 +1,72 @@ +import 'package:drift/drift.dart'; + +import '../../../enum/property_group.dart'; +import '../../../utils/db/db_key_value.dart'; +import '../app_database.dart'; + +part 'app_key_value_dao.g.dart'; + +@DriftAccessor( + include: {'../drift/app.drift'}, +) +class AppKeyValueDao extends DatabaseAccessor + with _$AppKeyValueDaoMixin + implements KeyValueDao { + AppKeyValueDao(super.attachedDatabase); + + @override + Future clear(AppPropertyGroup group) => + (delete(properties)..where((tbl) => tbl.group.equalsValue(group))).go(); + + @override + Future> getAll(AppPropertyGroup group) async { + final result = await (select(properties) + ..where((tbl) => tbl.group.equalsValue(group))) + .get(); + return Map.fromEntries(result.map((e) => MapEntry(e.key, e.value))); + } + + @override + Future getByKey(AppPropertyGroup group, String key) async { + final result = await (select(properties) + ..where((tbl) => tbl.group.equalsValue(group) & tbl.key.equals(key))) + .getSingleOrNull(); + return result?.value; + } + + @override + Future set(AppPropertyGroup group, String key, String? value) async { + if (value != null) { + await into(properties).insertOnConflictUpdate( + PropertiesCompanion.insert( + group: group, + key: key, + value: value, + ), + ); + } else { + await (delete(properties) + ..where( + (tbl) => tbl.group.equalsValue(group) & tbl.key.equals(key))) + .go(); + } + } + + @override + Stream> watchAll(AppPropertyGroup group) => + (select(properties)..where((tbl) => tbl.group.equalsValue(group))) + .watch() + .map((event) => + Map.fromEntries(event.map((e) => MapEntry(e.key, e.value)))); + + @override + Stream watchByKey(AppPropertyGroup group, String key) => (select( + properties) + ..where((tbl) => tbl.group.equalsValue(group) & tbl.key.equals(key))) + .watchSingleOrNull() + .map((event) => event?.value); + + @override + Stream watchTableHasChanged(AppPropertyGroup group) => + db.tableUpdates(TableUpdateQuery.onTable(db.properties)); +} diff --git a/lib/db/app/dao/app_key_value_dao.g.dart b/lib/db/app/dao/app_key_value_dao.g.dart new file mode 100644 index 0000000000..f5413cb9e2 --- /dev/null +++ b/lib/db/app/dao/app_key_value_dao.g.dart @@ -0,0 +1,8 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'app_key_value_dao.dart'; + +// ignore_for_file: type=lint +mixin _$AppKeyValueDaoMixin on DatabaseAccessor { + Properties get properties => attachedDatabase.properties; +} diff --git a/lib/db/app/drift/app.drift b/lib/db/app/drift/app.drift new file mode 100644 index 0000000000..6a7efb5725 --- /dev/null +++ b/lib/db/app/drift/app.drift @@ -0,0 +1,9 @@ + +import '../converter/app_property_group_converter.dart'; + +CREATE TABLE properties ( + "key" TEXT NOT NULL, + "group" TEXT NOT NULL MAPPED BY `const AppPropertyGroupConverter()`, + "value" TEXT NOT NULL, + PRIMARY KEY("key", "group") +); diff --git a/lib/db/converter/property_group_converter.dart b/lib/db/converter/property_group_converter.dart deleted file mode 100644 index aa1f6edca9..0000000000 --- a/lib/db/converter/property_group_converter.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:drift/drift.dart'; - -import '../../enum/property_group.dart'; - -class PropertyGroupConverter extends TypeConverter { - const PropertyGroupConverter(); - - @override - PropertyGroup fromSql(String fromDb) => PropertyGroup.values.byName(fromDb); - - @override - String toSql(PropertyGroup value) => value.name; -} diff --git a/lib/db/converter/user_property_group_converter.dart b/lib/db/converter/user_property_group_converter.dart new file mode 100644 index 0000000000..d830e37440 --- /dev/null +++ b/lib/db/converter/user_property_group_converter.dart @@ -0,0 +1,15 @@ +import 'package:drift/drift.dart'; + +import '../../enum/property_group.dart'; + +class UserPropertyGroupConverter + extends TypeConverter { + const UserPropertyGroupConverter(); + + @override + UserPropertyGroup fromSql(String fromDb) => + UserPropertyGroup.values.byName(fromDb); + + @override + String toSql(UserPropertyGroup value) => value.name; +} diff --git a/lib/db/dao/property_dao.dart b/lib/db/dao/property_dao.dart index 024c205e1a..0f5fd63b1f 100644 --- a/lib/db/dao/property_dao.dart +++ b/lib/db/dao/property_dao.dart @@ -1,6 +1,7 @@ import 'package:drift/drift.dart'; import '../../enum/property_group.dart'; +import '../../utils/db/db_key_value.dart'; import '../mixin_database.dart'; part 'property_dao.g.dart'; @@ -9,31 +10,32 @@ part 'property_dao.g.dart'; include: {'../moor/dao/property.drift'}, ) class PropertyDao extends DatabaseAccessor - with _$PropertyDaoMixin { + with _$PropertyDaoMixin + implements KeyValueDao { PropertyDao(super.attachedDatabase); - Future getProperty(PropertyGroup group, String key) async { + Future getProperty(UserPropertyGroup group, String key) async { final result = await (select(properties) ..where((tbl) => tbl.group.equalsValue(group) & tbl.key.equals(key))) .getSingleOrNull(); return result?.value; } - Future> getProperties(PropertyGroup group) async { + Future> getProperties(UserPropertyGroup group) async { final result = await (select(properties) ..where((tbl) => tbl.group.equalsValue(group))) .get(); return Map.fromEntries(result.map((e) => MapEntry(e.key, e.value))); } - Future removeProperty(PropertyGroup group, String key) async { + Future removeProperty(UserPropertyGroup group, String key) async { await (delete(properties) ..where((tbl) => tbl.group.equalsValue(group) & tbl.key.equals(key))) .go(); } Future setProperty( - PropertyGroup group, + UserPropertyGroup group, String key, String value, ) async { @@ -46,6 +48,62 @@ class PropertyDao extends DatabaseAccessor ); } - Future clearProperties(PropertyGroup group) => + Future clearProperties(UserPropertyGroup group) => (delete(properties)..where((tbl) => tbl.group.equalsValue(group))).go(); + + @override + Future clear(UserPropertyGroup group) => + (delete(properties)..where((tbl) => tbl.group.equalsValue(group))).go(); + + @override + Future> getAll(UserPropertyGroup group) async { + final result = await (select(properties) + ..where((tbl) => tbl.group.equalsValue(group))) + .get(); + return Map.fromEntries(result.map((e) => MapEntry(e.key, e.value))); + } + + @override + Future getByKey(UserPropertyGroup group, String key) async { + final result = await (select(properties) + ..where((tbl) => tbl.group.equalsValue(group) & tbl.key.equals(key))) + .getSingleOrNull(); + return result?.value; + } + + @override + Future set(UserPropertyGroup group, String key, String? value) async { + if (value != null) { + await into(properties).insertOnConflictUpdate( + PropertiesCompanion.insert( + group: group, + key: key, + value: value, + ), + ); + } else { + await (delete(properties) + ..where( + (tbl) => tbl.group.equalsValue(group) & tbl.key.equals(key))) + .go(); + } + } + + @override + Stream> watchAll(UserPropertyGroup group) => + (select(properties)..where((tbl) => tbl.group.equalsValue(group))) + .watch() + .map((event) => + Map.fromEntries(event.map((e) => MapEntry(e.key, e.value)))); + + @override + Stream watchByKey(UserPropertyGroup group, String key) => (select( + properties) + ..where((tbl) => tbl.group.equalsValue(group) & tbl.key.equals(key))) + .watchSingleOrNull() + .map((event) => event?.value); + + @override + Stream watchTableHasChanged(UserPropertyGroup group) => + db.tableUpdates(TableUpdateQuery.onTable(db.properties)); } diff --git a/lib/db/database.dart b/lib/db/database.dart index a539ac2bb6..9c698ed6db 100644 --- a/lib/db/database.dart +++ b/lib/db/database.dart @@ -1,9 +1,9 @@ import 'dart:async'; import '../ui/provider/slide_category_provider.dart'; +import '../utils/db/user_crypto_key_value.dart'; import '../utils/extension/extension.dart'; import '../utils/logger.dart'; -import '../utils/property/setting_property.dart'; import 'dao/app_dao.dart'; import 'dao/asset_dao.dart'; import 'dao/chain_dao.dart'; @@ -35,9 +35,8 @@ import 'fts_database.dart'; import 'mixin_database.dart'; class Database { - Database(this.mixinDatabase, this.ftsDatabase) { - settingProperties = SettingPropertyStorage(mixinDatabase.propertyDao); - } + Database(this.mixinDatabase, this.ftsDatabase) + : cryptoKeyValue = UserCryptoKeyValue(mixinDatabase.propertyDao); final MixinDatabase mixinDatabase; @@ -102,7 +101,7 @@ class Database { ExpiredMessageDao get expiredMessageDao => mixinDatabase.expiredMessageDao; - late final SettingPropertyStorage settingProperties; + final UserCryptoKeyValue cryptoKeyValue; Future dispose() async { await mixinDatabase.close(); diff --git a/lib/db/mixin_database.dart b/lib/db/mixin_database.dart index b900d96fbf..fb50305d8f 100644 --- a/lib/db/mixin_database.dart +++ b/lib/db/mixin_database.dart @@ -12,9 +12,9 @@ import 'converter/media_status_type_converter.dart'; import 'converter/message_status_type_converter.dart'; import 'converter/millis_date_converter.dart'; import 'converter/participant_role_converter.dart'; -import 'converter/property_group_converter.dart'; import 'converter/safe_deposit_type_converter.dart'; import 'converter/safe_withdrawal_type_converter.dart'; +import 'converter/user_property_group_converter.dart'; import 'converter/user_relationship_converter.dart'; import 'dao/address_dao.dart'; import 'dao/app_dao.dart'; diff --git a/lib/db/mixin_database.g.dart b/lib/db/mixin_database.g.dart index 42c4cbeb5e..2598272618 100644 --- a/lib/db/mixin_database.g.dart +++ b/lib/db/mixin_database.g.dart @@ -14375,12 +14375,12 @@ class Properties extends Table with TableInfo { requiredDuringInsert: true, $customConstraints: 'NOT NULL'); static const VerificationMeta _groupMeta = const VerificationMeta('group'); - late final GeneratedColumnWithTypeConverter group = + late final GeneratedColumnWithTypeConverter group = GeneratedColumn('group', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true, $customConstraints: 'NOT NULL') - .withConverter(Properties.$convertergroup); + .withConverter(Properties.$convertergroup); static const VerificationMeta _valueMeta = const VerificationMeta('value'); late final GeneratedColumn value = GeneratedColumn( 'value', aliasedName, false, @@ -14435,8 +14435,8 @@ class Properties extends Table with TableInfo { return Properties(attachedDatabase, alias); } - static TypeConverter $convertergroup = - const PropertyGroupConverter(); + static TypeConverter $convertergroup = + const UserPropertyGroupConverter(); @override List get customConstraints => const ['PRIMARY KEY("key", "group")']; @override @@ -14445,7 +14445,7 @@ class Properties extends Table with TableInfo { class Propertie extends DataClass implements Insertable { final String key; - final PropertyGroup group; + final UserPropertyGroup group; final String value; const Propertie( {required this.key, required this.group, required this.value}); @@ -14474,7 +14474,7 @@ class Propertie extends DataClass implements Insertable { serializer ??= driftRuntimeOptions.defaultSerializer; return Propertie( key: serializer.fromJson(json['key']), - group: serializer.fromJson(json['group']), + group: serializer.fromJson(json['group']), value: serializer.fromJson(json['value']), ); } @@ -14483,12 +14483,12 @@ class Propertie extends DataClass implements Insertable { serializer ??= driftRuntimeOptions.defaultSerializer; return { 'key': serializer.toJson(key), - 'group': serializer.toJson(group), + 'group': serializer.toJson(group), 'value': serializer.toJson(value), }; } - Propertie copyWith({String? key, PropertyGroup? group, String? value}) => + Propertie copyWith({String? key, UserPropertyGroup? group, String? value}) => Propertie( key: key ?? this.key, group: group ?? this.group, @@ -14517,7 +14517,7 @@ class Propertie extends DataClass implements Insertable { class PropertiesCompanion extends UpdateCompanion { final Value key; - final Value group; + final Value group; final Value value; final Value rowid; const PropertiesCompanion({ @@ -14528,7 +14528,7 @@ class PropertiesCompanion extends UpdateCompanion { }); PropertiesCompanion.insert({ required String key, - required PropertyGroup group, + required UserPropertyGroup group, required String value, this.rowid = const Value.absent(), }) : key = Value(key), @@ -14550,7 +14550,7 @@ class PropertiesCompanion extends UpdateCompanion { PropertiesCompanion copyWith( {Value? key, - Value? group, + Value? group, Value? value, Value? rowid}) { return PropertiesCompanion( diff --git a/lib/db/moor/mixin.drift b/lib/db/moor/mixin.drift index a367cb0eb7..5f2d0354bc 100644 --- a/lib/db/moor/mixin.drift +++ b/lib/db/moor/mixin.drift @@ -6,7 +6,7 @@ import '../converter/message_status_type_converter.dart'; import '../converter/user_relationship_converter.dart'; import '../converter/participant_role_converter.dart'; import '../converter/millis_date_converter.dart'; -import '../converter/property_group_converter.dart'; +import '../converter/user_property_group_converter.dart'; import '../converter/safe_deposit_type_converter.dart'; import '../converter/safe_withdrawal_type_converter.dart'; import '../../enum/media_status.dart'; @@ -70,7 +70,7 @@ CREATE TABLE expired_messages (message_id TEXT NOT NULL, expire_in INTEGER NOT N CREATE TABLE chains (chain_id TEXT NOT NULL, name TEXT NOT NULL, symbol TEXT NOT NULL, icon_url TEXT NOT NULL, threshold INTEGER NOT NULL, PRIMARY KEY(chain_id)); -CREATE TABLE properties ("key" TEXT NOT NULL, "group" TEXT NOT NULL MAPPED BY `const PropertyGroupConverter()`, "value" TEXT NOT NULL, PRIMARY KEY("key", "group")); +CREATE TABLE properties ("key" TEXT NOT NULL, "group" TEXT NOT NULL MAPPED BY `const UserPropertyGroupConverter()`, "value" TEXT NOT NULL, PRIMARY KEY("key", "group")); CREATE TABLE safe_snapshots ( snapshot_id TEXT NOT NULL, diff --git a/lib/db/util/open_database.dart b/lib/db/util/open_database.dart index c1856b02de..658f3d7b47 100644 --- a/lib/db/util/open_database.dart +++ b/lib/db/util/open_database.dart @@ -44,37 +44,41 @@ Future openQueryExecutor({ final foregroundPortName = 'one_mixin_drift_foreground_${identityNumber}_$dbName'; - final writeIsolate = await _crateIsolate( - identityNumber: identityNumber, + final dbFile = File( + p.join(mixinDocumentsDirectory.path, identityNumber, '$dbName.db'), + ); + final writeIsolate = await createOrConnectDriftIsolate( portName: backgroundPortName, - dbName: dbName, + dbFile: dbFile, fromMainIsolate: fromMainIsolate, - debugName: 'isolate_drift_${dbName}_write', + debugName: 'isolate_drift_write_${identityNumber}_$dbName', ); final write = await writeIsolate.connect(); final reads = await Future.wait(List.generate(readCount, (i) async { - final isolate = await _crateIsolate( - identityNumber: identityNumber, + final isolate = await createOrConnectDriftIsolate( portName: '${foregroundPortName}_$i', - dbName: dbName, + dbFile: dbFile, fromMainIsolate: fromMainIsolate, - debugName: 'one_mixin_drift_read_$i', + debugName: 'isolate_drift_read_${identityNumber}_${dbName}_$i', ); return isolate.connect(); })); + if (reads.isEmpty) { + return write; + } + return MultiExecutor.withReadPool( reads: reads.map((e) => e.executor).toList(), write: write.executor, ); } -Future _crateIsolate({ - required String identityNumber, +Future createOrConnectDriftIsolate({ required String portName, - required String dbName, + required File dbFile, required String? debugName, bool fromMainIsolate = false, }) async { @@ -87,8 +91,6 @@ Future _crateIsolate({ if (existingIsolate == null) { assert(fromMainIsolate, 'Isolate should be created from main isolate'); - final dbFile = File( - p.join(mixinDocumentsDirectory.path, identityNumber, '$dbName.db')); final receivePort = ReceivePort(); await Isolate.spawn( _startBackground, diff --git a/lib/db/util/property_storage.dart b/lib/db/util/property_storage.dart deleted file mode 100644 index 2b6c5359ae..0000000000 --- a/lib/db/util/property_storage.dart +++ /dev/null @@ -1,141 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; - -import 'package:flutter/foundation.dart'; - -import '../../enum/property_group.dart'; -import '../../utils/event_bus.dart'; -import '../../utils/logger.dart'; -import '../dao/property_dao.dart'; - -class _PropertyChangedEvent { - _PropertyChangedEvent(this.group); - - final PropertyGroup group; -} - -class PropertyStorage extends ChangeNotifier { - PropertyStorage(this.group, this.dao) { - _loadProperties(); - EventBus.instance.on - .where((event) => event is _PropertyChangedEvent) - .cast<_PropertyChangedEvent>() - .listen((event) { - if (event.group == group) { - _loadProperties(); - } - }); - } - - Future _loadProperties() async { - final properties = await dao.getProperties(group); - _data.addAll(properties); - notifyListeners(); - await onPropertiesLoaded(); - } - - @protected - @mustCallSuper - Future onPropertiesLoaded() async {} - - final Map _data = {}; - - final PropertyGroup group; - - final PropertyDao dao; - - void clear() { - _data.clear(); - notifyListeners(); - dao.clearProperties(group).whenComplete(() { - EventBus.instance.fire(_PropertyChangedEvent(group)); - }); - onPropertiesClear(); - } - - @protected - @mustCallSuper - Future onPropertiesClear() async {} - - void remove(String key) { - _data.remove(key); - notifyListeners(); - dao.removeProperty(group, key).whenComplete(() { - EventBus.instance.fire(_PropertyChangedEvent(group)); - }); - } - - void set(String key, T? value) { - if (value == null) { - remove(key); - return; - } - final String save; - if (value is Map || value is List) { - save = jsonEncode(value); - } else { - save = value is String ? value : value.toString(); - } - _data[key] = save; - notifyListeners(); - - dao.setProperty(group, key, save).whenComplete(() { - EventBus.instance.fire(_PropertyChangedEvent(group)); - }); - } - - T? get(String key) { - final value = _data[key]; - if (value == null) { - return null; - } - try { - switch (T) { - case const (String): - return value as T?; - case const (int): - return int.tryParse(value) as T?; - case const (double): - return double.tryParse(value) as T?; - case const (bool): - return (value == 'true') as T?; - case const (dynamic): - return value as T?; - default: - e('getProperty unknown type: $T'); - return null; - } - } catch (error, stacktrace) { - e('getProperty error: $error, stacktrace: $stacktrace'); - return null; - } - } - - List? getList(String key) { - final value = _data[key]; - if (value == null) { - return null; - } - try { - final list = jsonDecode(value) as List; - return list.cast(); - } catch (error, stacktrace) { - e('getProperty error: $error, stacktrace: $stacktrace'); - return null; - } - } - - Map? getMap(String key) { - final value = _data[key]; - if (value == null) { - return null; - } - try { - final map = jsonDecode(value) as Map; - return map.cast(); - } catch (error, stacktrace) { - e('getProperty error: $error, stacktrace: $stacktrace'); - return null; - } - } -} diff --git a/lib/enum/property_group.dart b/lib/enum/property_group.dart index f92c07c9a7..9048b8d0b7 100644 --- a/lib/enum/property_group.dart +++ b/lib/enum/property_group.dart @@ -1,3 +1,11 @@ -enum PropertyGroup { +enum UserPropertyGroup { + // Legacy setting which save proxy settings, unused now. setting, + crypto, +} + +enum AppPropertyGroup { + setting, + auth, + security, } diff --git a/lib/main.dart b/lib/main.dart index a091a207bc..3c5aec4048 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'dart:math' as math; import 'package:ansicolor/ansicolor.dart'; +import 'package:dio/dio.dart'; import 'package:drift/drift.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart'; @@ -20,7 +21,9 @@ import 'package:window_size/window_size.dart'; import 'app.dart'; import 'bloc/custom_bloc_observer.dart'; +import 'db/app/app_database.dart'; import 'ui/home/home.dart'; +import 'ui/provider/database_provider.dart'; import 'utils/app_lifecycle.dart'; import 'utils/event_bus.dart'; import 'utils/file.dart'; @@ -83,7 +86,12 @@ Future main(List args) async { e('FlutterError: ${details.exception} ${details.stack}'); }; PlatformDispatcher.instance.onError = (error, stack) { - e('unhandled error: $error $stack'); + e('unhandled error: $error'); + if (error is DioException) { + e('stacktrace: ${error.stackTrace}'); + } else { + e('stacktrace: $stack'); + } return true; }; @@ -96,7 +104,14 @@ Future main(List args) async { Bloc.observer = CustomBlocObserver(); } - runApp(const ProviderScope(child: OverlaySupport.global(child: App()))); + final appDatabase = AppDatabase.connect(fromMainIsolate: true); + await appDatabase.settingKeyValue.initialize; + runApp(ProviderScope( + overrides: [ + appDatabaseProvider.overrideWithValue(appDatabase), + ], + child: const OverlaySupport.global(child: App()), + )); if (kPlatformIsDesktop) { Size? windowSize; diff --git a/lib/ui/home/chat/chat_bar.dart b/lib/ui/home/chat/chat_bar.dart index 50bc50d753..a75b83b863 100644 --- a/lib/ui/home/chat/chat_bar.dart +++ b/lib/ui/home/chat/chat_bar.dart @@ -15,10 +15,10 @@ import '../../../widgets/conversation/verified_or_bot_widget.dart'; import '../../../widgets/high_light_text.dart'; import '../../../widgets/interactive_decorated_box.dart'; import '../../../widgets/window/move_window.dart'; -import '../../provider/abstract_responsive_navigator.dart'; import '../../provider/conversation_provider.dart'; import '../../provider/message_selection_provider.dart'; -import '../../provider/responsive_navigator_provider.dart'; +import '../../provider/navigation/abstract_responsive_navigator.dart'; +import '../../provider/navigation/responsive_navigator_provider.dart'; import 'chat_page.dart'; class ChatBar extends HookConsumerWidget { diff --git a/lib/ui/home/chat/chat_page.dart b/lib/ui/home/chat/chat_page.dart index 250992ce91..01123d14e0 100644 --- a/lib/ui/home/chat/chat_page.dart +++ b/lib/ui/home/chat/chat_page.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:desktop_drop/desktop_drop.dart'; @@ -9,8 +10,6 @@ import 'package:flutter_svg/svg.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart' hide Provider; import 'package:provider/provider.dart'; -import '../../../account/scam_warning_key_value.dart'; -import '../../../account/show_pin_message_key_value.dart'; import '../../../bloc/simple_cubit.dart'; import '../../../bloc/subscribe_mixin.dart'; import '../../../constants/resources.dart'; @@ -32,11 +31,11 @@ import '../../../widgets/message/message_bubble.dart'; import '../../../widgets/message/message_day_time.dart'; import '../../../widgets/pin_bubble.dart'; import '../../../widgets/toast.dart'; -import '../../../widgets/window/menus.dart'; -import '../../provider/abstract_responsive_navigator.dart'; import '../../provider/conversation_provider.dart'; import '../../provider/mention_cache_provider.dart'; +import '../../provider/menu_handle_provider.dart'; import '../../provider/message_selection_provider.dart'; +import '../../provider/navigation/abstract_responsive_navigator.dart'; import '../../provider/pending_jump_message_provider.dart'; import '../bloc/blink_cubit.dart'; import '../bloc/message_bloc.dart'; @@ -760,7 +759,7 @@ class _BottomBanner extends HookConsumerWidget { final showScamWarning = useMemoizedStream( () { if (userId == null || !isScam) return Stream.value(false); - return ScamWarningKeyValue.instance.watch(userId); + return context.hiveKeyValues.scamWarningKeyValue.watch(userId); }, initialData: false, keys: [userId], @@ -814,7 +813,7 @@ class _BottomBanner extends HookConsumerWidget { size: 20, onTap: () { if (userId == null) return; - ScamWarningKeyValue.instance.dismiss(userId); + context.hiveKeyValues.scamWarningKeyValue.dismiss(userId); }, ), ], @@ -857,7 +856,7 @@ class _PinMessagesBanner extends HookConsumerWidget { final conversationId = ref.read(currentConversationIdProvider); if (conversationId == null) return; - ShowPinMessageKeyValue.instance + context.hiveKeyValues.showPinMessageKeyValue .dismiss(conversationId); }, ), @@ -1112,12 +1111,12 @@ class _ChatMenuHandler extends HookConsumerWidget { final conversationId = ref.watch(currentConversationIdProvider); useEffect(() { - final cubit = ref.read(macMenuBarProvider.notifier); + final controller = ref.read(macMenuBarProvider.notifier); if (conversationId == null) return null; final handle = _ConversationHandle(context, conversationId); - Future(() => cubit.attach(handle)); - return () => Future(() => cubit.unAttach(handle)); + Future(() => controller.attach(handle)); + return () => Future(() => controller.unAttach(handle)); }, [conversationId]); return child; diff --git a/lib/ui/home/chat/input_container.dart b/lib/ui/home/chat/input_container.dart index 0e7bc83ce6..03b25cbc1b 100644 --- a/lib/ui/home/chat/input_container.dart +++ b/lib/ui/home/chat/input_container.dart @@ -42,10 +42,10 @@ import '../../../widgets/sticker_page/bloc/cubit/sticker_albums_cubit.dart'; import '../../../widgets/sticker_page/sticker_page.dart'; import '../../../widgets/toast.dart'; import '../../../widgets/user_selector/conversation_selector.dart'; -import '../../provider/abstract_responsive_navigator.dart'; import '../../provider/conversation_provider.dart'; import '../../provider/mention_cache_provider.dart'; import '../../provider/mention_provider.dart'; +import '../../provider/navigation/abstract_responsive_navigator.dart'; import '../../provider/quote_message_provider.dart'; import '../../provider/recall_message_reedit_provider.dart'; import 'chat_page.dart'; diff --git a/lib/ui/home/conversation/conversation_list.dart b/lib/ui/home/conversation/conversation_list.dart index ac487e3173..b2bdd0268a 100644 --- a/lib/ui/home/conversation/conversation_list.dart +++ b/lib/ui/home/conversation/conversation_list.dart @@ -24,7 +24,7 @@ import '../../../widgets/unread_text.dart'; import '../../provider/conversation_provider.dart'; import '../../provider/mention_cache_provider.dart'; import '../../provider/minute_timer_provider.dart'; -import '../../provider/responsive_navigator_provider.dart'; +import '../../provider/navigation/responsive_navigator_provider.dart'; import '../../provider/slide_category_provider.dart'; import '../bloc/conversation_list_bloc.dart'; import 'audio_player_bar.dart'; diff --git a/lib/ui/home/conversation/network_status.dart b/lib/ui/home/conversation/network_status.dart index c42dbbf817..4cf915c02f 100644 --- a/lib/ui/home/conversation/network_status.dart +++ b/lib/ui/home/conversation/network_status.dart @@ -9,19 +9,16 @@ import '../../../blaze/blaze.dart'; import '../../../constants/resources.dart'; import '../../../utils/extension/extension.dart'; import '../../../utils/file.dart'; -import '../../../utils/hook.dart'; import '../../../utils/uri_utils.dart'; import '../../../widgets/menu.dart'; +import '../../provider/account/account_server_provider.dart'; class NetworkStatus extends HookConsumerWidget { const NetworkStatus({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final connectedState = useMemoizedStream( - () => context.accountServer.connectedStateStream.distinct(), - initialData: ConnectedState.connecting) - .requireData; + final connectedState = ref.watch(blazeConnectedStateProvider).value; final hasDisconnectedBefore = useRef(false); diff --git a/lib/ui/home/conversation/unseen_conversation_list.dart b/lib/ui/home/conversation/unseen_conversation_list.dart index 98b1d00680..4b0b703857 100644 --- a/lib/ui/home/conversation/unseen_conversation_list.dart +++ b/lib/ui/home/conversation/unseen_conversation_list.dart @@ -3,7 +3,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import '../../provider/conversation_provider.dart'; -import '../../provider/responsive_navigator_provider.dart'; +import '../../provider/navigation/responsive_navigator_provider.dart'; import '../../provider/unseen_conversations_provider.dart'; import 'conversation_list.dart'; import 'menu_wrapper.dart'; diff --git a/lib/ui/home/home.dart b/lib/ui/home/home.dart index 068941f781..e4a04a2bab 100644 --- a/lib/ui/home/home.dart +++ b/lib/ui/home/home.dart @@ -9,7 +9,6 @@ import '../../blaze/blaze.dart'; import '../../utils/audio_message_player/audio_message_service.dart'; import '../../utils/device_transfer/device_transfer_widget.dart'; import '../../utils/extension/extension.dart'; -import '../../utils/hook.dart'; import '../../utils/platform.dart'; import '../../utils/system/text_input.dart'; import '../../widgets/automatic_keep_alive_client_widget.dart'; @@ -17,13 +16,13 @@ import '../../widgets/dialog.dart'; import '../../widgets/empty.dart'; import '../../widgets/protocol_handler.dart'; import '../../widgets/toast.dart'; +import '../provider/account/account_server_provider.dart'; +import '../provider/account/multi_auth_provider.dart'; import '../provider/conversation_provider.dart'; -import '../provider/multi_auth_provider.dart'; -import '../provider/responsive_navigator_provider.dart'; +import '../provider/navigation/responsive_navigator_provider.dart'; import '../provider/setting_provider.dart'; import '../provider/slide_category_provider.dart'; import '../setting/setting_page.dart'; - import 'command_palette_wrapper.dart'; import 'conversation/conversation_hotkey.dart'; import 'conversation/conversation_page.dart'; @@ -48,12 +47,8 @@ class HomePage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final localTimeError = useMemoizedStream( - () => context.accountServer.connectedStateStream - .map((event) => event == ConnectedState.hasLocalTimeError) - .distinct(), - keys: [context.accountServer]).data ?? - false; + final localTimeError = ref.watch(blazeConnectedStateProvider + .select((value) => value.value == ConnectedState.hasLocalTimeError)); final isEmptyUserName = ref.watch(authAccountProvider .select((value) => value?.fullName?.isEmpty ?? true)); @@ -177,6 +172,7 @@ class _HomePage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + ref.watch(conversationProvider); // keep alive final maxWidth = constraints.maxWidth; final clampSlideWidth = (maxWidth - kResponsiveNavigationMinWidth) .clamp(kSlidePageMinWidth, kSlidePageMaxWidth); diff --git a/lib/ui/home/hook/pin_message.dart b/lib/ui/home/hook/pin_message.dart index 4f8fe8b1b1..a1fe45ad45 100644 --- a/lib/ui/home/hook/pin_message.dart +++ b/lib/ui/home/hook/pin_message.dart @@ -2,7 +2,6 @@ import 'package:equatable/equatable.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import '../../../account/show_pin_message_key_value.dart'; import '../../../blaze/vo/pin_message_minimal.dart'; import '../../../db/database_event_bus.dart'; import '../../../utils/extension/extension.dart'; @@ -59,7 +58,7 @@ PinMessageState usePinMessageState(String? conversationId) { final showLastPinMessage = useMemoizedStream( () { if (conversationId == null) return Stream.value(false); - return ShowPinMessageKeyValue.instance.watch(conversationId); + return context.hiveKeyValues.showPinMessageKeyValue.watch(conversationId); }, initialData: false, keys: [conversationId], diff --git a/lib/ui/home/route/responsive_navigator.dart b/lib/ui/home/route/responsive_navigator.dart index 3d15c393ac..0aa9569ec4 100644 --- a/lib/ui/home/route/responsive_navigator.dart +++ b/lib/ui/home/route/responsive_navigator.dart @@ -5,8 +5,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import '../../provider/abstract_responsive_navigator.dart'; -import '../../provider/responsive_navigator_provider.dart'; +import '../../provider/navigation/abstract_responsive_navigator.dart'; +import '../../provider/navigation/responsive_navigator_provider.dart'; abstract class AbstractResponsiveNavigatorCubit extends Cubit { diff --git a/lib/ui/home/slide_page.dart b/lib/ui/home/slide_page.dart index fe49eeadc5..b389b82cf7 100644 --- a/lib/ui/home/slide_page.dart +++ b/lib/ui/home/slide_page.dart @@ -24,7 +24,8 @@ import '../../widgets/select_item.dart'; import '../../widgets/toast.dart'; import '../../widgets/user_selector/conversation_selector.dart'; import '../../widgets/window/move_window.dart'; -import '../provider/multi_auth_provider.dart'; +import '../landing/landing.dart'; +import '../provider/account/multi_auth_provider.dart'; import '../provider/setting_provider.dart'; import '../provider/slide_category_provider.dart'; @@ -117,7 +118,8 @@ class SlidePage extends StatelessWidget { ), ), title: Text(context.l10n.collapse), - onTap: () => context.settingChangeNotifier + onTap: () => ref + .read(settingProvider) .collapsedSidebar = !collapse, ); }), @@ -142,45 +144,137 @@ class _CurrentUser extends HookConsumerWidget { .select((value) => value.type == SlideCategoryType.setting)); return MoveWindowBarrier( - child: SelectItem( - icon: Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: AvatarWidget( - avatarUrl: account?.avatarUrl, - size: 24, - name: account?.fullName, - userId: account?.userId, + child: _MultiAccountPopupButton( + child: SelectItem( + icon: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: AvatarWidget( + avatarUrl: account?.avatarUrl, + size: 24, + name: account?.fullName, + userId: account?.userId, + ), + ), + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + account?.fullName ?? '', + style: const TextStyle(fontSize: 14), + ), + const SizedBox(height: 2), + Text( + '${account?.identityNumber}', + style: + TextStyle(color: context.theme.secondaryText, fontSize: 12), + ) + ], ), + selected: selected, + onTap: () { + ref + .read(slideCategoryStateProvider.notifier) + .select(SlideCategoryType.setting); + + if (ModalRoute.of(context)?.canPop == true) { + Navigator.pop(context); + } + }, ), - title: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - account?.fullName ?? '', - style: const TextStyle(fontSize: 14), - ), - const SizedBox(height: 2), - Text( - '${account?.identityNumber}', - style: - TextStyle(color: context.theme.secondaryText, fontSize: 12), - ) - ], + ), + ); + } +} + +class _MultiAccountPopupButton extends HookConsumerWidget { + const _MultiAccountPopupButton({required this.child}); + + final Widget child; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final mas = ref.watch(multiAuthStateNotifierProvider); + return ContextMenuPortalEntry( + autofocus: true, + buildMenus: () => [ + for (final account in mas.auths) + _AccountMenuItem( + account: account.account, + selected: account.userId == mas.activeUserId, + ), + Divider( + color: context.theme.divider, + height: 1, + indent: 8, + endIndent: 8, ), - selected: selected, + ContextMenu( + title: 'Add Account', + icon: Resources.assetsImagesIcAddSvg, + onTap: () { + showDialog( + context: context, + builder: (context) => const LandingDialog(), + ); + }, + ), + ], + child: child, + ); + } +} + +class _AccountMenuItem extends ConsumerWidget { + const _AccountMenuItem({ + required this.account, + required this.selected, + }); + + final Account account; + final bool selected; + + @override + Widget build(BuildContext context, WidgetRef ref) => ContextMenuLayout( onTap: () { ref - .read(slideCategoryStateProvider.notifier) - .select(SlideCategoryType.setting); - - if (ModalRoute.of(context)?.canPop == true) { - Navigator.pop(context); - } + .read(multiAuthStateNotifierProvider.notifier) + .active(account.userId); + context.closeMenu(); }, - ), - ); - } + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only(right: 8), + child: AvatarWidget( + avatarUrl: account.avatarUrl, + size: 24, + name: account.fullName, + userId: account.userId, + ), + ), + Expanded( + child: Text( + account.fullName ?? '', + style: TextStyle( + fontSize: 14, + color: context.theme.text, + ), + ), + ), + if (selected) + SvgPicture.asset( + Resources.assetsImagesCheckedSvg, + width: 24, + height: 24, + colorFilter: ColorFilter.mode( + context.theme.secondaryText, + BlendMode.srcIn, + ), + ), + ], + ), + ); } class _CircleList extends HookConsumerWidget { diff --git a/lib/ui/landing/bloc/landing_cubit.dart b/lib/ui/landing/bloc/landing_cubit.dart deleted file mode 100644 index bf567abd0b..0000000000 --- a/lib/ui/landing/bloc/landing_cubit.dart +++ /dev/null @@ -1,198 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; -import 'dart:ui'; - -import 'package:bloc/bloc.dart'; -import 'package:dio/dio.dart'; -import 'package:ed25519_edwards/ed25519_edwards.dart' as ed; -import 'package:flutter/material.dart'; -import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart' as signal; -import 'package:mixin_bot_sdk_dart/mixin_bot_sdk_dart.dart'; - -import '../../../account/account_key_value.dart'; -import '../../../crypto/crypto_key_value.dart'; -import '../../../crypto/signal/signal_protocol.dart'; -import '../../../generated/l10n.dart'; -import '../../../utils/extension/extension.dart'; -import '../../../utils/logger.dart'; -import '../../../utils/platform.dart'; -import '../../../utils/system/package_info.dart'; -import '../../provider/multi_auth_provider.dart'; -import 'landing_state.dart'; - -class LandingCubit extends Cubit { - LandingCubit( - this.multiAuthChangeNotifier, - Locale locale, - T initialState, { - String? userAgent, - String? deviceId, - }) : client = Client( - dioOptions: BaseOptions( - headers: { - 'Accept-Language': locale.languageCode, - if (userAgent != null) 'User-Agent': userAgent, - if (deviceId != null) 'Mixin-Device-Id': deviceId, - }, - ), - ), - super(initialState); - final Client client; - final MultiAuthStateNotifier multiAuthChangeNotifier; -} - -class LandingQrCodeCubit extends LandingCubit { - LandingQrCodeCubit( - MultiAuthStateNotifier multiAuthChangeNotifier, Locale locale) - : super( - multiAuthChangeNotifier, - locale, - LandingState( - status: multiAuthChangeNotifier.current != null - ? LandingStatus.provisioning - : LandingStatus.init, - ), - ) { - if (multiAuthChangeNotifier.current != null) return; - requestAuthUrl(); - } - - final StreamController<(int, String, signal.ECKeyPair)> - periodicStreamController = - StreamController<(int, String, signal.ECKeyPair)>(); - - StreamSubscription? _periodicSubscription; - - void _cancelPeriodicSubscription() { - final periodicSubscription = _periodicSubscription; - _periodicSubscription = null; - unawaited(periodicSubscription?.cancel()); - } - - Future requestAuthUrl() async { - _cancelPeriodicSubscription(); - try { - final rsp = await client.provisioningApi - .getProvisioningId(Platform.operatingSystem); - final keyPair = signal.Curve.generateKeyPair(); - final pubKey = - Uri.encodeComponent(base64Encode(keyPair.publicKey.serialize())); - - emit(state.copyWith( - authUrl: 'mixin://device/auth?id=${rsp.data.deviceId}&pub_key=$pubKey', - status: LandingStatus.ready, - )); - - _periodicSubscription = Stream.periodic( - const Duration(milliseconds: 1500), - (i) => i, - ) - .asyncBufferMap( - (event) => _checkLanding(event.last, rsp.data.deviceId, keyPair)) - .listen((event) {}); - } catch (error, stack) { - e('requestAuthUrl failed: $error $stack'); - emit(state.needReload('Failed to request auth: $error')); - } - } - - Future _checkLanding( - int count, - String deviceId, - signal.ECKeyPair keyPair, - ) async { - if (_periodicSubscription == null) return; - - if (count > 60) { - _cancelPeriodicSubscription(); - emit(state.needReload(Localization.current.qrCodeExpiredDesc)); - return; - } - - String secret; - try { - secret = - (await client.provisioningApi.getProvisioning(deviceId)).data.secret; - } catch (e) { - return; - } - if (secret.isEmpty) return; - - _cancelPeriodicSubscription(); - emit(state.copyWith(status: LandingStatus.provisioning)); - - try { - final (acount, privateKey) = await _verify(secret, keyPair); - multiAuthChangeNotifier - .signIn(AuthState(account: acount, privateKey: privateKey)); - } catch (error, stack) { - emit(state.needReload('Failed to verify: $error')); - e('_verify: $error $stack'); - } - } - - FutureOr<(Account, String)> _verify( - String secret, signal.ECKeyPair keyPair) async { - final result = - signal.decrypt(base64Encode(keyPair.privateKey.serialize()), secret); - final msg = - json.decode(String.fromCharCodes(result)) as Map; - - final edKeyPair = ed.generateKey(); - final private = base64.decode(msg['identity_key_private'] as String); - final registrationId = await SignalProtocol.initSignal(private); - - final sessionId = msg['session_id'] as String; - final info = await getPackageInfo(); - final appVersion = '${info.version}(${info.buildNumber})'; - final platformVersion = await getPlatformVersion(); - final rsp = await client.provisioningApi.verifyProvisioning( - ProvisioningRequest( - code: msg['provisioning_code'] as String, - userId: msg['user_id'] as String, - sessionId: sessionId, - purpose: 'SESSION', - sessionSecret: base64Encode(edKeyPair.publicKey.bytes), - appVersion: appVersion, - registrationId: registrationId, - platform: 'Desktop', - platformVersion: platformVersion, - ), - ); - - final privateKey = base64Encode(edKeyPair.privateKey.bytes); - - await AccountKeyValue.instance.init(rsp.data.identityNumber); - AccountKeyValue.instance.primarySessionId = sessionId; - await CryptoKeyValue.instance.init(rsp.data.identityNumber); - CryptoKeyValue.instance.localRegistrationId = registrationId; - - return ( - rsp.data, - privateKey, - ); - } - - @override - Future close() async { - await _periodicSubscription?.cancel(); - await periodicStreamController.close(); - await super.close(); - } -} - -class LandingMobileCubit extends LandingCubit { - LandingMobileCubit( - MultiAuthStateNotifier multiAuthChangeNotifier, - Locale locale, { - required String deviceId, - required String userAgent, - }) : super( - multiAuthChangeNotifier, - locale, - null, - deviceId: deviceId, - userAgent: userAgent, - ); -} diff --git a/lib/ui/landing/bloc/landing_state.dart b/lib/ui/landing/bloc/landing_state.dart deleted file mode 100644 index e69c334df1..0000000000 --- a/lib/ui/landing/bloc/landing_state.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:equatable/equatable.dart'; - -enum LandingStatus { - needReload, - provisioning, - ready, - init, -} - -class LandingState extends Equatable { - const LandingState({ - this.authUrl, - this.status = LandingStatus.init, - this.errorMessage, - }); - - final String? authUrl; - final LandingStatus status; - - final String? errorMessage; - - @override - List get props => [authUrl, status, errorMessage]; - - LandingState needReload(String errorMessage) => LandingState( - status: LandingStatus.needReload, - errorMessage: errorMessage, - authUrl: authUrl, - ); - - LandingState copyWith({ - String? authUrl, - LandingStatus? status, - }) => - LandingState( - authUrl: authUrl ?? this.authUrl, - status: status ?? this.status, - ); -} diff --git a/lib/ui/landing/landing.dart b/lib/ui/landing/landing.dart index cdd5baad3b..1269ad6caa 100644 --- a/lib/ui/landing/landing.dart +++ b/lib/ui/landing/landing.dart @@ -1,58 +1,103 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_portal/flutter_portal.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:mixin_bot_sdk_dart/mixin_bot_sdk_dart.dart'; -import '../../account/account_key_value.dart'; import '../../crypto/signal/signal_database.dart'; import '../../utils/extension/extension.dart'; -import '../../utils/hive_key_values.dart'; import '../../utils/hook.dart'; +import '../../utils/logger.dart'; import '../../utils/mixin_api_client.dart'; import '../../utils/system/package_info.dart'; +import '../../widgets/buttons.dart'; import '../../widgets/dialog.dart'; import '../../widgets/toast.dart'; -import '../provider/account_server_provider.dart'; +import '../provider/account/account_server_provider.dart'; +import '../provider/database_provider.dart'; +import '../provider/hive_key_value_provider.dart'; import 'landing_mobile.dart'; import 'landing_qrcode.dart'; -enum LandingMode { +enum _LandingMode { qrcode, mobile, } -class LandingModeCubit extends Cubit { - LandingModeCubit() : super(LandingMode.qrcode); - - void changeMode(LandingMode mode) => emit(mode); -} +final _landingModeProvider = + StateProvider.autoDispose<_LandingMode>((ref) => _LandingMode.qrcode); class LandingPage extends HookConsumerWidget { const LandingPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { + ref.watch(landingKeyValuesProvider); final accountServerHasError = ref.watch(accountServerProvider.select((value) => value.hasError)); - - final modeCubit = useBloc(LandingModeCubit.new); - final mode = useBlocState(bloc: modeCubit); - + final mode = ref.watch(_landingModeProvider); Widget child; switch (mode) { - case LandingMode.qrcode: + case _LandingMode.qrcode: child = const LandingQrCodeWidget(); - case LandingMode.mobile: + case _LandingMode.mobile: child = const LoginWithMobileWidget(); } if (accountServerHasError) { child = const _LoginFailed(); } - return BlocProvider.value( - value: modeCubit, - child: LandingScaffold(child: child), + return LandingScaffold(child: child); + } +} + +/// LandingDialog for add account +class LandingDialog extends ConsumerWidget { + const LandingDialog({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final mode = ref.watch(_landingModeProvider); + Widget child; + switch (mode) { + case _LandingMode.qrcode: + child = const LandingQrCodeWidget(); + case _LandingMode.mobile: + child = const LoginWithMobileWidget(); + } + return Portal( + child: Scaffold( + backgroundColor: context.dynamicColor( + const Color(0xFFE5E5E5), + darkColor: const Color.fromRGBO(35, 39, 43, 1), + ), + resizeToAvoidBottomInset: false, + body: Stack( + children: [ + Center( + child: SizedBox( + width: 520, + height: 418, + child: Material( + color: context.theme.popUp, + borderRadius: const BorderRadius.all(Radius.circular(13)), + elevation: 10, + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(13)), + child: child, + ), + ), + ), + ), + const Align( + alignment: Alignment.topRight, + child: Padding( + padding: EdgeInsets.all(20), + child: MixinCloseButton(), + ), + ) + ], + ), + ), ); } } @@ -139,18 +184,34 @@ class _LoginFailed extends HookConsumerWidget { final authState = context.auth; if (authState == null) return; + final hiveKeyValues = await ref.read(hiveKeyValueProvider( + authState.account.identityNumber, + ).future); + + final accountKeyValue = hiveKeyValues.accountKeyValue; + await createClient( userId: authState.account.userId, sessionId: authState.account.sessionId, privateKey: authState.privateKey, loginByPhoneNumber: - AccountKeyValue.instance.primarySessionId == null, + accountKeyValue.primarySessionId == null, ) .accountApi .logout(LogoutRequest(authState.account.sessionId)); - await clearKeyValues(); - await SignalDatabase.get.clear(); - context.multiAuthChangeNotifier.signOut(); + await hiveKeyValues.clearAll(); + final signalDb = await SignalDatabase.connect( + identityNumber: authState.account.identityNumber, + fromMainIsolate: true, + ); + await signalDb.clear(); + await signalDb.close(); + final userDb = ref.read(databaseProvider).valueOrNull; + if (userDb != null) { + await userDb.cryptoKeyValue.clear(); + } + context.multiAuthChangeNotifier + .signOut(authState.account.userId); }, child: Text(context.l10n.retry), ), @@ -220,22 +281,22 @@ class LandingModeSwitchButton extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final mode = useBlocState(); + final mode = ref.watch(_landingModeProvider); final String buttonText; switch (mode) { - case LandingMode.qrcode: + case _LandingMode.qrcode: buttonText = context.l10n.signWithPhoneNumber; - case LandingMode.mobile: + case _LandingMode.mobile: buttonText = context.l10n.signWithQrcode; } return TextButton( onPressed: () { - final modeCubit = context.read(); + final notifier = ref.read(_landingModeProvider.notifier); switch (mode) { - case LandingMode.qrcode: - modeCubit.changeMode(LandingMode.mobile); - case LandingMode.mobile: - modeCubit.changeMode(LandingMode.qrcode); + case _LandingMode.qrcode: + notifier.state = _LandingMode.mobile; + case _LandingMode.mobile: + notifier.state = _LandingMode.qrcode; } }, child: Text( @@ -249,3 +310,23 @@ class LandingModeSwitchButton extends HookConsumerWidget { ); } } + +final landingIdentityNumberProvider = StateProvider.autoDispose((ref) { + assert(() { + ref.onDispose(() { + w('landingIdentityNumberProvider dispose'); + }); + return true; + }()); + return null; +}); + +final landingKeyValuesProvider = FutureProvider.autoDispose( + (ref) async { + final identityNumber = ref.watch(landingIdentityNumberProvider); + if (identityNumber == null) { + return null; + } + return ref.watch(hiveKeyValueProvider(identityNumber).future); + }, +); diff --git a/lib/ui/landing/landing_initialize.dart b/lib/ui/landing/landing_initialize.dart new file mode 100644 index 0000000000..81852cf740 --- /dev/null +++ b/lib/ui/landing/landing_initialize.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; + +import '../../utils/extension/extension.dart'; +import 'landing.dart'; + +class AppInitializingPage extends StatelessWidget { + const AppInitializingPage({super.key}); + + @override + Widget build(BuildContext context) => LandingScaffold( + child: Center( + child: LoadingWidget( + title: context.l10n.initializing, + message: context.l10n.chatHintE2e, + ), + ), + ); +} + +class LoadingWidget extends StatelessWidget { + const LoadingWidget({ + required this.title, + required this.message, + super.key, + }); + + final String title; + final String message; + + @override + Widget build(BuildContext context) { + final primaryColor = context.theme.text; + return SizedBox( + width: 375, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(primaryColor), + ), + const SizedBox(height: 24), + Text( + title, + style: TextStyle( + color: primaryColor, + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 8), + Text( + message, + textAlign: TextAlign.center, + style: TextStyle( + color: context.dynamicColor( + const Color.fromRGBO(188, 190, 195, 1), + darkColor: const Color.fromRGBO(255, 255, 255, 0.4), + ), + fontSize: 16, + ), + ), + ], + ), + ); + } +} diff --git a/lib/ui/landing/landing_mobile.dart b/lib/ui/landing/landing_mobile.dart index 4b76822c4d..514f49606e 100644 --- a/lib/ui/landing/landing_mobile.dart +++ b/lib/ui/landing/landing_mobile.dart @@ -1,25 +1,22 @@ -// ignore_for_file: implementation_imports - import 'dart:async'; import 'dart:convert'; import 'package:ed25519_edwards/ed25519_edwards.dart' as ed; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:intl/intl.dart'; +import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:mixin_bot_sdk_dart/mixin_bot_sdk_dart.dart'; import 'package:pin_code_fields/pin_code_fields.dart'; import '../../account/session_key_value.dart'; import '../../constants/resources.dart'; -import '../../crypto/crypto_key_value.dart'; import '../../crypto/signal/signal_protocol.dart'; import '../../utils/extension/extension.dart'; -import '../../utils/hook.dart'; import '../../utils/logger.dart'; +import '../../utils/mixin_api_client.dart'; import '../../utils/platform.dart'; import '../../utils/system/package_info.dart'; import '../../widgets/action_button.dart'; @@ -28,42 +25,36 @@ import '../../widgets/toast.dart'; import '../../widgets/user/captcha_web_view_dialog.dart'; import '../../widgets/user/phone_number_input.dart'; import '../../widgets/user/verification_dialog.dart'; -import '../provider/multi_auth_provider.dart'; -import 'bloc/landing_cubit.dart'; +import '../provider/account/multi_auth_provider.dart'; import 'landing.dart'; +final _mobileClientProvider = Provider.autoDispose( + (ref) => createLandingClient(), +); + class LoginWithMobileWidget extends HookConsumerWidget { const LoginWithMobileWidget({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final locale = useMemoized(() => Localizations.localeOf(context)); - final userAgent = useMemoizedFuture(generateUserAgent, null).data; - final deviceId = useMemoizedFuture(getDeviceId, null).data; - - if (userAgent == null || deviceId == null) { - return const Center(child: CircularProgressIndicator()); - } - return BlocProvider( - create: (_) => LandingMobileCubit(context.multiAuthChangeNotifier, locale, - userAgent: userAgent, deviceId: deviceId), - child: Navigator( - onPopPage: (_, __) => true, - pages: const [ - MaterialPage( - child: _PhoneNumberInputScene(), - ), - ], - ), + // keep client provider alive in mobile widget. + ref.watch(_mobileClientProvider); + return Navigator( + onPopPage: (_, __) => true, + pages: const [ + MaterialPage( + child: _PhoneNumberInputScene(), + ), + ], ); } } -class _PhoneNumberInputScene extends StatelessWidget { +class _PhoneNumberInputScene extends ConsumerWidget { const _PhoneNumberInputScene(); @override - Widget build(BuildContext context) => Column( + Widget build(BuildContext context, WidgetRef ref) => Column( children: [ const SizedBox(height: 56), Expanded( @@ -79,6 +70,7 @@ class _PhoneNumberInputScene extends StatelessWidget { final response = await _requestVerificationCode( phone: phoneNumber, context: context, + client: ref.read(_mobileClientProvider), ); Toast.dismiss(); if (response.deactivatedAt?.isNotEmpty ?? false) { @@ -150,7 +142,7 @@ class _CodeInputScene extends HookConsumerWidget { assert(code.length == 4, 'Invalid code length: $code'); showToastLoading(); try { - final registrationId = await SignalProtocol.initSignal(null); + final registrationId = generateRegistrationId(false); final sessionKey = ed.generateKey(); final sessionSecret = base64Encode(sessionKey.publicKey.bytes); @@ -169,19 +161,24 @@ class _CodeInputScene extends HookConsumerWidget { sessionSecret: sessionSecret, pin: '', ); - final client = context.read().client; + final client = ref.read(_mobileClientProvider); final response = await client.accountApi.create( verification.value.id, accountRequest, ); - final privateKey = base64Encode(sessionKey.privateKey.bytes); final identityNumber = response.data.identityNumber; - await CryptoKeyValue.instance.init(identityNumber); - CryptoKeyValue.instance.localRegistrationId = registrationId; + await SignalProtocol.initSignal(identityNumber, registrationId, null); + ref.read(landingIdentityNumberProvider.notifier).state = identityNumber; + + final privateKey = base64Encode(sessionKey.privateKey.bytes); + + final hiveKeyValues = await ref.read(landingKeyValuesProvider.future); + if (hiveKeyValues == null) { + throw Exception('hiveKeyValues is null'); + } - await SessionKeyValue.instance.init(identityNumber); - SessionKeyValue.instance.pinToken = base64Encode(decryptPinToken( + hiveKeyValues.sessionKeyValue.pinToken = base64Encode(decryptPinToken( response.data.pinToken, sessionKey.privateKey, )); @@ -269,6 +266,7 @@ class _CodeInputScene extends HookConsumerWidget { final response = await _requestVerificationCode( phone: phoneNumber, context: context, + client: ref.read(_mobileClientProvider), ); Toast.dismiss(); verification.value = response; @@ -307,6 +305,7 @@ class _CodeInputScene extends HookConsumerWidget { Future _requestVerificationCode({ required String phone, required BuildContext context, + required Client client, (CaptchaType, String)? captcha, }) async { final request = VerificationRequest( @@ -318,8 +317,7 @@ Future _requestVerificationCode({ hCaptchaResponse: captcha?.$1 == CaptchaType.hCaptcha ? captcha?.$2 : null, ); try { - final cubit = context.read(); - final response = await cubit.client.accountApi.verification(request); + final response = await client.accountApi.verification(request); return response.data; } on MixinApiError catch (error) { final mixinError = error.error! as MixinError; @@ -334,6 +332,7 @@ Future _requestVerificationCode({ return _requestVerificationCode( phone: phone, context: context, + client: client, captcha: (type, token), ); } diff --git a/lib/ui/landing/landing_qrcode.dart b/lib/ui/landing/landing_qrcode.dart index 1aa9ad0592..e24005d5a2 100644 --- a/lib/ui/landing/landing_qrcode.dart +++ b/lib/ui/landing/landing_qrcode.dart @@ -1,47 +1,44 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:ed25519_edwards/ed25519_edwards.dart' as ed; +import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart' as signal; +import 'package:mixin_bot_sdk_dart/mixin_bot_sdk_dart.dart'; +import 'package:mixin_logger/mixin_logger.dart'; import '../../constants/resources.dart'; +import '../../crypto/signal/signal_protocol.dart'; +import '../../generated/l10n.dart'; import '../../utils/extension/extension.dart'; -import '../../utils/hook.dart'; +import '../../utils/mixin_api_client.dart'; import '../../utils/platform.dart'; +import '../../utils/system/package_info.dart'; import '../../widgets/qr_code.dart'; -import 'bloc/landing_cubit.dart'; -import 'bloc/landing_state.dart'; +import '../provider/account/multi_auth_provider.dart'; import 'landing.dart'; +import 'landing_initialize.dart'; + +final _qrCodeLoginProvider = + StateNotifierProvider.autoDispose<_QrCodeLoginNotifier, LandingState>( + _QrCodeLoginNotifier.new, +); class LandingQrCodeWidget extends HookConsumerWidget { const LandingQrCodeWidget({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final locale = useMemoized(() => Localizations.localeOf(context)); - - final landingCubit = useBloc(() => LandingQrCodeCubit( - context.multiAuthChangeNotifier, - locale, - )); - final status = - useBlocStateConverter( - bloc: landingCubit, - converter: (state) => state.status, - ); - + ref.watch(_qrCodeLoginProvider.select((value) => value.status)); final Widget child; - if (status == LandingStatus.init) { + if (status == LandingStatus.provisioning) { child = Center( - child: _Loading( - title: context.l10n.initializing, - message: context.l10n.chatHintE2e, - ), - ); - } else if (status == LandingStatus.provisioning) { - child = Center( - child: _Loading( + child: LoadingWidget( title: context.l10n.loading, message: context.l10n.chatHintE2e, ), @@ -68,10 +65,7 @@ class LandingQrCodeWidget extends HookConsumerWidget { ], ); } - return BlocProvider.value( - value: landingCubit, - child: child, - ); + return child; } } @@ -81,17 +75,13 @@ class _QrCode extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final url = - useBlocStateConverter( - converter: (state) => state.authUrl); + ref.watch(_qrCodeLoginProvider.select((value) => value.authUrl)); - final visible = - useBlocStateConverter( - converter: (state) => state.status == LandingStatus.needReload); + final visible = ref.watch(_qrCodeLoginProvider + .select((value) => value.status == LandingStatus.needReload)); final errorMessage = - useBlocStateConverter( - converter: (state) => state.errorMessage, - ); + ref.watch(_qrCodeLoginProvider.select((value) => value.errorMessage)); Widget? qrCode; @@ -118,8 +108,9 @@ class _QrCode extends HookConsumerWidget { visible: visible, child: _Retry( errorMessage: errorMessage, - onTap: () => - context.read().requestAuthUrl(), + onTap: () => ref + .read(_qrCodeLoginProvider.notifier) + .requestAuthUrl(), ), ), ], @@ -161,53 +152,6 @@ class _QrCode extends HookConsumerWidget { } } -class _Loading extends StatelessWidget { - const _Loading({ - required this.title, - required this.message, - }); - - final String title; - final String message; - - @override - Widget build(BuildContext context) { - final primaryColor = context.theme.text; - return SizedBox( - width: 375, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation(primaryColor), - ), - const SizedBox(height: 24), - Text( - title, - style: TextStyle( - color: primaryColor, - fontSize: 18, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(height: 8), - Text( - message, - textAlign: TextAlign.center, - style: TextStyle( - color: context.dynamicColor( - const Color.fromRGBO(188, 190, 195, 1), - darkColor: const Color.fromRGBO(255, 255, 255, 0.4), - ), - fontSize: 16, - ), - ), - ], - ), - ); - } -} - class _Retry extends StatelessWidget { const _Retry({ required this.onTap, @@ -257,3 +201,179 @@ class _Retry extends StatelessWidget { ), ); } + +enum LandingStatus { + needReload, + provisioning, + ready, +} + +class LandingState extends Equatable { + const LandingState({ + required this.status, + this.authUrl, + this.errorMessage, + }); + + final String? authUrl; + final LandingStatus status; + + final String? errorMessage; + + @override + List get props => [authUrl, status, errorMessage]; + + LandingState needReload(String errorMessage) => LandingState( + status: LandingStatus.needReload, + errorMessage: errorMessage, + authUrl: authUrl, + ); + + LandingState copyWith({ + String? authUrl, + LandingStatus? status, + }) => + LandingState( + authUrl: authUrl ?? this.authUrl, + status: status ?? this.status, + ); +} + +class _QrCodeLoginNotifier extends StateNotifier { + _QrCodeLoginNotifier(this.ref) + : multiAuth = ref.read(multiAuthStateNotifierProvider.notifier), + super(const LandingState(status: LandingStatus.provisioning)) { + requestAuthUrl(); + } + + final MultiAuthStateNotifier multiAuth; + final client = createLandingClient(); + final Ref ref; + + final StreamController<(int, String, signal.ECKeyPair)> + periodicStreamController = + StreamController<(int, String, signal.ECKeyPair)>(); + + StreamSubscription? _periodicSubscription; + + void _cancelPeriodicSubscription() { + final periodicSubscription = _periodicSubscription; + _periodicSubscription = null; + unawaited(periodicSubscription?.cancel()); + } + + Future requestAuthUrl() async { + _cancelPeriodicSubscription(); + try { + final rsp = await client.provisioningApi + .getProvisioningId(Platform.operatingSystem); + final keyPair = signal.Curve.generateKeyPair(); + final pubKey = + Uri.encodeComponent(base64Encode(keyPair.publicKey.serialize())); + + state = state.copyWith( + authUrl: 'mixin://device/auth?id=${rsp.data.deviceId}&pub_key=$pubKey', + status: LandingStatus.ready, + ); + + _periodicSubscription = Stream.periodic( + const Duration(milliseconds: 1500), + (i) => i, + ) + .asyncBufferMap( + (event) => _checkLanding(event.last, rsp.data.deviceId, keyPair)) + .listen((event) {}); + } catch (error, stack) { + e('requestAuthUrl failed: $error $stack'); + state = state.needReload('Failed to request auth: $error'); + } + } + + Future _checkLanding( + int count, + String deviceId, + signal.ECKeyPair keyPair, + ) async { + if (_periodicSubscription == null) return; + + if (count > 40) { + _cancelPeriodicSubscription(); + state = state.needReload(Localization.current.qrCodeExpiredDesc); + return; + } + + String secret; + try { + secret = + (await client.provisioningApi.getProvisioning(deviceId)).data.secret; + } catch (e) { + return; + } + if (secret.isEmpty) return; + + _cancelPeriodicSubscription(); + state = state.copyWith(status: LandingStatus.provisioning); + + try { + final (acount, privateKey) = await _verify(secret, keyPair); + multiAuth.signIn(AuthState(account: acount, privateKey: privateKey)); + } catch (error, stack) { + state = state.needReload('Failed to verify: $error'); + e('_verify: $error $stack'); + } + } + + FutureOr<(Account, String)> _verify( + String secret, signal.ECKeyPair keyPair) async { + final result = + signal.decrypt(base64Encode(keyPair.privateKey.serialize()), secret); + final msg = + json.decode(String.fromCharCodes(result)) as Map; + + final edKeyPair = ed.generateKey(); + final private = base64.decode(msg['identity_key_private'] as String); + final registrationId = signal.generateRegistrationId(false); + + final sessionId = msg['session_id'] as String; + final info = await getPackageInfo(); + final appVersion = '${info.version}(${info.buildNumber})'; + final platformVersion = await getPlatformVersion(); + final rsp = await client.provisioningApi.verifyProvisioning( + ProvisioningRequest( + code: msg['provisioning_code'] as String, + userId: msg['user_id'] as String, + sessionId: sessionId, + purpose: 'SESSION', + sessionSecret: base64Encode(edKeyPair.publicKey.bytes), + appVersion: appVersion, + registrationId: registrationId, + platform: 'Desktop', + platformVersion: platformVersion, + ), + ); + + final identityNumber = rsp.data.identityNumber; + await SignalProtocol.initSignal(identityNumber, registrationId, private); + ref.read(landingIdentityNumberProvider.notifier).state = identityNumber; + + final privateKey = base64Encode(edKeyPair.privateKey.bytes); + + final hiveKeyValues = await ref.read(landingKeyValuesProvider.future); + if (hiveKeyValues == null) { + throw Exception('can not init hiveKeyValues'); + } + hiveKeyValues.accountKeyValue.primarySessionId = sessionId; + + return ( + rsp.data, + privateKey, + ); + } + + @override + Future dispose() async { + await _periodicSubscription?.cancel(); + await periodicStreamController.close(); + super.dispose(); + } +} diff --git a/lib/ui/provider/account/account_server_provider.dart b/lib/ui/provider/account/account_server_provider.dart new file mode 100644 index 0000000000..e9b85f9e74 --- /dev/null +++ b/lib/ui/provider/account/account_server_provider.dart @@ -0,0 +1,159 @@ +import 'dart:async'; + +import 'package:equatable/equatable.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import '../../../account/account_server.dart'; +import '../../../blaze/blaze.dart'; +import '../../../db/database.dart'; +import '../../../utils/rivepod.dart'; +import '../../../utils/synchronized.dart'; +import '../conversation_provider.dart'; +import '../database_provider.dart'; +import '../hive_key_value_provider.dart'; +import 'multi_auth_provider.dart'; + +typedef GetCurrentConversationId = String? Function(); + +class AccountServerOpener + extends DistinctStateNotifier> { + AccountServerOpener._(this.ref) : super(const AsyncValue.loading()) { + _subscription = ref.listen>( + _argsProvider, + (previous, next) { + _onNewArgs(next.valueOrNull); + }, + ); + } + + final AutoDisposeRef ref; + ProviderSubscription? _subscription; + + final _lock = Lock(); + + _Args? _previousArgs; + + Future _onNewArgs(_Args? args) => _lock.synchronized(() async { + if (_previousArgs == args) { + return; + } + _previousArgs = args; + if (args == null) { + unawaited(state.valueOrNull?.stop()); + state = const AsyncValue.loading(); + return; + } + final before = state.valueOrNull; + state = await AsyncValue.guard( + () => _openAccountServer(args), + ); + unawaited(before?.stop()); + }); + + Future _openAccountServer(_Args args) async { + final accountServer = AccountServer( + multiAuthNotifier: args.multiAuthChangeNotifier, + database: args.database, + currentConversationId: args.currentConversationId, + ref: ref, + hiveKeyValues: args.hiveKeyValues, + ); + await accountServer.initServer( + args.userId, + args.sessionId, + args.identityNumber, + args.privateKey, + ); + return accountServer; + } + + @override + void dispose() { + _subscription?.close(); + state.valueOrNull?.stop(); + super.dispose(); + } +} + +// create _Args for equatable +class _Args extends Equatable { + const _Args({ + required this.database, + required this.userId, + required this.sessionId, + required this.identityNumber, + required this.privateKey, + required this.multiAuthChangeNotifier, + required this.currentConversationId, + required this.hiveKeyValues, + }); + + final Database database; + final String userId; + final String sessionId; + final String identityNumber; + final String privateKey; + final MultiAuthStateNotifier multiAuthChangeNotifier; + final GetCurrentConversationId currentConversationId; + final HiveKeyValues hiveKeyValues; + + @override + List get props => [ + database, + userId, + sessionId, + identityNumber, + privateKey, + multiAuthChangeNotifier, + currentConversationId, + hiveKeyValues, + ]; +} + +final Provider _currentConversationIdProvider = + Provider( + (ref) => () => ref.read(currentConversationIdProvider), +); + +final _argsProvider = FutureProvider.autoDispose((ref) async { + final database = + ref.watch(databaseProvider.select((value) => value.valueOrNull)); + if (database == null) { + return null; + } + final auth = ref.watch(authProvider); + if (auth == null) { + return null; + } + final multiAuthChangeNotifier = + ref.watch(multiAuthStateNotifierProvider.notifier); + final currentConversationId = ref.read(_currentConversationIdProvider); + final hiveKeyValues = + await ref.watch(hiveKeyValueProvider(auth.account.identityNumber).future); + + return _Args( + database: database, + userId: auth.userId, + sessionId: auth.account.sessionId, + identityNumber: auth.account.identityNumber, + privateKey: auth.privateKey, + multiAuthChangeNotifier: multiAuthChangeNotifier, + currentConversationId: currentConversationId, + hiveKeyValues: hiveKeyValues, + ); +}); + +final accountServerProvider = StateNotifierProvider.autoDispose< + AccountServerOpener, AsyncValue>( + AccountServerOpener._, +); + +final blazeConnectedStateProvider = + StreamProvider.autoDispose((ref) { + final accountServer = + ref.watch(accountServerProvider.select((value) => value.valueOrNull)); + if (accountServer == null) { + return const Stream.empty(); + } + return accountServer.connectedStateStream; +}); diff --git a/lib/ui/provider/account/multi_auth_provider.dart b/lib/ui/provider/account/multi_auth_provider.dart new file mode 100644 index 0000000000..f55f80f939 --- /dev/null +++ b/lib/ui/provider/account/multi_auth_provider.dart @@ -0,0 +1,303 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:equatable/equatable.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:hydrated_bloc/hydrated_bloc.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:mixin_bot_sdk_dart/mixin_bot_sdk_dart.dart'; +import 'package:mixin_logger/mixin_logger.dart'; +import 'package:path/path.dart' as p; + +import '../../../enum/property_group.dart'; +import '../../../utils/db/db_key_value.dart'; +import '../../../utils/extension/extension.dart'; +import '../../../utils/file.dart'; +import '../../../utils/hydrated_bloc.dart'; +import '../../../utils/rivepod.dart'; +import '../database_provider.dart'; + +part 'multi_auth_provider.g.dart'; + +@JsonSerializable() +class AuthState extends Equatable { + const AuthState({ + required this.account, + required this.privateKey, + }); + + factory AuthState.fromJson(Map map) => + _$AuthStateFromJson(map); + + @JsonKey(name: 'account') + final Account account; + @JsonKey(name: 'privateKey') + final String privateKey; + + String get userId => account.userId; + + @override + List get props => [account, privateKey]; + + Map toJson() => _$AuthStateToJson(this); +} + +@JsonSerializable() +class MultiAuthState extends Equatable { + MultiAuthState({ + this.auths = const [], + String? activeUserId, + }) : activeUserId = activeUserId ?? auths.lastOrNull?.userId; + + factory MultiAuthState.fromJson(Map map) => + _$MultiAuthStateFromJson(map); + + @JsonKey(name: 'auths') + final List auths; + + /// activeUserId is the current activated account userId + @JsonKey(name: 'activeUserId') + final String? activeUserId; + + AuthState? get current { + if (auths.isEmpty) { + return null; + } + if (activeUserId != null) { + return auths + .firstWhereOrNull((element) => element.userId == activeUserId); + } + w('activeUserId is null'); + return auths.lastOrNull; + } + + @override + List get props => [auths, activeUserId]; + + Map toJson() => _$MultiAuthStateToJson(this); +} + +class MultiAuthStateNotifier extends DistinctStateNotifier { + MultiAuthStateNotifier(this._multiAuthKeyValue) : super(MultiAuthState()) { + _init().whenComplete(_initialized.complete); + } + + final _initialized = Completer(); + + Future get initialized => _initialized.future; + + Future _init() async { + await _multiAuthKeyValue.initialize; + final auths = _multiAuthKeyValue.authList; + var activeUserId = _multiAuthKeyValue.activeUserId; + final migrated = _multiAuthKeyValue.authMigrated; + unawaited(_multiAuthKeyValue.setAuthMigrated()); + if (auths.isEmpty && !migrated) { + // check if old auths exist. + final oldAuthState = _getLegacyMultiAuthState()?.auths.lastOrNull; + if (oldAuthState != null) { + i('migrate legacy auths'); + final signalDbMigrated = await _migrationLegacySignalDatabase( + oldAuthState.account.identityNumber); + if (signalDbMigrated) { + auths.add(oldAuthState); + activeUserId = oldAuthState.userId; + } else { + w('migration legacy signal database failed, ignore legacy auths.'); + } + } + } + try { + _removeLegacyMultiAuthState(); + unawaited(_removeLegacySignalDatabase()); + } catch (error, stacktrace) { + e('remove legacy auths error: $error\n$stacktrace'); + } + super.state = MultiAuthState(auths: auths, activeUserId: activeUserId); + } + + final MultiAuthKeyValue _multiAuthKeyValue; + + AuthState? get current => state.current; + + void signIn(AuthState authState) { + state = MultiAuthState( + auths: [ + ...state.auths.where((element) => element.userId != authState.userId), + authState, + ], + activeUserId: authState.userId, + ); + } + + void updateAccount(Account account) { + final index = + state.auths.indexWhere((element) => element.userId == account.userId); + if (index == -1) { + i('update account, but ${account.userId} auth state not found.'); + return; + } + final auths = state.auths.toList(); + auths[index] = AuthState( + account: account, + privateKey: state.auths[index].privateKey, + ); + state = MultiAuthState(auths: auths, activeUserId: state.activeUserId); + } + + void signOut(String userId) { + if (state.auths.isEmpty) return; + final auths = state.auths.toList() + ..removeWhere((element) => element.userId == userId); + final activeUserId = state.activeUserId == userId + ? auths.lastOrNull?.userId + : state.activeUserId; + state = MultiAuthState(auths: auths, activeUserId: activeUserId); + } + + @override + set state(MultiAuthState value) { + _multiAuthKeyValue + ..setAuthList(value.auths) + ..setActiveUserId(value.activeUserId); + super.state = value; + } + + void active(String userId) { + final exist = state.auths.any((element) => element.userId == userId); + if (!exist) { + e('failed to active, no account exist for id: $userId'); + return; + } + i('active account: $userId'); + state = MultiAuthState( + auths: state.auths, + activeUserId: userId, + ); + } +} + +const _kMultiAuthCubitKey = 'MultiAuthCubit'; + +MultiAuthState? _getLegacyMultiAuthState() { + final oldJson = HydratedBloc.storage.read(_kMultiAuthCubitKey); + if (oldJson == null) { + return null; + } + return fromHydratedJson(oldJson, MultiAuthState.fromJson); +} + +void _removeLegacyMultiAuthState() { + HydratedBloc.storage.delete(_kMultiAuthCubitKey); +} + +Future _removeLegacySignalDatabase() async { + final dbFolder = mixinDocumentsDirectory.path; + const dbFiles = ['signal.db', 'signal.db-shm', 'signal.db-wal']; + final files = dbFiles.map((e) => File(p.join(dbFolder, e))); + for (final file in files) { + try { + if (file.existsSync()) { + i('remove legacy signal database: ${file.path}'); + await file.delete(); + } + } catch (error, stacktrace) { + e('_removeLegacySignalDatabase ${file.path} error: $error, stacktrace: $stacktrace'); + } + } +} + +Future _migrationLegacySignalDatabase(String identityNumber) async { + final dbFolder = p.join(mixinDocumentsDirectory.path, identityNumber); + + final dbFile = File(p.join(dbFolder, 'signal.db')); + // migration only when new database file not exists. + if (dbFile.existsSync()) { + await _removeLegacySignalDatabase(); + return false; + } + + final legacyDbFolder = mixinDocumentsDirectory.path; + final legacyDbFile = File(p.join(legacyDbFolder, 'signal.db')); + if (!legacyDbFile.existsSync()) { + return false; + } + const dbFiles = ['signal.db', 'signal.db-shm', 'signal.db-wal']; + final legacyFiles = dbFiles.map((e) => File(p.join(legacyDbFolder, e))); + var hasError = false; + for (final file in legacyFiles) { + try { + final newLocation = p.join(dbFolder, p.basename(file.path)); + // delete new location file if exists + final newFile = File(newLocation); + if (newFile.existsSync()) { + await newFile.delete(); + } + if (file.existsSync()) { + await file.copy(newLocation); + } + i('migrate legacy signal database: ${file.path}'); + } catch (error, stacktrace) { + e('_migrationLegacySignalDatabaseIfNecessary ${file.path} error: $error, stacktrace: $stacktrace'); + hasError = true; + } + } + if (hasError) { + // migration error. remove copied database file. + for (final name in dbFiles) { + final file = File(p.join(dbFolder, name)); + if (file.existsSync()) { + await file.delete(); + } + } + } + await _removeLegacySignalDatabase(); + return !hasError; +} + +final multiAuthStateNotifierProvider = + StateNotifierProvider((ref) { + final multiAuthKeyValue = ref.watch(multiAuthKeyValueProvider); + return MultiAuthStateNotifier(multiAuthKeyValue); +}); + +final authProvider = + multiAuthStateNotifierProvider.select((value) => value.current); + +final authAccountProvider = authProvider.select((value) => value?.account); + +const _keyAuths = 'auths'; +const _keyActiveUserId = 'active_user_id'; +const _keyAuthMigrated = 'auth_migrated_from_hive'; + +final multiAuthKeyValueProvider = Provider((ref) { + final dao = ref.watch(appDatabaseProvider).appKeyValueDao; + return MultiAuthKeyValue(dao: dao); +}); + +class MultiAuthKeyValue extends AppKeyValue { + MultiAuthKeyValue({required super.dao}) : super(group: AppPropertyGroup.auth); + + List get authList { + final json = get>>(_keyAuths); + if (json == null) { + return []; + } + return json.map(AuthState.fromJson).toList(); + } + + String? get activeUserId => get(_keyActiveUserId); + + Future setAuthList(List auths) => + set(_keyAuths, auths.map((e) => e.toJson()).toList()); + + Future setActiveUserId(String? userId) => set(_keyActiveUserId, userId); + + Future setAuthMigrated() => set(_keyAuthMigrated, true); + + /// In old version, we use hive to store auths. + /// from the vision of support multi account feature we use db key value to store auths. + /// We only do one time migration from old version, because the signal database only + /// migrate once. + bool get authMigrated => get(_keyAuthMigrated) ?? false; +} diff --git a/lib/ui/provider/account/multi_auth_provider.g.dart b/lib/ui/provider/account/multi_auth_provider.g.dart new file mode 100644 index 0000000000..c188762859 --- /dev/null +++ b/lib/ui/provider/account/multi_auth_provider.g.dart @@ -0,0 +1,32 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'multi_auth_provider.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +AuthState _$AuthStateFromJson(Map json) => AuthState( + account: Account.fromJson(json['account'] as Map), + privateKey: json['privateKey'] as String, + ); + +Map _$AuthStateToJson(AuthState instance) => { + 'account': instance.account.toJson(), + 'privateKey': instance.privateKey, + }; + +MultiAuthState _$MultiAuthStateFromJson(Map json) => + MultiAuthState( + auths: (json['auths'] as List?) + ?.map((e) => AuthState.fromJson(e as Map)) + .toList() ?? + const [], + activeUserId: json['activeUserId'] as String?, + ); + +Map _$MultiAuthStateToJson(MultiAuthState instance) => + { + 'auths': instance.auths.map((e) => e.toJson()).toList(), + 'activeUserId': instance.activeUserId, + }; diff --git a/lib/ui/provider/account/security_key_value_provider.dart b/lib/ui/provider/account/security_key_value_provider.dart new file mode 100644 index 0000000000..f841993c27 --- /dev/null +++ b/lib/ui/provider/account/security_key_value_provider.dart @@ -0,0 +1,32 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import '../../../enum/property_group.dart'; +import '../../../utils/db/db_key_value.dart'; +import '../database_provider.dart'; + +const _keyPasscode = 'passcode'; +const _keyBiometric = 'biometric'; +const _keyLockDuration = 'lockDuration'; + +final securityKeyValueProvider = ChangeNotifierProvider( + (ref) => SecurityKeyValue(dao: ref.watch(appDatabaseProvider).appKeyValueDao), +); + +class SecurityKeyValue extends AppKeyValue { + SecurityKeyValue({required super.dao}) + : super(group: AppPropertyGroup.setting); + + String? get passcode => get(_keyPasscode); + + set passcode(String? value) => set(_keyPasscode, value); + + bool get biometric => get(_keyBiometric) ?? false; + + set biometric(bool value) => set(_keyBiometric, value); + + bool get hasPasscode => passcode != null; + + Duration get lockDuration => Duration(minutes: get(_keyLockDuration) ?? 1); + + set lockDuration(Duration? value) => set(_keyLockDuration, value?.inMinutes); +} diff --git a/lib/ui/provider/account_server_provider.dart b/lib/ui/provider/account_server_provider.dart deleted file mode 100644 index 786125b06e..0000000000 --- a/lib/ui/provider/account_server_provider.dart +++ /dev/null @@ -1,157 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; - -import '../../account/account_server.dart'; -import '../../db/database.dart'; -import '../../utils/logger.dart'; -import '../../utils/rivepod.dart'; -import 'conversation_provider.dart'; -import 'database_provider.dart'; -import 'multi_auth_provider.dart'; -import 'setting_provider.dart'; - -typedef GetCurrentConversationId = String? Function(); - -class AccountServerOpener - extends DistinctStateNotifier> { - AccountServerOpener() : super(const AsyncValue.loading()); - - AccountServerOpener.open({ - required this.multiAuthChangeNotifier, - required this.settingChangeNotifier, - required this.database, - required this.userId, - required this.sessionId, - required this.identityNumber, - required this.privateKey, - required this.currentConversationId, - }) : super(const AsyncValue.loading()) { - _init(); - } - - late final MultiAuthStateNotifier multiAuthChangeNotifier; - late final SettingChangeNotifier settingChangeNotifier; - late final Database database; - - late final String userId; - late final String sessionId; - late final String identityNumber; - late final String privateKey; - late final GetCurrentConversationId currentConversationId; - - Future _init() async { - final accountServer = AccountServer( - multiAuthNotifier: multiAuthChangeNotifier, - settingChangeNotifier: settingChangeNotifier, - database: database, - currentConversationId: currentConversationId, - ); - - await accountServer.initServer( - userId, - sessionId, - identityNumber, - privateKey, - ); - - state = AsyncValue.data(accountServer); - } - - @override - Future dispose() async { - await state.valueOrNull?.stop(); - super.dispose(); - } -} - -// create _Args for equatable -class _Args extends Equatable { - const _Args({ - required this.database, - required this.userId, - required this.sessionId, - required this.identityNumber, - required this.privateKey, - required this.multiAuthChangeNotifier, - required this.settingChangeNotifier, - required this.currentConversationId, - }); - - final Database? database; - final String? userId; - final String? sessionId; - final String? identityNumber; - final String? privateKey; - final MultiAuthStateNotifier multiAuthChangeNotifier; - final SettingChangeNotifier settingChangeNotifier; - final GetCurrentConversationId currentConversationId; - - @override - List get props => [ - database, - userId, - sessionId, - identityNumber, - privateKey, - multiAuthChangeNotifier, - settingChangeNotifier, - currentConversationId, - ]; -} - -final Provider _currentConversationIdProvider = - Provider( - (ref) => () => ref.read(currentConversationIdProvider), -); - -final _argsProvider = Provider.autoDispose((ref) { - final database = - ref.watch(databaseProvider.select((value) => value.valueOrNull)); - final (userId, sessionId, identityNumber, privateKey) = - ref.watch(authProvider.select((value) => ( - value?.account.userId, - value?.account.sessionId, - value?.account.identityNumber, - value?.privateKey, - ))); - final multiAuthChangeNotifier = - ref.watch(multiAuthStateNotifierProvider.notifier); - final settingChangeNotifier = ref.watch(settingProvider); - final currentConversationId = ref.read(_currentConversationIdProvider); - - return _Args( - database: database, - userId: userId, - sessionId: sessionId, - identityNumber: identityNumber, - privateKey: privateKey, - multiAuthChangeNotifier: multiAuthChangeNotifier, - settingChangeNotifier: settingChangeNotifier, - currentConversationId: currentConversationId, - ); -}); - -final accountServerProvider = StateNotifierProvider.autoDispose< - AccountServerOpener, AsyncValue>((ref) { - final args = ref.watch(_argsProvider); - - if (args.database == null) return AccountServerOpener(); - if (args.userId == null || - args.sessionId == null || - args.identityNumber == null || - args.privateKey == null) { - w('[accountServerProvider] Account not ready'); - return AccountServerOpener(); - } - - return AccountServerOpener.open( - multiAuthChangeNotifier: args.multiAuthChangeNotifier, - settingChangeNotifier: args.settingChangeNotifier, - database: args.database!, - userId: args.userId!, - sessionId: args.sessionId!, - identityNumber: args.identityNumber!, - privateKey: args.privateKey!, - currentConversationId: args.currentConversationId, - ); -}); diff --git a/lib/ui/provider/conversation_provider.dart b/lib/ui/provider/conversation_provider.dart index 1b51f104b3..e8936bd11e 100644 --- a/lib/ui/provider/conversation_provider.dart +++ b/lib/ui/provider/conversation_provider.dart @@ -20,10 +20,10 @@ import '../../utils/rivepod.dart'; import '../../widgets/toast.dart'; import '../home/bloc/conversation_list_bloc.dart'; import '../home/bloc/subscriber_mixin.dart'; -import 'account_server_provider.dart'; +import 'account/account_server_provider.dart'; import 'is_bot_group_provider.dart'; +import 'navigation/responsive_navigator_provider.dart'; import 'recent_conversation_provider.dart'; -import 'responsive_navigator_provider.dart'; class ConversationState extends Equatable { const ConversationState({ @@ -137,10 +137,9 @@ EncryptCategory _getEncryptCategory(App? app) { class ConversationStateNotifier extends DistinctStateNotifier with SubscriberMixin { ConversationStateNotifier({ - required AccountServer accountServer, + required this.ref, required ResponsiveNavigatorStateNotifier responsiveNavigatorStateNotifier, }) : _responsiveNavigatorStateNotifier = responsiveNavigatorStateNotifier, - _accountServer = accountServer, super(null) { addSubscription(stream .map((event) => event?.conversationId) @@ -211,7 +210,10 @@ class ConversationStateNotifier appActiveListener.addListener(onListen); } - final AccountServer _accountServer; + final Ref ref; + + AccountServer get _accountServer => + ref.read(accountServerProvider).valueOrNull!; final ResponsiveNavigatorStateNotifier _responsiveNavigatorStateNotifier; late final Database _database = _accountServer.database; late final String _currentUserId = _accountServer.userId; @@ -402,24 +404,12 @@ class _LastConversationNotifier final conversationProvider = StateNotifierProvider.autoDispose< ConversationStateNotifier, ConversationState?>((ref) { - final keepAlive = ref.keepAlive(); - - final accountServerAsync = ref.watch(accountServerProvider); - - if (!accountServerAsync.hasValue) { - throw Exception('accountServer is not ready'); - } - + // refresh when accountServerProvider changed + ref.watch(accountServerProvider); final responsiveNavigatorNotifier = ref.watch(responsiveNavigatorProvider.notifier); - - ref - ..listen(accountServerProvider, (previous, next) => keepAlive.close()) - ..listen(responsiveNavigatorProvider.notifier, - (previous, next) => keepAlive.close()); - return ConversationStateNotifier( - accountServer: accountServerAsync.requireValue, + ref: ref, responsiveNavigatorStateNotifier: responsiveNavigatorNotifier, ); }); diff --git a/lib/ui/provider/database_provider.dart b/lib/ui/provider/database_provider.dart index 1f3c3af89f..6431c08467 100644 --- a/lib/ui/provider/database_provider.dart +++ b/lib/ui/provider/database_provider.dart @@ -1,23 +1,29 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:mixin_logger/mixin_logger.dart'; +import '../../crypto/crypto_key_value.dart'; +import '../../db/app/app_database.dart'; import '../../db/database.dart'; import '../../db/fts_database.dart'; import '../../db/mixin_database.dart'; import '../../utils/rivepod.dart'; import '../../utils/synchronized.dart'; -import 'multi_auth_provider.dart'; +import 'account/multi_auth_provider.dart'; +import 'hive_key_value_provider.dart'; import 'slide_category_provider.dart'; +final appDatabaseProvider = + Provider((ref) => throw UnimplementedError()); + final databaseProvider = StateNotifierProvider.autoDispose>( (ref) { final identityNumber = ref.watch(authAccountProvider.select((value) => value?.identityNumber)); - if (identityNumber == null) return DatabaseOpener(); + if (identityNumber == null) return DatabaseOpener(ref); - return DatabaseOpener.open(identityNumber); + return DatabaseOpener.open(identityNumber, ref); }, ); @@ -27,18 +33,22 @@ extension _DatabaseExt on MixinDatabase { } class DatabaseOpener extends DistinctStateNotifier> { - DatabaseOpener() : super(const AsyncValue.loading()); + DatabaseOpener(this.ref) : super(const AsyncValue.loading()); - DatabaseOpener.open(this.identityNumber) : super(const AsyncValue.loading()) { + DatabaseOpener.open(this.identityNumber, this.ref) + : super(const AsyncValue.loading()) { open(); } - late final String identityNumber; + String? identityNumber; + + final Ref ref; final Lock _lock = Lock(); Future open() => _lock.synchronized(() async { - i('connect to database: $identityNumber'); + final identityNumber = this.identityNumber!; + d('connect to database: $identityNumber'); if (state.hasValue) { e('database already opened'); return; @@ -52,6 +62,11 @@ class DatabaseOpener extends DistinctStateNotifier> { ); // Do a database query, to ensure database has properly initialized. await mixinDatabase.doInitVerify(); + try { + await _onDatabaseOpenSucceed(db, identityNumber); + } catch (error, stacktrace) { + e('_onDatabaseOpenSucceed has error: $error, $stacktrace'); + } state = AsyncValue.data(db); } catch (error, stacktrace) { e('failed to open database: $error, $stacktrace'); @@ -59,6 +74,22 @@ class DatabaseOpener extends DistinctStateNotifier> { } }); + Future _onDatabaseOpenSucceed( + Database database, String identityNumber) async { + // migrate old crypto key value to new crypto key value + try { + final hive = ref.read(hiveProvider(identityNumber)); + final oldCryptoKeyValue = CryptoKeyValue(); + await oldCryptoKeyValue.migrateToNewCryptoKeyValue( + hive, + identityNumber, + database.cryptoKeyValue, + ); + } catch (error, stacktrace) { + e('migrateToNewCryptoKeyValue has error: $error, $stacktrace'); + } + } + @override Future dispose() async { await close(); @@ -66,6 +97,9 @@ class DatabaseOpener extends DistinctStateNotifier> { } Future close() async { + if (identityNumber != null) { + i('close database: $identityNumber'); + } await state.valueOrNull?.dispose(); state = const AsyncValue.loading(); } diff --git a/lib/ui/provider/hive_key_value_provider.dart b/lib/ui/provider/hive_key_value_provider.dart new file mode 100644 index 0000000000..89a7cae4c3 --- /dev/null +++ b/lib/ui/provider/hive_key_value_provider.dart @@ -0,0 +1,152 @@ +// ignore_for_file: implementation_imports + +import 'package:equatable/equatable.dart'; +import 'package:hive/hive.dart'; +import 'package:hive/src/hive_impl.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:mixin_logger/mixin_logger.dart'; + +import '../../account/account_key_value.dart'; +import '../../account/scam_warning_key_value.dart'; +import '../../account/session_key_value.dart'; +import '../../account/show_pin_message_key_value.dart'; +import '../../crypto/privacy_key_value.dart'; +import '../../utils/attachment/download_key_value.dart'; +import '../../utils/hive_key_values.dart'; +import 'account/multi_auth_provider.dart'; + +final hiveProvider = + Provider.family((ref, identityNumber) => HiveImpl()); + +FutureProviderFamily + _createHiveKeyValueProvider( + T Function() create, +) => + FutureProviderFamily( + (ref, identityNumber) async { + final hive = ref.watch(hiveProvider(identityNumber)); + final keyValue = create(); + ref.onDispose(keyValue.dispose); + await keyValue.init(hive, identityNumber); + return keyValue; + }, + ); + +final accountKeyValueProvider = + _createHiveKeyValueProvider(AccountKeyValue.new); + +final currentAccountKeyValueProvider = + FutureProvider.autoDispose( + (ref) async { + final identityNumber = + ref.watch(authAccountProvider.select((value) => value?.identityNumber)); + if (identityNumber == null) { + return null; + } + return ref.watch(accountKeyValueProvider(identityNumber).future); + }, +); + +final downloadKeyValueProvider = + _createHiveKeyValueProvider(DownloadKeyValue.new); + +final sessionKeyValueProvider = + _createHiveKeyValueProvider(SessionKeyValue.new); + +final currentSessionKeyValueProvider = + FutureProvider.autoDispose( + (ref) async { + final identityNumber = + ref.watch(authAccountProvider.select((value) => value?.identityNumber)); + if (identityNumber == null) { + return null; + } + return ref.watch(sessionKeyValueProvider(identityNumber).future); + }, +); + +final privacyKeyValueProvider = + _createHiveKeyValueProvider(PrivacyKeyValue.new); + +final showPinMessageKeyValueProvider = + _createHiveKeyValueProvider(ShowPinMessageKeyValue.new); + +final scamWarningKeyValueProvider = + _createHiveKeyValueProvider(ScamWarningKeyValue.new); + +class HiveKeyValues with EquatableMixin { + HiveKeyValues({ + required this.identityNumber, + required this.accountKeyValue, + required this.sessionKeyValue, + required this.privacyKeyValue, + required this.downloadKeyValue, + required this.showPinMessageKeyValue, + required this.scamWarningKeyValue, + }); + + final String identityNumber; + + final AccountKeyValue accountKeyValue; + final SessionKeyValue sessionKeyValue; + final PrivacyKeyValue privacyKeyValue; + final DownloadKeyValue downloadKeyValue; + final ShowPinMessageKeyValue showPinMessageKeyValue; + final ScamWarningKeyValue scamWarningKeyValue; + + @override + List get props => [ + identityNumber, + accountKeyValue, + sessionKeyValue, + privacyKeyValue, + downloadKeyValue, + showPinMessageKeyValue, + scamWarningKeyValue, + ]; + + Future clearAll() { + i('clear hive key values: $identityNumber'); + return Future.wait([ + accountKeyValue.clear(), + sessionKeyValue.clear(), + privacyKeyValue.clear(), + downloadKeyValue.clear(), + showPinMessageKeyValue.clear(), + scamWarningKeyValue.clear(), + ]); + } +} + +final hiveKeyValueProvider = + FutureProvider.autoDispose.family( + (ref, identityNumber) async { + assert(() { + ref.onDispose(() { + w('hiveKeyValueProvider: dispose $identityNumber'); + }); + return true; + }()); + final accountKeyValue = + await ref.watch(accountKeyValueProvider(identityNumber).future); + final sessionKeyValue = + await ref.watch(sessionKeyValueProvider(identityNumber).future); + final privacyKeyValue = + await ref.watch(privacyKeyValueProvider(identityNumber).future); + final downloadKeyValue = + await ref.watch(downloadKeyValueProvider(identityNumber).future); + final showPinMessageKeyValue = + await ref.watch(showPinMessageKeyValueProvider(identityNumber).future); + final scamWarningKeyValue = + await ref.watch(scamWarningKeyValueProvider(identityNumber).future); + return HiveKeyValues( + identityNumber: identityNumber, + accountKeyValue: accountKeyValue, + sessionKeyValue: sessionKeyValue, + privacyKeyValue: privacyKeyValue, + downloadKeyValue: downloadKeyValue, + showPinMessageKeyValue: showPinMessageKeyValue, + scamWarningKeyValue: scamWarningKeyValue, + ); + }, +); diff --git a/lib/ui/provider/mention_provider.dart b/lib/ui/provider/mention_provider.dart index a1a5de377f..fa0e214b5d 100644 --- a/lib/ui/provider/mention_provider.dart +++ b/lib/ui/provider/mention_provider.dart @@ -14,9 +14,9 @@ import '../../utils/reg_exp_utils.dart'; import '../../utils/rivepod.dart'; import '../../widgets/mention_panel.dart'; import '../home/bloc/subscriber_mixin.dart'; +import 'account/account_server_provider.dart'; +import 'account/multi_auth_provider.dart'; import 'conversation_provider.dart'; -import 'database_provider.dart'; -import 'multi_auth_provider.dart'; class MentionState extends Equatable { const MentionState({ @@ -209,8 +209,8 @@ class MentionStateNotifier extends DistinctStateNotifier final mentionProvider = StateNotifierProvider.autoDispose .family>( (ref, stream) { - final userDao = ref - .watch(databaseProvider.select((value) => value.requireValue.userDao)); + final userDao = ref.watch(accountServerProvider + .select((value) => value.requireValue.database.userDao)); final authStateNotifier = ref.watch(multiAuthStateNotifierProvider.notifier); final (conversationId, isGroup, isBot) = ref.watch( diff --git a/lib/ui/provider/menu_handle_provider.dart b/lib/ui/provider/menu_handle_provider.dart new file mode 100644 index 0000000000..21dd74fa86 --- /dev/null +++ b/lib/ui/provider/menu_handle_provider.dart @@ -0,0 +1,63 @@ +import 'dart:io'; + +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import '../../utils/rivepod.dart'; +import 'account/multi_auth_provider.dart'; + +abstract class ConversationMenuHandle { + Stream get isMuted; + + Stream get isPinned; + + void mute(); + + void unmute(); + + void showSearch(); + + void pin(); + + void unPin(); + + void toggleSideBar(); + + void delete(); +} + +class MacMenuBarStateNotifier + extends DistinctStateNotifier { + MacMenuBarStateNotifier(super.state); + + void attach(ConversationMenuHandle handle) { + if (!Platform.isMacOS) return; + Future(() => state = handle); + } + + void unAttach(ConversationMenuHandle handle) { + if (!Platform.isMacOS) return; + if (state != handle) return; + state = null; + } + + void _clear() { + if (!Platform.isMacOS) return; + state = null; + } +} + +final macMenuBarProvider = + StateNotifierProvider( + (ref) { + // clear state when account changed + ref.listen( + authAccountProvider.select((value) => value?.identityNumber), + (previous, next) { + if (previous != null && next != null) { + ref.notifier._clear(); + } + }, + ); + return MacMenuBarStateNotifier(null); + }, +); diff --git a/lib/ui/provider/multi_auth_provider.dart b/lib/ui/provider/multi_auth_provider.dart deleted file mode 100644 index 7b626d298e..0000000000 --- a/lib/ui/provider/multi_auth_provider.dart +++ /dev/null @@ -1,144 +0,0 @@ -// ignore_for_file: deprecated_member_use_from_same_package - -import 'dart:convert'; - -import 'package:equatable/equatable.dart'; -import 'package:flutter/foundation.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:hydrated_bloc/hydrated_bloc.dart'; -import 'package:mixin_bot_sdk_dart/mixin_bot_sdk_dart.dart'; -import 'package:mixin_logger/mixin_logger.dart'; - -import '../../utils/hydrated_bloc.dart'; -import '../../utils/rivepod.dart'; - -class AuthState extends Equatable { - const AuthState({ - required this.account, - required this.privateKey, - }); - - factory AuthState.fromMap(Map map) => AuthState( - account: Account.fromJson(map['account'] as Map), - privateKey: map['privateKey'] as String, - ); - - final Account account; - final String privateKey; - - String get userId => account.userId; - - @override - List get props => [account, privateKey]; - - Map toMap() => { - 'account': account.toJson(), - 'privateKey': privateKey, - }; -} - -class MultiAuthState extends Equatable { - const MultiAuthState({ - Set auths = const {}, - }) : _auths = auths; - - factory MultiAuthState.fromMap(Map map) { - final list = map['auths'] as Iterable?; - return MultiAuthState( - auths: list - ?.map((e) => AuthState.fromMap(e as Map)) - .toSet() ?? - {}, - ); - } - - factory MultiAuthState.fromJson(String source) => - MultiAuthState.fromMap(json.decode(source) as Map); - - final Set _auths; - - AuthState? get current => _auths.isNotEmpty ? _auths.last : null; - - @override - List get props => [_auths]; - - Map toMap() => { - 'auths': _auths.map((x) => x.toMap()).toList(), - }; - - String toJson() => json.encode(toMap()); -} - -class MultiAuthStateNotifier extends DistinctStateNotifier { - MultiAuthStateNotifier(super.state); - - AuthState? get current => state.current; - - void signIn(AuthState authState) { - state = MultiAuthState( - auths: { - ...state._auths.where( - (element) => element.account.userId != authState.account.userId), - authState, - }, - ); - } - - void updateAccount(Account account) { - var authState = state._auths - .cast() - .firstWhere((element) => element?.account.userId == account.userId); - if (authState == null) { - i('update account, but ${account.userId} auth state not found.'); - return; - } - authState = AuthState(account: account, privateKey: authState.privateKey); - state = MultiAuthState( - auths: { - ...state._auths.where( - (element) => element.account.userId != authState?.account.userId), - authState, - }, - ); - } - - void signOut() { - if (state._auths.isEmpty) return; - state = - MultiAuthState(auths: state._auths.toSet()..remove(state._auths.last)); - } - - @override - @protected - set state(MultiAuthState value) { - final hydratedJson = toHydratedJson(state.toMap()); - HydratedBloc.storage.write(_kMultiAuthCubitKey, hydratedJson); - super.state = value; - } -} - -@Deprecated('Use multiAuthNotifierProvider instead') -const _kMultiAuthCubitKey = 'MultiAuthCubit'; - -final multiAuthStateNotifierProvider = - StateNotifierProvider.autoDispose( - (ref) { - ref.keepAlive(); - - final oldJson = HydratedBloc.storage.read(_kMultiAuthCubitKey); - if (oldJson != null) { - final multiAuthState = fromHydratedJson(oldJson, MultiAuthState.fromMap); - if (multiAuthState == null) { - return MultiAuthStateNotifier(const MultiAuthState()); - } - - return MultiAuthStateNotifier(multiAuthState); - } - - return MultiAuthStateNotifier(const MultiAuthState()); -}); - -final authProvider = - multiAuthStateNotifierProvider.select((value) => value.current); - -final authAccountProvider = authProvider.select((value) => value?.account); diff --git a/lib/ui/provider/abstract_responsive_navigator.dart b/lib/ui/provider/navigation/abstract_responsive_navigator.dart similarity index 98% rename from lib/ui/provider/abstract_responsive_navigator.dart rename to lib/ui/provider/navigation/abstract_responsive_navigator.dart index 3089f9aa07..c83f73c031 100644 --- a/lib/ui/provider/abstract_responsive_navigator.dart +++ b/lib/ui/provider/navigation/abstract_responsive_navigator.dart @@ -3,7 +3,7 @@ import 'dart:math'; import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; -import '../../utils/rivepod.dart'; +import '../../../utils/rivepod.dart'; class ResponsiveNavigatorState extends Equatable { const ResponsiveNavigatorState({ diff --git a/lib/ui/provider/responsive_navigator_provider.dart b/lib/ui/provider/navigation/responsive_navigator_provider.dart similarity index 88% rename from lib/ui/provider/responsive_navigator_provider.dart rename to lib/ui/provider/navigation/responsive_navigator_provider.dart index ba957d2217..aa401dc102 100644 --- a/lib/ui/provider/responsive_navigator_provider.dart +++ b/lib/ui/provider/navigation/responsive_navigator_provider.dart @@ -1,19 +1,19 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import '../home/chat/chat_page.dart'; -import '../setting/about_page.dart'; -import '../setting/account_delete_page.dart'; -import '../setting/account_page.dart'; -import '../setting/appearance_page.dart'; -import '../setting/backup_page.dart'; -import '../setting/edit_profile_page.dart'; -import '../setting/notification_page.dart'; -import '../setting/proxy_page.dart'; -import '../setting/security_page.dart'; -import '../setting/storage_page.dart'; -import '../setting/storage_usage_detail_page.dart'; -import '../setting/storage_usage_list_page.dart'; +import '../../home/chat/chat_page.dart'; +import '../../setting/about_page.dart'; +import '../../setting/account_delete_page.dart'; +import '../../setting/account_page.dart'; +import '../../setting/appearance_page.dart'; +import '../../setting/backup_page.dart'; +import '../../setting/edit_profile_page.dart'; +import '../../setting/notification_page.dart'; +import '../../setting/proxy_page.dart'; +import '../../setting/security_page.dart'; +import '../../setting/storage_page.dart'; +import '../../setting/storage_usage_detail_page.dart'; +import '../../setting/storage_usage_list_page.dart'; import 'abstract_responsive_navigator.dart'; class ResponsiveNavigatorStateNotifier diff --git a/lib/ui/provider/setting_provider.dart b/lib/ui/provider/setting_provider.dart index 0ebb1497d6..772473846e 100644 --- a/lib/ui/provider/setting_provider.dart +++ b/lib/ui/provider/setting_provider.dart @@ -1,15 +1,233 @@ -// ignore_for_file: deprecated_consistency -// ignore_for_file: deprecated_member_use_from_same_package - import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart'; +import 'package:mixin_logger/mixin_logger.dart'; +import '../../enum/property_group.dart'; +import '../../utils/db/db_key_value.dart'; +import '../../utils/extension/extension.dart'; import '../../utils/hydrated_bloc.dart'; -import '../../utils/logger.dart'; +import '../../utils/proxy.dart'; +import 'database_provider.dart'; + +final settingProvider = ChangeNotifierProvider( + (ref) => ref.watch(appDatabaseProvider).settingKeyValue); + +class AppSettingKeyValue extends AppKeyValue { + AppSettingKeyValue(KeyValueDao dao) + : super(group: AppPropertyGroup.setting, dao: dao) { + _migration(); + } +} + +const _kEnableProxyKey = 'enable_proxy'; +const _kSelectedProxyKey = 'selected_proxy'; +const _kProxyListKey = 'proxy_list'; + +extension ProxySetting on AppSettingKeyValue { + bool get enableProxy => get(_kEnableProxyKey) ?? false; + + set enableProxy(bool value) => set(_kEnableProxyKey, value); + + String? get selectedProxyId => get(_kSelectedProxyKey); + + set selectedProxyId(String? value) => set(_kSelectedProxyKey, value); + + List get proxyList { + final list = get>>(_kProxyListKey); + if (list == null || list.isEmpty) { + return []; + } + try { + return list.map(ProxyConfig.fromJson).toList(); + } catch (error, stacktrace) { + e('load proxyList error: $error, $stacktrace'); + } + return []; + } + + ProxyConfig? get activatedProxy { + if (!enableProxy) { + return null; + } + final list = proxyList; + if (list.isEmpty) { + return null; + } + if (selectedProxyId == null) { + return list.first; + } + return list.firstWhereOrNull((element) => element.id == selectedProxyId); + } + + void addProxy(ProxyConfig config) { + final list = [...proxyList, config]; + set(_kProxyListKey, list); + } + + void removeProxy(String id) { + final list = proxyList.where((element) => element.id != id).toList(); + set(_kProxyListKey, list); + } +} + +const _keyChatFontSizeDelta = 'chatFontSizeDelta'; +const _keyMessageShowIdentityNumber = 'messageShowIdentityNumber'; +const _keyMessageShowAvatar = 'messageShowAvatar'; + +extension ChatSetting on AppSettingKeyValue { + double get chatFontSizeDelta => get(_keyChatFontSizeDelta) ?? 0.0; + + set chatFontSizeDelta(double value) => set(_keyChatFontSizeDelta, value); + + bool get messageShowIdentityNumber => + get(_keyMessageShowIdentityNumber) ?? false; + + set messageShowIdentityNumber(bool value) => + set(_keyMessageShowIdentityNumber, value); + + bool get messageShowAvatar => get(_keyMessageShowAvatar) ?? true; + + set messageShowAvatar(bool value) => set(_keyMessageShowAvatar, value); +} + +const _keyBrightness = 'brightness'; +const _keyCollapsedSidebar = 'collapsedSidebar'; + +extension ApperenceSetting on AppSettingKeyValue { + /// [brightness] null to follow system. + set brightness(Brightness? value) { + switch (value) { + case Brightness.dark: + _brightness = 1; + case Brightness.light: + _brightness = 2; + case null: + _brightness = 0; + } + } + + Brightness? get brightness { + switch (_brightness) { + case 0: + case null: + return null; + case 1: + return Brightness.dark; + case 2: + return Brightness.light; + default: + w('invalid value for brightness. $_brightness'); + return null; + } + } + + ThemeMode get themeMode { + switch (brightness) { + case Brightness.dark: + return ThemeMode.dark; + case Brightness.light: + return ThemeMode.light; + case null: + return ThemeMode.system; + } + } + + int? get _brightness => get(_keyBrightness); + + set _brightness(int? value) => set(_keyBrightness, value); + + bool get collapsedSidebar => get(_keyCollapsedSidebar) ?? false; + + set collapsedSidebar(bool value) => set(_keyCollapsedSidebar, value); +} + +const _keyMessagePreview = 'messagePreview'; + +extension NotificationSetting on AppSettingKeyValue { + bool get messagePreview => get(_keyMessagePreview) ?? true; + + set messagePreview(bool value) => set(_keyMessagePreview, value); +} + +const _keyPhotoAutoDownload = 'photoAutoDownload'; +const _keyVideoAutoDownload = 'videoAutoDownload'; +const _keyFileAutoDownload = 'fileAutoDownload'; + +extension AutoDownloadSetting on AppSettingKeyValue { + bool get photoAutoDownload => get(_keyPhotoAutoDownload) ?? true; + + set photoAutoDownload(bool value) => set(_keyPhotoAutoDownload, value); -@Deprecated('Use settingProvider instead') + bool get videoAutoDownload => get(_keyVideoAutoDownload) ?? true; + + set videoAutoDownload(bool value) => set(_keyVideoAutoDownload, value); + + bool get fileAutoDownload => get(_keyFileAutoDownload) ?? true; + + set fileAutoDownload(bool value) => set(_keyFileAutoDownload, value); +} + +const _keySettingHasMigratedFromHive = 'settingHasMigratedFromHive'; + +extension SettingMigration on AppSettingKeyValue { + bool get settingHasMigratedFromHive => + get(_keySettingHasMigratedFromHive) ?? false; + + set settingHasMigratedFromHive(bool value) => + set(_keySettingHasMigratedFromHive, value); + + Future _migration() async { + await initialize; + if (settingHasMigratedFromHive) { + return; + } + settingHasMigratedFromHive = true; + final oldJson = HydratedBloc.storage.read(_kSettingCubitKey); + if (oldJson == null) { + return; + } + // we have not necessary to delete the old key + // unawaited(HydratedBloc.storage.delete(_kSettingCubitKey)); + final settingState = fromHydratedJson(oldJson, _SettingState.fromMap); + if (settingState == null) { + return; + } + if (settingState._brightness != null) { + _brightness = settingState._brightness; + } + if (settingState._messageShowAvatar != null) { + messageShowAvatar = settingState._messageShowAvatar; + } + if (settingState._messagePreview != null) { + messagePreview = settingState._messagePreview; + } + if (settingState._photoAutoDownload != null) { + photoAutoDownload = settingState._photoAutoDownload; + } + if (settingState._videoAutoDownload != null) { + videoAutoDownload = settingState._videoAutoDownload; + } + if (settingState._fileAutoDownload != null) { + fileAutoDownload = settingState._fileAutoDownload; + } + if (settingState._collapsedSidebar != null) { + collapsedSidebar = settingState._collapsedSidebar; + } + if (settingState._chatFontSizeDelta != null) { + chatFontSizeDelta = settingState._chatFontSizeDelta; + } + if (settingState._messageShowIdentityNumber != null) { + messageShowIdentityNumber = settingState._messageShowIdentityNumber; + } + } +} + +// setting cubit key in legacy hive +const _kSettingCubitKey = 'SettingCubit'; + +// setting cubit object in legacy hive class _SettingState extends Equatable { const _SettingState({ int? brightness, @@ -53,24 +271,6 @@ class _SettingState extends Equatable { final double? _chatFontSizeDelta; final bool? _messageShowIdentityNumber; - int get brightness => _brightness ?? 0; - - bool get messageShowAvatar => _messageShowAvatar ?? false; - - bool get messagePreview => _messagePreview ?? true; - - bool get photoAutoDownload => _photoAutoDownload ?? true; - - bool get videoAutoDownload => _videoAutoDownload ?? true; - - bool get fileAutoDownload => _fileAutoDownload ?? true; - - bool get collapsedSidebar => _collapsedSidebar ?? false; - - double get chatFontSizeDelta => _chatFontSizeDelta ?? 0; - - bool get messageShowIdentityNumber => _messageShowIdentityNumber ?? false; - @override List get props => [ _brightness, @@ -120,202 +320,3 @@ class _SettingState extends Equatable { messageShowIdentityNumber ?? _messageShowIdentityNumber, ); } - -class SettingChangeNotifier extends ChangeNotifier { - SettingChangeNotifier({ - int? brightness, - bool? messageShowAvatar, - bool? messagePreview, - bool? photoAutoDownload, - bool? videoAutoDownload, - bool? fileAutoDownload, - bool? collapsedSidebar, - double? chatFontSizeDelta, - bool? messageShowIdentityNumber, - }) : _brightness = brightness, - _messageShowAvatar = messageShowAvatar, - _messagePreview = messagePreview, - _photoAutoDownload = photoAutoDownload, - _videoAutoDownload = videoAutoDownload, - _fileAutoDownload = fileAutoDownload, - _collapsedSidebar = collapsedSidebar, - _chatFontSizeDelta = chatFontSizeDelta, - _messageShowIdentityNumber = messageShowIdentityNumber; - - /// The brightness of theme. - /// 0 : follow system - /// 1 : dark - /// 2 : light - /// - /// The reason [int] instead of [Brightness] enum is that Hive has limited - /// support for custom data class. - /// https://docs.hivedb.dev/#/custom-objects/type_adapters?id=register-adapter - /// https://github.com/hivedb/hive/issues/525 - /// https://github.com/hivedb/hive/issues/518 - int? _brightness; - bool? _messageShowAvatar; - bool? _messageShowIdentityNumber; - bool? _messagePreview; - bool? _photoAutoDownload; - bool? _videoAutoDownload; - bool? _fileAutoDownload; - bool? _collapsedSidebar; - double? _chatFontSizeDelta; - - /// [brightness] null to follow system. - set brightness(Brightness? value) { - switch (value) { - case Brightness.dark: - _brightness = 1; - case Brightness.light: - _brightness = 2; - case null: - _brightness = 0; - } - notifyListeners(); - } - - Brightness? get brightness { - switch (_brightness) { - case 0: - case null: - return null; - case 1: - return Brightness.dark; - case 2: - return Brightness.light; - default: - w('invalid value for brightness. $_brightness'); - return null; - } - } - - ThemeMode get themeMode { - switch (brightness) { - case Brightness.dark: - return ThemeMode.dark; - case Brightness.light: - return ThemeMode.light; - case null: - return ThemeMode.system; - } - } - - set messageShowAvatar(bool value) { - if (_messageShowAvatar == value) return; - - _messageShowAvatar = value; - notifyListeners(); - } - - bool get messageShowAvatar => _messageShowAvatar ?? false; - - set messageShowIdentityNumber(bool value) { - if (_messageShowIdentityNumber == value) return; - - _messageShowIdentityNumber = value; - notifyListeners(); - } - - bool get messageShowIdentityNumber => _messageShowIdentityNumber ?? false; - - set messagePreview(bool value) { - if (_messagePreview == value) return; - - _messagePreview = value; - notifyListeners(); - } - - bool get messagePreview => _messagePreview ?? true; - - set photoAutoDownload(bool value) { - if (_photoAutoDownload == value) return; - - _photoAutoDownload = value; - notifyListeners(); - } - - bool get photoAutoDownload => _photoAutoDownload ?? true; - - set videoAutoDownload(bool value) { - if (_videoAutoDownload == value) return; - - _videoAutoDownload = value; - notifyListeners(); - } - - bool get videoAutoDownload => _videoAutoDownload ?? true; - - set fileAutoDownload(bool value) { - if (_fileAutoDownload == value) return; - - _fileAutoDownload = value; - notifyListeners(); - } - - bool get fileAutoDownload => _fileAutoDownload ?? true; - - set collapsedSidebar(bool value) { - if (_collapsedSidebar == value) return; - - _collapsedSidebar = value; - notifyListeners(); - } - - bool get collapsedSidebar => _collapsedSidebar ?? false; - - set chatFontSizeDelta(double value) { - if (_chatFontSizeDelta == value) return; - - _chatFontSizeDelta = value; - notifyListeners(); - } - - double get chatFontSizeDelta => _chatFontSizeDelta ?? 0; - - @override - void notifyListeners() { - HydratedBloc.storage.write( - _kSettingCubitKey, - _SettingState( - brightness: _brightness, - messageShowAvatar: _messageShowAvatar, - messagePreview: _messagePreview, - photoAutoDownload: _photoAutoDownload, - videoAutoDownload: _videoAutoDownload, - fileAutoDownload: _fileAutoDownload, - collapsedSidebar: _collapsedSidebar, - chatFontSizeDelta: _chatFontSizeDelta, - messageShowIdentityNumber: _messageShowIdentityNumber, - ).toMap()); - super.notifyListeners(); - } -} - -@Deprecated('Use SettingChangeNotifier instead') -const _kSettingCubitKey = 'SettingCubit'; - -final settingProvider = - ChangeNotifierProvider.autoDispose((ref) { - ref.keepAlive(); - - //migrate - final oldJson = HydratedBloc.storage.read(_kSettingCubitKey); - if (oldJson != null) { - final settingState = fromHydratedJson(oldJson, _SettingState.fromMap); - if (settingState == null) return SettingChangeNotifier(); - return SettingChangeNotifier( - brightness: settingState.brightness, - messageShowAvatar: settingState.messageShowAvatar, - messagePreview: settingState.messagePreview, - photoAutoDownload: settingState.photoAutoDownload, - videoAutoDownload: settingState.videoAutoDownload, - fileAutoDownload: settingState.fileAutoDownload, - collapsedSidebar: settingState.collapsedSidebar, - chatFontSizeDelta: settingState.chatFontSizeDelta, - messageShowIdentityNumber: settingState.messageShowIdentityNumber, - ); - } - - return SettingChangeNotifier(); -}); diff --git a/lib/ui/provider/slide_category_provider.dart b/lib/ui/provider/slide_category_provider.dart index ecd76baa23..bf21de7251 100644 --- a/lib/ui/provider/slide_category_provider.dart +++ b/lib/ui/provider/slide_category_provider.dart @@ -2,6 +2,7 @@ import 'package:equatable/equatable.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import '../../utils/rivepod.dart'; +import 'account/account_server_provider.dart'; enum SlideCategoryType { chats, @@ -42,6 +43,10 @@ class SlideCategoryStateNotifier } } -final slideCategoryStateProvider = - StateNotifierProvider( - (ref) => SlideCategoryStateNotifier()); +final slideCategoryStateProvider = StateNotifierProvider.autoDispose< + SlideCategoryStateNotifier, SlideCategoryState>((ref) { + ref.listen(accountServerProvider, (previous, next) { + ref.notifier.switchToChatsIfSettings(); + }); + return SlideCategoryStateNotifier(); +}); diff --git a/lib/ui/provider/transfer_provider.dart b/lib/ui/provider/transfer_provider.dart index 8a8b923052..7b79818b4a 100644 --- a/lib/ui/provider/transfer_provider.dart +++ b/lib/ui/provider/transfer_provider.dart @@ -3,7 +3,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import '../../db/database_event_bus.dart'; import '../../db/mixin_database.dart'; import '../../utils/extension/extension.dart'; -import 'account_server_provider.dart'; +import 'account/account_server_provider.dart'; final tokenProvider = StreamProvider.autoDispose.family( (ref, assetId) { @@ -11,15 +11,13 @@ final tokenProvider = StreamProvider.autoDispose.family( return const Stream.empty(); } final database = ref.read(accountServerProvider).requireValue.database; - final stream = - database.tokenDao.tokenById(assetId).watchSingleOrNullWithStream( + return database.tokenDao.tokenById(assetId).watchSingleOrNullWithStream( eventStreams: [ DataBaseEventBus.instance.updateTokenStream .where((event) => event.contains(assetId)), ], duration: kDefaultThrottleDuration, ); - return stream; }, ); @@ -30,7 +28,7 @@ final safeSnapshotProvider = return const Stream.empty(); } final database = ref.read(accountServerProvider).requireValue.database; - final stream = database.safeSnapshotDao + return database.safeSnapshotDao .safeSnapshotById(snapshotId) .watchSingleOrNullWithStream( eventStreams: [ @@ -39,7 +37,6 @@ final safeSnapshotProvider = ], duration: kDefaultThrottleDuration, ); - return stream; }, ); @@ -49,13 +46,12 @@ final assetChainProvider = StreamProvider.autoDispose.family( return const Stream.empty(); } final database = ref.read(accountServerProvider).requireValue.database; - final stream = database.chainDao.chain(assetId).watchSingleOrNullWithStream( + return database.chainDao.chain(assetId).watchSingleOrNullWithStream( eventStreams: [ DataBaseEventBus.instance.updateAssetStream .where((event) => event.contains(assetId)), ], duration: kDefaultThrottleDuration, ); - return stream; }, ); diff --git a/lib/ui/setting/account_delete_page.dart b/lib/ui/setting/account_delete_page.dart index 4046099f1a..94557e264e 100644 --- a/lib/ui/setting/account_delete_page.dart +++ b/lib/ui/setting/account_delete_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:intl/intl.dart'; import 'package:mixin_bot_sdk_dart/mixin_bot_sdk_dart.dart' hide encryptPin; @@ -20,11 +21,11 @@ import '../../widgets/user/change_number_dialog.dart'; import '../../widgets/user/pin_verification_dialog.dart'; import '../../widgets/user/verification_dialog.dart'; -class AccountDeletePage extends StatelessWidget { +class AccountDeletePage extends ConsumerWidget { const AccountDeletePage({super.key}); @override - Widget build(BuildContext context) => Scaffold( + Widget build(BuildContext context, WidgetRef ref) => Scaffold( backgroundColor: context.theme.background, appBar: MixinAppBar( title: Text(context.l10n.deleteMyAccount), @@ -43,7 +44,9 @@ class AccountDeletePage extends StatelessWidget { title: Text(context.l10n.deleteMyAccount), color: context.theme.red, onTap: () async { - if (!SessionKeyValue.instance.checkPinToken()) { + final sessionKeyValue = + context.hiveKeyValues.sessionKeyValue; + if (!sessionKeyValue.checkPinToken()) { showToastFailed( ToastError(context.l10n.errorNoPinToken), ); @@ -112,7 +115,8 @@ class AccountDeletePage extends StatelessWidget { if (deleted) { w('account deleted'); await context.accountServer.signOutAndClear(); - context.multiAuthChangeNotifier.signOut(); + context.multiAuthChangeNotifier + .signOut(context.accountServer.userId); } } else { e('delete account no pin'); @@ -264,7 +268,10 @@ class _DeleteAccountPinDialog extends StatelessWidget { PinInputLayout( doVerify: (String pin) async { await context.accountServer.client.accountApi.deactive( - DeactivateRequest(encryptPin(pin)!, verificationId), + DeactivateRequest( + context.hiveKeyValues.sessionKeyValue.encryptPin(pin)!, + verificationId, + ), ); Navigator.pop(context, true); }, diff --git a/lib/ui/setting/account_page.dart b/lib/ui/setting/account_page.dart index c295544c7a..d816feb7a1 100644 --- a/lib/ui/setting/account_page.dart +++ b/lib/ui/setting/account_page.dart @@ -5,7 +5,7 @@ import '../../utils/extension/extension.dart'; import '../../widgets/app_bar.dart'; import '../../widgets/cell.dart'; import '../../widgets/user/change_number_dialog.dart'; -import '../provider/responsive_navigator_provider.dart'; +import '../provider/navigation/responsive_navigator_provider.dart'; class AccountPage extends HookConsumerWidget { const AccountPage({super.key}); diff --git a/lib/ui/setting/appearance_page.dart b/lib/ui/setting/appearance_page.dart index da1d7a214e..2f7179f634 100644 --- a/lib/ui/setting/appearance_page.dart +++ b/lib/ui/setting/appearance_page.dart @@ -30,9 +30,13 @@ class AppearancePage extends StatelessWidget { appBar: MixinAppBar( title: Text(context.l10n.appearance), ), - body: const Align( - alignment: Alignment.topCenter, - child: _Body(), + body: ConstrainedBox( + constraints: const BoxConstraints( + minWidth: double.infinity, + ), + child: const SingleChildScrollView( + child: _Body(), + ), ), ); } @@ -41,66 +45,68 @@ class _Body extends HookConsumerWidget { const _Body(); @override - Widget build(BuildContext context, WidgetRef ref) => SingleChildScrollView( - child: Container( - padding: const EdgeInsets.only(top: 20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(left: 10, bottom: 14), - child: Text( - context.l10n.theme, - style: TextStyle( - color: context.theme.secondaryText, - fontSize: 14, + Widget build(BuildContext context, WidgetRef ref) { + final brightness = + ref.watch(settingProvider.select((value) => value.brightness)); + return Container( + padding: const EdgeInsets.only(top: 20), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only(left: 10, bottom: 14), + child: Text( + context.l10n.theme, + style: TextStyle( + color: context.theme.secondaryText, + fontSize: 14, + ), + ), + ), + CellGroup( + cellBackgroundColor: context.theme.settingCellBackgroundColor, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CellItem( + title: RadioItem( + title: Text(context.l10n.followSystem), + groupValue: brightness, + onChanged: (value) => + ref.read(settingProvider).brightness = value, + value: null, ), + trailing: null, ), - ), - CellGroup( - cellBackgroundColor: context.theme.settingCellBackgroundColor, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - CellItem( - title: RadioItem( - title: Text(context.l10n.followSystem), - groupValue: ref.watch(settingProvider).brightness, - onChanged: (value) => - context.settingChangeNotifier.brightness = value, - value: null, - ), - trailing: null, - ), - CellItem( - title: RadioItem( - title: Text(context.l10n.light), - groupValue: ref.watch(settingProvider).brightness, - onChanged: (value) => - context.settingChangeNotifier.brightness = value, - value: Brightness.light, - ), - trailing: null, - ), - CellItem( - title: RadioItem( - title: Text(context.l10n.dark), - groupValue: ref.watch(settingProvider).brightness, - onChanged: (value) => - context.settingChangeNotifier.brightness = value, - value: Brightness.dark, - ), - trailing: null, - ), - ], + CellItem( + title: RadioItem( + title: Text(context.l10n.light), + groupValue: brightness, + onChanged: (value) => + ref.read(settingProvider).brightness = value, + value: Brightness.light, + ), + trailing: null, ), - ), - const _MessageAvatarSetting(), - const _ChatTextSizeSetting(), - ], + CellItem( + title: RadioItem( + title: Text(context.l10n.dark), + groupValue: brightness, + onChanged: (value) => + ref.read(settingProvider).brightness = value, + value: Brightness.dark, + ), + trailing: null, + ), + ], + ), ), - ), - ); + const _MessageAvatarSetting(), + const _ChatTextSizeSetting(), + const SizedBox(height: 36), + ], + ), + ); + } } class _MessageAvatarSetting extends HookConsumerWidget { @@ -139,7 +145,7 @@ class _MessageAvatarSetting extends HookConsumerWidget { activeColor: context.theme.accent, value: showAvatar, onChanged: (bool value) => - context.settingChangeNotifier.messageShowAvatar = value, + ref.read(settingProvider).messageShowAvatar = value, ), ), ), @@ -150,7 +156,8 @@ class _MessageAvatarSetting extends HookConsumerWidget { child: CupertinoSwitch( activeColor: context.theme.accent, value: showIdentityNumber, - onChanged: (bool value) => context.settingChangeNotifier + onChanged: (bool value) => ref + .read(settingProvider) .messageShowIdentityNumber = value, ), ), @@ -216,7 +223,7 @@ class _ChatTextSizeSetting extends HookConsumerWidget { max: 4, onChanged: (value) { debugPrint('fontSize: $value'); - context.settingChangeNotifier.chatFontSizeDelta = value; + ref.read(settingProvider).chatFontSizeDelta = value; }, ), ), diff --git a/lib/ui/setting/edit_profile_page.dart b/lib/ui/setting/edit_profile_page.dart index 49add31ceb..5e6e95b74b 100644 --- a/lib/ui/setting/edit_profile_page.dart +++ b/lib/ui/setting/edit_profile_page.dart @@ -10,7 +10,7 @@ import '../../widgets/app_bar.dart'; import '../../widgets/avatar_view/avatar_view.dart'; import '../../widgets/dialog.dart'; import '../../widgets/toast.dart'; -import '../provider/multi_auth_provider.dart'; +import '../provider/account/multi_auth_provider.dart'; class EditProfilePage extends HookConsumerWidget { const EditProfilePage({super.key}); diff --git a/lib/ui/setting/notification_page.dart b/lib/ui/setting/notification_page.dart index fc3c05222a..e212088cd4 100644 --- a/lib/ui/setting/notification_page.dart +++ b/lib/ui/setting/notification_page.dart @@ -50,7 +50,7 @@ class NotificationPage extends HookConsumerWidget { activeColor: context.theme.accent, value: currentMessagePreview, onChanged: (bool value) => - context.settingChangeNotifier.messagePreview = value, + ref.read(settingProvider).messagePreview = value, ), ), ), diff --git a/lib/ui/setting/proxy_page.dart b/lib/ui/setting/proxy_page.dart index 1a017038e1..06f9904382 100644 --- a/lib/ui/setting/proxy_page.dart +++ b/lib/ui/setting/proxy_page.dart @@ -9,13 +9,14 @@ import 'package:uuid/uuid.dart'; import '../../constants/constants.dart'; import '../../constants/resources.dart'; import '../../utils/extension/extension.dart'; -import '../../utils/hook.dart'; import '../../utils/logger.dart'; import '../../utils/proxy.dart'; import '../../widgets/action_button.dart'; import '../../widgets/app_bar.dart'; import '../../widgets/cell.dart'; import '../../widgets/dialog.dart'; +import '../provider/database_provider.dart'; +import '../provider/setting_provider.dart'; class ProxyPage extends StatelessWidget { const ProxyPage({super.key}); @@ -45,17 +46,11 @@ class _ProxySettingWidget extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final enableProxy = useListenableConverter( - context.database.settingProperties, - converter: (settingProperties) => settingProperties.enableProxy, - ).data ?? - false; - final hasProxyConfig = useListenableConverter( - context.database.settingProperties, - converter: (settingProperties) => - settingProperties.proxyList.isNotEmpty, - ).data ?? - false; + final enableProxy = + ref.watch(settingProvider.select((value) => value.enableProxy)); + final hasProxyConfig = ref.watch(settingProvider.select( + (value) => value.proxyList.isNotEmpty, + )); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -70,8 +65,9 @@ class _ProxySettingWidget extends HookConsumerWidget { value: hasProxyConfig && enableProxy, onChanged: !hasProxyConfig ? null - : (bool value) => context - .database.settingProperties.enableProxy = value, + : (bool value) => ref + .read(settingProvider.notifier) + .enableProxy = value, )), ), ), @@ -108,16 +104,11 @@ class _ProxyItemList extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final proxyList = useListenableConverter( - context.database.settingProperties, - converter: (settingProperties) => settingProperties.proxyList, - ).data ?? - const []; - final selectedProxyId = useListenableConverter( - context.database.settingProperties, - converter: (settingProperties) => settingProperties.selectedProxyId, - ).data ?? - proxyList.firstOrNull?.id; + final proxyList = + ref.watch(settingProvider.select((value) => value.proxyList)); + final selectedProxyId = + ref.watch(settingProvider.select((value) => value.selectedProxyId)) ?? + proxyList.firstOrNull?.id; return Column( children: proxyList .map( @@ -131,7 +122,7 @@ class _ProxyItemList extends HookConsumerWidget { } } -class _ProxyItemWidget extends StatelessWidget { +class _ProxyItemWidget extends ConsumerWidget { const _ProxyItemWidget({ required this.proxy, required this.selected, @@ -142,7 +133,7 @@ class _ProxyItemWidget extends StatelessWidget { final bool selected; @override - Widget build(BuildContext context) => Material( + Widget build(BuildContext context, WidgetRef ref) => Material( color: context.theme.settingCellBackgroundColor, child: ListTile( leading: SizedBox( @@ -175,10 +166,12 @@ class _ProxyItemWidget extends StatelessWidget { name: Resources.assetsImagesDeleteSvg, color: context.theme.icon, onTap: () { - context.database.settingProperties.removeProxy(proxy.id); + final settingKeyValue = ref.read(settingProvider.notifier) + ..removeProxy(proxy.id); if (selected) { - context.database.settingProperties.selectedProxyId = null; - context.database.settingProperties.enableProxy = false; + settingKeyValue + ..selectedProxyId = null + ..enableProxy = false; } }, ), @@ -186,7 +179,7 @@ class _ProxyItemWidget extends StatelessWidget { if (selected) { return; } - context.database.settingProperties.selectedProxyId = proxy.id; + ref.read(settingProvider.notifier).selectedProxyId = proxy.id; }, ), ); @@ -278,7 +271,7 @@ class _ProxyAddDialog extends HookConsumerWidget { id: id, ); i('add proxy config: ${config.type} ${config.host}:${config.port}'); - context.database.settingProperties.addProxy(config); + ref.read(appDatabaseProvider).settingKeyValue.addProxy(config); Navigator.pop(context); }, child: Text(context.l10n.add), diff --git a/lib/ui/setting/security_page.dart b/lib/ui/setting/security_page.dart index 70ff9076b3..78b562194d 100644 --- a/lib/ui/setting/security_page.dart +++ b/lib/ui/setting/security_page.dart @@ -5,15 +5,14 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:pin_code_fields/pin_code_fields.dart'; -import '../../account/security_key_value.dart'; import '../../utils/authentication.dart'; import '../../utils/extension/extension.dart'; -import '../../utils/hook.dart'; import '../../widgets/app_bar.dart'; import '../../widgets/buttons.dart'; import '../../widgets/cell.dart'; import '../../widgets/dialog.dart'; import '../../widgets/toast.dart'; +import '../provider/account/security_key_value_provider.dart'; class SecurityPage extends StatelessWidget { const SecurityPage({super.key}); @@ -46,17 +45,15 @@ class _Passcode extends HookConsumerWidget { final globalKey = useMemoized(GlobalKey>.new, []); - final hasPasscode = - useMemoizedStream(SecurityKeyValue.instance.watchHasPasscode).data ?? - SecurityKeyValue.instance.hasPasscode; + final hasPasscode = ref + .watch(securityKeyValueProvider.select((value) => value.hasPasscode)); final enableBiometric = - useMemoizedStream(SecurityKeyValue.instance.watchBiometric).data ?? - SecurityKeyValue.instance.biometric; + ref.watch(securityKeyValueProvider.select((value) => value.biometric)); - final minutes = useStream(SecurityKeyValue.instance - .watchLockDuration() - .map((event) => event.inMinutes)).data; + final minutes = ref + .watch(securityKeyValueProvider.select((value) => value.lockDuration)) + .inMinutes; return Column( mainAxisSize: MainAxisSize.min, @@ -75,7 +72,7 @@ class _Passcode extends HookConsumerWidget { value: hasPasscode, onChanged: (value) { if (!value) { - SecurityKeyValue.instance.passcode = null; + ref.read(securityKeyValueProvider).passcode = null; return; } showMixinDialog( @@ -125,9 +122,9 @@ class _Passcode extends HookConsumerWidget { ) .toList(), onSelected: (value) => - SecurityKeyValue.instance.lockDuration = value, + ref.read(securityKeyValueProvider).lockDuration = value, child: Text( - (minutes == null || minutes == 0) + minutes == 0 ? context.l10n.disabled : minutes < 60 ? context.l10n.minute(minutes, minutes) @@ -156,7 +153,7 @@ class _Passcode extends HookConsumerWidget { return; } - SecurityKeyValue.instance.biometric = value; + ref.read(securityKeyValueProvider).biometric = value; }, ), ), @@ -206,7 +203,7 @@ class _InputPasscode extends HookConsumerWidget { return; } - SecurityKeyValue.instance.passcode = passcode.value; + ref.read(securityKeyValueProvider).passcode = passcode.value; Navigator.maybePop(context); }); diff --git a/lib/ui/setting/setting_page.dart b/lib/ui/setting/setting_page.dart index 8e6b69cee8..a51b45b593 100644 --- a/lib/ui/setting/setting_page.dart +++ b/lib/ui/setting/setting_page.dart @@ -6,7 +6,6 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_svg/svg.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import '../../account/account_key_value.dart'; import '../../constants/resources.dart'; import '../../utils/app_lifecycle.dart'; import '../../utils/extension/extension.dart'; @@ -19,8 +18,8 @@ import '../../widgets/cell.dart'; import '../../widgets/high_light_text.dart'; import '../../widgets/toast.dart'; import '../home/home.dart'; -import '../provider/multi_auth_provider.dart'; -import '../provider/responsive_navigator_provider.dart'; +import '../provider/account/multi_auth_provider.dart'; +import '../provider/navigation/responsive_navigator_provider.dart'; class SettingPage extends HookConsumerWidget { const SettingPage({super.key}); @@ -80,7 +79,7 @@ class SettingPage extends HookConsumerWidget { children: [ if (Platform.isIOS && userHasPin && - AccountKeyValue.instance.primarySessionId == null) + context.accountServer.loginByPhoneNumber) _Item( leadingAssetName: Resources.assetsImagesAccountSvg, @@ -156,7 +155,8 @@ class SettingPage extends HookConsumerWidget { context.accountServer.signOutAndClear(), ); if (!succeed) return; - context.multiAuthChangeNotifier.signOut(); + context.multiAuthChangeNotifier + .signOut(context.accountServer.userId); }, color: context.theme.red, trailing: const SizedBox(), diff --git a/lib/ui/setting/storage_page.dart b/lib/ui/setting/storage_page.dart index 67955bb95e..ef4a8532db 100644 --- a/lib/ui/setting/storage_page.dart +++ b/lib/ui/setting/storage_page.dart @@ -1,11 +1,10 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; - import '../../utils/extension/extension.dart'; import '../../widgets/app_bar.dart'; import '../../widgets/cell.dart'; -import '../provider/responsive_navigator_provider.dart'; +import '../provider/navigation/responsive_navigator_provider.dart'; import '../provider/setting_provider.dart'; class StoragePage extends HookConsumerWidget { @@ -44,8 +43,8 @@ class StoragePage extends HookConsumerWidget { child: CupertinoSwitch( activeColor: context.theme.accent, value: photoAutoDownload, - onChanged: (bool value) => context - .settingChangeNotifier + onChanged: (bool value) => ref + .read(settingProvider) .photoAutoDownload = value, )), ), @@ -56,8 +55,8 @@ class StoragePage extends HookConsumerWidget { child: CupertinoSwitch( activeColor: context.theme.accent, value: videoAutoDownload, - onChanged: (bool value) => context - .settingChangeNotifier + onChanged: (bool value) => ref + .read(settingProvider) .videoAutoDownload = value, )), ), @@ -68,8 +67,9 @@ class StoragePage extends HookConsumerWidget { child: CupertinoSwitch( activeColor: context.theme.accent, value: fileAutoDownload, - onChanged: (bool value) => context - .settingChangeNotifier.fileAutoDownload = value, + onChanged: (bool value) => ref + .read(settingProvider) + .fileAutoDownload = value, )), ), ], diff --git a/lib/ui/setting/storage_usage_list_page.dart b/lib/ui/setting/storage_usage_list_page.dart index 093c63cacf..01d47a7fc9 100644 --- a/lib/ui/setting/storage_usage_list_page.dart +++ b/lib/ui/setting/storage_usage_list_page.dart @@ -11,7 +11,7 @@ import '../../utils/hook.dart'; import '../../widgets/app_bar.dart'; import '../../widgets/avatar_view/avatar_view.dart'; import '../../widgets/cell.dart'; -import '../provider/responsive_navigator_provider.dart'; +import '../provider/navigation/responsive_navigator_provider.dart'; class StorageUsageListPage extends HookConsumerWidget { const StorageUsageListPage({super.key}); diff --git a/lib/utils/attachment/attachment_util.dart b/lib/utils/attachment/attachment_util.dart index 1a895a564a..751080c7ac 100644 --- a/lib/utils/attachment/attachment_util.dart +++ b/lib/utils/attachment/attachment_util.dart @@ -16,6 +16,7 @@ import '../../db/database.dart'; import '../../db/mixin_database.dart'; import '../../db/util/util.dart'; import '../../enum/media_status.dart'; +import '../../ui/provider/setting_provider.dart'; import '../../widgets/message/send_message_dialog/attachment_extra.dart'; import '../../widgets/toast.dart'; import '../crypto_util.dart'; @@ -23,7 +24,6 @@ import '../extension/extension.dart'; import '../file.dart'; import '../load_balancer_utils.dart'; import '../logger.dart'; -import '../property/setting_property.dart'; import '../proxy.dart'; import 'download_key_value.dart'; @@ -143,6 +143,7 @@ class AttachmentUtil extends AttachmentUtilBase with ChangeNotifier { this._transcriptMessageDao, this._settingProperties, super.mediaPath, + this.downloadKeyValue, ) { final httpClientAdapter = _dio.httpClientAdapter; if (httpClientAdapter is IOHttpClientAdapter) { @@ -158,7 +159,8 @@ class AttachmentUtil extends AttachmentUtilBase with ChangeNotifier { final MessageDao _messageDao; final TranscriptMessageDao _transcriptMessageDao; final Client _client; - final SettingPropertyStorage _settingProperties; + final AppSettingKeyValue _settingProperties; + final DownloadKeyValue downloadKeyValue; final _attachmentJob = {}; @@ -461,6 +463,8 @@ class AttachmentUtil extends AttachmentUtilBase with ChangeNotifier { Client client, Database database, String identityNumber, + DownloadKeyValue downloadKeyValue, + AppSettingKeyValue settingKeyValue, ) { final documentDirectory = mixinDocumentsDirectory; final mediaDirectory = @@ -469,8 +473,9 @@ class AttachmentUtil extends AttachmentUtilBase with ChangeNotifier { client, database.messageDao, database.transcriptMessageDao, - database.settingProperties, + settingKeyValue, mediaDirectory.path, + downloadKeyValue, ); } @@ -483,12 +488,12 @@ class AttachmentUtil extends AttachmentUtilBase with ChangeNotifier { void _setAttachmentJob(String messageId, _AttachmentJobBase job) { _attachmentJob[messageId] = job; if (job is _AttachmentDownloadJob) { - DownloadKeyValue.instance.addMessageId(messageId); + downloadKeyValue.addMessageId(messageId); } } Future removeAttachmentJob(String messageId) async { - await DownloadKeyValue.instance.removeMessageId(messageId); + await downloadKeyValue.removeMessageId(messageId); _attachmentJob[messageId]?.cancel(); _attachmentJob.remove(messageId); } @@ -496,7 +501,7 @@ class AttachmentUtil extends AttachmentUtilBase with ChangeNotifier { Future cancelProgressAttachmentJob(String messageId) async { if (!_hasAttachmentJob(messageId)) return false; await _messageDao.updateMediaStatus(messageId, MediaStatus.canceled); - await DownloadKeyValue.instance.removeMessageId(messageId); + await downloadKeyValue.removeMessageId(messageId); _attachmentJob[messageId]?.cancel(); _attachmentJob.remove(messageId); return true; diff --git a/lib/utils/attachment/download_key_value.dart b/lib/utils/attachment/download_key_value.dart index dc03e87bd9..97c31fc211 100644 --- a/lib/utils/attachment/download_key_value.dart +++ b/lib/utils/attachment/download_key_value.dart @@ -1,11 +1,7 @@ import '../hive_key_values.dart'; class DownloadKeyValue extends HiveKeyValue { - DownloadKeyValue._() : super(_hiveName); - - static DownloadKeyValue? _instance; - - static DownloadKeyValue get instance => _instance ??= DownloadKeyValue._(); + DownloadKeyValue() : super(_hiveName); static const _hiveName = 'download_box'; @@ -14,6 +10,4 @@ class DownloadKeyValue extends HiveKeyValue { Future addMessageId(String messageId) => box.put(messageId, messageId); Future removeMessageId(String messageId) => box.delete(messageId); - - Future clear() => box.clear(); } diff --git a/lib/utils/db/db_key_value.dart b/lib/utils/db/db_key_value.dart new file mode 100644 index 0000000000..098f7fe350 --- /dev/null +++ b/lib/utils/db/db_key_value.dart @@ -0,0 +1,155 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; +import 'package:mixin_logger/mixin_logger.dart'; + +import '../../enum/property_group.dart'; + +abstract class KeyValueDao { + Future getByKey(Group group, String key); + + Future> getAll(Group group); + + Future set(Group group, String key, String? value); + + Future clear(Group group); + + Stream> watchAll(Group group); + + Stream watchByKey(Group group, String key); + + Stream watchTableHasChanged(Group group); +} + +typedef BaseLazyUserKeyValue = _BaseLazyDbKeyValue; +typedef BaseLazyAppKeyValue = _BaseLazyDbKeyValue; + +class _BaseLazyDbKeyValue { + _BaseLazyDbKeyValue({ + required this.group, + required this.dao, + }); + + final G group; + final KeyValueDao dao; + + Stream watch(String key) => + dao.watchByKey(group, key).map(convertToType); + + Future get(String key) async { + final value = await dao.getByKey(group, key); + return convertToType(value); + } + + Future set(String key, T? value) => + dao.set(group, key, convertToString(value)); + + Future clear() { + i('clear key value: $group'); + return dao.clear(group); + } +} + +typedef AppKeyValue = _BaseDbKeyValue; + +class _BaseDbKeyValue extends ChangeNotifier { + _BaseDbKeyValue({required this.group, required KeyValueDao dao}) + : _dao = dao { + _loadProperties().whenComplete(_initCompleter.complete); + _subscription = dao.watchTableHasChanged(group).listen((event) { + _loadProperties(); + }); + } + + @protected + final G group; + final KeyValueDao _dao; + + final Map _data = {}; + + final Completer _initCompleter = Completer(); + StreamSubscription? _subscription; + + Future get initialize => _initCompleter.future; + + bool get initialized => _initCompleter.isCompleted; + + Future _loadProperties() async { + final properties = await _dao.getAll(group); + _data + ..clear() + ..addAll(properties); + notifyListeners(); + } + + T? get(String key) => convertToType(_data[key]); + + Future set(String key, T? value) { + final converted = convertToString(value); + if (converted != null) { + _data[key] = converted; + } else { + _data.remove(key); + } + return _dao.set(group, key, converted); + } + + Future clear() { + i('clear key value: $group'); + _data.clear(); + return _dao.clear(group); + } + + @override + void dispose() { + super.dispose(); + _subscription?.cancel(); + } +} + +@visibleForTesting +T? convertToType(String? value) { + if (value == null) { + return null; + } + try { + switch (T) { + case const (String): + return value as T; + case const (int): + return int.parse(value) as T; + case const (double): + return double.parse(value) as T; + case const (bool): + return (value == 'true') as T; + case const (Map): + case const (Map): + case const (List): + return jsonDecode(value) as T; + case const (List): + return (jsonDecode(value) as List).cast() as T; + case const (List>): + return (jsonDecode(value) as List).cast>() as T; + case const (dynamic): + case const (Object): + return value as T; + default: + throw UnsupportedError('unsupported type $T'); + } + } catch (error, stacktrace) { + e('failed to convert $value to type $T : $error\n$stacktrace'); + return null; + } +} + +@visibleForTesting +String? convertToString(dynamic value) { + if (value == null) { + return null; + } + if (value is String) { + return value; + } + return jsonEncode(value); +} diff --git a/lib/utils/db/user_crypto_key_value.dart b/lib/utils/db/user_crypto_key_value.dart new file mode 100644 index 0000000000..ba54be1639 --- /dev/null +++ b/lib/utils/db/user_crypto_key_value.dart @@ -0,0 +1,38 @@ +import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; + +import '../../enum/property_group.dart'; +import '../crypto_util.dart'; +import 'db_key_value.dart'; + +class UserCryptoKeyValue extends BaseLazyUserKeyValue { + UserCryptoKeyValue(KeyValueDao dao) + : super(group: UserPropertyGroup.crypto, dao: dao); + + static const _kNextPreKeyId = 'next_pre_key_id'; + static const _kLocalRegistrationId = 'local_registration_id'; + static const _kNextSignedPreKeyId = 'next_signed_pre_key_id'; + static const _kActiveSignedPreKeyId = 'active_signed_pre_key_id'; + + Future getNextPreKeyId() async => + (await get(_kNextPreKeyId)) ?? generateRandomInt(maxValue); + + Future setNextPreKeyId(int preKeyId) => set(_kNextPreKeyId, preKeyId); + + Future getLocalRegistrationId() async => + (await get(_kLocalRegistrationId)) ?? 0; + + Future setLocalRegistrationId(int registrationId) => + set(_kLocalRegistrationId, registrationId); + + Future getNextSignedPreKeyId() async => + (await get(_kNextSignedPreKeyId)) ?? generateRandomInt(maxValue); + + Future setNextSignedPreKeyId(int preKeyId) => + set(_kNextSignedPreKeyId, preKeyId); + + Future getActiveSignedPreKeyId() async => + (await get(_kActiveSignedPreKeyId)) ?? -1; + + Future setActiveSignedPreKeyId(int preKeyId) => + set(_kActiveSignedPreKeyId, preKeyId); +} diff --git a/lib/utils/extension/extension.dart b/lib/utils/extension/extension.dart index 73c57a3e85..7b3e533854 100644 --- a/lib/utils/extension/extension.dart +++ b/lib/utils/extension/extension.dart @@ -24,10 +24,10 @@ import '../../account/account_server.dart'; import '../../db/dao/snapshot_dao.dart'; import '../../db/database.dart'; import '../../generated/l10n.dart'; -import '../../ui/provider/account_server_provider.dart'; +import '../../ui/provider/account/account_server_provider.dart'; +import '../../ui/provider/account/multi_auth_provider.dart'; import '../../ui/provider/database_provider.dart'; -import '../../ui/provider/multi_auth_provider.dart'; -import '../../ui/provider/setting_provider.dart'; +import '../../ui/provider/hive_key_value_provider.dart'; import '../../widgets/brightness_observer.dart'; import '../audio_message_player/audio_message_service.dart'; import '../platform.dart'; @@ -55,17 +55,29 @@ export 'src/file.dart'; export 'src/platforms.dart'; part 'src/db.dart'; + part 'src/duration.dart'; + part 'src/image.dart'; + part 'src/info.dart'; + part 'src/iterable.dart'; + part 'src/key_event.dart'; + part 'src/markdown.dart'; + part 'src/number.dart'; + part 'src/provider.dart'; + part 'src/regexp.dart'; + part 'src/stream.dart'; + part 'src/string.dart'; + part 'src/ui.dart'; void importExtension() {} diff --git a/lib/utils/extension/src/provider.dart b/lib/utils/extension/src/provider.dart index adc955300c..899e8c5edb 100644 --- a/lib/utils/extension/src/provider.dart +++ b/lib/utils/extension/src/provider.dart @@ -8,15 +8,14 @@ extension ProviderExtension on BuildContext { Account? get account => providerContainer.read(authAccountProvider); - SettingChangeNotifier get settingChangeNotifier => - providerContainer.read(settingProvider); - AccountServer get accountServer => providerContainer.read(accountServerProvider.select((value) { if (!value.hasValue) throw Exception('AccountServerProvider not ready'); return value.requireValue; })); + HiveKeyValues get hiveKeyValues => accountServer.hiveKeyValues; + AudioMessagePlayService get audioMessageService => read(); @@ -32,8 +31,7 @@ extension ProviderExtension on BuildContext { double get brightnessValue => BrightnessData.of(this); - Brightness get brightness => - settingChangeNotifier.brightness ?? MediaQuery.platformBrightnessOf(this); + Brightness get brightness => BrightnessData.brightnessOf(this); Color dynamicColor( Color color, { diff --git a/lib/utils/hive_key_values.dart b/lib/utils/hive_key_values.dart index 5d44e2eec3..c0470d37dd 100644 --- a/lib/utils/hive_key_values.dart +++ b/lib/utils/hive_key_values.dart @@ -3,78 +3,54 @@ import 'dart:io'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; +import 'package:mixin_logger/mixin_logger.dart'; import 'package:path/path.dart' as p; -import '../account/account_key_value.dart'; -import '../account/scam_warning_key_value.dart'; -import '../account/security_key_value.dart'; -import '../account/session_key_value.dart'; -import '../account/show_pin_message_key_value.dart'; -import '../crypto/crypto_key_value.dart'; -import '../crypto/privacy_key_value.dart'; -import 'attachment/download_key_value.dart'; import 'file.dart'; -Future initKeyValues(String identityNumber) => Future.wait([ - PrivacyKeyValue.instance.init(identityNumber), - CryptoKeyValue.instance.init(identityNumber), - AccountKeyValue.instance.init(identityNumber), - ShowPinMessageKeyValue.instance.init(identityNumber), - ScamWarningKeyValue.instance.init(identityNumber), - DownloadKeyValue.instance.init(identityNumber), - SessionKeyValue.instance.init(identityNumber), - SecurityKeyValue.instance.init(identityNumber), - ]); - -Future clearKeyValues() => Future.wait([ - PrivacyKeyValue.instance.delete(), - CryptoKeyValue.instance.delete(), - AccountKeyValue.instance.delete(), - ShowPinMessageKeyValue.instance.delete(), - ScamWarningKeyValue.instance.delete(), - DownloadKeyValue.instance.delete(), - SessionKeyValue.instance.delete(), - SecurityKeyValue.instance.delete(), - ]); - abstract class HiveKeyValue { - HiveKeyValue(this._boxName); + HiveKeyValue(this.boxName); - final String _boxName; + final String boxName; late Box box; bool _hasInit = false; - Future init(String identityNumber) async { + String? _identityNumber; + + Future init(HiveInterface hive, String identityNumber) async { if (_hasInit) { return; } final dbFolder = mixinDocumentsDirectory; - - final legacyBoxDirectory = Directory(p.join(dbFolder.path, _boxName)); - final directory = - Directory(p.join(dbFolder.path, identityNumber, _boxName)); - - if (legacyBoxDirectory.existsSync()) { - // copy legacy file to new file - if (directory.existsSync()) directory.deleteSync(recursive: true); - legacyBoxDirectory.renameSync(directory.path); - } - + final directory = Directory(p.join(dbFolder.path, identityNumber, boxName)); WidgetsFlutterBinding.ensureInitialized(); if (!kIsWeb) { - Hive.init(directory.absolute.path); + hive.init(directory.absolute.path); } - box = await Hive.openBox(_boxName); + box = await hive.openBox(boxName); + i('HiveKeyValue: open $boxName'); + _identityNumber = identityNumber; _hasInit = true; } - Future delete() async { - if (!_hasInit) return; - try { - await Hive.deleteBoxFromDisk(_boxName); - } catch (_) { - // ignore already deleted + Future dispose() async { + if (!_hasInit) { + return; } + i('HiveKeyValue: dispose $boxName $_identityNumber'); + await box.close(); _hasInit = false; } + + Future clear() async { + if (!_hasInit) { + return; + } + i('HiveKeyValue: clear $boxName $_identityNumber'); + await box.clear(); + } + + @override + String toString() => + 'HiveKeyValue{boxName: $boxName, _hasInit: $_hasInit, _identityNumber: $_identityNumber}'; } diff --git a/lib/utils/local_notification_center.dart b/lib/utils/local_notification_center.dart index f84eef2378..c6d7a91e8c 100644 --- a/lib/utils/local_notification_center.dart +++ b/lib/utils/local_notification_center.dart @@ -44,6 +44,8 @@ abstract class _NotificationManager { Future dismissByMessageId(String messageId, String conversationId); + Future dismissAll(); + Future requestPermission(); @protected @@ -171,6 +173,9 @@ class _LocalNotificationManager extends _NotificationManager { await flutterLocalNotificationsPlugin.cancel(id); notifications.remove(notification); } + + @override + Future dismissAll() => flutterLocalNotificationsPlugin.cancelAll(); } class _WindowsNotificationManager extends _NotificationManager { @@ -243,6 +248,9 @@ class _WindowsNotificationManager extends _NotificationManager { @override Future requestPermission() async => null; + + @override + Future dismissAll() => WinToast.instance().clear(); } enum NotificationScheme { diff --git a/lib/utils/mixin_api_client.dart b/lib/utils/mixin_api_client.dart index 5a48a1eb8f..ce38f2bd6a 100644 --- a/lib/utils/mixin_api_client.dart +++ b/lib/utils/mixin_api_client.dart @@ -18,26 +18,35 @@ const kRequestTimeStampKey = 'requestTimeStamp'; Future _userAgent = generateUserAgent(); Future _deviceId = getDeviceId(); -Client createClient({ - required String userId, - required String sessionId, - required String privateKey, - // Hive didn't support multi isolate. - required bool loginByPhoneNumber, +final _httpLogLevel = switch (kHttpLogLevel) { + 'null' => null, + 'none' => HttpLogLevel.none, + 'headers' => HttpLogLevel.headers, + 'body' => HttpLogLevel.body, + _ => HttpLogLevel.all, +}; + +Client createLandingClient() => _createClient(); + +Client _createClient({ + String? userId, + String? sessionId, + String? privateKey, + String? scp, List interceptors = const [], }) { final client = Client( userId: userId, sessionId: sessionId, privateKey: privateKey, - scp: loginByPhoneNumber ? scpFull : scp, + scp: scp, + httpLogLevel: _httpLogLevel, dioOptions: BaseOptions( connectTimeout: tenSecond, receiveTimeout: tenSecond, sendTimeout: tenSecond, followRedirects: false, ), - // httpLogLevel: HttpLogLevel.none, jsonDecodeCallback: jsonDecode, interceptors: [ ...interceptors, @@ -61,6 +70,7 @@ Client createClient({ 'requestTimeStamp = ${requestTimeStamp?.outputFormat()} ' 'serverTimeStamp = ${serverTimeStamp?.outputFormat()} ' 'now = ${DateTime.now().outputFormat()}'); + w('error: ${e.message} ${e.stackTrace}'); handler.next(e); }, ), @@ -83,6 +93,22 @@ extension DioNativeAdapter on Dio { } } +Client createClient({ + required String userId, + required String sessionId, + required String privateKey, + // Hive didn't support multi isolate. + required bool loginByPhoneNumber, + List interceptors = const [], +}) => + _createClient( + userId: userId, + sessionId: sessionId, + privateKey: privateKey, + scp: loginByPhoneNumber ? scpFull : scp, + interceptors: interceptors, + ); + final _formatter = DateFormat('yyyy-MM-dd HH:mm:ss.SSS'); extension _DateTimeFormatter on DateTime { diff --git a/lib/utils/property/setting_property.dart b/lib/utils/property/setting_property.dart deleted file mode 100644 index 5b6c512fe2..0000000000 --- a/lib/utils/property/setting_property.dart +++ /dev/null @@ -1,66 +0,0 @@ -import 'dart:convert'; - -import 'package:mixin_logger/mixin_logger.dart'; - -import '../../db/dao/property_dao.dart'; -import '../../db/util/property_storage.dart'; -import '../../enum/property_group.dart'; -import '../extension/extension.dart'; -import '../proxy.dart'; - -const _kEnableProxyKey = 'enable_proxy'; -const _kSelectedProxyKey = 'selected_proxy'; -const _kProxyListKey = 'proxy_list'; - -class SettingPropertyStorage extends PropertyStorage { - SettingPropertyStorage(PropertyDao dao) : super(PropertyGroup.setting, dao); - - bool get enableProxy => get(_kEnableProxyKey) ?? false; - - set enableProxy(bool value) => set(_kEnableProxyKey, value); - - String? get selectedProxyId => get(_kSelectedProxyKey); - - set selectedProxyId(String? value) => set(_kSelectedProxyKey, value); - - List get proxyList { - final json = get(_kProxyListKey); - if (json == null || json.isEmpty) { - return []; - } - try { - final list = jsonDecode(json) as List; - return list - .cast>() - .map(ProxyConfig.fromJson) - .toList(); - } catch (error, stacktrace) { - e('load proxyList error: $error, $stacktrace'); - } - return []; - } - - ProxyConfig? get activatedProxy { - if (!enableProxy) { - return null; - } - final list = proxyList; - if (list.isEmpty) { - return null; - } - if (selectedProxyId == null) { - return list.first; - } - return list.firstWhereOrNull((element) => element.id == selectedProxyId); - } - - void addProxy(ProxyConfig config) { - final list = [...proxyList, config]; - set(_kProxyListKey, jsonEncode(list)); - } - - void removeProxy(String id) { - final list = proxyList.where((element) => element.id != id).toList(); - set(_kProxyListKey, jsonEncode(list)); - } -} diff --git a/lib/utils/proxy.dart b/lib/utils/proxy.dart index 812af64a41..161c250c09 100644 --- a/lib/utils/proxy.dart +++ b/lib/utils/proxy.dart @@ -7,9 +7,9 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:mixin_bot_sdk_dart/mixin_bot_sdk_dart.dart'; import 'package:mixin_logger/mixin_logger.dart'; +import '../ui/provider/setting_provider.dart'; import 'extension/extension.dart'; import 'mixin_api_client.dart'; -import 'property/setting_property.dart'; part 'proxy.g.dart'; @@ -56,22 +56,22 @@ class ProxyConfig with EquatableMixin { extension DioProxyExt on Dio { void applyProxy(ProxyConfig? config) { if (config != null) { - i('apply client proxy $config'); + i('with dio with client proxy $config'); httpClientAdapter = IOHttpClientAdapter(); (httpClientAdapter as IOHttpClientAdapter).createHttpClient = () => HttpClient()..setProxy(config); } else { - i('remove client proxy'); + i('create dio without client proxy'); userCustomAdapter(); } } } extension ClientExt on Client { - void configProxySetting(SettingPropertyStorage settingProperties) { - var proxyConfig = settingProperties.activatedProxy; - settingProperties.addListener(() { - final config = settingProperties.activatedProxy; + void configProxySetting(AppSettingKeyValue settingKeyValue) { + var proxyConfig = settingKeyValue.activatedProxy; + settingKeyValue.addListener(() { + final config = settingKeyValue.activatedProxy; if (config != proxyConfig) { proxyConfig = config; dio.applyProxy(config); diff --git a/lib/utils/web_view/web_view_desktop.dart b/lib/utils/web_view/web_view_desktop.dart index 9f10b78d99..f99d53d290 100644 --- a/lib/utils/web_view/web_view_desktop.dart +++ b/lib/utils/web_view/web_view_desktop.dart @@ -16,7 +16,6 @@ import '../extension/extension.dart'; import '../file.dart'; import '../system/package_info.dart'; import '../uri_utils.dart'; - import 'web_view_interface.dart'; class DesktopMixinWebView extends MixinWebView { @@ -41,8 +40,7 @@ class DesktopMixinWebView extends MixinWebView { ) async { assert(context.auth != null); - final mode = context.settingChangeNotifier.brightness ?? - MediaQuery.platformBrightnessOf(context); + final mode = context.brightness; final info = await getPackageInfo(); debugPrint( 'info: appName: ${info.appName} packageName: ${info.packageName} version: ${info.version} buildNumber: ${info.buildNumber} buildSignature: ${info.buildSignature} '); @@ -74,7 +72,7 @@ class DesktopMixinWebView extends MixinWebView { App? app, AppCardData? appCardData, }) async { - final brightness = context.settingChangeNotifier.brightness; + final brightness = context.brightness; final packageInfo = await getPackageInfo(); final webView = await WebviewWindow.create( configuration: CreateConfiguration( @@ -110,6 +108,7 @@ bool runWebViewNavigationBar(List args) => runWebViewTitleBarWidget( builder: (context) => const BrightnessData( brightnessThemeData: lightBrightnessThemeData, value: 1, + brightness: Brightness.light, child: WebViewNavigationBar(), ), backgroundColor: const Color(0xFFF0E7EA), diff --git a/lib/widgets/app_bar.dart b/lib/widgets/app_bar.dart index 7ec3fd77bd..8a2ae9c7b5 100644 --- a/lib/widgets/app_bar.dart +++ b/lib/widgets/app_bar.dart @@ -50,6 +50,7 @@ class MixinAppBar extends StatelessWidget implements PreferredSizeWidget { elevation: 0, centerTitle: true, backgroundColor: backgroundColor ?? context.theme.primary, + scrolledUnderElevation: 0, leading: MoveWindowBarrier( child: Builder( builder: (context) => diff --git a/lib/widgets/auth.dart b/lib/widgets/auth.dart index 7387185474..cb25a1e39a 100644 --- a/lib/widgets/auth.dart +++ b/lib/widgets/auth.dart @@ -7,109 +7,129 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_svg/svg.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:pin_code_fields/pin_code_fields.dart'; -import 'package:rxdart/rxdart.dart'; -import '../account/security_key_value.dart'; import '../constants/resources.dart'; -import '../ui/provider/account_server_provider.dart'; +import '../ui/provider/account/multi_auth_provider.dart'; +import '../ui/provider/account/security_key_value_provider.dart'; import '../utils/app_lifecycle.dart'; import '../utils/authentication.dart'; -import '../utils/event_bus.dart'; import '../utils/extension/extension.dart'; -import '../utils/hook.dart'; +import '../utils/logger.dart'; import 'dialog.dart'; -const _lockDuration = Duration(minutes: 1); +final securityLockProvider = StateNotifierProvider( + LockStateNotifier.new, +); -enum LockEvent { lock, unlock } +class LockStateNotifier extends StateNotifier { + LockStateNotifier(this.ref) : super(false) { + _initialize(); + } -class AuthGuard extends HookConsumerWidget { - const AuthGuard({ - required this.child, - super.key, - }); + final Ref ref; - final Widget child; + Timer? _inactiveTimer; - @override - Widget build(BuildContext context, WidgetRef ref) { - final signed = - ref.watch(accountServerProvider.select((value) => value.hasValue)); + SecurityKeyValue get securityKeyValue => ref.read(securityKeyValueProvider); - if (signed) return _AuthGuard(child: child); + bool get signed => ref.read(authProvider) != null; - return child; + Future _initialize() async { + await securityKeyValue.initialize; + if (securityKeyValue.hasPasscode && signed) { + lock(); + } + appActiveListener.addListener(_onAppActiveChanged); + ref.listen(multiAuthStateNotifierProvider, (previous, next) { + if (next.auths.isEmpty) { + unlock(); + // remove passcode + securityKeyValue + ..passcode = null + ..lockDuration = null; + } + }); } -} - -class _AuthGuard extends HookConsumerWidget { - const _AuthGuard({required this.child}); - final Widget child; + void _onAppActiveChanged() { + if (state) { + // already locked + return; + } - @override - Widget build(BuildContext context, WidgetRef ref) { - final focusNode = useFocusNode(); - final textEditingController = useTextEditingController(); + void clearTimer() { + _inactiveTimer?.cancel(); + _inactiveTimer = null; + } - final hasPasscode = - useMemoizedStream(SecurityKeyValue.instance.watchHasPasscode).data ?? - SecurityKeyValue.instance.hasPasscode; + clearTimer(); - final enableBiometric = - useMemoizedStream(SecurityKeyValue.instance.watchBiometric).data ?? - SecurityKeyValue.instance.biometric; + final needLock = !isAppActive && securityKeyValue.hasPasscode && signed; + if (!needLock) { + return; + } + final lockDuration = securityKeyValue.lockDuration; + if (lockDuration.inMinutes > 0) { + d('schedule lock after ${lockDuration.inMinutes} minutes'); + _inactiveTimer = Timer(lockDuration, () { + if (securityKeyValue.hasPasscode && signed) { + lock(); + clearTimer(); + } + }); + } + } - final hasError = useState(false); - final lock = useState(SecurityKeyValue.instance.hasPasscode); + void lock() { + if (!signed) { + throw Exception('not signed'); + } + if (!securityKeyValue.hasPasscode) { + throw Exception('no passcode'); + } + state = true; + } - useEffect(() { - final listen = - EventBus.instance.on.whereType().listen((event) { - lock.value = event == LockEvent.lock; - }); + // for unlock by biometric + void unlock() { + state = false; + } - return listen.cancel; - }, []); + bool unlockWithPin(String input) { + assert(securityKeyValue.hasPasscode, 'no passcode'); + if (securityKeyValue.passcode == input) { + state = false; + return true; + } + return false; + } - useEffect(() { - Timer? timer; - void dispose() { - timer?.cancel(); - timer = null; - } + @override + void dispose() { + _inactiveTimer?.cancel(); + appActiveListener.removeListener(_onAppActiveChanged); + super.dispose(); + } +} - void listener() { - if (lock.value) return; +class AuthGuard extends HookConsumerWidget { + const AuthGuard({ + required this.child, + super.key, + }); - final needLock = !isAppActive; + final Widget child; - final lockDuration = - SecurityKeyValue.instance.lockDuration ?? _lockDuration; - if (needLock) { - if (lockDuration.inMinutes > 0) { - timer = Timer(lockDuration, () { - if (!hasPasscode) { - lock.value = false; - return; - } + @override + Widget build(BuildContext context, WidgetRef ref) { + final focusNode = useFocusNode(); + final textEditingController = useTextEditingController(); - lock.value = !isAppActive; - }); - } - } else { - dispose(); - lock.value = needLock; - } - } + final enableBiometric = + ref.watch(securityKeyValueProvider.select((value) => value.biometric)); - listener(); - appActiveListener.addListener(listener); - return () { - dispose(); - appActiveListener.removeListener(listener); - }; - }, [hasPasscode]); + final hasError = useState(false); + final lock = ref.watch(securityLockProvider); useEffect(() { focusNode.requestFocus(); @@ -131,12 +151,12 @@ class _AuthGuard extends HookConsumerWidget { FocusManager.instance.removeListener(listener); ServicesBinding.instance.keyboard.removeHandler(handler); }; - }, [lock.value]); + }, [lock]); return Stack( children: [ child, - if (lock.value) + if (lock) GestureDetector( onTap: focusNode.requestFocus, behavior: HitTestBehavior.translucent, @@ -201,9 +221,9 @@ class _AuthGuard extends HookConsumerWidget { showCursor: false, onCompleted: (value) { textEditingController.text = ''; - if (SecurityKeyValue.instance.passcode == value) { - lock.value = false; - } else { + if (!ref + .read(securityLockProvider.notifier) + .unlockWithPin(value)) { hasError.value = true; } }, @@ -236,7 +256,9 @@ class _AuthGuard extends HookConsumerWidget { padding: const EdgeInsets.all(24), onTap: () async { if (await authenticate()) { - lock.value = false; + ref + .read(securityLockProvider.notifier) + .unlock(); return; } }, diff --git a/lib/widgets/brightness_observer.dart b/lib/widgets/brightness_observer.dart index 64e122e033..6a09c31c7b 100644 --- a/lib/widgets/brightness_observer.dart +++ b/lib/widgets/brightness_observer.dart @@ -82,6 +82,7 @@ class BrightnessObserver extends HookConsumerWidget { value: value, brightnessThemeData: BrightnessThemeData.lerp(lightThemeData, darkThemeData, value), + brightness: currentBrightness, child: child!, ), child: child, @@ -94,16 +95,19 @@ class BrightnessData extends InheritedWidget { required this.value, required super.child, required this.brightnessThemeData, + required this.brightness, super.key, }); final double value; final BrightnessThemeData brightnessThemeData; + final Brightness brightness; @override bool updateShouldNotify(covariant BrightnessData oldWidget) => value != oldWidget.value || - brightnessThemeData != oldWidget.brightnessThemeData; + brightnessThemeData != oldWidget.brightnessThemeData || + brightness != oldWidget.brightness; static double of(BuildContext context) => context.dependOnInheritedWidgetOfExactType()!.value; @@ -112,6 +116,9 @@ class BrightnessData extends InheritedWidget { .dependOnInheritedWidgetOfExactType()! .brightnessThemeData; + static Brightness brightnessOf(BuildContext context) => + context.dependOnInheritedWidgetOfExactType()!.brightness; + static Color dynamicColor( BuildContext context, Color color, { diff --git a/lib/widgets/cache_image.dart b/lib/widgets/cache_image.dart index 797c463d14..8a364ca042 100644 --- a/lib/widgets/cache_image.dart +++ b/lib/widgets/cache_image.dart @@ -7,11 +7,12 @@ import 'package:crypto/crypto.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:http_client_helper/http_client_helper.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; -import '../utils/extension/extension.dart'; +import '../ui/provider/setting_provider.dart'; import '../utils/logger.dart'; import '../utils/proxy.dart'; @@ -19,7 +20,7 @@ typedef PlaceholderWidgetBuilder = Widget Function(); typedef LoadingErrorWidgetBuilder = Widget Function(); -class CacheImage extends StatelessWidget { +class CacheImage extends ConsumerWidget { const CacheImage( this.src, { this.width, @@ -41,8 +42,9 @@ class CacheImage extends StatelessWidget { final BoxFit fit; @override - Widget build(BuildContext context) { - final proxyUrl = context.database.settingProperties.activatedProxy; + Widget build(BuildContext context, WidgetRef ref) { + final proxyUrl = + ref.watch(settingProvider.select((value) => value.activatedProxy)); return Image( image: MixinNetworkImageProvider( src, diff --git a/lib/widgets/cell.dart b/lib/widgets/cell.dart index 9d4412aa0d..2dad6441ca 100644 --- a/lib/widgets/cell.dart +++ b/lib/widgets/cell.dart @@ -3,7 +3,7 @@ import 'package:flutter_svg/svg.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import '../constants/resources.dart'; -import '../ui/provider/responsive_navigator_provider.dart'; +import '../ui/provider/navigation/responsive_navigator_provider.dart'; import '../utils/extension/extension.dart'; import 'interactive_decorated_box.dart'; diff --git a/lib/widgets/markdown.dart b/lib/widgets/markdown.dart index 87ceffe915..a78129d986 100644 --- a/lib/widgets/markdown.dart +++ b/lib/widgets/markdown.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:html/dom.dart' as h; import 'package:html/dom_parsing.dart'; import 'package:html/parser.dart'; @@ -42,7 +43,7 @@ class MarkdownColumn extends StatelessWidget { } } -class Markdown extends StatelessWidget { +class Markdown extends ConsumerWidget { const Markdown({ required this.data, super.key, @@ -55,7 +56,7 @@ class Markdown extends StatelessWidget { final ScrollPhysics? physics; @override - Widget build(BuildContext context) => DefaultTextStyle.merge( + Widget build(BuildContext context, WidgetRef ref) => DefaultTextStyle.merge( style: TextStyle(color: context.theme.text), child: MarkdownWidget( data: data, diff --git a/lib/widgets/menu.dart b/lib/widgets/menu.dart index 6dab85ad29..bc6fc63586 100644 --- a/lib/widgets/menu.dart +++ b/lib/widgets/menu.dart @@ -132,6 +132,7 @@ class ContextMenuPortalEntry extends HookConsumerWidget { this.enable = true, this.onTap, this.interactive = true, + this.autofocus = false, }); final Widget child; @@ -143,6 +144,8 @@ class ContextMenuPortalEntry extends HookConsumerWidget { /// Whether right click or long press to show menu final bool interactive; + final bool autofocus; + @override Widget build(BuildContext context, WidgetRef ref) { final offsetCubit = useBloc(() => _OffsetCubit(null)); @@ -155,8 +158,13 @@ class ContextMenuPortalEntry extends HookConsumerWidget { converter: (state) => state != null, ); + final node = useFocusScopeNode(); + useEffect(() { showedMenu?.call(visible); + if (visible && autofocus && !node.hasFocus) { + node.requestFocus(); + } }, [visible]); useEffect(() { @@ -201,7 +209,8 @@ class ContextMenuPortalEntry extends HookConsumerWidget { } }, onTap: onTap, - child: Focus( + child: FocusScope( + node: node, onKeyEvent: (node, key) { final show = offset != null && visible; if (show && key.logicalKey == LogicalKeyboardKey.escape) { @@ -342,6 +351,46 @@ class _ContextMenuContainerLayout extends StatelessWidget { } } +class ContextMenuLayout extends StatelessWidget { + const ContextMenuLayout({ + required this.child, + super.key, + this.onTap, + this.onTapUp, + }); + + final VoidCallback? onTap; + final GestureTapUpCallback? onTapUp; + + final Widget child; + + @override + Widget build(BuildContext context) { + final backgroundColor = context.dynamicColor( + const Color.fromRGBO(255, 255, 255, 1), + darkColor: const Color.fromRGBO(62, 65, 72, 1), + ); + return ConstrainedBox( + constraints: const BoxConstraints(minWidth: 160), + child: InteractiveDecoratedBox.color( + decoration: BoxDecoration( + color: backgroundColor, + ), + tapDowningColor: Color.alphaBlend( + context.theme.listSelected, + backgroundColor, + ), + onTap: onTap, + onTapUp: onTapUp, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: child, + ), + ), + ); + } +} + class ContextMenu extends StatelessWidget { const ContextMenu({ required this.title, @@ -366,71 +415,54 @@ class ContextMenu extends StatelessWidget { @override Widget build(BuildContext context) { - final backgroundColor = context.dynamicColor( - const Color.fromRGBO(255, 255, 255, 1), - darkColor: const Color.fromRGBO(62, 65, 72, 1), - ); final color = isDestructiveAction ? context.theme.red : context.dynamicColor( const Color.fromRGBO(0, 0, 0, 1), darkColor: const Color.fromRGBO(255, 255, 255, 0.9), ); - return ConstrainedBox( - constraints: const BoxConstraints(minWidth: 160), - child: InteractiveDecoratedBox.color( - decoration: BoxDecoration( - color: backgroundColor, - ), - tapDowningColor: Color.alphaBlend( - context.theme.listSelected, - backgroundColor, - ), - onTap: () { - onTap?.call(); - if (!_subMenuMode) context.closeMenu(); - }, - onTapUp: (details) { - if (_subMenuMode && details.kind == PointerDeviceKind.touch) { - const SubMenuClickedByTouchNotification().dispatch(context); - } - }, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - child: Row( - children: [ - if (icon != null) - Padding( - padding: const EdgeInsets.only(right: 8), - child: SvgPicture.asset( - icon!, - colorFilter: ColorFilter.mode(color, BlendMode.srcIn), - width: 20, - height: 20, - ), - ), - Expanded( - child: Text( - title, - style: TextStyle( - fontSize: 14, - color: color, - ), - ), + return ContextMenuLayout( + onTap: () { + onTap?.call(); + if (!_subMenuMode) context.closeMenu(); + }, + onTapUp: (details) { + if (_subMenuMode && details.kind == PointerDeviceKind.touch) { + const SubMenuClickedByTouchNotification().dispatch(context); + } + }, + child: Row( + children: [ + if (icon != null) + Padding( + padding: const EdgeInsets.only(right: 8), + child: SvgPicture.asset( + icon!, + colorFilter: ColorFilter.mode(color, BlendMode.srcIn), + width: 20, + height: 20, ), - if (_subMenuMode) - SvgPicture.asset( - Resources.assetsImagesIcArrowRightSvg, - width: 20, - height: 20, - colorFilter: ColorFilter.mode( - context.theme.secondaryText, - BlendMode.srcIn, - ), - ), - ], + ), + Expanded( + child: Text( + title, + style: TextStyle( + fontSize: 14, + color: color, + ), + ), ), - ), + if (_subMenuMode) + SvgPicture.asset( + Resources.assetsImagesIcArrowRightSvg, + width: 20, + height: 20, + colorFilter: ColorFilter.mode( + context.theme.secondaryText, + BlendMode.srcIn, + ), + ), + ], ), ); } diff --git a/lib/widgets/message/item/action/action_message.dart b/lib/widgets/message/item/action/action_message.dart index 6fe5547962..1ee35560a9 100644 --- a/lib/widgets/message/item/action/action_message.dart +++ b/lib/widgets/message/item/action/action_message.dart @@ -70,7 +70,8 @@ class ActionMessage extends HookConsumerWidget { // ignore: avoid_dynamic_calls e.label, style: TextStyle( - fontSize: context.messageStyle.primaryFontSize, + fontSize: + ref.watch(messageStyleProvider).primaryFontSize, // ignore: avoid_dynamic_calls color: colorHex(e.color) ?? Colors.black, height: 1, diff --git a/lib/widgets/message/item/action_card/action_message.dart b/lib/widgets/message/item/action_card/action_message.dart index b65c5851ae..caea5a610e 100644 --- a/lib/widgets/message/item/action_card/action_message.dart +++ b/lib/widgets/message/item/action_card/action_message.dart @@ -93,7 +93,7 @@ class AppCardItem extends HookConsumerWidget { data.title, style: TextStyle( color: context.theme.text, - fontSize: context.messageStyle.secondaryFontSize, + fontSize: ref.watch(messageStyleProvider).secondaryFontSize, ), maxLines: 1, overflow: TextOverflow.ellipsis, @@ -103,7 +103,7 @@ class AppCardItem extends HookConsumerWidget { maxLines: 1, style: TextStyle( color: context.theme.secondaryText, - fontSize: context.messageStyle.tertiaryFontSize, + fontSize: ref.watch(messageStyleProvider).tertiaryFontSize, ), ), ], diff --git a/lib/widgets/message/item/audio_message.dart b/lib/widgets/message/item/audio_message.dart index 224e0ecb27..2943229912 100644 --- a/lib/widgets/message/item/audio_message.dart +++ b/lib/widgets/message/item/audio_message.dart @@ -126,7 +126,8 @@ class AudioMessage extends HookConsumerWidget { Text( duration.asMinutesSeconds, style: TextStyle( - fontSize: context.messageStyle.tertiaryFontSize, + fontSize: + ref.watch(messageStyleProvider).tertiaryFontSize, color: context.theme.secondaryText, ), ), diff --git a/lib/widgets/message/item/contact_message_widget.dart b/lib/widgets/message/item/contact_message_widget.dart index 9f7f05af49..00dab86713 100644 --- a/lib/widgets/message/item/contact_message_widget.dart +++ b/lib/widgets/message/item/contact_message_widget.dart @@ -49,7 +49,7 @@ class ContactMessageWidget extends HookConsumerWidget { } } -class ContactItem extends StatelessWidget { +class ContactItem extends ConsumerWidget { const ContactItem({ required this.avatarUrl, required this.userId, @@ -68,7 +68,7 @@ class ContactItem extends StatelessWidget { final String identityNumber; @override - Widget build(BuildContext context) => Row( + Widget build(BuildContext context, WidgetRef ref) => Row( mainAxisSize: MainAxisSize.min, children: [ AvatarWidget( @@ -91,7 +91,8 @@ class ContactItem extends StatelessWidget { fullName?.overflow ?? '', style: TextStyle( color: context.theme.text, - fontSize: context.messageStyle.primaryFontSize, + fontSize: + ref.watch(messageStyleProvider).primaryFontSize, ), maxLines: 1, overflow: TextOverflow.ellipsis, @@ -107,7 +108,7 @@ class ContactItem extends StatelessWidget { identityNumber, style: TextStyle( color: context.theme.secondaryText, - fontSize: context.messageStyle.secondaryFontSize, + fontSize: ref.watch(messageStyleProvider).secondaryFontSize, ), ), ], diff --git a/lib/widgets/message/item/file_message.dart b/lib/widgets/message/item/file_message.dart index 7e57dfaa09..38dddd5a4e 100644 --- a/lib/widgets/message/item/file_message.dart +++ b/lib/widgets/message/item/file_message.dart @@ -144,7 +144,7 @@ class MessageFile extends HookConsumerWidget { Text( mediaName.overflow, style: TextStyle( - fontSize: context.messageStyle.secondaryFontSize, + fontSize: ref.watch(messageStyleProvider).secondaryFontSize, color: context.theme.text, ), overflow: TextOverflow.ellipsis, @@ -153,7 +153,7 @@ class MessageFile extends HookConsumerWidget { Text( mediaSizeText, style: TextStyle( - fontSize: context.messageStyle.tertiaryFontSize, + fontSize: ref.watch(messageStyleProvider).tertiaryFontSize, color: context.theme.secondaryText, ), maxLines: 1, diff --git a/lib/widgets/message/item/pin_message.dart b/lib/widgets/message/item/pin_message.dart index 0ec9b6d6f3..c39bcfb5c6 100644 --- a/lib/widgets/message/item/pin_message.dart +++ b/lib/widgets/message/item/pin_message.dart @@ -93,7 +93,7 @@ class PinMessageWidget extends HookConsumerWidget { child: CustomText( text, style: TextStyle( - fontSize: context.messageStyle.secondaryFontSize, + fontSize: ref.watch(messageStyleProvider).secondaryFontSize, color: context.dynamicColor( const Color.fromRGBO(0, 0, 0, 1), ), diff --git a/lib/widgets/message/item/post_message.dart b/lib/widgets/message/item/post_message.dart index 921c13cc09..488dd9bd1b 100644 --- a/lib/widgets/message/item/post_message.dart +++ b/lib/widgets/message/item/post_message.dart @@ -32,7 +32,8 @@ class PostMessage extends HookConsumerWidget { child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 400), child: DefaultTextStyle.merge( - style: TextStyle(fontSize: context.messageStyle.primaryFontSize), + style: TextStyle( + fontSize: ref.watch(messageStyleProvider).primaryFontSize), child: MessagePost(showStatus: true, content: content), ), ), diff --git a/lib/widgets/message/item/quote_message.dart b/lib/widgets/message/item/quote_message.dart index b2dbdef36e..4186587c25 100644 --- a/lib/widgets/message/item/quote_message.dart +++ b/lib/widgets/message/item/quote_message.dart @@ -388,7 +388,7 @@ class _QuoteImage extends HookWidget { } } -class _QuoteMessageBase extends StatelessWidget { +class _QuoteMessageBase extends ConsumerWidget { const _QuoteMessageBase({ required this.messageId, required this.quoteMessageId, @@ -412,7 +412,7 @@ class _QuoteMessageBase extends StatelessWidget { final VoidCallback? onTap; @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { final iterator = LineSplitter.split(description).iterator; final _description = '${iterator.moveNext() ? iterator.current : ''}${iterator.moveNext() ? '...' : ''}'; @@ -481,8 +481,9 @@ class _QuoteMessageBase extends StatelessWidget { child: CustomText( name!, style: TextStyle( - fontSize: - context.messageStyle.secondaryFontSize, + fontSize: ref + .watch(messageStyleProvider) + .secondaryFontSize, color: color, height: 1, ), @@ -501,8 +502,9 @@ class _QuoteMessageBase extends StatelessWidget { child: CustomText( _description, style: TextStyle( - fontSize: - context.messageStyle.tertiaryFontSize, + fontSize: ref + .watch(messageStyleProvider) + .tertiaryFontSize, color: context.theme.secondaryText, ), maxLines: 1, diff --git a/lib/widgets/message/item/recall_message.dart b/lib/widgets/message/item/recall_message.dart index 4bbf57d846..91632f01d2 100644 --- a/lib/widgets/message/item/recall_message.dart +++ b/lib/widgets/message/item/recall_message.dart @@ -52,7 +52,7 @@ class RecallMessage extends HookConsumerWidget { ), ]), style: TextStyle( - fontSize: context.messageStyle.primaryFontSize, + fontSize: ref.watch(messageStyleProvider).primaryFontSize, color: context.theme.text, ), ), diff --git a/lib/widgets/message/item/secret_message.dart b/lib/widgets/message/item/secret_message.dart index 4183d4ea9f..ea565e1bb0 100644 --- a/lib/widgets/message/item/secret_message.dart +++ b/lib/widgets/message/item/secret_message.dart @@ -1,15 +1,15 @@ import 'package:flutter/widgets.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import '../../../utils/extension/extension.dart'; import '../../../utils/uri_utils.dart'; - import '../message_style.dart'; -class SecretMessage extends StatelessWidget { +class SecretMessage extends ConsumerWidget { const SecretMessage({super.key}); @override - Widget build(BuildContext context) => Center( + Widget build(BuildContext context, WidgetRef ref) => Center( child: Padding( padding: const EdgeInsets.only(left: 8, right: 8, bottom: 4), child: MouseRegion( @@ -26,7 +26,8 @@ class SecretMessage extends StatelessWidget { child: Text( context.l10n.messageE2ee, style: TextStyle( - fontSize: context.messageStyle.secondaryFontSize, + fontSize: + ref.watch(messageStyleProvider).secondaryFontSize, color: context.dynamicColor( const Color.fromRGBO(0, 0, 0, 1), ), diff --git a/lib/widgets/message/item/stranger_message.dart b/lib/widgets/message/item/stranger_message.dart index ccd959e6f2..4e7e5a15af 100644 --- a/lib/widgets/message/item/stranger_message.dart +++ b/lib/widgets/message/item/stranger_message.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import '../../../enum/encrypt_category.dart'; import '../../../utils/extension/extension.dart'; @@ -9,11 +10,11 @@ import '../../toast.dart'; import '../message.dart'; import '../message_style.dart'; -class StrangerMessage extends StatelessWidget { +class StrangerMessage extends ConsumerWidget { const StrangerMessage({super.key}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { final isBotConversation = useMessageConverter(converter: (state) => state.appId != null); @@ -24,7 +25,7 @@ class StrangerMessage extends StatelessWidget { ? context.l10n.chatBotReceptionTitle : context.l10n.strangerHint, style: TextStyle( - fontSize: context.messageStyle.primaryFontSize, + fontSize: ref.watch(messageStyleProvider).primaryFontSize, color: context.theme.text, ), ), @@ -73,7 +74,7 @@ class StrangerMessage extends StatelessWidget { } } -class _StrangerButton extends StatelessWidget { +class _StrangerButton extends ConsumerWidget { const _StrangerButton( this.text, { this.onTap, @@ -83,7 +84,8 @@ class _StrangerButton extends StatelessWidget { final VoidCallback? onTap; @override - Widget build(BuildContext context) => InteractiveDecoratedBox.color( + Widget build(BuildContext context, WidgetRef ref) => + InteractiveDecoratedBox.color( onTap: onTap, decoration: BoxDecoration( color: context.theme.primary, @@ -104,7 +106,7 @@ class _StrangerButton extends StatelessWidget { child: Text( text, style: TextStyle( - fontSize: context.messageStyle.primaryFontSize, + fontSize: ref.watch(messageStyleProvider).primaryFontSize, color: context.theme.accent, ), ), diff --git a/lib/widgets/message/item/system_message.dart b/lib/widgets/message/item/system_message.dart index 3e34702175..f6cda58049 100644 --- a/lib/widgets/message/item/system_message.dart +++ b/lib/widgets/message/item/system_message.dart @@ -55,7 +55,7 @@ class SystemMessage extends HookConsumerWidget { expireIn: int.tryParse(content ?? '0'), ), style: TextStyle( - fontSize: context.messageStyle.secondaryFontSize, + fontSize: ref.watch(messageStyleProvider).secondaryFontSize, color: context.dynamicColor( const Color.fromRGBO(0, 0, 0, 1), ), diff --git a/lib/widgets/message/item/text/text_message.dart b/lib/widgets/message/item/text/text_message.dart index 3da7962f2b..3a1cc7f93b 100644 --- a/lib/widgets/message/item/text/text_message.dart +++ b/lib/widgets/message/item/text/text_message.dart @@ -86,7 +86,7 @@ class TextMessage extends HookConsumerWidget { content: CustomText( content, style: TextStyle( - fontSize: context.messageStyle.primaryFontSize, + fontSize: ref.watch(messageStyleProvider).primaryFontSize, color: context.theme.text, ), textMatchers: [ diff --git a/lib/widgets/message/item/transcript_message.dart b/lib/widgets/message/item/transcript_message.dart index e2ae302971..169200526a 100644 --- a/lib/widgets/message/item/transcript_message.dart +++ b/lib/widgets/message/item/transcript_message.dart @@ -131,7 +131,9 @@ class TranscriptMessageWidget extends HookConsumerWidget { context.l10n.transcript, style: TextStyle( color: context.theme.text, - fontSize: context.messageStyle.primaryFontSize, + fontSize: ref + .watch(messageStyleProvider) + .primaryFontSize, ), ), const Spacer(), @@ -168,8 +170,9 @@ class TranscriptMessageWidget extends HookConsumerWidget { text, style: TextStyle( color: context.theme.secondaryText, - fontSize: - context.messageStyle.tertiaryFontSize, + fontSize: ref + .watch(messageStyleProvider) + .tertiaryFontSize, ), maxLines: 1, ), diff --git a/lib/widgets/message/item/transfer/safe_transfer_dialog.dart b/lib/widgets/message/item/transfer/safe_transfer_dialog.dart index 3f45bed212..b998253923 100644 --- a/lib/widgets/message/item/transfer/safe_transfer_dialog.dart +++ b/lib/widgets/message/item/transfer/safe_transfer_dialog.dart @@ -7,8 +7,8 @@ import 'package:mixin_bot_sdk_dart/mixin_bot_sdk_dart.dart' import '../../../../db/database_event_bus.dart'; import '../../../../db/mixin_database.dart' hide Offset; -import '../../../../ui/provider/account_server_provider.dart'; -import '../../../../ui/provider/multi_auth_provider.dart'; +import '../../../../ui/provider/account/account_server_provider.dart'; +import '../../../../ui/provider/account/multi_auth_provider.dart'; import '../../../../ui/provider/transfer_provider.dart'; import '../../../../utils/extension/extension.dart'; import '../../../buttons.dart'; diff --git a/lib/widgets/message/item/transfer/transfer_message.dart b/lib/widgets/message/item/transfer/transfer_message.dart index e401fa97ee..e0a584ca15 100644 --- a/lib/widgets/message/item/transfer/transfer_message.dart +++ b/lib/widgets/message/item/transfer/transfer_message.dart @@ -95,7 +95,9 @@ class TransferMessage extends HookConsumerWidget { snapshotAmount!.numberFormat(), style: TextStyle( color: context.theme.text, - fontSize: context.messageStyle.secondaryFontSize, + fontSize: ref + .watch(messageStyleProvider) + .secondaryFontSize, ), ); }), @@ -106,7 +108,7 @@ class TransferMessage extends HookConsumerWidget { assetSymbol, style: TextStyle( color: context.theme.secondaryText, - fontSize: context.messageStyle.tertiaryFontSize, + fontSize: ref.watch(messageStyleProvider).tertiaryFontSize, ), ), ], diff --git a/lib/widgets/message/item/unknown_message.dart b/lib/widgets/message/item/unknown_message.dart index 17084e95bd..c48594ca09 100644 --- a/lib/widgets/message/item/unknown_message.dart +++ b/lib/widgets/message/item/unknown_message.dart @@ -1,6 +1,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import '../../../utils/extension/extension.dart'; import '../../../utils/uri_utils.dart'; @@ -9,16 +10,16 @@ import '../message_datetime_and_status.dart'; import '../message_layout.dart'; import '../message_style.dart'; -class UnknownMessage extends StatelessWidget { +class UnknownMessage extends ConsumerWidget { const UnknownMessage({super.key}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { final content = RichText( text: TextSpan( text: context.l10n.messageNotSupport, style: TextStyle( - fontSize: context.messageStyle.primaryFontSize, + fontSize: ref.watch(messageStyleProvider).primaryFontSize, color: context.theme.text, ), children: [ @@ -27,7 +28,7 @@ class UnknownMessage extends StatelessWidget { mouseCursor: SystemMouseCursors.click, text: context.l10n.learnMore, style: TextStyle( - fontSize: context.messageStyle.primaryFontSize, + fontSize: ref.watch(messageStyleProvider).primaryFontSize, color: context.theme.accent, ), recognizer: TapGestureRecognizer() diff --git a/lib/widgets/message/item/video_message.dart b/lib/widgets/message/item/video_message.dart index 6314293e71..21a9aa89bc 100644 --- a/lib/widgets/message/item/video_message.dart +++ b/lib/widgets/message/item/video_message.dart @@ -210,7 +210,8 @@ class _VideoMessageOverlayInfo extends HookConsumerWidget { child: Text( durationText, style: TextStyle( - fontSize: context.messageStyle.tertiaryFontSize, + fontSize: + ref.watch(messageStyleProvider).tertiaryFontSize, color: Colors.white, ), ), diff --git a/lib/widgets/message/item/waiting_message.dart b/lib/widgets/message/item/waiting_message.dart index 6f146e0d72..5a78914c3c 100644 --- a/lib/widgets/message/item/waiting_message.dart +++ b/lib/widgets/message/item/waiting_message.dart @@ -30,7 +30,7 @@ class WaitingMessage extends HookConsumerWidget { : userFullName!, ), style: TextStyle( - fontSize: context.messageStyle.primaryFontSize, + fontSize: ref.watch(messageStyleProvider).primaryFontSize, color: context.theme.text, ), children: [ @@ -38,7 +38,7 @@ class WaitingMessage extends HookConsumerWidget { mouseCursor: SystemMouseCursors.click, text: context.l10n.learnMore, style: TextStyle( - fontSize: context.messageStyle.primaryFontSize, + fontSize: ref.watch(messageStyleProvider).primaryFontSize, color: context.theme.accent, ), recognizer: TapGestureRecognizer() diff --git a/lib/widgets/message/message.dart b/lib/widgets/message/message.dart index 75e268e7ce..bc774e7871 100644 --- a/lib/widgets/message/message.dart +++ b/lib/widgets/message/message.dart @@ -860,11 +860,11 @@ class _MessageBubbleMargin extends HookConsumerWidget { } } -class _UnreadMessageBar extends StatelessWidget { +class _UnreadMessageBar extends ConsumerWidget { const _UnreadMessageBar(); @override - Widget build(BuildContext context) => Container( + Widget build(BuildContext context, WidgetRef ref) => Container( color: context.theme.background, padding: const EdgeInsets.symmetric(vertical: 4), margin: const EdgeInsets.symmetric(vertical: 6), @@ -873,7 +873,7 @@ class _UnreadMessageBar extends StatelessWidget { context.l10n.unreadMessages, style: TextStyle( color: context.theme.secondaryText, - fontSize: context.messageStyle.secondaryFontSize, + fontSize: ref.watch(messageStyleProvider).secondaryFontSize, ), ), ); diff --git a/lib/widgets/message/message_datetime_and_status.dart b/lib/widgets/message/message_datetime_and_status.dart index b27b506bad..d81f1a2dbc 100644 --- a/lib/widgets/message/message_datetime_and_status.dart +++ b/lib/widgets/message/message_datetime_and_status.dart @@ -144,7 +144,7 @@ class _MessageDatetime extends HookConsumerWidget { return Text( text, style: TextStyle( - fontSize: context.messageStyle.statusFontSize, + fontSize: ref.watch(messageStyleProvider).statusFontSize, color: color ?? context.dynamicColor( const Color.fromRGBO(131, 145, 158, 1), diff --git a/lib/widgets/message/message_day_time.dart b/lib/widgets/message/message_day_time.dart index 1ce5dcc414..faf9fce7b6 100644 --- a/lib/widgets/message/message_day_time.dart +++ b/lib/widgets/message/message_day_time.dart @@ -61,7 +61,7 @@ class _MessageDayTimeWidget extends HookConsumerWidget { dateTimeString, textAlign: TextAlign.center, style: TextStyle( - fontSize: context.messageStyle.secondaryFontSize, + fontSize: ref.watch(messageStyleProvider).secondaryFontSize, color: Colors.black, ), ), diff --git a/lib/widgets/message/message_name.dart b/lib/widgets/message/message_name.dart index 53de108129..77817876b7 100644 --- a/lib/widgets/message/message_name.dart +++ b/lib/widgets/message/message_name.dart @@ -29,7 +29,7 @@ class MessageName extends ConsumerWidget { Widget widget = CustomText( userName, style: TextStyle( - fontSize: context.messageStyle.secondaryFontSize, + fontSize: ref.watch(messageStyleProvider).secondaryFontSize, color: getNameColorById(userId), ), ); @@ -45,7 +45,7 @@ class MessageName extends ConsumerWidget { child: Text( '@$userIdentityNumber', style: TextStyle( - fontSize: context.messageStyle.statusFontSize, + fontSize: ref.watch(messageStyleProvider).statusFontSize, color: context.theme.text.withOpacity(0.5), ), ), diff --git a/lib/widgets/message/message_style.dart b/lib/widgets/message/message_style.dart index fba0d0db08..19340e0595 100644 --- a/lib/widgets/message/message_style.dart +++ b/lib/widgets/message/message_style.dart @@ -1,11 +1,12 @@ -import 'package:flutter/cupertino.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; -import '../../utils/extension/extension.dart'; +import '../../ui/provider/setting_provider.dart'; -extension MessageStyleExt on BuildContext { - MessageStyle get messageStyle => - MessageStyle.defaultStyle + settingChangeNotifier.chatFontSizeDelta; -} +final messageStyleProvider = Provider((ref) { + final chatFontSizeDelta = + ref.watch(settingProvider.select((value) => value.chatFontSizeDelta)); + return MessageStyle.defaultStyle + chatFontSizeDelta; +}); class MessageStyle { const MessageStyle({ diff --git a/lib/widgets/sticker_page/emoji_page.dart b/lib/widgets/sticker_page/emoji_page.dart index 42a7516f9d..b65b7c4455 100644 --- a/lib/widgets/sticker_page/emoji_page.dart +++ b/lib/widgets/sticker_page/emoji_page.dart @@ -6,8 +6,8 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_svg/svg.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import '../../account/account_key_value.dart'; import '../../constants/resources.dart'; +import '../../ui/provider/hive_key_value_provider.dart'; import '../../utils/emoji.dart'; import '../../utils/extension/extension.dart'; import '../interactive_decorated_box.dart'; @@ -69,8 +69,12 @@ class _EmojiPageBody extends HookConsumerWidget { } }, [layoutWidth]); - final recentUsedEmoji = - useMemoized(() => AccountKeyValue.instance.recentUsedEmoji); + final accountKeyValue = + ref.watch(currentAccountKeyValueProvider).valueOrNull; + final recentUsedEmoji = useMemoized( + () => accountKeyValue?.recentUsedEmoji ?? [], + [accountKeyValue], + ); final groupedEmojis = useMemoized( () => [ @@ -321,16 +325,16 @@ class _EmojiGroupTitle extends StatelessWidget { ); } -class _EmojiItem extends StatelessWidget { +class _EmojiItem extends ConsumerWidget { const _EmojiItem({required this.emoji}); final String emoji; @override - Widget build(BuildContext context) => Padding( + Widget build(BuildContext context, WidgetRef ref) => Padding( padding: const EdgeInsets.all(2), child: InteractiveDecoratedBox( - onTap: () { + onTap: () async { final textController = context.read(); final textEditingValue = textController.value; final selection = textEditingValue.selection; @@ -345,7 +349,9 @@ class _EmojiItem extends StatelessWidget { textController.value = collapsedTextEditingValue.replaced(selection, emoji); } - AccountKeyValue.instance.onEmojiUsed(emoji); + final accountKeyValue = + await ref.read(currentAccountKeyValueProvider.future); + accountKeyValue?.onEmojiUsed(emoji); }, hoveringDecoration: BoxDecoration( color: context.dynamicColor( diff --git a/lib/widgets/sticker_page/sticker_page.dart b/lib/widgets/sticker_page/sticker_page.dart index 59ee6dedbc..9cbf29f975 100644 --- a/lib/widgets/sticker_page/sticker_page.dart +++ b/lib/widgets/sticker_page/sticker_page.dart @@ -4,12 +4,12 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_svg/svg.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import '../../account/account_key_value.dart'; import '../../bloc/bloc_converter.dart'; import '../../constants/resources.dart'; import '../../db/database_event_bus.dart'; import '../../db/mixin_database.dart'; import '../../ui/provider/conversation_provider.dart'; +import '../../ui/provider/hive_key_value_provider.dart'; import '../../utils/extension/extension.dart'; import '../../utils/hook.dart'; import '../automatic_keep_alive_client_widget.dart'; @@ -299,7 +299,9 @@ class _StickerAlbumBar extends HookConsumerWidget { if (tabController.index != 0) return; HoverOverlay.forceHidden(context); - AccountKeyValue.instance.hasNewAlbum = false; + final accountKeyValue = + await ref.read(currentAccountKeyValueProvider.future); + accountKeyValue?.hasNewAlbum = false; setPreviousIndex(); if (!(await showStickerStorePageDialog(context))) return; @@ -361,13 +363,16 @@ class _StickerAlbumBarItem extends StatelessWidget { child: _StickerGroupIconHoverContainer( child: Center( child: Center( - child: Builder( - builder: (context) { + child: Consumer( + builder: (context, ref, child) { final presetStickerAlbum = { - PresetStickerGroup.store: - AccountKeyValue.instance.hasNewAlbum - ? Resources.assetsImagesStickerStoreRedDotSvg - : Resources.assetsImagesStickerStoreSvg, + PresetStickerGroup.store: ref + .watch(currentAccountKeyValueProvider) + .valueOrNull + ?.hasNewAlbum ?? + false + ? Resources.assetsImagesStickerStoreRedDotSvg + : Resources.assetsImagesStickerStoreSvg, PresetStickerGroup.emoji: Resources.assetsImagesEmojiStickerSvg, PresetStickerGroup.recent: diff --git a/lib/widgets/user/change_number_dialog.dart b/lib/widgets/user/change_number_dialog.dart index 16a9daad25..bee8a822f6 100644 --- a/lib/widgets/user/change_number_dialog.dart +++ b/lib/widgets/user/change_number_dialog.dart @@ -60,7 +60,8 @@ Future showChangeNumberDialog(BuildContext context) async { platformVersion: platformVersion, appVersion: packageInfo.version, packageName: 'one.mixin.messenger', - pin: encryptPin(pinCode), + pin: context.accountServer.hiveKeyValues.sessionKeyValue + .encryptPin(pinCode), code: code, ), ); diff --git a/lib/widgets/user/pin_verification_dialog.dart b/lib/widgets/user/pin_verification_dialog.dart index 20688fff84..5ac9e1cdee 100644 --- a/lib/widgets/user/pin_verification_dialog.dart +++ b/lib/widgets/user/pin_verification_dialog.dart @@ -46,8 +46,10 @@ class _PinVerificationDialog extends StatelessWidget { const SizedBox(height: 20), PinInputLayout( doVerify: (String pin) async { + final sessionKeyValue = + context.hiveKeyValues.sessionKeyValue; await context.accountServer.client.accountApi - .verifyPin(encryptPin(pin)!); + .verifyPin(sessionKeyValue.encryptPin(pin)!); Navigator.pop(context, pin); }, ), diff --git a/lib/widgets/window/menus.dart b/lib/widgets/window/menus.dart index 3475f69a06..37e956c69d 100644 --- a/lib/widgets/window/menus.dart +++ b/lib/widgets/window/menus.dart @@ -7,59 +7,18 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:window_manager/window_manager.dart'; -import '../../account/security_key_value.dart'; import '../../ui/home/conversation/conversation_hotkey.dart'; -import '../../ui/provider/account_server_provider.dart'; +import '../../ui/provider/account/account_server_provider.dart'; +import '../../ui/provider/account/security_key_value_provider.dart'; +import '../../ui/provider/menu_handle_provider.dart'; import '../../ui/provider/slide_category_provider.dart'; import '../../utils/device_transfer/device_transfer_dialog.dart'; -import '../../utils/event_bus.dart'; import '../../utils/extension/extension.dart'; import '../../utils/hook.dart'; -import '../../utils/rivepod.dart'; import '../../utils/uri_utils.dart'; import '../actions/actions.dart'; import '../auth.dart'; -abstract class ConversationMenuHandle { - Stream get isMuted; - - Stream get isPinned; - - void mute(); - - void unmute(); - - void showSearch(); - - void pin(); - - void unPin(); - - void toggleSideBar(); - - void delete(); -} - -class MacMenuBarStateNotifier - extends DistinctStateNotifier { - MacMenuBarStateNotifier(super.state); - - void attach(ConversationMenuHandle handle) { - if (!Platform.isMacOS) return; - Future(() => state = handle); - } - - void unAttach(ConversationMenuHandle handle) { - if (!Platform.isMacOS) return; - if (state != handle) return; - state = null; - } -} - -final macMenuBarProvider = - StateNotifierProvider( - (ref) => MacMenuBarStateNotifier(null)); - class MacosMenuBar extends HookConsumerWidget { const MacosMenuBar({ required this.child, @@ -101,11 +60,8 @@ class _Menus extends HookConsumerWidget { ).data ?? false; - final hasPasscode = useMemoizedStream(signed - ? SecurityKeyValue.instance.watchHasPasscode - : () => Stream.value(false)) - .data ?? - false; + final hasPasscode = ref + .watch(securityKeyValueProvider.select((value) => value.hasPasscode)); PlatformMenu buildConversationMenu() => PlatformMenu( label: context.l10n.conversation, @@ -182,8 +138,8 @@ class _Menus extends HookConsumerWidget { meta: true, shift: true, ), - onSelected: hasPasscode - ? () => EventBus.instance.fire(LockEvent.lock) + onSelected: hasPasscode && signed + ? () => ref.read(securityLockProvider.notifier).lock() : null, ), ]), diff --git a/lib/workers/decrypt_message.dart b/lib/workers/decrypt_message.dart index d0f0390573..c76d24b8d6 100644 --- a/lib/workers/decrypt_message.dart +++ b/lib/workers/decrypt_message.dart @@ -68,6 +68,7 @@ class DecryptMessage extends Injector { this._updateAssetJob, this._deviceTransfer, this._updateTokenJob, + this._signalDatabase, ) : super(userId, database, client) { _encryptedProtocol = EncryptedProtocol(); } @@ -89,6 +90,7 @@ class DecryptMessage extends Injector { final UpdateAssetJob _updateAssetJob; final UpdateTokenJob _updateTokenJob; final DeviceTransferIsolateController? _deviceTransfer; + final SignalDatabase _signalDatabase; final refreshKeyMap = {}; @@ -228,7 +230,7 @@ class DecryptMessage extends Injector { } final address = SignalProtocolAddress(data.senderId, deviceId); - final status = (await SignalDatabase.get.ratchetSenderKeyDao + final status = (await _signalDatabase.ratchetSenderKeyDao .getRatchetSenderKey(data.conversationId, address.toString())) ?.status; if (status == RatchetStatus.requesting.name) { @@ -240,13 +242,13 @@ class DecryptMessage extends Injector { i('decrypt failed ${data.messageId}, $e'); await _refreshSignalKeys(data.conversationId); if (data.category == MessageCategory.signalKey) { - await SignalDatabase.get.ratchetSenderKeyDao.deleteByGroupIdAndSenderId( + await _signalDatabase.ratchetSenderKeyDao.deleteByGroupIdAndSenderId( data.conversationId, SignalProtocolAddress(data.senderId, deviceId).toString()); } else { await _insertFailedMessage(data); final address = SignalProtocolAddress(data.senderId, deviceId); - final status = (await SignalDatabase.get.ratchetSenderKeyDao + final status = (await _signalDatabase.ratchetSenderKeyDao .getRatchetSenderKey(data.conversationId, address.toString())) ?.status; if (status == null) { @@ -1238,7 +1240,7 @@ class DecryptMessage extends Injector { senderId: address.toString(), status: RatchetStatus.requesting.name, createdAt: DateTime.now().millisecondsSinceEpoch.toString()); - await SignalDatabase.get.ratchetSenderKeyDao.insertSenderKey(ratchet); + await _signalDatabase.ratchetSenderKeyDao.insertSenderKey(ratchet); } } @@ -1256,7 +1258,7 @@ class DecryptMessage extends Injector { conversationId, userId, encoded, sessionId: sessionId)); unawaited(_sender.deliver(bm)); - await SignalDatabase.get.ratchetSenderKeyDao.deleteByGroupIdAndSenderId( + await _signalDatabase.ratchetSenderKeyDao.deleteByGroupIdAndSenderId( conversationId, SignalProtocolAddress(userId, sessionId.getDeviceId()).toString()); } @@ -1277,8 +1279,8 @@ class DecryptMessage extends Injector { if (count.preKeyCount >= preKeyMinNum) { return; } - final bm = - createSyncSignalKeys(createSyncSignalKeysParam(await generateKeys())); + final bm = createSyncSignalKeys(createSyncSignalKeysParam( + await generateKeys(_signalDatabase, database.cryptoKeyValue))); final result = await _sender.signalKeysChannel(bm); if (result == null) { i('Registering new pre keys...'); diff --git a/lib/workers/message_worker_isolate.dart b/lib/workers/message_worker_isolate.dart index b294a09f23..22b5141a84 100644 --- a/lib/workers/message_worker_isolate.dart +++ b/lib/workers/message_worker_isolate.dart @@ -14,7 +14,9 @@ import 'package:rxdart/rxdart.dart'; import 'package:stream_channel/isolate_channel.dart'; import '../blaze/blaze.dart'; +import '../crypto/signal/signal_database.dart'; import '../crypto/signal/signal_protocol.dart'; +import '../db/app/app_database.dart'; import '../db/database.dart'; import '../db/database_event_bus.dart'; import '../db/fts_database.dart'; @@ -127,6 +129,7 @@ class _MessageProcessRunner { late Blaze blaze; late Sender _sender; late SignalProtocol signalProtocol; + late AppDatabase appDatabase; late SendingJob _sendingJob; late AckJob _ackJob; @@ -142,11 +145,18 @@ class _MessageProcessRunner { Timer? _nextExpiredMessageRunner; Future init(IsolateInitParams initParams) async { + appDatabase = AppDatabase.connect(); + database = Database( await connectToDatabase(identityNumber, readCount: 4), await FtsDatabase.connect(identityNumber), ); + final signalDb = await SignalDatabase.connect( + identityNumber: identityNumber, + fromMainIsolate: false, + ); + client = createClient( userId: userId, sessionId: sessionId, @@ -164,7 +174,7 @@ class _MessageProcessRunner { ), ], loginByPhoneNumber: initParams.loginByPhoneNumber, - )..configProxySetting(database.settingProperties); + )..configProxySetting(appDatabase.settingKeyValue); _ackJob = AckJob( database: database, @@ -185,6 +195,7 @@ class _MessageProcessRunner { await generateUserAgent(), _ackJob, _floodJob, + appDatabase.settingKeyValue, ); blaze.connectedStateStream.listen((event) { @@ -192,7 +203,7 @@ class _MessageProcessRunner { WorkerIsolateEventType.onBlazeConnectStateChanged, event); }); - signalProtocol = SignalProtocol(userId)..init(); + signalProtocol = SignalProtocol(userId, signalDb)..init(); _sender = Sender( signalProtocol, @@ -264,6 +275,7 @@ class _MessageProcessRunner { _updateAssetJob, _deviceTransfer, _updateTokenJob, + signalDb, ); _floodJob.start(); } @@ -365,6 +377,7 @@ class _MessageProcessRunner { void dispose() { blaze.dispose(); database.dispose(); + appDatabase.close(); jobSubscribers.forEach((subscription) => subscription.cancel()); _deviceTransfer?.dispose(); } diff --git a/pubspec.yaml b/pubspec.yaml index 1a42334fe4..d2f0e5c45e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -243,4 +243,4 @@ msix_config: toast_activator: clsid: "94B64592-528D-48B4-B37B-C82D634F1BE7" arguments: "-ToastActivated" - display_name: "Mixin Messenger" + display_name: "Mixin Messenger" \ No newline at end of file diff --git a/test/crypto/crypto_key_value_test.dart b/test/crypto/crypto_key_value_test.dart new file mode 100644 index 0000000000..2738249352 --- /dev/null +++ b/test/crypto/crypto_key_value_test.dart @@ -0,0 +1,67 @@ +@TestOn('linux || mac-os') +library; + +import 'dart:io'; + +import 'package:drift/native.dart'; +import 'package:flutter_app/crypto/crypto_key_value.dart'; +import 'package:flutter_app/db/mixin_database.dart'; +import 'package:flutter_app/utils/db/user_crypto_key_value.dart'; +import 'package:flutter_app/utils/file.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:hive/hive.dart'; +import 'package:hive/src/hive_impl.dart'; +import 'package:mixin_logger/mixin_logger.dart'; +import 'package:path/path.dart' as p; + +void main() { + late MixinDatabase database; + late HiveInterface hive; + + setUp(() { + database = MixinDatabase(NativeDatabase.memory()); + mixinDocumentsDirectory = Directory(p.join( + Directory.systemTemp.path, + 'mixin_test_hive', + )); + // remove all data + try { + if (mixinDocumentsDirectory.existsSync()) { + mixinDocumentsDirectory.deleteSync(recursive: true); + } + } catch (error, stackTrace) { + e('delete hive path error: $error, $stackTrace'); + } + mixinDocumentsDirectory.createSync(recursive: true); + hive = HiveImpl(); + }); + + tearDown(() async { + await database.close(); + }); + + test('no migration', () async { + final oldCryptoKeyValue = CryptoKeyValue(); + final cryptoKeyValue = UserCryptoKeyValue(database.propertyDao); + await oldCryptoKeyValue.migrateToNewCryptoKeyValue( + hive, 'test', cryptoKeyValue); + expect(await cryptoKeyValue.get('next_pre_key_id'), null); + expect(await cryptoKeyValue.get('next_signed_pre_key_id'), null); + expect(await cryptoKeyValue.get('active_signed_pre_key_id'), null); + }); + + test('migration', () async { + final oldCryptoKeyValue = CryptoKeyValue(); + final cryptoKeyValue = UserCryptoKeyValue(database.propertyDao); + hive.init(p.join(mixinDocumentsDirectory.path, 'test', 'crypto_box')); + final box = await hive.openBox('crypto_box'); + await box.put('next_pre_key_id', 1); + await box.put('next_signed_pre_key_id', 2); + await box.put('active_signed_pre_key_id', 3); + await oldCryptoKeyValue.migrateToNewCryptoKeyValue( + hive, 'test', cryptoKeyValue); + expect(await cryptoKeyValue.get('next_pre_key_id'), 1); + expect(await cryptoKeyValue.get('next_signed_pre_key_id'), 2); + expect(await cryptoKeyValue.get('active_signed_pre_key_id'), 3); + }); +} diff --git a/test/db/property_storage_test.dart b/test/db/property_storage_test.dart deleted file mode 100644 index 5a02789099..0000000000 --- a/test/db/property_storage_test.dart +++ /dev/null @@ -1,56 +0,0 @@ -@TestOn('linux || mac-os') -library; - -import 'package:drift/native.dart'; -import 'package:flutter_app/db/mixin_database.dart'; -import 'package:flutter_app/db/util/property_storage.dart'; -import 'package:flutter_app/enum/property_group.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - test('tet PropertyStorage', () async { - final database = MixinDatabase(NativeDatabase.memory()); - final storage = - PropertyStorage(PropertyGroup.setting, database.propertyDao); - - expect(storage.get('test_empty'), null); - storage.set('test_empty', false); - expect(storage.get('test_empty'), null); - expect(storage.get('test_empty'), null); - expect(storage.get('test_empty'), 'false'); - expect(storage.getList('test_empty'), null); - expect(storage.getMap('test_empty'), null); - - storage.set('test_int', 12345); - expect(storage.get('test_int'), 12345); - storage.set('test_int', null); - expect(storage.get('test_int'), null); - - expect(storage.get('test_string'), null); - storage.set('test_string', '12345'); - expect(storage.get('test_string'), '12345'); - - storage.set('test_bool', true); - expect(storage.get('test_bool'), true); - storage.set('test_bool', false); - expect(storage.get('test_bool'), false); - - storage.set('test_double', 12345.6789); - expect(storage.get('test_double'), 12345.6789); - expect(storage.get('test_double'), '12345.6789'); - expect(storage.get('test_double'), null); - expect(storage.get('test_double'), null); - - storage.set('test_map', {'a': 1, 'b': 2}); - expect(storage.getMap('test_map'), {'a': 1, 'b': 2}); - expect(storage.getMap('test_map'), {'a': 1, 'b': 2}); - - storage.set('test_list', [1, 2, 3]); - expect(storage.getList('test_list'), [1, 2, 3]); - expect(storage.getList('test_list'), [1, 2, 3]); - - storage.set('test_list_string', ['1', '2', '3']); - expect(storage.getList('test_list_string'), ['1', '2', '3']); - expect(storage.getList('test_list_string'), ['1', '2', '3']); - }); -} diff --git a/test/utils/db_key_value_test.dart b/test/utils/db_key_value_test.dart new file mode 100644 index 0000000000..8fdb00b2d2 --- /dev/null +++ b/test/utils/db_key_value_test.dart @@ -0,0 +1,89 @@ +@TestOn('linux || mac-os') +library; + +import 'dart:async'; + +import 'package:drift/native.dart'; +import 'package:flutter_app/db/app/app_database.dart'; +import 'package:flutter_app/enum/property_group.dart'; +import 'package:flutter_app/utils/db/db_key_value.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('tet key value', () async { + final database = AppDatabase(NativeDatabase.memory()); + final storage = AppKeyValue( + group: AppPropertyGroup.setting, + dao: database.appKeyValueDao, + ); + + await storage.initialize; + + expect(storage.get('test_empty'), null); + unawaited(storage.set('test_empty', false)); + expect(storage.get('test_empty'), null); + expect(storage.get('test_empty'), null); + expect(storage.get('test_empty'), 'false'); + expect(storage.get('test_empty'), null); + expect(storage.get('test_empty'), null); + + unawaited(storage.set('test_int', 12345)); + expect(storage.get('test_int'), 12345); + unawaited(storage.set('test_int', null)); + expect(storage.get('test_int'), null); + + expect(storage.get('test_string'), null); + unawaited(storage.set('test_string', '12345')); + expect(storage.get('test_string'), '12345'); + + unawaited(storage.set('test_bool', true)); + expect(storage.get('test_bool'), true); + unawaited(storage.set('test_bool', false)); + expect(storage.get('test_bool'), false); + + unawaited(storage.set('test_double', 12345.6789)); + expect(storage.get('test_double'), 12345.6789); + expect(storage.get('test_double'), '12345.6789'); + expect(storage.get('test_double'), null); + expect(storage.get('test_double'), null); + + unawaited(storage.set('test_map', {'a': 1, 'b': 2})); + expect(storage.get>('test_map'), {'a': 1, 'b': 2}); + expect(storage.get('test_map'), {'a': 1, 'b': 2}); + + unawaited(storage.set('test_list', [1, 2, 3])); + expect(storage.get>('test_list'), [1, 2, 3]); + expect(storage.get('test_list'), [1, 2, 3]); + + unawaited(storage.set('test_list_string', ['1', '2', '3'])); + expect(storage.get>('test_list_string'), null); + expect(storage.get('test_list_string'), ['1', '2', '3']); + }); + + test('convert to string', () { + expect(convertToString(123), '123'); + expect(convertToString(123.456), '123.456'); + expect(convertToString(true), 'true'); + expect(convertToString(false), 'false'); + expect(convertToString(null), null); + expect(convertToString('123'), '123'); + expect(convertToString({'a': 1, 'b': 2}), '{"a":1,"b":2}'); + expect(convertToString([1, 2, 3]), '[1,2,3]'); + }); + + test('convert to type', () { + expect(convertToType('123'), '123'); + expect(convertToType('123'), 123); + expect(convertToType('123.456'), 123.456); + expect(convertToType('true'), true); + expect(convertToType('false'), false); + expect( + convertToType>('{"a":1,"b":2}'), {'a': 1, 'b': 2}); + expect(convertToType>('[1,2,3]'), [1, 2, 3]); + expect(convertToType('[1,2,3]'), [1, 2, 3]); + expect(convertToType('["1","2","3"]'), ['1', '2', '3']); + expect(convertToType>>('[{"a":1,"b":2}]'), [ + {'a': 1, 'b': 2} + ]); + }); +}