// SPDX-FileCopyrightText: 2019-Present Christian Kußowski // SPDX-FileCopyrightText: 2019-Present Contributors to FluffyChat // // SPDX-License-Identifier: AGPL-3.0-or-later import 'package:matrix/matrix.dart'; import 'package:tatchat/utils/miniapp_meta.dart'; /// Account data key that holds the list of installed mini app IDs. const String installedMiniAppsKey = 'im.tatuchat.miniapps.installed'; /// Prefix of the account data keys that hold a mini app's JSON document. const String miniAppDataKeyPrefix = 'im.tatuchat.miniapps.data.'; /// Prefix of the account data keys that hold the HTML source of an imported /// mini app. The key is ``{prefix}{appId}`` and the content is /// `{name: '...', html: '...'}`. const String miniAppSourceKeyPrefix = 'im.tatuchat.miniapps.source.'; /// Extension that gives every [Client] access to the mini apps store which is /// persisted in the user's account data ("localStorage on the server"). extension MiniAppStoreExtension on Client { List get installedMiniApps { final apps = accountData[installedMiniAppsKey]?.content['apps']; if (apps is List) return [...apps.whereType()]; return []; } bool isMiniAppInstalled(String appId) => installedMiniApps.contains(appId); Future installMiniApp(String appId) async { final apps = installedMiniApps; if (apps.contains(appId)) return; apps.add(appId); await setAccountData(userID!, installedMiniAppsKey, {'apps': apps}); _cacheAccountData(installedMiniAppsKey, {'apps': apps}); } Future uninstallMiniApp(String appId) async { final apps = installedMiniApps..remove(appId); await setAccountData(userID!, installedMiniAppsKey, {'apps': apps}); _cacheAccountData(installedMiniAppsKey, {'apps': apps}); // Clear the custom HTML source if present. await setAccountData(userID!, '$miniAppSourceKeyPrefix$appId', { 'html': '', 'name': '', }); _cacheAccountData( '$miniAppSourceKeyPrefix$appId', {'html': '', 'name': ''}, ); } /// Reads the JSON document of the given mini app. Returns an empty map if /// the app never stored anything. Map getMiniAppData(String appId) => accountData['$miniAppDataKeyPrefix$appId']?.content ?? const {}; /// Overwrites the JSON document of the given mini app. Future setMiniAppData(String appId, Map data) async { final key = '$miniAppDataKeyPrefix$appId'; await setAccountData(userID!, key, data); _cacheAccountData(key, data); } // ---------- Imported (custom) apps ---------- /// Stores the HTML source and the manifest of a user-imported mini app and /// adds it to the installed list if not already present. The manifest's /// [MiniAppMeta.id] is used as the account data key for the app. Future importMiniApp({ required MiniAppMeta meta, required String html, }) async { final sourceKey = '$miniAppSourceKeyPrefix${meta.id}'; final content = { 'name': meta.name, 'category': meta.category, 'description': meta.description, 'logo': meta.logo, 'inChat': meta.inChat, 'html': html, }; await setAccountData(userID!, sourceKey, content); _cacheAccountData(sourceKey, content); final apps = installedMiniApps; if (!apps.contains(meta.id)) { apps.add(meta.id); await setAccountData(userID!, installedMiniAppsKey, {'apps': apps}); _cacheAccountData(installedMiniAppsKey, {'apps': apps}); } } /// Mirrors the just-written account data into the in-memory cache so that /// reads right after a write (e.g. a mini app saving and immediately /// loading its store) see the new value without waiting for the next sync. void _cacheAccountData(String key, Map content) { accountData[key] = BasicEvent(type: key, content: content); } /// Returns the raw HTML source of an imported mini app, or null if the app /// is a built-in app or was never imported. String? getMiniAppSource(String appId) { final content = accountData['$miniAppSourceKeyPrefix$appId']?.content; if (content == null) return null; final html = content['html']; return (html is String && html.isNotEmpty) ? html : null; } /// Returns the display name of an imported mini app, or null. String? getMiniAppName(String appId) { final content = accountData['$miniAppSourceKeyPrefix$appId']?.content; return _stringContent(content, 'name'); } /// Returns the category of an imported mini app, or null. String? getMiniAppCategory(String appId) { final content = accountData['$miniAppSourceKeyPrefix$appId']?.content; return _stringContent(content, 'category'); } /// Returns the description of an imported mini app, or null. String? getMiniAppDescription(String appId) { final content = accountData['$miniAppSourceKeyPrefix$appId']?.content; return _stringContent(content, 'description'); } /// Returns the logo (data URI or URL) of an imported mini app, or null. String? getMiniAppLogo(String appId) { final content = accountData['$miniAppSourceKeyPrefix$appId']?.content; return _stringContent(content, 'logo'); } /// Whether an imported mini app declares that it can run inside a chat. bool getMiniAppInChat(String appId) { final content = accountData['$miniAppSourceKeyPrefix$appId']?.content; return content?['inChat'] == true; } String? _stringContent(Map? content, String key) { final value = content?[key]; return (value is String && value.isNotEmpty) ? value : null; } }