146 lines
5.5 KiB
Dart
146 lines
5.5 KiB
Dart
// 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<String> get installedMiniApps {
|
|
final apps = accountData[installedMiniAppsKey]?.content['apps'];
|
|
if (apps is List) return [...apps.whereType<String>()];
|
|
return [];
|
|
}
|
|
|
|
bool isMiniAppInstalled(String appId) => installedMiniApps.contains(appId);
|
|
|
|
Future<void> 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<void> 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<String, dynamic> getMiniAppData(String appId) =>
|
|
accountData['$miniAppDataKeyPrefix$appId']?.content ?? const {};
|
|
|
|
/// Overwrites the JSON document of the given mini app.
|
|
Future<void> setMiniAppData(String appId, Map<String, Object?> 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<void> importMiniApp({
|
|
required MiniAppMeta meta,
|
|
required String html,
|
|
}) async {
|
|
final sourceKey = '$miniAppSourceKeyPrefix${meta.id}';
|
|
final content = <String, Object?>{
|
|
'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<String, Object?> 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<String, Object?>? content, String key) {
|
|
final value = content?[key];
|
|
return (value is String && value.isNotEmpty) ? value : null;
|
|
}
|
|
}
|