Initial commit
Some checks failed
Main Deploy Workflow / deploy_web (push) Has been cancelled
Main Deploy Workflow / deploy_playstore_internal (push) Has been cancelled
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
2026-09-11 15:59:56 -03:00
commit 0037b0b9cf
651 changed files with 199705 additions and 0 deletions

View File

@@ -0,0 +1,49 @@
// SPDX-FileCopyrightText: 2019-Present Christian Kußowski
// SPDX-FileCopyrightText: 2019-Present Contributors to FluffyChat
//
// SPDX-License-Identifier: AGPL-3.0-or-later
import 'package:material_ui/material_ui.dart';
import 'package:matrix/matrix.dart';
IconData _getIconFromName(String displayname) {
final name = displayname.toLowerCase();
if ({'android'}.any(name.contains)) {
return Icons.phone_android_outlined;
}
if ({'ios', 'ipad', 'iphone', 'ipod'}.any(name.contains)) {
return Icons.phone_iphone_outlined;
}
if ({
'web',
'http://',
'https://',
'firefox',
'chrome',
'/_matrix',
'safari',
'opera',
}.any(name.contains)) {
return Icons.web_outlined;
}
if ({'desktop', 'windows', 'macos', 'linux', 'ubuntu'}.any(name.contains)) {
return Icons.desktop_mac_outlined;
}
return Icons.device_unknown_outlined;
}
extension DeviceExtension on Device {
String get displayname =>
(displayName?.isNotEmpty ?? false) ? displayName! : 'Unknown device';
IconData get icon => _getIconFromName(displayname);
}
extension DeviceKeysExtension on DeviceKeys {
String get displayname => (deviceDisplayName?.isNotEmpty ?? false)
? deviceDisplayName!
: 'Unknown device';
IconData get icon => _getIconFromName(displayname);
}

View File

@@ -0,0 +1,68 @@
// SPDX-FileCopyrightText: 2019-Present Christian Kußowski
// SPDX-FileCopyrightText: 2019-Present Contributors to FluffyChat
//
// SPDX-License-Identifier: AGPL-3.0-or-later
import 'package:async/async.dart' as async;
import 'package:tatchat/utils/size_string.dart';
import 'package:tatchat/widgets/future_loading_dialog.dart';
import 'package:flutter/foundation.dart';
import 'package:material_ui/material_ui.dart';
import 'package:matrix/matrix.dart';
import 'matrix_file_extension.dart';
extension LocalizedBody on Event {
Future<async.Result<MatrixFile?>> _getFile(BuildContext context) =>
showFutureLoadingDialog(
context: context,
futureWithProgress: (onProgress) {
final fileSize = infoMap['size'] is int
? infoMap['size'] as int
: null;
return downloadAndDecryptAttachment(
onDownloadProgress: fileSize == null
? null
: (bytes) => onProgress(bytes / fileSize),
);
},
);
Future<void> saveFile(BuildContext context) async {
final matrixFile = await _getFile(context);
if (!context.mounted) return;
matrixFile.result?.save(context);
}
Future<void> shareFile(BuildContext context) async {
final matrixFile = await _getFile(context);
if (!context.mounted) return;
matrixFile.result?.share(context);
}
bool get isAttachmentSmallEnough =>
infoMap['size'] is int &&
(infoMap['size'] as int) < room.client.database.maxFileSize;
bool get isThumbnailSmallEnough =>
thumbnailInfoMap['size'] is int &&
(thumbnailInfoMap['size'] as int) < room.client.database.maxFileSize;
bool get showThumbnail =>
[
MessageTypes.Image,
MessageTypes.Sticker,
MessageTypes.Video,
].contains(messageType) &&
(kIsWeb ||
isAttachmentSmallEnough ||
isThumbnailSmallEnough ||
(content['url'] is String));
String? get sizeString => content
.tryGetMap<String, Object?>('info')
?.tryGet<int>('size')
?.sizeString;
}

View File

@@ -0,0 +1,57 @@
// SPDX-FileCopyrightText: 2019-Present Christian Kußowski
// SPDX-FileCopyrightText: 2019-Present Contributors to FluffyChat
//
// SPDX-License-Identifier: AGPL-3.0-or-later
import 'package:tatchat/config/setting_keys.dart';
import 'package:matrix/matrix.dart';
extension VisibleInGuiExtension on List<Event> {
List<Event> filterByVisibleInGui({
String? exceptionEventId,
String? threadId,
}) => where((event) {
if (threadId != null &&
event.relationshipType != RelationshipTypes.reaction) {
if ((event.relationshipType != RelationshipTypes.thread ||
event.relationshipEventId != threadId) &&
event.eventId != threadId) {
return false;
}
} else if (event.relationshipType == RelationshipTypes.thread) {
return false;
}
return event.isVisibleInGui || event.eventId == exceptionEventId;
}).toList();
}
extension IsStateExtension on Event {
bool get isVisibleInGui =>
// always filter out edit and reaction relationships
!{
RelationshipTypes.edit,
RelationshipTypes.reaction,
}.contains(relationshipType) &&
// always filter out m.key.* and other known but unimportant events
!isKnownHiddenStates &&
// event types to hide: redaction and reaction events
// if a reaction has been redacted we also want it to be hidden in the timeline
!{EventTypes.Reaction, EventTypes.Redaction}.contains(type) &&
// if we enabled to hide all redacted events, don't show those
(!AppSettings.hideRedactedEvents.value || !redacted) &&
// if we enabled to hide all unknown events, don't show those
(!AppSettings.hideUnknownEvents.value || isEventTypeKnown);
bool get isState => !{EventTypes.Message, EventTypes.Sticker}.contains(type);
bool get isCollapsedState => !{
EventTypes.Message,
EventTypes.Sticker,
EventTypes.RoomCreate,
EventTypes.RoomTombstone,
}.contains(type);
bool get isKnownHiddenStates =>
{PollEventContent.responseType}.contains(type) ||
type.startsWith('m.key.verification.');
}

View File

@@ -0,0 +1,214 @@
// SPDX-FileCopyrightText: 2019-Present Christian Kußowski
// SPDX-FileCopyrightText: 2019-Present Contributors to FluffyChat
//
// SPDX-License-Identifier: AGPL-3.0-or-later
import 'dart:io';
import 'package:tatchat/l10n/l10n.dart';
import 'package:tatchat/utils/client_manager.dart';
import 'package:tatchat/utils/platform_infos.dart';
import 'package:flutter/foundation.dart';
import 'package:matrix/matrix.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path_provider_foundation/path_provider_foundation.dart';
import 'package:sqflite_common/utils/utils.dart' as sqflite_utils;
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import 'package:universal_html/html.dart' as html;
import 'cipher.dart';
Future<DatabaseApi> flutterMatrixSdkDatabaseBuilder(String clientName) async {
try {
return await _constructDatabase(clientName);
} catch (e, s) {
Logs().wtf('Unable to construct database!', e, s);
try {
// Send error notification:
final l10n = await lookupL10n(PlatformDispatcher.instance.locale);
// We expect that the database cannot be open on iOS 2.8.0 due to that
// the team ID has changed and te app can no longer access the database
// key in the iOS keychain. This should be removed from 2.9.0 on.
if (!PlatformInfos.isIOS) {
ClientManager.sendInitNotification(l10n.initAppError, e.toString());
}
} catch (e, s) {
Logs().e('Unable to send error notification', e, s);
}
// Delete database file:
if (!kIsWeb) {
final dbFile = File(await _getDatabasePath(clientName));
if (await dbFile.exists()) await dbFile.delete();
}
// Try again
return await _constructDatabase(clientName);
}
}
Future<Directory?> getFileStorageLocation() async {
try {
late final Directory temporaryDirectory;
if (PlatformInfos.isIOS) {
final containerPath = await PathProviderFoundation().getContainerPath(
appGroupIdentifier: 'group.im.fluffychat.app',
);
temporaryDirectory = Directory(containerPath!);
} else if (PlatformInfos.isLinux) {
temporaryDirectory = await getApplicationCacheDirectory();
} else {
temporaryDirectory = await getTemporaryDirectory();
}
return await Directory(
join(temporaryDirectory.path, 'fluffychat_download_cache'),
).create(recursive: true);
} on MissingPlatformDirectoryException catch (_) {
Logs().w(
'No temporary directory for file cache available on this platform.',
);
}
return null;
}
Future<MatrixSdkDatabase> _constructDatabase(String clientName) async {
if (kIsWeb) {
html.window.navigator.storage?.persist();
return await MatrixSdkDatabase.init(clientName);
}
final cipher = await getDatabaseCipher();
final fileStorageLocation = await getFileStorageLocation();
final path = await _getDatabasePath(clientName);
// import the SQLite / SQLCipher shared objects / dynamic libraries
final factory = createDatabaseFactoryFfi();
// required for [getDatabasesPath]
databaseFactory = factory;
// migrate from potential previous SQLite database path to current one
await _migrateLegacyLocation(path, clientName);
// in case we got a cipher, we use the encryption helper
// to manage SQLite encryption
final helper = cipher == null
? null
: SQfLiteEncryptionHelper(factory: factory, path: path, cipher: cipher);
// check whether the DB is already encrypted and otherwise do so
await helper?.ensureDatabaseFileEncrypted();
final database = await factory.openDatabase(
path,
options: OpenDatabaseOptions(
version: 1,
// most important : apply encryption when opening the DB
onConfigure: helper?.applyPragmaKey,
),
);
Logs().i('Database file size', await File(database.path).length());
final pageCount = sqflite_utils.firstIntValue(
await database.rawQuery('PRAGMA page_count'),
);
final freePages = sqflite_utils.firstIntValue(
await database.rawQuery('PRAGMA freelist_count'),
);
final pageSize = sqflite_utils.firstIntValue(
await database.rawQuery('PRAGMA page_size'),
);
Logs().i(
'DB pages: $pageCount total, $freePages free (~${(freePages ?? 0) * (pageSize ?? 0)} bytes wasted)',
);
final tables = await database.rawQuery(
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'",
);
for (final t in tables) {
final name = t['name'] as String;
final c = sqflite_utils.firstIntValue(
await database.rawQuery('SELECT COUNT(*) FROM "$name"'),
);
Logs().i('Table $name: $c rows');
}
await _ensureIncrementalAutoVacuum(database);
return await MatrixSdkDatabase.init(
clientName,
database: database,
maxFileSize: 1000 * 1000 * 10,
fileStorageLocation: fileStorageLocation?.uri,
deleteFilesAfterDuration: const Duration(days: 30),
);
}
/// Without auto vacuum the database can never really shrink
Future<void> _ensureIncrementalAutoVacuum(Database database) async {
const incrementalAutoVacuum = 2;
final currentMode = sqflite_utils.firstIntValue(
await database.rawQuery('PRAGMA auto_vacuum'),
);
if (currentMode != incrementalAutoVacuum) {
Logs().i('Switching database to incremental auto_vacuum...');
await database.execute('PRAGMA auto_vacuum = $incrementalAutoVacuum');
await database.execute('VACUUM');
return;
}
await database.execute('PRAGMA incremental_vacuum');
}
Future<String> _getDatabaseDirectory() async {
if (PlatformInfos.isIOS) {
final containerPath = await PathProviderFoundation().getContainerPath(
appGroupIdentifier: 'group.im.fluffychat.app',
);
if (containerPath == null) {
Logs().w('No container path found for iOS app!');
return (await getLibraryDirectory()).path;
}
return containerPath;
}
if (PlatformInfos.isMacOS) {
return (await getLibraryDirectory()).path;
}
return (await getApplicationSupportDirectory()).path;
}
Future<String> _getDatabasePath(String clientName) async {
final databaseDirectory = await _getDatabaseDirectory();
return join(databaseDirectory, '$clientName.sqlite');
}
Future<void> _migrateLegacyLocation(
String sqlFilePath,
String clientName,
) async {
final oldPath = PlatformInfos.isDesktop
? (await getApplicationSupportDirectory()).path
: PlatformInfos.isIOS
? (await getLibraryDirectory()).path
: await getDatabasesPath();
final oldFilePath = join(oldPath, '$clientName.sqlite');
if (oldFilePath == sqlFilePath) return;
final maybeOldFile = File(oldFilePath);
if (await maybeOldFile.exists()) {
Logs().i(
'Migrate legacy location for database from "$oldFilePath" to "$sqlFilePath"',
);
await maybeOldFile.copy(sqlFilePath);
await maybeOldFile.delete();
}
}

View File

@@ -0,0 +1,88 @@
// SPDX-FileCopyrightText: 2019-Present Christian Kußowski
// SPDX-FileCopyrightText: 2019-Present Contributors to FluffyChat
//
// SPDX-License-Identifier: AGPL-3.0-or-later
import 'dart:convert';
import 'dart:math';
import 'package:tatchat/config/setting_keys.dart';
import 'package:tatchat/l10n/l10n.dart';
import 'package:tatchat/utils/client_manager.dart';
import 'package:tatchat/utils/platform_infos.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:matrix/matrix.dart';
const _passwordStorageKey = 'database_password';
Future<String?> getDatabaseCipher() async {
String? password;
const iosOptions = IOSOptions(groupId: 'group.im.fluffychat.app');
try {
password = await FlutterSecureStorage(
iOptions: iosOptions,
).read(key: _passwordStorageKey, iOptions: iosOptions);
if (password != null) return password;
if (PlatformInfos.isIOS) {
final legacyPassword = await FlutterSecureStorage().read(
key: _passwordStorageKey,
);
if (legacyPassword != null) {
Logs().i('Migrate database key location on iOS...');
await FlutterSecureStorage().delete(key: _passwordStorageKey);
await FlutterSecureStorage(iOptions: iosOptions).write(
key: _passwordStorageKey,
value: legacyPassword,
iOptions: iosOptions,
);
return legacyPassword;
}
}
final rng = Random.secure();
final list = Uint8List(32);
list.setAll(0, Iterable.generate(list.length, (i) => rng.nextInt(256)));
final newPassword = base64UrlEncode(list);
await FlutterSecureStorage(
iOptions: iosOptions,
).write(key: _passwordStorageKey, value: newPassword, iOptions: iosOptions);
// workaround for if we just wrote to the key and it still doesn't exist
password = await FlutterSecureStorage(
iOptions: iosOptions,
).read(key: _passwordStorageKey, iOptions: iosOptions);
if (password == null) throw MissingPluginException();
return password;
} on MissingPluginException catch (e) {
FlutterSecureStorage(
iOptions: iosOptions,
).delete(key: _passwordStorageKey, iOptions: iosOptions).catchError((_) {});
Logs().w('Database encryption is not supported on this platform', e);
_sendNoEncryptionWarning(e);
} catch (e, s) {
FlutterSecureStorage(
iOptions: iosOptions,
).delete(key: _passwordStorageKey, iOptions: iosOptions).catchError((_) {});
Logs().w('Unable to init database encryption', e, s);
_sendNoEncryptionWarning(e);
}
return null;
}
Future<void> _sendNoEncryptionWarning(Object exception) async {
final isStored = AppSettings.noEncryptionWarningShown.value;
if (isStored == true) return;
final l10n = await lookupL10n(PlatformDispatcher.instance.locale);
ClientManager.sendInitNotification(
l10n.noDatabaseEncryption,
exception.toString(),
);
await AppSettings.noEncryptionWarningShown.setItem(true);
}

View File

@@ -0,0 +1,70 @@
// SPDX-FileCopyrightText: 2019-Present Christian Kußowski
// SPDX-FileCopyrightText: 2019-Present Contributors to FluffyChat
//
// SPDX-License-Identifier: AGPL-3.0-or-later
import 'package:file_picker/file_picker.dart';
import 'package:tatchat/l10n/l10n.dart';
import 'package:tatchat/utils/size_string.dart';
import 'package:material_ui/material_ui.dart';
import 'package:matrix/matrix.dart';
import 'package:share_plus/share_plus.dart';
extension MatrixFileExtension on MatrixFile {
Future<void> save(BuildContext context) async {
final scaffoldMessenger = ScaffoldMessenger.of(context);
final l10n = L10n.of(context);
final downloadPath = await FilePicker.saveFile(
dialogTitle: l10n.saveFile,
fileName: name,
type: filePickerFileType,
bytes: bytes,
);
if (downloadPath == null) return;
scaffoldMessenger.showSnackBar(
SnackBar(
content: Text(l10n.fileHasBeenSavedAt(downloadPath.toString())),
showCloseIcon: true,
),
);
}
FileType get filePickerFileType {
if (this is MatrixImageFile) return FileType.image;
if (this is MatrixAudioFile) return FileType.audio;
if (this is MatrixVideoFile) return FileType.video;
return FileType.any;
}
Future<void> share(BuildContext context) async {
// Workaround for iPad from
// https://github.com/fluttercommunity/plus_plugins/tree/main/packages/share_plus/share_plus#ipad
final box = context.findRenderObject() as RenderBox?;
await SharePlus.instance.share(
ShareParams(
files: [XFile.fromData(bytes, name: name, mimeType: mimeType)],
sharePositionOrigin: box == null
? null
: box.localToGlobal(Offset.zero) & box.size,
),
);
return;
}
MatrixFile get detectFileType {
if (msgType == MessageTypes.Image) {
return MatrixImageFile(bytes: bytes, name: name);
}
if (msgType == MessageTypes.Video) {
return MatrixVideoFile(bytes: bytes, name: name);
}
if (msgType == MessageTypes.Audio) {
return MatrixAudioFile(bytes: bytes, name: name);
}
return this;
}
String get sizeString => size.sizeString;
}

View File

@@ -0,0 +1,382 @@
// SPDX-FileCopyrightText: 2019-Present Christian Kußowski
// SPDX-FileCopyrightText: 2019-Present Contributors to FluffyChat
//
// SPDX-License-Identifier: AGPL-3.0-or-later
import 'package:tatchat/l10n/l10n.dart';
import 'package:matrix/matrix.dart';
/// This is a temporary helper class until there is a proper solution to this with the new system
class MatrixLocals extends MatrixLocalizations {
final L10n l10n;
MatrixLocals(this.l10n);
@override
String acceptedTheInvitation(String targetName) {
return l10n.acceptedTheInvitation(targetName);
}
@override
String activatedEndToEndEncryption(String senderName) {
return l10n.activatedEndToEndEncryption(senderName);
}
@override
String answeredTheCall(String senderName) {
return l10n.answeredTheCall(senderName);
}
@override
String get anyoneCanJoin => l10n.anyoneCanJoin;
@override
String bannedUser(String senderName, String targetName) {
return l10n.bannedUser(senderName, targetName);
}
@override
String changedTheChatAvatar(String senderName) {
return l10n.changedTheChatAvatar(senderName);
}
@override
String changedTheChatDescriptionTo(String senderName, _) {
return l10n.changedTheChatDescription(senderName);
}
@override
String changedTheChatNameTo(String senderName, _) {
return l10n.changedTheChatName(senderName);
}
@override
String changedTheChatPermissions(String senderName) {
return l10n.changedTheChatPermissions(senderName);
}
@override
String changedTheDisplaynameTo(String targetName, String newDisplayname) {
return l10n.changedTheDisplaynameTo(targetName, newDisplayname);
}
@override
String changedTheGuestAccessRules(String senderName) {
return l10n.changedTheGuestAccessRules(senderName);
}
@override
String changedTheGuestAccessRulesTo(
String senderName,
String localizedString,
) {
return l10n.changedTheGuestAccessRulesTo(senderName, localizedString);
}
@override
String changedTheHistoryVisibility(String senderName) {
return l10n.changedTheHistoryVisibility(senderName);
}
@override
String changedTheHistoryVisibilityTo(
String senderName,
String localizedString,
) {
return l10n.changedTheHistoryVisibilityTo(senderName, localizedString);
}
@override
String changedTheJoinRules(String senderName) {
return l10n.changedTheJoinRules(senderName);
}
@override
String changedTheJoinRulesTo(String senderName, String localizedString) {
return l10n.changedTheJoinRulesTo(senderName, localizedString);
}
@override
String changedTheProfileAvatar(String targetName) {
return l10n.changedTheProfileAvatar(targetName);
}
@override
String changedTheRoomAliases(String senderName) {
return l10n.changedTheRoomAliases(senderName);
}
@override
String changedTheRoomInvitationLink(String senderName) {
return l10n.changedTheRoomInvitationLink(senderName);
}
@override
String get channelCorruptedDecryptError => l10n.channelCorruptedDecryptError;
@override
String couldNotDecryptMessage(String errorText) {
return l10n.couldNotDecryptMessage(errorText);
}
@override
String createdTheChat(String senderName) {
return l10n.createdTheChat(senderName);
}
@override
String get emptyChat => l10n.emptyChat;
@override
String get encryptionNotEnabled => l10n.encryptionNotEnabled;
@override
String endedTheCall(String senderName) {
return l10n.endedTheCall(senderName);
}
@override
String get fromJoining => l10n.fromJoining;
@override
String get fromTheInvitation => l10n.fromTheInvitation;
@override
String groupWith(String displayname) {
return l10n.groupWith(displayname);
}
@override
String get guestsAreForbidden => l10n.guestsAreForbidden;
@override
String get guestsCanJoin => l10n.guestsCanJoin;
@override
String hasWithdrawnTheInvitationFor(String senderName, String targetName) {
return l10n.hasWithdrawnTheInvitationFor(senderName, targetName);
}
@override
String invitedUser(String senderName, String targetName) {
return l10n.invitedUser(senderName, targetName);
}
@override
String get invitedUsersOnly => l10n.invitedUsersOnly;
@override
String joinedTheChat(String targetName) {
return l10n.joinedTheChat(targetName);
}
@override
String kicked(String senderName, String targetName) {
return l10n.kicked(senderName, targetName);
}
@override
String kickedAndBanned(String senderName, String targetName) {
return l10n.kickedAndBanned(senderName, targetName);
}
@override
String get needPantalaimonWarning => l10n.oopsSomethingWentWrong;
@override
String get noPermission => l10n.noKeyForThisMessage;
@override
String redactedAnEvent(Event redactedEvent) {
return l10n.redactedAnEvent(
redactedEvent.redactedBecause?.senderFromMemoryOrFallback
.calcDisplayname() ??
l10n.user,
);
}
@override
String rejectedTheInvitation(String targetName) {
return l10n.rejectedTheInvitation(targetName);
}
@override
String removedBy(Event redactedEvent) {
return l10n.redactedBy(
redactedEvent.senderFromMemoryOrFallback.calcDisplayname(),
);
}
@override
String get roomHasBeenUpgraded => l10n.roomHasBeenUpgraded;
@override
String sentAFile(String senderName) {
return l10n.sentAFile(senderName);
}
@override
String sentAPicture(String senderName) {
return l10n.sentAPicture(senderName);
}
@override
String sentASticker(String senderName) {
return l10n.sentASticker(senderName);
}
@override
String sentAVideo(String senderName) {
return l10n.sentAVideo(senderName);
}
@override
String sentAnAudio(String senderName) {
return l10n.sentAnAudio(senderName);
}
@override
String sentCallInformations(String senderName) {
return l10n.sentCallInformations(senderName);
}
@override
String sharedTheLocation(String senderName) {
return l10n.sharedTheLocation(senderName);
}
@override
String startedACall(String senderName) {
return l10n.startedACall(senderName);
}
@override
String unbannedUser(String senderName, String targetName) {
return l10n.unbannedUser(senderName, targetName);
}
@override
String get unknownEncryptionAlgorithm => l10n.unknownEncryptionAlgorithm;
@override
String unknownEvent(String typeKey) {
return l10n.userSentUnknownEvent('User', typeKey);
}
@override
String userLeftTheChat(String targetName) {
return l10n.userLeftTheChat(targetName);
}
@override
String get visibleForAllParticipants => l10n.visibleForAllParticipants;
@override
String get visibleForEveryone => l10n.visibleForEveryone;
@override
String get you => l10n.you;
@override
String sentReaction(String senderName, String reactionKey) =>
l10n.reactedWith(senderName, reactionKey);
@override
// TODO: implement youAcceptedTheInvitation
String get youAcceptedTheInvitation => l10n.youAcceptedTheInvitation;
@override
String youBannedUser(String targetName) => l10n.youBannedUser(targetName);
@override
String youHaveWithdrawnTheInvitationFor(String targetName) =>
l10n.youHaveWithdrawnTheInvitationFor(targetName);
@override
String youInvitedBy(String senderName) => l10n.youInvitedBy(senderName);
@override
String youInvitedUser(String targetName) => l10n.youInvitedUser(targetName);
@override
// TODO: implement youJoinedTheChat
String get youJoinedTheChat => l10n.youJoinedTheChat;
@override
String youKicked(String targetName) => l10n.youKicked(targetName);
@override
String youKickedAndBanned(String targetName) =>
l10n.youKickedAndBanned(targetName);
@override
// TODO: implement youRejectedTheInvitation
String get youRejectedTheInvitation => l10n.youRejectedTheInvitation;
@override
String youUnbannedUser(String targetName) => l10n.youUnbannedUser(targetName);
@override
String wasDirectChatDisplayName(String oldDisplayName) =>
l10n.wasDirectChatDisplayName(oldDisplayName);
@override
String get unknownUser => l10n.user;
@override
String hasKnocked(String targetName) => l10n.hasKnocked(targetName);
@override
String acceptedKeyVerification(String senderName) =>
l10n.acceptedKeyVerification(senderName);
@override
String canceledKeyVerification(String senderName) =>
l10n.canceledKeyVerification(senderName);
@override
String completedKeyVerification(String senderName) =>
l10n.completedKeyVerification(senderName);
@override
String isReadyForKeyVerification(String senderName) =>
l10n.isReadyForKeyVerification(senderName);
@override
String requestedKeyVerification(String senderName) =>
l10n.requestedKeyVerification(senderName);
@override
String startedKeyVerification(String senderName) =>
l10n.startedKeyVerification(senderName);
@override
String invitedBy(String senderName) => senderName;
@override
String get cancelledSend => l10n.sendCanceled;
@override
String voiceMessage(
String senderName,
Duration? duration,
) => l10n.sentVoiceMessage(
senderName,
duration == null
? ''
: '${duration.inMinutes.toString().padLeft(2, '0')}:${(duration.inSeconds % 60).toString().padLeft(2, '0')}',
);
@override
String get refreshingLastEvent => l10n.loadingPleaseWait;
@override
String startedAPoll(String senderName) => '$senderName started a poll';
@override
String get pollHasBeenEnded => l10n.pollHasBeenEnded;
@override
String incomingCallFrom(String senderName) =>
l10n.incomingCallFrom(senderName);
}

View File

@@ -0,0 +1,29 @@
// 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';
extension OidcSessionJsonExtension on OidcLoginSession {
static const String storeKey = 'oidc_session';
static const String homeserverStoreKey = 'oidc_stored_homeserver';
Map<String, Object?> toJson() => {
'oidc_client_data': oidcClientData.toJson(),
'authentication_uri': authenticationUri.toString(),
'redirect_uri': redirectUri.toString(),
'code_verifier': codeVerifier,
'state': state,
};
static OidcLoginSession fromJson(Map<String, Object?> json) =>
OidcLoginSession(
oidcClientData: OidcClientData.fromJson(
json['oidc_client_data'] as Map<String, Object?>,
),
authenticationUri: Uri.parse(json['authentication_uri'] as String),
redirectUri: Uri.parse(json['redirect_uri'] as String),
codeVerifier: json['code_verifier'] as String,
state: json['state'] as String,
);
}