// SPDX-FileCopyrightText: 2019-Present Christian Kußowski // SPDX-FileCopyrightText: 2019-Present Contributors to FluffyChat // // SPDX-License-Identifier: AGPL-3.0-or-later import 'package:collection/collection.dart'; import 'package:tatchat/config/app_config.dart'; import 'package:tatchat/config/setting_keys.dart'; import 'package:tatchat/l10n/l10n.dart'; import 'package:tatchat/utils/custom_http_client.dart'; import 'package:tatchat/utils/custom_image_resizer.dart'; import 'package:tatchat/utils/init_with_restore.dart'; import 'package:tatchat/utils/matrix_live_kit_calls/matrix_live_kit_call_member.dart'; import 'package:tatchat/utils/matrix_sdk_extensions/room_extension.dart'; import 'package:tatchat/utils/platform_infos.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_vodozemac/flutter_vodozemac.dart' as vod; import 'package:matrix/encryption/utils/key_verification.dart'; import 'package:matrix/matrix.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:universal_html/html.dart' as html; import 'matrix_sdk_extensions/flutter_matrix_dart_sdk_database/builder.dart'; abstract class ClientManager { static const String clientNamespace = 'im.fluffychat.store.clients'; static Future> getClients({ bool initialize = true, required SharedPreferences store, }) async { final clientNames = {}; try { final clientNamesList = store.getStringList(clientNamespace) ?? []; clientNames.addAll(clientNamesList); } catch (e, s) { Logs().w('Client names in store are corrupted', e, s); await store.remove(clientNamespace); } if (clientNames.isEmpty) { clientNames.add(PlatformInfos.appDisplayName); await store.setStringList(clientNamespace, clientNames.toList()); } final clients = await Future.wait( clientNames.map((name) => createClient(name, store)), ); if (initialize) { await Future.wait( clients.map( (client) => client .initWithRestore( onMigration: () async { final l10n = await lookupL10n( PlatformDispatcher.instance.locale, ); sendInitNotification( l10n.databaseMigrationTitle, l10n.databaseMigrationBody, ); }, ) .catchError( (e, s) => Logs().e('Unable to initialize client', e, s), ), ), ); } if (clients.length > 1 && clients.any((c) => !c.isLogged())) { final loggedOutClients = clients.where((c) => !c.isLogged()).toList(); for (final client in loggedOutClients) { Logs().w( 'Multi account is enabled but client ${client.userID} is not logged in. Removing...', ); clientNames.remove(client.clientName); clients.remove(client); } await store.setStringList(clientNamespace, clientNames.toList()); } return clients; } static Future addClientNameToStore( String clientName, SharedPreferences store, ) async { final clientNamesList = store.getStringList(clientNamespace) ?? []; clientNamesList.add(clientName); await store.setStringList(clientNamespace, clientNamesList); } static Future removeClientNameFromStore( String clientName, SharedPreferences store, ) async { final clientNamesList = store.getStringList(clientNamespace) ?? []; clientNamesList.remove(clientName); await store.setStringList(clientNamespace, clientNamesList); } static NativeImplementations get nativeImplementations => kIsWeb ? NativeImplementationsWebWorker( Uri.parse('native_executor.js'), timeout: const Duration(minutes: 1), ) : NativeImplementationsIsolate( compute, vodozemacInit: () => vod.init( wasmPath: './assets/assets/vodozemac/${AppConfig.vodozemacVersion}/', ), ); static Future createClient( String clientName, SharedPreferences store, ) async { final shareKeysWith = AppSettings.shareKeysWith.value; final enableSoftLogout = AppSettings.enableSoftLogout.value; return Client( clientName, httpClient: CustomHttpClient.createHTTPClient(), verificationMethods: { KeyVerificationMethod.numbers, if (kIsWeb || PlatformInfos.isMobile || PlatformInfos.isLinux) KeyVerificationMethod.emoji, }, importantStateEvents: { // To make room emotes work 'im.ponies.room_emotes', // To display call state in chat list MatrixRtcCallMember.eventType, // Make the canal/mural room type marker available in the chat list // right after sync, even for rooms that are still partially loaded. roomTypeStateEvent, }, customImageResizer: PlatformInfos.supportsCustomImageResizer ? customImageResizer : null, logLevel: Level.verbose, database: await flutterMatrixSdkDatabaseBuilder(clientName), supportedLoginTypes: { AuthenticationTypes.password, AuthenticationTypes.sso, }, nativeImplementations: nativeImplementations, defaultNetworkRequestTimeout: const Duration(minutes: 30), enableDehydratedDevices: true, shareKeysWith: ShareKeysWith.values.singleWhereOrNull( (share) => share.name == shareKeysWith, ) ?? ShareKeysWith.all, onSoftLogout: enableSoftLogout ? (c) => c.refreshAccessToken() : null, sendTimelineEventTimeout: Duration( seconds: AppSettings.sendTimelineEventTimeout.value, ), ); } static Future sendInitNotification(String title, String body) async { if (kIsWeb) { html.Notification(title, body: body); return; } final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); await flutterLocalNotificationsPlugin.initialize( settings: const InitializationSettings( android: AndroidInitializationSettings('notifications_icon'), iOS: DarwinInitializationSettings(), ), ); await flutterLocalNotificationsPlugin.show( id: 0, title: title, body: body, notificationDetails: const NotificationDetails( android: AndroidNotificationDetails( 'error_message', 'Error Messages', importance: Importance.high, priority: Priority.max, ), iOS: DarwinNotificationDetails(sound: 'notification.caf'), ), ); } }