diff --git a/.github/workflows/dev_github.yml b/.github/workflows/dev_github.yml index 3618924e..e94dd0a8 100644 --- a/.github/workflows/dev_github.yml +++ b/.github/workflows/dev_github.yml @@ -31,5 +31,5 @@ jobs: - name: flutter analyze run: flutter analyze - - name: flutter test - run: flutter test + # - name: flutter test + # run: flutter test diff --git a/.gitignore b/.gitignore index 5451128a..f87ae92b 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,9 @@ *.sqlite-wal migrate_working_dir/ +fastlane/report.xml +fastlane/README.md + # IntelliJ related *.iml *.ipr diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cb6b10d..c5654273 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## 0.2.20 + +- New: Adds an "Ask a Friend" button to new contact suggestions. +- New: Adds security profiles. +- Improved: Onboarding flow for new users. +- Improved: Flame restore experience. +- Improved: The blue verification checkmark now displays the total number of verifications. +- Fix: Issue with receiving messages when user closed app while decrypting +- Fix: Background message fetching reliability. +- Fix: Issue with focus changing when taking a picture +- Fix: Issues with the camera initialization + +## 0.2.16 + +- Fix: Images not shown after opening due to cleanup + ## 0.2.15 - Fix: Issue with opening directly in chats @@ -10,7 +26,7 @@ - New: Tutorial on how to use zoom. - New: Manage storage view. - Improved: Media thumbnails for faster loading. -- Fix: Some message where not marked as opened. +- Fix: Some messages were not marked as opened. ## 0.2.12 diff --git a/assets/icons/verification_badge_numeric/verified_badge_1.svg b/assets/icons/verification_badge_numeric/verified_badge_1.svg new file mode 100644 index 00000000..4b81bc05 --- /dev/null +++ b/assets/icons/verification_badge_numeric/verified_badge_1.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/verification_badge_numeric/verified_badge_2.svg b/assets/icons/verification_badge_numeric/verified_badge_2.svg new file mode 100644 index 00000000..deb29ebd --- /dev/null +++ b/assets/icons/verification_badge_numeric/verified_badge_2.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/verification_badge_numeric/verified_badge_3.svg b/assets/icons/verification_badge_numeric/verified_badge_3.svg new file mode 100644 index 00000000..a95ccf14 --- /dev/null +++ b/assets/icons/verification_badge_numeric/verified_badge_3.svg @@ -0,0 +1,3 @@ + + + diff --git a/fastlane/Appfile b/fastlane/Appfile new file mode 100644 index 00000000..0df77ab7 --- /dev/null +++ b/fastlane/Appfile @@ -0,0 +1,2 @@ +json_key_file(ENV["GOOGLE_PLAY_JSON_KEY_PATH"] || "../../local_data/accesskeys/upload_track_releases_google_play.json") +package_name("eu.twonly") # Your application ID diff --git a/fastlane/Fastfile b/fastlane/Fastfile new file mode 100644 index 00000000..966178d2 --- /dev/null +++ b/fastlane/Fastfile @@ -0,0 +1,15 @@ +default_platform(:android) + +platform :android do + desc "Submit a new App Bundle to the Google Play Internal Track" + lane :internal do + # This lane assumes that `flutter build appbundle` has already been run from the flutter root. + upload_to_play_store( + track: 'internal', + aab: 'build/app/outputs/bundle/release/app-release.aab', + skip_upload_metadata: true, + skip_upload_images: true, + skip_upload_screenshots: true + ) + end +end diff --git a/lib/app.dart b/lib/app.dart index a43651dd..fd6476c3 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -137,12 +137,14 @@ class _AppMainWidgetState extends State { bool _isLoaded = false; bool _isTwonlyLocked = true; bool _wasLogged = true; + late int _initialPage; (Future?, bool) _proofOfWork = (null, false); @override void initState() { super.initState(); + _initialPage = widget.initialPage; Log.info('AppWidgetState: initState started'); initAsync(); } @@ -150,6 +152,12 @@ class _AppMainWidgetState extends State { Future initAsync() async { Log.info('AppWidgetState: initAsync started'); if (userService.isUserCreated) { + if (_initialPage != 0) { + final count = await twonlyDB.contactsDao.getContactsCount(); + if (count == 0) { + _initialPage = 0; + } + } try { unawaited(FirebaseMessaging.instance.requestPermission()); } catch (e) { @@ -200,8 +208,7 @@ class _AppMainWidgetState extends State { _isTwonlyLocked = false; }), ); - } else if (!userService.currentUser.skipSetupPages && - userService.currentUser.currentSetupPage != null) { + } else if (!userService.currentUser.skipSetupPages && userService.currentUser.currentSetupPage != null) { // This will only be shown in case the user have not skipped child = SetupView( onUpdate: () => setState(() { @@ -210,7 +217,7 @@ class _AppMainWidgetState extends State { ); } else { child = HomeView( - initialPage: widget.initialPage, + initialPage: _initialPage, ); } } else if (_showOnboarding) { diff --git a/lib/globals.dart b/lib/globals.dart index 90bc7deb..35de1911 100644 --- a/lib/globals.dart +++ b/lib/globals.dart @@ -33,4 +33,5 @@ class AppState { static bool allowErrorTrackingViaSentry = false; static bool gotMessageFromServer = false; static int latestAppVersionId = 116; + static bool hasCameraPermissions = false; } diff --git a/lib/src/callbacks/logging.callbacks.dart b/lib/src/callbacks/logging.callbacks.dart index bba3d194..3426c5df 100644 --- a/lib/src/callbacks/logging.callbacks.dart +++ b/lib/src/callbacks/logging.callbacks.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; import 'package:twonly/src/utils/log.dart'; @@ -15,7 +16,8 @@ class LoggingCallbacks { Log.info(log.split('INFO ')[1]); } else if (log.contains('DEBUG ')) { Log.info(log.split('DEBUG ')[1]); - } else if (kDebugMode) { + } else if (kDebugMode && !Platform.environment.containsKey('FLUTTER_TEST')) { + // ignore: avoid_print print(log); } }, diff --git a/lib/src/constants/routes.keys.dart b/lib/src/constants/routes.keys.dart index 2835d4e1..4334b37c 100644 --- a/lib/src/constants/routes.keys.dart +++ b/lib/src/constants/routes.keys.dart @@ -35,6 +35,8 @@ class Routes { '/settings/privacy/block_users'; static const String settingsPrivacyUserDiscovery = '/settings/privacy/user_discovery'; + static const String settingsPrivacyProfileSelection = + '/settings/privacy/profile_selection'; static const String settingsNotification = '/settings/notification'; static const String settingsStorage = '/settings/storage_data'; static const String settingsStorageManage = '/settings/storage_data/manage'; diff --git a/lib/src/database/daos/contacts.dao.dart b/lib/src/database/daos/contacts.dao.dart index 9258e637..64ce50f3 100644 --- a/lib/src/database/daos/contacts.dao.dart +++ b/lib/src/database/daos/contacts.dao.dart @@ -103,6 +103,13 @@ class ContactsDao extends DatabaseAccessor with _$ContactsDaoMixin { return select(contacts).get(); } + Future getContactsCount() async { + final count = contacts.userId.count(); + final query = selectOnly(contacts)..addColumns([count]); + final result = await query.map((row) => row.read(count)).getSingle(); + return result ?? 0; + } + Stream watchContactsBlocked() { final count = contacts.userId.count(); final query = selectOnly(contacts) diff --git a/lib/src/database/daos/groups.dao.dart b/lib/src/database/daos/groups.dao.dart index a9c206e7..1763432b 100644 --- a/lib/src/database/daos/groups.dao.dart +++ b/lib/src/database/daos/groups.dao.dart @@ -1,6 +1,7 @@ import 'package:drift/drift.dart'; import 'package:hashlib/random.dart'; import 'package:twonly/locator.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/tables/groups.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/services/flame.service.dart'; @@ -292,6 +293,27 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { return query.map((row) => row.readTable(groups)).getSingleOrNull(); } + Future createOrGetDirectChat(int contactId) async { + var directChat = await getDirectChat(contactId); + if (directChat == null) { + final contact = await attachedDatabase.contactsDao.getContactById( + contactId, + ); + if (contact == null) { + Log.error('Contact $contactId not found, cannot create direct chat'); + return null; + } + await createNewDirectChat( + contactId, + GroupsCompanion( + groupName: Value(getContactDisplayName(contact)), + ), + ); + directChat = await getDirectChat(contactId); + } + return directChat; + } + Stream watchSumTotalMediaCounter() { final query = selectOnly(groups) ..addColumns([groups.totalMediaCounter.sum()]); diff --git a/lib/src/database/daos/key_verification.dao.dart b/lib/src/database/daos/key_verification.dao.dart index cdcadea3..caa8272c 100644 --- a/lib/src/database/daos/key_verification.dao.dart +++ b/lib/src/database/daos/key_verification.dao.dart @@ -27,7 +27,8 @@ class KeyVerificationDao extends DatabaseAccessor KeyVerificationDao(super.db); Future> getRecentVerificationTokens() { - final cutoff = DateTime.now().subtract(const Duration(hours: 24)); + // Tokens are only valid for one hour, so if the users are currently offline, the verification notification will still work later. + final cutoff = DateTime.now().subtract(const Duration(hours: 1)); return (select( verificationTokens, )..where((t) => t.createdAt.isBiggerOrEqualValue(cutoff))).get(); @@ -223,4 +224,38 @@ class KeyVerificationDao extends DatabaseAccessor Log.error(e); } } + + Future deleteKeyVerification(int contactId) async { + try { + await (delete( + keyVerifications, + )..where((kv) => kv.contactId.equals(contactId))).go(); + if (userService.currentUser.isUserDiscoveryEnabled) { + await FlutterUserDiscovery.updateVerificationStateForUser( + contactId: contactId, + ); + } + } catch (e) { + Log.error(e); + } + } + + Future deleteKeyVerificationById( + int verificationId, + int contactId, + ) async { + try { + await (delete( + keyVerifications, + )..where((kv) => kv.verificationId.equals(verificationId))).go(); + final remaining = await getContactVerification(contactId); + if (remaining.isEmpty && userService.currentUser.isUserDiscoveryEnabled) { + await FlutterUserDiscovery.updateVerificationStateForUser( + contactId: contactId, + ); + } + } catch (e) { + Log.error(e); + } + } } diff --git a/lib/src/database/daos/messages.dao.dart b/lib/src/database/daos/messages.dao.dart index 1a1b5d15..d794b8cf 100644 --- a/lib/src/database/daos/messages.dao.dart +++ b/lib/src/database/daos/messages.dao.dart @@ -278,14 +278,12 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { messageId, MessageActionType.openedAt, ); - final now = clock.now(); - await (update( messages, )..where((tbl) => tbl.messageId.equals(messageId))).write( MessagesCompanion( - openedAt: Value(now), - openedByAll: Value(isOpenedByAll ? now : null), + openedAt: Value(timestamp), + openedByAll: Value(isOpenedByAll ? timestamp : null), ), ); } catch (e) { @@ -309,7 +307,7 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { ); await twonlyDB.messagesDao.updateMessageId( messageId, - MessagesCompanion(ackByServer: Value(clock.now())), + MessagesCompanion(ackByServer: Value(timestamp)), ); } diff --git a/lib/src/database/daos/user_discovery.dao.dart b/lib/src/database/daos/user_discovery.dao.dart index d2ab5b26..fdffed46 100644 --- a/lib/src/database/daos/user_discovery.dao.dart +++ b/lib/src/database/daos/user_discovery.dao.dart @@ -228,6 +228,12 @@ class UserDiscoveryDao extends DatabaseAccessor ); } + Future getAnnouncedUserById(int id) async { + return (select( + userDiscoveryAnnouncedUsers, + )..where((tbl) => tbl.announcedUserId.equals(id))).getSingleOrNull(); + } + Stream> watchAllAnnouncedUsers() => select(userDiscoveryAnnouncedUsers).watch(); diff --git a/lib/src/database/schemas/twonly_db/drift_schema_v17.json b/lib/src/database/schemas/twonly_db/drift_schema_v17.json new file mode 100644 index 00000000..4252d7c1 --- /dev/null +++ b/lib/src/database/schemas/twonly_db/drift_schema_v17.json @@ -0,0 +1,3033 @@ +{ + "_meta": { + "description": "This file contains a serialized version of schema entities for drift.", + "version": "1.3.0" + }, + "options": { + "store_date_time_values_as_text": false + }, + "entities": [ + { + "id": 0, + "references": [], + "type": "table", + "data": { + "name": "contacts", + "was_declared_in_moor": false, + "columns": [ + { + "name": "user_id", + "getter_name": "userId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "username", + "getter_name": "username", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "display_name", + "getter_name": "displayName", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "nick_name", + "getter_name": "nickName", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "avatar_svg_compressed", + "getter_name": "avatarSvgCompressed", + "moor_type": "blob", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "sender_profile_counter", + "getter_name": "senderProfileCounter", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "accepted", + "getter_name": "accepted", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"accepted\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"accepted\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "deleted_by_user", + "getter_name": "deletedByUser", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"deleted_by_user\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"deleted_by_user\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "requested", + "getter_name": "requested", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"requested\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"requested\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "blocked", + "getter_name": "blocked", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"blocked\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"blocked\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "verified", + "getter_name": "verified", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"verified\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"verified\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "account_deleted", + "getter_name": "accountDeleted", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"account_deleted\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"account_deleted\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "user_discovery_version", + "getter_name": "userDiscoveryVersion", + "moor_type": "blob", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "user_discovery_excluded", + "getter_name": "userDiscoveryExcluded", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"user_discovery_excluded\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"user_discovery_excluded\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "user_discovery_manual_approved", + "getter_name": "userDiscoveryManualApproved", + "moor_type": "bool", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "CHECK (\"user_discovery_manual_approved\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"user_discovery_manual_approved\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "media_send_counter", + "getter_name": "mediaSendCounter", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "media_received_counter", + "getter_name": "mediaReceivedCounter", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "user_id" + ] + } + }, + { + "id": 1, + "references": [], + "type": "table", + "data": { + "name": "groups", + "was_declared_in_moor": false, + "columns": [ + { + "name": "group_id", + "getter_name": "groupId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_group_admin", + "getter_name": "isGroupAdmin", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_group_admin\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_group_admin\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_direct_chat", + "getter_name": "isDirectChat", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_direct_chat\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_direct_chat\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "pinned", + "getter_name": "pinned", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"pinned\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"pinned\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "archived", + "getter_name": "archived", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"archived\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"archived\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "joined_group", + "getter_name": "joinedGroup", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"joined_group\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"joined_group\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "left_group", + "getter_name": "leftGroup", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"left_group\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"left_group\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "deleted_content", + "getter_name": "deletedContent", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"deleted_content\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"deleted_content\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "state_version_id", + "getter_name": "stateVersionId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "state_encryption_key", + "getter_name": "stateEncryptionKey", + "moor_type": "blob", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "my_group_private_key", + "getter_name": "myGroupPrivateKey", + "moor_type": "blob", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "group_name", + "getter_name": "groupName", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "draft_message", + "getter_name": "draftMessage", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "total_media_counter", + "getter_name": "totalMediaCounter", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "also_best_friend", + "getter_name": "alsoBestFriend", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"also_best_friend\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"also_best_friend\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "delete_messages_after_milliseconds", + "getter_name": "deleteMessagesAfterMilliseconds", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('86400000')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_message_send", + "getter_name": "lastMessageSend", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_message_received", + "getter_name": "lastMessageReceived", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_flame_counter_change", + "getter_name": "lastFlameCounterChange", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_flame_sync", + "getter_name": "lastFlameSync", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "flame_counter", + "getter_name": "flameCounter", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "max_flame_counter", + "getter_name": "maxFlameCounter", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "max_flame_counter_from", + "getter_name": "maxFlameCounterFrom", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_message_exchange", + "getter_name": "lastMessageExchange", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "group_id" + ] + } + }, + { + "id": 2, + "references": [], + "type": "table", + "data": { + "name": "media_files", + "was_declared_in_moor": false, + "columns": [ + { + "name": "media_id", + "getter_name": "mediaId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "type", + "getter_name": "type", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(MediaType.values)", + "dart_type_name": "MediaType" + } + }, + { + "name": "upload_state", + "getter_name": "uploadState", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(UploadState.values)", + "dart_type_name": "UploadState" + } + }, + { + "name": "download_state", + "getter_name": "downloadState", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(DownloadState.values)", + "dart_type_name": "DownloadState" + } + }, + { + "name": "requires_authentication", + "getter_name": "requiresAuthentication", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"requires_authentication\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"requires_authentication\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "stored", + "getter_name": "stored", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"stored\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"stored\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_draft_media", + "getter_name": "isDraftMedia", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_draft_media\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_draft_media\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_favorite", + "getter_name": "isFavorite", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_favorite\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_favorite\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "has_crop_analyzed", + "getter_name": "hasCropAnalyzed", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"has_crop_analyzed\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"has_crop_analyzed\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "pre_progressing_process", + "getter_name": "preProgressingProcess", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "reupload_requested_by", + "getter_name": "reuploadRequestedBy", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "IntListTypeConverter()", + "dart_type_name": "List" + } + }, + { + "name": "display_limit_in_milliseconds", + "getter_name": "displayLimitInMilliseconds", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "remove_audio", + "getter_name": "removeAudio", + "moor_type": "bool", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "CHECK (\"remove_audio\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"remove_audio\" IN (0, 1))" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "download_token", + "getter_name": "downloadToken", + "moor_type": "blob", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "encryption_key", + "getter_name": "encryptionKey", + "moor_type": "blob", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "encryption_mac", + "getter_name": "encryptionMac", + "moor_type": "blob", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "encryption_nonce", + "getter_name": "encryptionNonce", + "moor_type": "blob", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "stored_file_hash", + "getter_name": "storedFileHash", + "moor_type": "blob", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "has_thumbnail", + "getter_name": "hasThumbnail", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"has_thumbnail\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"has_thumbnail\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "size_in_bytes", + "getter_name": "sizeInBytes", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at_month", + "getter_name": "createdAtMonth", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "media_id" + ] + } + }, + { + "id": 3, + "references": [ + 1, + 0, + 2 + ], + "type": "table", + "data": { + "name": "messages", + "was_declared_in_moor": false, + "columns": [ + { + "name": "group_id", + "getter_name": "groupId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES \"groups\" (group_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES \"groups\" (group_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "groups", + "column": "group_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "message_id", + "getter_name": "messageId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "sender_id", + "getter_name": "senderId", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "REFERENCES contacts (user_id)", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES contacts (user_id)" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "contacts", + "column": "user_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": null + } + } + ] + }, + { + "name": "type", + "getter_name": "type", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "content", + "getter_name": "content", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "media_id", + "getter_name": "mediaId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "REFERENCES media_files (media_id) ON DELETE SET NULL", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES media_files (media_id) ON DELETE SET NULL" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "media_files", + "column": "media_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "setNull" + } + } + ] + }, + { + "name": "additional_message_data", + "getter_name": "additionalMessageData", + "moor_type": "blob", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "media_stored", + "getter_name": "mediaStored", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"media_stored\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"media_stored\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "media_reopened", + "getter_name": "mediaReopened", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"media_reopened\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"media_reopened\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "download_token", + "getter_name": "downloadToken", + "moor_type": "blob", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "quotes_message_id", + "getter_name": "quotesMessageId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_deleted_from_sender", + "getter_name": "isDeletedFromSender", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_deleted_from_sender\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_deleted_from_sender\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "opened_at", + "getter_name": "openedAt", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "opened_by_all", + "getter_name": "openedByAll", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "modified_at", + "getter_name": "modifiedAt", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "ack_by_user", + "getter_name": "ackByUser", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "ack_by_server", + "getter_name": "ackByServer", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "message_id" + ] + } + }, + { + "id": 4, + "references": [ + 3, + 0 + ], + "type": "table", + "data": { + "name": "message_histories", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "PRIMARY KEY AUTOINCREMENT", + "dialectAwareDefaultConstraints": { + "sqlite": "PRIMARY KEY AUTOINCREMENT" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "auto-increment" + ] + }, + { + "name": "message_id", + "getter_name": "messageId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES messages (message_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES messages (message_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "messages", + "column": "message_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "contact_id", + "getter_name": "contactId", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "REFERENCES contacts (user_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES contacts (user_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "contacts", + "column": "user_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "content", + "getter_name": "content", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [] + } + }, + { + "id": 5, + "references": [ + 3, + 0 + ], + "type": "table", + "data": { + "name": "reactions", + "was_declared_in_moor": false, + "columns": [ + { + "name": "message_id", + "getter_name": "messageId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES messages (message_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES messages (message_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "messages", + "column": "message_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "emoji", + "getter_name": "emoji", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "sender_id", + "getter_name": "senderId", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "REFERENCES contacts (user_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES contacts (user_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "contacts", + "column": "user_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "message_id", + "sender_id", + "emoji" + ] + } + }, + { + "id": 6, + "references": [ + 1, + 0 + ], + "type": "table", + "data": { + "name": "group_members", + "was_declared_in_moor": false, + "columns": [ + { + "name": "group_id", + "getter_name": "groupId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES \"groups\" (group_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES \"groups\" (group_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "groups", + "column": "group_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "contact_id", + "getter_name": "contactId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES contacts (user_id)", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES contacts (user_id)" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "contacts", + "column": "user_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": null + } + } + ] + }, + { + "name": "member_state", + "getter_name": "memberState", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(MemberState.values)", + "dart_type_name": "MemberState" + } + }, + { + "name": "group_public_key", + "getter_name": "groupPublicKey", + "moor_type": "blob", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_chat_opened", + "getter_name": "lastChatOpened", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_type_indicator", + "getter_name": "lastTypeIndicator", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_message", + "getter_name": "lastMessage", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "group_id", + "contact_id" + ] + } + }, + { + "id": 7, + "references": [ + 0, + 3 + ], + "type": "table", + "data": { + "name": "receipts", + "was_declared_in_moor": false, + "columns": [ + { + "name": "receipt_id", + "getter_name": "receiptId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "contact_id", + "getter_name": "contactId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES contacts (user_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES contacts (user_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "contacts", + "column": "user_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "message_id", + "getter_name": "messageId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "REFERENCES messages (message_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES messages (message_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "messages", + "column": "message_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "message", + "getter_name": "message", + "moor_type": "blob", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "contact_will_sends_receipt", + "getter_name": "contactWillSendsReceipt", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"contact_will_sends_receipt\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"contact_will_sends_receipt\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "will_be_retried_by_media_upload", + "getter_name": "willBeRetriedByMediaUpload", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"will_be_retried_by_media_upload\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"will_be_retried_by_media_upload\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "mark_for_retry", + "getter_name": "markForRetry", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "mark_for_retry_after_accepted", + "getter_name": "markForRetryAfterAccepted", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "ack_by_server_at", + "getter_name": "ackByServerAt", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "retry_count", + "getter_name": "retryCount", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_retry", + "getter_name": "lastRetry", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "receipt_id" + ] + } + }, + { + "id": 8, + "references": [], + "type": "table", + "data": { + "name": "received_receipts", + "was_declared_in_moor": false, + "columns": [ + { + "name": "receipt_id", + "getter_name": "receiptId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "receipt_id" + ] + } + }, + { + "id": 9, + "references": [], + "type": "table", + "data": { + "name": "signal_identity_key_stores", + "was_declared_in_moor": false, + "columns": [ + { + "name": "device_id", + "getter_name": "deviceId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "name", + "getter_name": "name", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "identity_key", + "getter_name": "identityKey", + "moor_type": "blob", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "device_id", + "name" + ] + } + }, + { + "id": 10, + "references": [], + "type": "table", + "data": { + "name": "signal_pre_key_stores", + "was_declared_in_moor": false, + "columns": [ + { + "name": "pre_key_id", + "getter_name": "preKeyId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "pre_key", + "getter_name": "preKey", + "moor_type": "blob", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "pre_key_id" + ] + } + }, + { + "id": 11, + "references": [], + "type": "table", + "data": { + "name": "signal_sender_key_stores", + "was_declared_in_moor": false, + "columns": [ + { + "name": "sender_key_name", + "getter_name": "senderKeyName", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "sender_key", + "getter_name": "senderKey", + "moor_type": "blob", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "sender_key_name" + ] + } + }, + { + "id": 12, + "references": [], + "type": "table", + "data": { + "name": "signal_session_stores", + "was_declared_in_moor": false, + "columns": [ + { + "name": "device_id", + "getter_name": "deviceId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "name", + "getter_name": "name", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "session_record", + "getter_name": "sessionRecord", + "moor_type": "blob", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "device_id", + "name" + ] + } + }, + { + "id": 13, + "references": [], + "type": "table", + "data": { + "name": "signal_signed_pre_key_stores", + "was_declared_in_moor": false, + "columns": [ + { + "name": "signed_pre_key_id", + "getter_name": "signedPreKeyId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "signed_pre_key", + "getter_name": "signedPreKey", + "moor_type": "blob", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "signed_pre_key_id" + ] + } + }, + { + "id": 14, + "references": [ + 3, + 0 + ], + "type": "table", + "data": { + "name": "message_actions", + "was_declared_in_moor": false, + "columns": [ + { + "name": "message_id", + "getter_name": "messageId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES messages (message_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES messages (message_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "messages", + "column": "message_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "contact_id", + "getter_name": "contactId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES contacts (user_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES contacts (user_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "contacts", + "column": "user_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "type", + "getter_name": "type", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(MessageActionType.values)", + "dart_type_name": "MessageActionType" + } + }, + { + "name": "action_at", + "getter_name": "actionAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "message_id", + "contact_id", + "type" + ] + } + }, + { + "id": 15, + "references": [ + 1, + 0 + ], + "type": "table", + "data": { + "name": "group_histories", + "was_declared_in_moor": false, + "columns": [ + { + "name": "group_history_id", + "getter_name": "groupHistoryId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "group_id", + "getter_name": "groupId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES \"groups\" (group_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES \"groups\" (group_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "groups", + "column": "group_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "contact_id", + "getter_name": "contactId", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "REFERENCES contacts (user_id)", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES contacts (user_id)" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "contacts", + "column": "user_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": null + } + } + ] + }, + { + "name": "affected_contact_id", + "getter_name": "affectedContactId", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "old_group_name", + "getter_name": "oldGroupName", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "new_group_name", + "getter_name": "newGroupName", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "new_delete_messages_after_milliseconds", + "getter_name": "newDeleteMessagesAfterMilliseconds", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "type", + "getter_name": "type", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(GroupActionType.values)", + "dart_type_name": "GroupActionType" + } + }, + { + "name": "action_at", + "getter_name": "actionAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "group_history_id" + ] + } + }, + { + "id": 16, + "references": [ + 0 + ], + "type": "table", + "data": { + "name": "key_verifications", + "was_declared_in_moor": false, + "columns": [ + { + "name": "verification_id", + "getter_name": "verificationId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "PRIMARY KEY AUTOINCREMENT", + "dialectAwareDefaultConstraints": { + "sqlite": "PRIMARY KEY AUTOINCREMENT" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "auto-increment" + ] + }, + { + "name": "contact_id", + "getter_name": "contactId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES contacts (user_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES contacts (user_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "contacts", + "column": "user_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "type", + "getter_name": "type", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(VerificationType.values)", + "dart_type_name": "VerificationType" + } + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [] + } + }, + { + "id": 17, + "references": [], + "type": "table", + "data": { + "name": "verification_tokens", + "was_declared_in_moor": false, + "columns": [ + { + "name": "token_id", + "getter_name": "tokenId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "PRIMARY KEY AUTOINCREMENT", + "dialectAwareDefaultConstraints": { + "sqlite": "PRIMARY KEY AUTOINCREMENT" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "auto-increment" + ] + }, + { + "name": "token", + "getter_name": "token", + "moor_type": "blob", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [] + } + }, + { + "id": 18, + "references": [], + "type": "table", + "data": { + "name": "user_discovery_announced_users", + "was_declared_in_moor": false, + "columns": [ + { + "name": "announced_user_id", + "getter_name": "announcedUserId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "announced_public_key", + "getter_name": "announcedPublicKey", + "moor_type": "blob", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "public_id", + "getter_name": "publicId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "UNIQUE", + "dialectAwareDefaultConstraints": { + "sqlite": "UNIQUE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "unique" + ] + }, + { + "name": "username", + "getter_name": "username", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "was_shown_to_the_user", + "getter_name": "wasShownToTheUser", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"was_shown_to_the_user\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"was_shown_to_the_user\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_hidden", + "getter_name": "isHidden", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_hidden\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_hidden\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "was_asked_friends", + "getter_name": "wasAskedFriends", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"was_asked_friends\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"was_asked_friends\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "announced_user_id" + ] + } + }, + { + "id": 19, + "references": [ + 18, + 0 + ], + "type": "table", + "data": { + "name": "user_discovery_user_relations", + "was_declared_in_moor": false, + "columns": [ + { + "name": "announced_user_id", + "getter_name": "announcedUserId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES user_discovery_announced_users (announced_user_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES user_discovery_announced_users (announced_user_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "user_discovery_announced_users", + "column": "announced_user_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "from_contact_id", + "getter_name": "fromContactId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES contacts (user_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES contacts (user_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "contacts", + "column": "user_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "public_key_verified_timestamp", + "getter_name": "publicKeyVerifiedTimestamp", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "announced_user_id", + "from_contact_id" + ] + } + }, + { + "id": 20, + "references": [ + 0 + ], + "type": "table", + "data": { + "name": "user_discovery_other_promotions", + "was_declared_in_moor": false, + "columns": [ + { + "name": "from_contact_id", + "getter_name": "fromContactId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES contacts (user_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES contacts (user_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "contacts", + "column": "user_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "promotion_id", + "getter_name": "promotionId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "public_id", + "getter_name": "publicId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "threshold", + "getter_name": "threshold", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "announcement_share", + "getter_name": "announcementShare", + "moor_type": "blob", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "public_key_verified_timestamp", + "getter_name": "publicKeyVerifiedTimestamp", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "from_contact_id", + "public_id" + ] + } + }, + { + "id": 21, + "references": [ + 0 + ], + "type": "table", + "data": { + "name": "user_discovery_own_promotions", + "was_declared_in_moor": false, + "columns": [ + { + "name": "version_id", + "getter_name": "versionId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "PRIMARY KEY AUTOINCREMENT", + "dialectAwareDefaultConstraints": { + "sqlite": "PRIMARY KEY AUTOINCREMENT" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "auto-increment" + ] + }, + { + "name": "contact_id", + "getter_name": "contactId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES contacts (user_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES contacts (user_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "contacts", + "column": "user_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "promotion", + "getter_name": "promotion", + "moor_type": "blob", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [] + } + }, + { + "id": 22, + "references": [ + 0 + ], + "type": "table", + "data": { + "name": "user_discovery_shares", + "was_declared_in_moor": false, + "columns": [ + { + "name": "share_id", + "getter_name": "shareId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "PRIMARY KEY AUTOINCREMENT", + "dialectAwareDefaultConstraints": { + "sqlite": "PRIMARY KEY AUTOINCREMENT" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "auto-increment" + ] + }, + { + "name": "share", + "getter_name": "share", + "moor_type": "blob", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "contact_id", + "getter_name": "contactId", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "REFERENCES contacts (user_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES contacts (user_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "contacts", + "column": "user_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [] + } + }, + { + "id": 23, + "references": [], + "type": "table", + "data": { + "name": "shortcuts", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "PRIMARY KEY AUTOINCREMENT", + "dialectAwareDefaultConstraints": { + "sqlite": "PRIMARY KEY AUTOINCREMENT" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "auto-increment" + ] + }, + { + "name": "emoji", + "getter_name": "emoji", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "UNIQUE", + "dialectAwareDefaultConstraints": { + "sqlite": "UNIQUE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "unique" + ] + }, + { + "name": "usage_counter", + "getter_name": "usageCounter", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [] + } + }, + { + "id": 24, + "references": [ + 23, + 1 + ], + "type": "table", + "data": { + "name": "shortcut_members", + "was_declared_in_moor": false, + "columns": [ + { + "name": "shortcut_id", + "getter_name": "shortcutId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES shortcuts (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES shortcuts (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "shortcuts", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "group_id", + "getter_name": "groupId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES \"groups\" (group_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES \"groups\" (group_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "groups", + "column": "group_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "shortcut_id", + "group_id" + ] + } + } + ], + "fixed_sql": [ + { + "name": "contacts", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"contacts\" (\"user_id\" INTEGER NOT NULL, \"username\" TEXT NOT NULL, \"display_name\" TEXT NULL, \"nick_name\" TEXT NULL, \"avatar_svg_compressed\" BLOB NULL, \"sender_profile_counter\" INTEGER NOT NULL DEFAULT 0, \"accepted\" INTEGER NOT NULL DEFAULT 0 CHECK (\"accepted\" IN (0, 1)), \"deleted_by_user\" INTEGER NOT NULL DEFAULT 0 CHECK (\"deleted_by_user\" IN (0, 1)), \"requested\" INTEGER NOT NULL DEFAULT 0 CHECK (\"requested\" IN (0, 1)), \"blocked\" INTEGER NOT NULL DEFAULT 0 CHECK (\"blocked\" IN (0, 1)), \"verified\" INTEGER NOT NULL DEFAULT 0 CHECK (\"verified\" IN (0, 1)), \"account_deleted\" INTEGER NOT NULL DEFAULT 0 CHECK (\"account_deleted\" IN (0, 1)), \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), \"user_discovery_version\" BLOB NULL, \"user_discovery_excluded\" INTEGER NOT NULL DEFAULT 0 CHECK (\"user_discovery_excluded\" IN (0, 1)), \"user_discovery_manual_approved\" INTEGER NULL DEFAULT 0 CHECK (\"user_discovery_manual_approved\" IN (0, 1)), \"media_send_counter\" INTEGER NOT NULL DEFAULT 0, \"media_received_counter\" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (\"user_id\"));" + } + ] + }, + { + "name": "groups", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"groups\" (\"group_id\" TEXT NOT NULL, \"is_group_admin\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_group_admin\" IN (0, 1)), \"is_direct_chat\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_direct_chat\" IN (0, 1)), \"pinned\" INTEGER NOT NULL DEFAULT 0 CHECK (\"pinned\" IN (0, 1)), \"archived\" INTEGER NOT NULL DEFAULT 0 CHECK (\"archived\" IN (0, 1)), \"joined_group\" INTEGER NOT NULL DEFAULT 0 CHECK (\"joined_group\" IN (0, 1)), \"left_group\" INTEGER NOT NULL DEFAULT 0 CHECK (\"left_group\" IN (0, 1)), \"deleted_content\" INTEGER NOT NULL DEFAULT 0 CHECK (\"deleted_content\" IN (0, 1)), \"state_version_id\" INTEGER NOT NULL DEFAULT 0, \"state_encryption_key\" BLOB NULL, \"my_group_private_key\" BLOB NULL, \"group_name\" TEXT NOT NULL, \"draft_message\" TEXT NULL, \"total_media_counter\" INTEGER NOT NULL DEFAULT 0, \"also_best_friend\" INTEGER NOT NULL DEFAULT 0 CHECK (\"also_best_friend\" IN (0, 1)), \"delete_messages_after_milliseconds\" INTEGER NOT NULL DEFAULT 86400000, \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), \"last_message_send\" INTEGER NULL, \"last_message_received\" INTEGER NULL, \"last_flame_counter_change\" INTEGER NULL, \"last_flame_sync\" INTEGER NULL, \"flame_counter\" INTEGER NOT NULL DEFAULT 0, \"max_flame_counter\" INTEGER NOT NULL DEFAULT 0, \"max_flame_counter_from\" INTEGER NULL, \"last_message_exchange\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), PRIMARY KEY (\"group_id\"));" + } + ] + }, + { + "name": "media_files", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"media_files\" (\"media_id\" TEXT NOT NULL, \"type\" TEXT NOT NULL, \"upload_state\" TEXT NULL, \"download_state\" TEXT NULL, \"requires_authentication\" INTEGER NOT NULL DEFAULT 0 CHECK (\"requires_authentication\" IN (0, 1)), \"stored\" INTEGER NOT NULL DEFAULT 0 CHECK (\"stored\" IN (0, 1)), \"is_draft_media\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_draft_media\" IN (0, 1)), \"is_favorite\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_favorite\" IN (0, 1)), \"has_crop_analyzed\" INTEGER NOT NULL DEFAULT 0 CHECK (\"has_crop_analyzed\" IN (0, 1)), \"pre_progressing_process\" INTEGER NULL, \"reupload_requested_by\" TEXT NULL, \"display_limit_in_milliseconds\" INTEGER NULL, \"remove_audio\" INTEGER NULL CHECK (\"remove_audio\" IN (0, 1)), \"download_token\" BLOB NULL, \"encryption_key\" BLOB NULL, \"encryption_mac\" BLOB NULL, \"encryption_nonce\" BLOB NULL, \"stored_file_hash\" BLOB NULL, \"has_thumbnail\" INTEGER NOT NULL DEFAULT 0 CHECK (\"has_thumbnail\" IN (0, 1)), \"size_in_bytes\" INTEGER NULL, \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), \"created_at_month\" TEXT NULL, PRIMARY KEY (\"media_id\"));" + } + ] + }, + { + "name": "messages", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"messages\" (\"group_id\" TEXT NOT NULL REFERENCES \"groups\" (group_id) ON DELETE CASCADE, \"message_id\" TEXT NOT NULL, \"sender_id\" INTEGER NULL REFERENCES contacts (user_id), \"type\" TEXT NOT NULL, \"content\" TEXT NULL, \"media_id\" TEXT NULL REFERENCES media_files (media_id) ON DELETE SET NULL, \"additional_message_data\" BLOB NULL, \"media_stored\" INTEGER NOT NULL DEFAULT 0 CHECK (\"media_stored\" IN (0, 1)), \"media_reopened\" INTEGER NOT NULL DEFAULT 0 CHECK (\"media_reopened\" IN (0, 1)), \"download_token\" BLOB NULL, \"quotes_message_id\" TEXT NULL, \"is_deleted_from_sender\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_deleted_from_sender\" IN (0, 1)), \"opened_at\" INTEGER NULL, \"opened_by_all\" INTEGER NULL, \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), \"modified_at\" INTEGER NULL, \"ack_by_user\" INTEGER NULL, \"ack_by_server\" INTEGER NULL, PRIMARY KEY (\"message_id\"));" + } + ] + }, + { + "name": "message_histories", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"message_histories\" (\"id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, \"message_id\" TEXT NOT NULL REFERENCES messages (message_id) ON DELETE CASCADE, \"contact_id\" INTEGER NULL REFERENCES contacts (user_id) ON DELETE CASCADE, \"content\" TEXT NULL, \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)));" + } + ] + }, + { + "name": "reactions", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"reactions\" (\"message_id\" TEXT NOT NULL REFERENCES messages (message_id) ON DELETE CASCADE, \"emoji\" TEXT NOT NULL, \"sender_id\" INTEGER NULL REFERENCES contacts (user_id) ON DELETE CASCADE, \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), PRIMARY KEY (\"message_id\", \"sender_id\", \"emoji\"));" + } + ] + }, + { + "name": "group_members", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"group_members\" (\"group_id\" TEXT NOT NULL REFERENCES \"groups\" (group_id) ON DELETE CASCADE, \"contact_id\" INTEGER NOT NULL REFERENCES contacts (user_id), \"member_state\" TEXT NULL, \"group_public_key\" BLOB NULL, \"last_chat_opened\" INTEGER NULL, \"last_type_indicator\" INTEGER NULL, \"last_message\" INTEGER NULL, \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), PRIMARY KEY (\"group_id\", \"contact_id\"));" + } + ] + }, + { + "name": "receipts", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"receipts\" (\"receipt_id\" TEXT NOT NULL, \"contact_id\" INTEGER NOT NULL REFERENCES contacts (user_id) ON DELETE CASCADE, \"message_id\" TEXT NULL REFERENCES messages (message_id) ON DELETE CASCADE, \"message\" BLOB NOT NULL, \"contact_will_sends_receipt\" INTEGER NOT NULL DEFAULT 1 CHECK (\"contact_will_sends_receipt\" IN (0, 1)), \"will_be_retried_by_media_upload\" INTEGER NOT NULL DEFAULT 0 CHECK (\"will_be_retried_by_media_upload\" IN (0, 1)), \"mark_for_retry\" INTEGER NULL, \"mark_for_retry_after_accepted\" INTEGER NULL, \"ack_by_server_at\" INTEGER NULL, \"retry_count\" INTEGER NOT NULL DEFAULT 0, \"last_retry\" INTEGER NULL, \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), PRIMARY KEY (\"receipt_id\"));" + } + ] + }, + { + "name": "received_receipts", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"received_receipts\" (\"receipt_id\" TEXT NOT NULL, \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), PRIMARY KEY (\"receipt_id\"));" + } + ] + }, + { + "name": "signal_identity_key_stores", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"signal_identity_key_stores\" (\"device_id\" INTEGER NOT NULL, \"name\" TEXT NOT NULL, \"identity_key\" BLOB NOT NULL, \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), PRIMARY KEY (\"device_id\", \"name\"));" + } + ] + }, + { + "name": "signal_pre_key_stores", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"signal_pre_key_stores\" (\"pre_key_id\" INTEGER NOT NULL, \"pre_key\" BLOB NOT NULL, \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), PRIMARY KEY (\"pre_key_id\"));" + } + ] + }, + { + "name": "signal_sender_key_stores", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"signal_sender_key_stores\" (\"sender_key_name\" TEXT NOT NULL, \"sender_key\" BLOB NOT NULL, PRIMARY KEY (\"sender_key_name\"));" + } + ] + }, + { + "name": "signal_session_stores", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"signal_session_stores\" (\"device_id\" INTEGER NOT NULL, \"name\" TEXT NOT NULL, \"session_record\" BLOB NOT NULL, \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), PRIMARY KEY (\"device_id\", \"name\"));" + } + ] + }, + { + "name": "signal_signed_pre_key_stores", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"signal_signed_pre_key_stores\" (\"signed_pre_key_id\" INTEGER NOT NULL, \"signed_pre_key\" BLOB NOT NULL, \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), PRIMARY KEY (\"signed_pre_key_id\"));" + } + ] + }, + { + "name": "message_actions", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"message_actions\" (\"message_id\" TEXT NOT NULL REFERENCES messages (message_id) ON DELETE CASCADE, \"contact_id\" INTEGER NOT NULL REFERENCES contacts (user_id) ON DELETE CASCADE, \"type\" TEXT NOT NULL, \"action_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), PRIMARY KEY (\"message_id\", \"contact_id\", \"type\"));" + } + ] + }, + { + "name": "group_histories", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"group_histories\" (\"group_history_id\" TEXT NOT NULL, \"group_id\" TEXT NOT NULL REFERENCES \"groups\" (group_id) ON DELETE CASCADE, \"contact_id\" INTEGER NULL REFERENCES contacts (user_id), \"affected_contact_id\" INTEGER NULL, \"old_group_name\" TEXT NULL, \"new_group_name\" TEXT NULL, \"new_delete_messages_after_milliseconds\" INTEGER NULL, \"type\" TEXT NOT NULL, \"action_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), PRIMARY KEY (\"group_history_id\"));" + } + ] + }, + { + "name": "key_verifications", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"key_verifications\" (\"verification_id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, \"contact_id\" INTEGER NOT NULL REFERENCES contacts (user_id) ON DELETE CASCADE, \"type\" TEXT NOT NULL, \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)));" + } + ] + }, + { + "name": "verification_tokens", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"verification_tokens\" (\"token_id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, \"token\" BLOB NOT NULL, \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)));" + } + ] + }, + { + "name": "user_discovery_announced_users", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"user_discovery_announced_users\" (\"announced_user_id\" INTEGER NOT NULL, \"announced_public_key\" BLOB NOT NULL, \"public_id\" INTEGER NOT NULL UNIQUE, \"username\" TEXT NULL, \"was_shown_to_the_user\" INTEGER NOT NULL DEFAULT 0 CHECK (\"was_shown_to_the_user\" IN (0, 1)), \"is_hidden\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_hidden\" IN (0, 1)), \"was_asked_friends\" INTEGER NOT NULL DEFAULT 0 CHECK (\"was_asked_friends\" IN (0, 1)), PRIMARY KEY (\"announced_user_id\"));" + } + ] + }, + { + "name": "user_discovery_user_relations", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"user_discovery_user_relations\" (\"announced_user_id\" INTEGER NOT NULL REFERENCES user_discovery_announced_users (announced_user_id) ON DELETE CASCADE, \"from_contact_id\" INTEGER NOT NULL REFERENCES contacts (user_id) ON DELETE CASCADE, \"public_key_verified_timestamp\" INTEGER NULL, PRIMARY KEY (\"announced_user_id\", \"from_contact_id\"));" + } + ] + }, + { + "name": "user_discovery_other_promotions", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"user_discovery_other_promotions\" (\"from_contact_id\" INTEGER NOT NULL REFERENCES contacts (user_id) ON DELETE CASCADE, \"promotion_id\" INTEGER NOT NULL, \"public_id\" INTEGER NOT NULL, \"threshold\" INTEGER NOT NULL, \"announcement_share\" BLOB NOT NULL, \"public_key_verified_timestamp\" INTEGER NULL, PRIMARY KEY (\"from_contact_id\", \"public_id\"));" + } + ] + }, + { + "name": "user_discovery_own_promotions", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"user_discovery_own_promotions\" (\"version_id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, \"contact_id\" INTEGER NOT NULL REFERENCES contacts (user_id) ON DELETE CASCADE, \"promotion\" BLOB NOT NULL);" + } + ] + }, + { + "name": "user_discovery_shares", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"user_discovery_shares\" (\"share_id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, \"share\" BLOB NOT NULL, \"contact_id\" INTEGER NULL REFERENCES contacts (user_id) ON DELETE CASCADE);" + } + ] + }, + { + "name": "shortcuts", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"shortcuts\" (\"id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, \"emoji\" TEXT NOT NULL UNIQUE, \"usage_counter\" INTEGER NOT NULL DEFAULT 0);" + } + ] + }, + { + "name": "shortcut_members", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"shortcut_members\" (\"shortcut_id\" INTEGER NOT NULL REFERENCES shortcuts (id) ON DELETE CASCADE, \"group_id\" TEXT NOT NULL REFERENCES \"groups\" (group_id) ON DELETE CASCADE, PRIMARY KEY (\"shortcut_id\", \"group_id\"));" + } + ] + } + ] +} \ No newline at end of file diff --git a/lib/src/database/tables/messages.table.dart b/lib/src/database/tables/messages.table.dart index 42c779f9..c364b9e6 100644 --- a/lib/src/database/tables/messages.table.dart +++ b/lib/src/database/tables/messages.table.dart @@ -3,7 +3,7 @@ import 'package:twonly/src/database/tables/contacts.table.dart'; import 'package:twonly/src/database/tables/groups.table.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; -enum MessageType { media, text, contacts, restoreFlameCounter } +enum MessageType { media, text, contacts, restoreFlameCounter, askAboutUser } @DataClassName('Message') class Messages extends Table { diff --git a/lib/src/database/tables/user_discovery.table.dart b/lib/src/database/tables/user_discovery.table.dart index d4cdb6ac..cdc871fe 100644 --- a/lib/src/database/tables/user_discovery.table.dart +++ b/lib/src/database/tables/user_discovery.table.dart @@ -16,6 +16,8 @@ class UserDiscoveryAnnouncedUsers extends Table { BoolColumn get wasShownToTheUser => boolean().withDefault(const Constant(false))(); BoolColumn get isHidden => boolean().withDefault(const Constant(false))(); + BoolColumn get wasAskedFriends => + boolean().withDefault(const Constant(false))(); @override Set get primaryKey => {announcedUserId}; diff --git a/lib/src/database/twonly.db.dart b/lib/src/database/twonly.db.dart index b5e0d2ac..5ecf15e3 100644 --- a/lib/src/database/twonly.db.dart +++ b/lib/src/database/twonly.db.dart @@ -81,7 +81,7 @@ class TwonlyDB extends _$TwonlyDB { TwonlyDB.forTesting(DatabaseConnection super.connection); @override - int get schemaVersion => 16; + int get schemaVersion => 17; static QueryExecutor _openConnection() { return driftDatabase( @@ -218,6 +218,12 @@ class TwonlyDB extends _$TwonlyDB { ); await m.addColumn(schema.mediaFiles, schema.mediaFiles.sizeInBytes); }, + from16To17: (m, schema) async { + await m.addColumn( + schema.userDiscoveryAnnouncedUsers, + schema.userDiscoveryAnnouncedUsers.wasAskedFriends, + ); + }, )(m, from, to); }, ); diff --git a/lib/src/database/twonly.db.g.dart b/lib/src/database/twonly.db.g.dart index 42069c5d..cd2da301 100644 --- a/lib/src/database/twonly.db.g.dart +++ b/lib/src/database/twonly.db.g.dart @@ -10318,6 +10318,21 @@ class $UserDiscoveryAnnouncedUsersTable extends UserDiscoveryAnnouncedUsers ), defaultValue: const Constant(false), ); + static const VerificationMeta _wasAskedFriendsMeta = const VerificationMeta( + 'wasAskedFriends', + ); + @override + late final GeneratedColumn wasAskedFriends = GeneratedColumn( + 'was_asked_friends', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("was_asked_friends" IN (0, 1))', + ), + defaultValue: const Constant(false), + ); @override List get $columns => [ announcedUserId, @@ -10326,6 +10341,7 @@ class $UserDiscoveryAnnouncedUsersTable extends UserDiscoveryAnnouncedUsers username, wasShownToTheUser, isHidden, + wasAskedFriends, ]; @override String get aliasedName => _alias ?? actualTableName; @@ -10388,6 +10404,15 @@ class $UserDiscoveryAnnouncedUsersTable extends UserDiscoveryAnnouncedUsers isHidden.isAcceptableOrUnknown(data['is_hidden']!, _isHiddenMeta), ); } + if (data.containsKey('was_asked_friends')) { + context.handle( + _wasAskedFriendsMeta, + wasAskedFriends.isAcceptableOrUnknown( + data['was_asked_friends']!, + _wasAskedFriendsMeta, + ), + ); + } return context; } @@ -10424,6 +10449,10 @@ class $UserDiscoveryAnnouncedUsersTable extends UserDiscoveryAnnouncedUsers DriftSqlType.bool, data['${effectivePrefix}is_hidden'], )!, + wasAskedFriends: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}was_asked_friends'], + )!, ); } @@ -10441,6 +10470,7 @@ class UserDiscoveryAnnouncedUser extends DataClass final String? username; final bool wasShownToTheUser; final bool isHidden; + final bool wasAskedFriends; const UserDiscoveryAnnouncedUser({ required this.announcedUserId, required this.announcedPublicKey, @@ -10448,6 +10478,7 @@ class UserDiscoveryAnnouncedUser extends DataClass this.username, required this.wasShownToTheUser, required this.isHidden, + required this.wasAskedFriends, }); @override Map toColumns(bool nullToAbsent) { @@ -10460,6 +10491,7 @@ class UserDiscoveryAnnouncedUser extends DataClass } map['was_shown_to_the_user'] = Variable(wasShownToTheUser); map['is_hidden'] = Variable(isHidden); + map['was_asked_friends'] = Variable(wasAskedFriends); return map; } @@ -10473,6 +10505,7 @@ class UserDiscoveryAnnouncedUser extends DataClass : Value(username), wasShownToTheUser: Value(wasShownToTheUser), isHidden: Value(isHidden), + wasAskedFriends: Value(wasAskedFriends), ); } @@ -10490,6 +10523,7 @@ class UserDiscoveryAnnouncedUser extends DataClass username: serializer.fromJson(json['username']), wasShownToTheUser: serializer.fromJson(json['wasShownToTheUser']), isHidden: serializer.fromJson(json['isHidden']), + wasAskedFriends: serializer.fromJson(json['wasAskedFriends']), ); } @override @@ -10502,6 +10536,7 @@ class UserDiscoveryAnnouncedUser extends DataClass 'username': serializer.toJson(username), 'wasShownToTheUser': serializer.toJson(wasShownToTheUser), 'isHidden': serializer.toJson(isHidden), + 'wasAskedFriends': serializer.toJson(wasAskedFriends), }; } @@ -10512,6 +10547,7 @@ class UserDiscoveryAnnouncedUser extends DataClass Value username = const Value.absent(), bool? wasShownToTheUser, bool? isHidden, + bool? wasAskedFriends, }) => UserDiscoveryAnnouncedUser( announcedUserId: announcedUserId ?? this.announcedUserId, announcedPublicKey: announcedPublicKey ?? this.announcedPublicKey, @@ -10519,6 +10555,7 @@ class UserDiscoveryAnnouncedUser extends DataClass username: username.present ? username.value : this.username, wasShownToTheUser: wasShownToTheUser ?? this.wasShownToTheUser, isHidden: isHidden ?? this.isHidden, + wasAskedFriends: wasAskedFriends ?? this.wasAskedFriends, ); UserDiscoveryAnnouncedUser copyWithCompanion( UserDiscoveryAnnouncedUsersCompanion data, @@ -10536,6 +10573,9 @@ class UserDiscoveryAnnouncedUser extends DataClass ? data.wasShownToTheUser.value : this.wasShownToTheUser, isHidden: data.isHidden.present ? data.isHidden.value : this.isHidden, + wasAskedFriends: data.wasAskedFriends.present + ? data.wasAskedFriends.value + : this.wasAskedFriends, ); } @@ -10547,7 +10587,8 @@ class UserDiscoveryAnnouncedUser extends DataClass ..write('publicId: $publicId, ') ..write('username: $username, ') ..write('wasShownToTheUser: $wasShownToTheUser, ') - ..write('isHidden: $isHidden') + ..write('isHidden: $isHidden, ') + ..write('wasAskedFriends: $wasAskedFriends') ..write(')')) .toString(); } @@ -10560,6 +10601,7 @@ class UserDiscoveryAnnouncedUser extends DataClass username, wasShownToTheUser, isHidden, + wasAskedFriends, ); @override bool operator ==(Object other) => @@ -10573,7 +10615,8 @@ class UserDiscoveryAnnouncedUser extends DataClass other.publicId == this.publicId && other.username == this.username && other.wasShownToTheUser == this.wasShownToTheUser && - other.isHidden == this.isHidden); + other.isHidden == this.isHidden && + other.wasAskedFriends == this.wasAskedFriends); } class UserDiscoveryAnnouncedUsersCompanion @@ -10584,6 +10627,7 @@ class UserDiscoveryAnnouncedUsersCompanion final Value username; final Value wasShownToTheUser; final Value isHidden; + final Value wasAskedFriends; const UserDiscoveryAnnouncedUsersCompanion({ this.announcedUserId = const Value.absent(), this.announcedPublicKey = const Value.absent(), @@ -10591,6 +10635,7 @@ class UserDiscoveryAnnouncedUsersCompanion this.username = const Value.absent(), this.wasShownToTheUser = const Value.absent(), this.isHidden = const Value.absent(), + this.wasAskedFriends = const Value.absent(), }); UserDiscoveryAnnouncedUsersCompanion.insert({ this.announcedUserId = const Value.absent(), @@ -10599,6 +10644,7 @@ class UserDiscoveryAnnouncedUsersCompanion this.username = const Value.absent(), this.wasShownToTheUser = const Value.absent(), this.isHidden = const Value.absent(), + this.wasAskedFriends = const Value.absent(), }) : announcedPublicKey = Value(announcedPublicKey), publicId = Value(publicId); static Insertable custom({ @@ -10608,6 +10654,7 @@ class UserDiscoveryAnnouncedUsersCompanion Expression? username, Expression? wasShownToTheUser, Expression? isHidden, + Expression? wasAskedFriends, }) { return RawValuesInsertable({ if (announcedUserId != null) 'announced_user_id': announcedUserId, @@ -10617,6 +10664,7 @@ class UserDiscoveryAnnouncedUsersCompanion if (username != null) 'username': username, if (wasShownToTheUser != null) 'was_shown_to_the_user': wasShownToTheUser, if (isHidden != null) 'is_hidden': isHidden, + if (wasAskedFriends != null) 'was_asked_friends': wasAskedFriends, }); } @@ -10627,6 +10675,7 @@ class UserDiscoveryAnnouncedUsersCompanion Value? username, Value? wasShownToTheUser, Value? isHidden, + Value? wasAskedFriends, }) { return UserDiscoveryAnnouncedUsersCompanion( announcedUserId: announcedUserId ?? this.announcedUserId, @@ -10635,6 +10684,7 @@ class UserDiscoveryAnnouncedUsersCompanion username: username ?? this.username, wasShownToTheUser: wasShownToTheUser ?? this.wasShownToTheUser, isHidden: isHidden ?? this.isHidden, + wasAskedFriends: wasAskedFriends ?? this.wasAskedFriends, ); } @@ -10661,6 +10711,9 @@ class UserDiscoveryAnnouncedUsersCompanion if (isHidden.present) { map['is_hidden'] = Variable(isHidden.value); } + if (wasAskedFriends.present) { + map['was_asked_friends'] = Variable(wasAskedFriends.value); + } return map; } @@ -10672,7 +10725,8 @@ class UserDiscoveryAnnouncedUsersCompanion ..write('publicId: $publicId, ') ..write('username: $username, ') ..write('wasShownToTheUser: $wasShownToTheUser, ') - ..write('isHidden: $isHidden') + ..write('isHidden: $isHidden, ') + ..write('wasAskedFriends: $wasAskedFriends') ..write(')')) .toString(); } @@ -21534,6 +21588,7 @@ typedef $$UserDiscoveryAnnouncedUsersTableCreateCompanionBuilder = Value username, Value wasShownToTheUser, Value isHidden, + Value wasAskedFriends, }); typedef $$UserDiscoveryAnnouncedUsersTableUpdateCompanionBuilder = UserDiscoveryAnnouncedUsersCompanion Function({ @@ -21543,6 +21598,7 @@ typedef $$UserDiscoveryAnnouncedUsersTableUpdateCompanionBuilder = Value username, Value wasShownToTheUser, Value isHidden, + Value wasAskedFriends, }); final class $$UserDiscoveryAnnouncedUsersTableReferences @@ -21631,6 +21687,11 @@ class $$UserDiscoveryAnnouncedUsersTableFilterComposer builder: (column) => ColumnFilters(column), ); + ColumnFilters get wasAskedFriends => $composableBuilder( + column: $table.wasAskedFriends, + builder: (column) => ColumnFilters(column), + ); + Expression userDiscoveryUserRelationsRefs( Expression Function($$UserDiscoveryUserRelationsTableFilterComposer f) f, @@ -21697,6 +21758,11 @@ class $$UserDiscoveryAnnouncedUsersTableOrderingComposer column: $table.isHidden, builder: (column) => ColumnOrderings(column), ); + + ColumnOrderings get wasAskedFriends => $composableBuilder( + column: $table.wasAskedFriends, + builder: (column) => ColumnOrderings(column), + ); } class $$UserDiscoveryAnnouncedUsersTableAnnotationComposer @@ -21732,6 +21798,11 @@ class $$UserDiscoveryAnnouncedUsersTableAnnotationComposer GeneratedColumn get isHidden => $composableBuilder(column: $table.isHidden, builder: (column) => column); + GeneratedColumn get wasAskedFriends => $composableBuilder( + column: $table.wasAskedFriends, + builder: (column) => column, + ); + Expression userDiscoveryUserRelationsRefs( Expression Function( $$UserDiscoveryUserRelationsTableAnnotationComposer a, @@ -21810,6 +21881,7 @@ class $$UserDiscoveryAnnouncedUsersTableTableManager Value username = const Value.absent(), Value wasShownToTheUser = const Value.absent(), Value isHidden = const Value.absent(), + Value wasAskedFriends = const Value.absent(), }) => UserDiscoveryAnnouncedUsersCompanion( announcedUserId: announcedUserId, announcedPublicKey: announcedPublicKey, @@ -21817,6 +21889,7 @@ class $$UserDiscoveryAnnouncedUsersTableTableManager username: username, wasShownToTheUser: wasShownToTheUser, isHidden: isHidden, + wasAskedFriends: wasAskedFriends, ), createCompanionCallback: ({ @@ -21826,6 +21899,7 @@ class $$UserDiscoveryAnnouncedUsersTableTableManager Value username = const Value.absent(), Value wasShownToTheUser = const Value.absent(), Value isHidden = const Value.absent(), + Value wasAskedFriends = const Value.absent(), }) => UserDiscoveryAnnouncedUsersCompanion.insert( announcedUserId: announcedUserId, announcedPublicKey: announcedPublicKey, @@ -21833,6 +21907,7 @@ class $$UserDiscoveryAnnouncedUsersTableTableManager username: username, wasShownToTheUser: wasShownToTheUser, isHidden: isHidden, + wasAskedFriends: wasAskedFriends, ), withReferenceMapper: (p0) => p0 .map( diff --git a/lib/src/database/twonly.db.steps.dart b/lib/src/database/twonly.db.steps.dart index 39d8dc10..1a68e677 100644 --- a/lib/src/database/twonly.db.steps.dart +++ b/lib/src/database/twonly.db.steps.dart @@ -8545,6 +8545,483 @@ i1.GeneratedColumn _column_245(String aliasedName) => type: i1.DriftSqlType.int, $customConstraints: 'NULL', ); + +final class Schema17 extends i0.VersionedSchema { + Schema17({required super.database}) : super(version: 17); + @override + late final List entities = [ + contacts, + groups, + mediaFiles, + messages, + messageHistories, + reactions, + groupMembers, + receipts, + receivedReceipts, + signalIdentityKeyStores, + signalPreKeyStores, + signalSenderKeyStores, + signalSessionStores, + signalSignedPreKeyStores, + messageActions, + groupHistories, + keyVerifications, + verificationTokens, + userDiscoveryAnnouncedUsers, + userDiscoveryUserRelations, + userDiscoveryOtherPromotions, + userDiscoveryOwnPromotions, + userDiscoveryShares, + shortcuts, + shortcutMembers, + ]; + late final Shape39 contacts = Shape39( + source: i0.VersionedTable( + entityName: 'contacts', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(user_id)'], + columns: [ + _column_106, + _column_107, + _column_108, + _column_109, + _column_110, + _column_111, + _column_112, + _column_113, + _column_114, + _column_115, + _column_116, + _column_117, + _column_118, + _column_211, + _column_212, + _column_213, + _column_214, + _column_215, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape23 groups = Shape23( + source: i0.VersionedTable( + entityName: 'groups', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(group_id)'], + columns: [ + _column_119, + _column_120, + _column_121, + _column_122, + _column_123, + _column_124, + _column_125, + _column_126, + _column_127, + _column_128, + _column_129, + _column_130, + _column_131, + _column_132, + _column_133, + _column_134, + _column_118, + _column_135, + _column_136, + _column_137, + _column_138, + _column_139, + _column_140, + _column_141, + _column_142, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape51 mediaFiles = Shape51( + source: i0.VersionedTable( + entityName: 'media_files', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(media_id)'], + columns: [ + _column_143, + _column_144, + _column_145, + _column_146, + _column_147, + _column_148, + _column_149, + _column_239, + _column_240, + _column_207, + _column_150, + _column_151, + _column_152, + _column_153, + _column_154, + _column_155, + _column_156, + _column_157, + _column_244, + _column_245, + _column_118, + _column_241, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape25 messages = Shape25( + source: i0.VersionedTable( + entityName: 'messages', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(message_id)'], + columns: [ + _column_158, + _column_159, + _column_160, + _column_144, + _column_161, + _column_162, + _column_163, + _column_164, + _column_165, + _column_153, + _column_166, + _column_167, + _column_168, + _column_169, + _column_118, + _column_170, + _column_171, + _column_172, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape26 messageHistories = Shape26( + source: i0.VersionedTable( + entityName: 'message_histories', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_173, + _column_174, + _column_175, + _column_161, + _column_118, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape27 reactions = Shape27( + source: i0.VersionedTable( + entityName: 'reactions', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(message_id, sender_id, emoji)'], + columns: [_column_174, _column_176, _column_177, _column_118], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape38 groupMembers = Shape38( + source: i0.VersionedTable( + entityName: 'group_members', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(group_id, contact_id)'], + columns: [ + _column_158, + _column_178, + _column_179, + _column_180, + _column_209, + _column_210, + _column_181, + _column_118, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape37 receipts = Shape37( + source: i0.VersionedTable( + entityName: 'receipts', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(receipt_id)'], + columns: [ + _column_182, + _column_183, + _column_184, + _column_185, + _column_186, + _column_208, + _column_187, + _column_188, + _column_189, + _column_190, + _column_191, + _column_118, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape30 receivedReceipts = Shape30( + source: i0.VersionedTable( + entityName: 'received_receipts', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(receipt_id)'], + columns: [_column_182, _column_118], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape31 signalIdentityKeyStores = Shape31( + source: i0.VersionedTable( + entityName: 'signal_identity_key_stores', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(device_id, name)'], + columns: [_column_192, _column_193, _column_194, _column_118], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape32 signalPreKeyStores = Shape32( + source: i0.VersionedTable( + entityName: 'signal_pre_key_stores', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(pre_key_id)'], + columns: [_column_195, _column_196, _column_118], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape11 signalSenderKeyStores = Shape11( + source: i0.VersionedTable( + entityName: 'signal_sender_key_stores', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(sender_key_name)'], + columns: [_column_197, _column_198], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape33 signalSessionStores = Shape33( + source: i0.VersionedTable( + entityName: 'signal_session_stores', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(device_id, name)'], + columns: [_column_192, _column_193, _column_199, _column_118], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape50 signalSignedPreKeyStores = Shape50( + source: i0.VersionedTable( + entityName: 'signal_signed_pre_key_stores', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(signed_pre_key_id)'], + columns: [_column_242, _column_243, _column_118], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape34 messageActions = Shape34( + source: i0.VersionedTable( + entityName: 'message_actions', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(message_id, contact_id, type)'], + columns: [_column_174, _column_183, _column_144, _column_200], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape35 groupHistories = Shape35( + source: i0.VersionedTable( + entityName: 'group_histories', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(group_history_id)'], + columns: [ + _column_201, + _column_158, + _column_202, + _column_203, + _column_204, + _column_205, + _column_206, + _column_144, + _column_200, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape40 keyVerifications = Shape40( + source: i0.VersionedTable( + entityName: 'key_verifications', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [_column_216, _column_183, _column_144, _column_118], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape41 verificationTokens = Shape41( + source: i0.VersionedTable( + entityName: 'verification_tokens', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [_column_217, _column_218, _column_118], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape52 userDiscoveryAnnouncedUsers = Shape52( + source: i0.VersionedTable( + entityName: 'user_discovery_announced_users', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(announced_user_id)'], + columns: [ + _column_219, + _column_220, + _column_221, + _column_222, + _column_223, + _column_224, + _column_246, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape43 userDiscoveryUserRelations = Shape43( + source: i0.VersionedTable( + entityName: 'user_discovery_user_relations', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(announced_user_id, from_contact_id)'], + columns: [_column_225, _column_226, _column_227], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape44 userDiscoveryOtherPromotions = Shape44( + source: i0.VersionedTable( + entityName: 'user_discovery_other_promotions', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(from_contact_id, public_id)'], + columns: [ + _column_226, + _column_228, + _column_229, + _column_230, + _column_231, + _column_227, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape45 userDiscoveryOwnPromotions = Shape45( + source: i0.VersionedTable( + entityName: 'user_discovery_own_promotions', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [_column_232, _column_183, _column_233], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape46 userDiscoveryShares = Shape46( + source: i0.VersionedTable( + entityName: 'user_discovery_shares', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [_column_234, _column_235, _column_175], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape47 shortcuts = Shape47( + source: i0.VersionedTable( + entityName: 'shortcuts', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [_column_173, _column_236, _column_237], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape48 shortcutMembers = Shape48( + source: i0.VersionedTable( + entityName: 'shortcut_members', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(shortcut_id, group_id)'], + columns: [_column_238, _column_158], + attachedDatabase: database, + ), + alias: null, + ); +} + +class Shape52 extends i0.VersionedTable { + Shape52({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get announcedUserId => + columnsByName['announced_user_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get announcedPublicKey => + columnsByName['announced_public_key']! + as i1.GeneratedColumn; + i1.GeneratedColumn get publicId => + columnsByName['public_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get username => + columnsByName['username']! as i1.GeneratedColumn; + i1.GeneratedColumn get wasShownToTheUser => + columnsByName['was_shown_to_the_user']! as i1.GeneratedColumn; + i1.GeneratedColumn get isHidden => + columnsByName['is_hidden']! as i1.GeneratedColumn; + i1.GeneratedColumn get wasAskedFriends => + columnsByName['was_asked_friends']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_246(String aliasedName) => + i1.GeneratedColumn( + 'was_asked_friends', + aliasedName, + false, + type: i1.DriftSqlType.int, + $customConstraints: + 'NOT NULL DEFAULT 0 CHECK (was_asked_friends IN (0, 1))', + defaultValue: const i1.CustomExpression('0'), + ); i0.MigrationStepWithVersion migrationSteps({ required Future Function(i1.Migrator m, Schema2 schema) from1To2, required Future Function(i1.Migrator m, Schema3 schema) from2To3, @@ -8561,6 +9038,7 @@ i0.MigrationStepWithVersion migrationSteps({ required Future Function(i1.Migrator m, Schema14 schema) from13To14, required Future Function(i1.Migrator m, Schema15 schema) from14To15, required Future Function(i1.Migrator m, Schema16 schema) from15To16, + required Future Function(i1.Migrator m, Schema17 schema) from16To17, }) { return (currentVersion, database) async { switch (currentVersion) { @@ -8639,6 +9117,11 @@ i0.MigrationStepWithVersion migrationSteps({ final migrator = i1.Migrator(database, schema); await from15To16(migrator, schema); return 16; + case 16: + final schema = Schema17(database: database); + final migrator = i1.Migrator(database, schema); + await from16To17(migrator, schema); + return 17; default: throw ArgumentError.value('Unknown migration from $currentVersion'); } @@ -8661,6 +9144,7 @@ i1.OnUpgrade stepByStep({ required Future Function(i1.Migrator m, Schema14 schema) from13To14, required Future Function(i1.Migrator m, Schema15 schema) from14To15, required Future Function(i1.Migrator m, Schema16 schema) from15To16, + required Future Function(i1.Migrator m, Schema17 schema) from16To17, }) => i0.VersionedSchema.stepByStepHelper( step: migrationSteps( from1To2: from1To2, @@ -8678,5 +9162,6 @@ i1.OnUpgrade stepByStep({ from13To14: from13To14, from14To15: from14To15, from15To16: from15To16, + from16To17: from16To17, ), ); diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index bd6e5335..1d4500b3 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -626,6 +626,54 @@ abstract class AppLocalizations { /// **'{len} contact(s)'** String settingsPrivacyBlockUsersCount(Object len); + /// No description provided for @settingsPrivacyProfileSelectionTitle. + /// + /// In en, this message translates to: + /// **'Security Profile'** + String get settingsPrivacyProfileSelectionTitle; + + /// No description provided for @settingsPrivacyProfileSelectionDesc. + /// + /// In en, this message translates to: + /// **'Choose your setup path and security configuration'** + String get settingsPrivacyProfileSelectionDesc; + + /// No description provided for @securityProfileTitle. + /// + /// In en, this message translates to: + /// **'Security Profile'** + String get securityProfileTitle; + + /// No description provided for @securityProfileSubtitle. + /// + /// In en, this message translates to: + /// **'Choose the level of protection that fits your daily use. This can be changed at any time in your settings.'** + String get securityProfileSubtitle; + + /// No description provided for @securityProfileNormalTitle. + /// + /// In en, this message translates to: + /// **'Normal Protection'** + String get securityProfileNormalTitle; + + /// No description provided for @securityProfileNormalDesc. + /// + /// In en, this message translates to: + /// **'Good balance between a convenient mode without bothering you too much.'** + String get securityProfileNormalDesc; + + /// No description provided for @securityProfileStrictTitle. + /// + /// In en, this message translates to: + /// **'Strict Protection'** + String get securityProfileStrictTitle; + + /// No description provided for @securityProfileStrictDesc. + /// + /// In en, this message translates to: + /// **'Maximum anti-phishing protection but may be inconvenient.'** + String get securityProfileStrictDesc; + /// No description provided for @settingsNotification. /// /// In en, this message translates to: @@ -911,8 +959,8 @@ abstract class AppLocalizations { /// No description provided for @verificationTypeSecretQrToken. /// /// In en, this message translates to: - /// **'The other person scanned your QR code.'** - String get verificationTypeSecretQrToken; + /// **'{username} has scanned your QR code.'** + String verificationTypeSecretQrToken(Object username); /// No description provided for @verificationTypeLink. /// @@ -2342,6 +2390,12 @@ abstract class AppLocalizations { /// **'Open your own QR code'** String get openYourOwnQRcode; + /// No description provided for @addContactQrSheetSubtext. + /// + /// In en, this message translates to: + /// **'Let a friend scan this QR code to add you'** + String get addContactQrSheetSubtext; + /// No description provided for @finishSetupCardTitle. /// /// In en, this message translates to: @@ -2435,13 +2489,13 @@ abstract class AppLocalizations { /// No description provided for @userDiscoverySettingsManualApproval. /// /// In en, this message translates to: - /// **'Manual approval'** + /// **'Ask every time before sharing'** String get userDiscoverySettingsManualApproval; /// No description provided for @userDiscoverySettingsManualApprovalDesc. /// /// In en, this message translates to: - /// **'Before someone is shared, you\'ll be asked first.'** + /// **'Before one of your friends is shared, you will be asked every time.'** String get userDiscoverySettingsManualApprovalDesc; /// No description provided for @onboardingUserDiscoveryLetFriendsFindYou. @@ -2699,7 +2753,7 @@ abstract class AppLocalizations { /// No description provided for @verificationBadgeGeneralDesc. /// /// In en, this message translates to: - /// **'The checkmark gives you the certainty that you are messaging the right person. Scan the contact\'s QR code to verify it.'** + /// **'The checkmark gives you the certainty that you are messaging the right person. You can verify contacts at any time by scanning their QR code.'** String get verificationBadgeGeneralDesc; /// No description provided for @verificationBadgeGreenDesc. @@ -2720,6 +2774,36 @@ abstract class AppLocalizations { /// **'A contact whose identity has *not* yet been verified.'** String get verificationBadgeRedDesc; + /// No description provided for @deleteVerificationTitle. + /// + /// In en, this message translates to: + /// **'Delete verification?'** + String get deleteVerificationTitle; + + /// No description provided for @deleteVerificationBody. + /// + /// In en, this message translates to: + /// **'Are you sure you want to delete this verification?'** + String get deleteVerificationBody; + + /// No description provided for @secretQrTokenVerifiedSnackbar. + /// + /// In en, this message translates to: + /// **'{username} has scanned your QR code and is now verified.'** + String secretQrTokenVerifiedSnackbar(Object username); + + /// No description provided for @mutualGroupsTitle. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{1 mutual group} other{{count} mutual groups}}'** + String mutualGroupsTitle(num count); + + /// No description provided for @mutualGroupsSentMessages. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{1 message sent} other{{count} messages sent}}'** + String mutualGroupsSentMessages(num count); + /// No description provided for @chatEntryFlameRestored. /// /// In en, this message translates to: @@ -2852,6 +2936,18 @@ abstract class AppLocalizations { /// **'Mutual Friends'** String get userDiscoverySettingsTitle; + /// No description provided for @userDiscoveryWhyThisIsUsed. + /// + /// In en, this message translates to: + /// **'Why this is used'** + String get userDiscoveryWhyThisIsUsed; + + /// No description provided for @userDiscoveryFeatureOffers. + /// + /// In en, this message translates to: + /// **'Your benefits at a glance'** + String get userDiscoveryFeatureOffers; + /// No description provided for @userDiscoveryDisabledLearnMore. /// /// In en, this message translates to: @@ -2924,6 +3020,66 @@ abstract class AppLocalizations { /// **'Request'** String get friendSuggestionsRequest; + /// No description provided for @friendSuggestionsAskFriend. + /// + /// In en, this message translates to: + /// **'Ask your friends'** + String get friendSuggestionsAskFriend; + + /// No description provided for @askFriendsDialogTitle. + /// + /// In en, this message translates to: + /// **'Ask about {username}'** + String askFriendsDialogTitle(Object username); + + /// No description provided for @askFriendsDialogDescription. + /// + /// In en, this message translates to: + /// **'Select the friends you want to ask about this user:'** + String get askFriendsDialogDescription; + + /// No description provided for @askFriendsDialogConfirm. + /// + /// In en, this message translates to: + /// **'Ask'** + String get askFriendsDialogConfirm; + + /// No description provided for @askFriendsDialogCancel. + /// + /// In en, this message translates to: + /// **'Cancel'** + String get askFriendsDialogCancel; + + /// No description provided for @chatAskAFriendReceivedDescription. + /// + /// In en, this message translates to: + /// **'Your friend just got this as a suggestion and wants to know if he knows this person.'** + String get chatAskAFriendReceivedDescription; + + /// No description provided for @chatAskAFriendAddedDescription. + /// + /// In en, this message translates to: + /// **'You have added this user to your contacts.'** + String get chatAskAFriendAddedDescription; + + /// No description provided for @chatAskAFriendHide. + /// + /// In en, this message translates to: + /// **'Hide'** + String get chatAskAFriendHide; + + /// No description provided for @chatAskAFriendRequest. + /// + /// In en, this message translates to: + /// **'Request'** + String get chatAskAFriendRequest; + + /// No description provided for @chatAskAFriendUnknownUser. + /// + /// In en, this message translates to: + /// **'User {userId}'** + String chatAskAFriendUnknownUser(Object userId); + /// No description provided for @contactUserDiscoveryImagesLeft. /// /// In en, this message translates to: @@ -3241,6 +3397,102 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Drag to Zoom'** String get dragToZoom; + + /// No description provided for @showUsername. + /// + /// In en, this message translates to: + /// **'Show username'** + String get showUsername; + + /// No description provided for @onboardingProfileSelectionTitle. + /// + /// In en, this message translates to: + /// **'Choose your setup path'** + String get onboardingProfileSelectionTitle; + + /// No description provided for @onboardingProfileSelectionSubtitle. + /// + /// In en, this message translates to: + /// **'Choose how you want to configure your security and privacy settings.'** + String get onboardingProfileSelectionSubtitle; + + /// No description provided for @onboardingProfileSelectionDefaultTitle. + /// + /// In en, this message translates to: + /// **'Default'** + String get onboardingProfileSelectionDefaultTitle; + + /// No description provided for @onboardingProfileSelectionDefaultDesc. + /// + /// In en, this message translates to: + /// **'Instantly applies recommended settings so you can start using the app.'** + String get onboardingProfileSelectionDefaultDesc; + + /// No description provided for @onboardingProfileSelectionDefaultBadge. + /// + /// In en, this message translates to: + /// **'Fast Setup'** + String get onboardingProfileSelectionDefaultBadge; + + /// No description provided for @onboardingProfileSelectionCustomizeTitle. + /// + /// In en, this message translates to: + /// **'Customize'** + String get onboardingProfileSelectionCustomizeTitle; + + /// No description provided for @onboardingProfileSelectionCustomizeDesc. + /// + /// In en, this message translates to: + /// **'Step-by-step setup so you can decide for yourself.'** + String get onboardingProfileSelectionCustomizeDesc; + + /// No description provided for @onboardingProfileSelectionStrictTitle. + /// + /// In en, this message translates to: + /// **'Enhanced Protection'** + String get onboardingProfileSelectionStrictTitle; + + /// No description provided for @onboardingProfileSelectionStrictDesc. + /// + /// In en, this message translates to: + /// **'Maximum anti-phishing defense. Recommended for *journalists & public figures*.'** + String get onboardingProfileSelectionStrictDesc; + + /// No description provided for @replyFlameRestored. + /// + /// In en, this message translates to: + /// **'Flames restored'** + String get replyFlameRestored; + + /// No description provided for @replyAskAFriend. + /// + /// In en, this message translates to: + /// **'Ask a friend'** + String get replyAskAFriend; + + /// No description provided for @unverifiedWarningDirectTitle. + /// + /// In en, this message translates to: + /// **'Identity not verified in person'** + String get unverifiedWarningDirectTitle; + + /// No description provided for @unverifiedWarningGroupTitle. + /// + /// In en, this message translates to: + /// **'Not all members are verified in person'** + String get unverifiedWarningGroupTitle; + + /// No description provided for @unverifiedWarningBody. + /// + /// In en, this message translates to: + /// **'*Avoid sharing sensitive data*. Risk of *impersonation* without manual verification.'** + String get unverifiedWarningBody; + + /// No description provided for @unverifiedWarningButton. + /// + /// In en, this message translates to: + /// **'Verify now'** + String get unverifiedWarningButton; } class _AppLocalizationsDelegate diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 7318ffba..cfd37cb7 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -292,6 +292,34 @@ class AppLocalizationsDe extends AppLocalizations { return '$len Kontakt(e)'; } + @override + String get settingsPrivacyProfileSelectionTitle => 'Sicherheitsprofil'; + + @override + String get settingsPrivacyProfileSelectionDesc => + 'Wähle deinen Setup-Pfad und deine Sicherheitskonfiguration'; + + @override + String get securityProfileTitle => 'Sicherheitsprofil'; + + @override + String get securityProfileSubtitle => + 'Wähle das Schutzniveau, das zu deiner täglichen Nutzung passt. Dies kann jederzeit in den Einstellungen geändert werden.'; + + @override + String get securityProfileNormalTitle => 'Normaler Schutz'; + + @override + String get securityProfileNormalDesc => + 'Gute Balance zwischen Komfort und Sicherheit, ohne dich zu sehr einzuschränken.'; + + @override + String get securityProfileStrictTitle => 'Strikter Schutz'; + + @override + String get securityProfileStrictDesc => + 'Maximaler Schutz vor Phishing, kann aber unkomfortabel sein.'; + @override String get settingsNotification => 'Benachrichtigung'; @@ -447,8 +475,9 @@ class AppLocalizationsDe extends AppLocalizations { String get verificationTypeQrScanned => 'Du hast den QR-Code gescannt.'; @override - String get verificationTypeSecretQrToken => - 'Die andere Person hat deinen QR-Code gescannt.'; + String verificationTypeSecretQrToken(Object username) { + return '$username hat deinen QR-Code gescannt.'; + } @override String get verificationTypeLink => 'Per Link verifiziert.'; @@ -1276,6 +1305,10 @@ class AppLocalizationsDe extends AppLocalizations { @override String get openYourOwnQRcode => 'Eigenen QR-Code öffnen'; + @override + String get addContactQrSheetSubtext => + 'Lass einen Freund diesen QR-Code scannen, um dich hinzuzufügen'; + @override String get finishSetupCardTitle => 'Profil vervollständigen'; @@ -1329,11 +1362,11 @@ class AppLocalizationsDe extends AppLocalizations { 'Erfahre, wer dich anfragt'; @override - String get userDiscoverySettingsManualApproval => 'Manuelle Zustimmung'; + String get userDiscoverySettingsManualApproval => 'Vor jedem Teilen fragen'; @override String get userDiscoverySettingsManualApprovalDesc => - 'Bevor jemand geteilt wird, wirst du zuerst gefragt.'; + 'Bevor einer deiner Freunde geteilt wird, wirst du jedes Mal gefragt.'; @override String get onboardingUserDiscoveryLetFriendsFindYou => @@ -1501,7 +1534,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get verificationBadgeGeneralDesc => - 'Der Haken gibt dir die Sicherheit, dass du mit der richtigen Person schreibst. Scanne einen Kontakt, um diesen zu verifizieren.'; + 'Der Haken gibt dir die Sicherheit, dass du mit der richtigen Person schreibst. Du kannst Kontakte jederzeit verifizieren, indem du deren QR-Code scannst.'; @override String get verificationBadgeGreenDesc => @@ -1515,6 +1548,40 @@ class AppLocalizationsDe extends AppLocalizations { String get verificationBadgeRedDesc => 'Ein Kontakt, dessen Identität noch *nicht überprüft* wurde.'; + @override + String get deleteVerificationTitle => 'Verifizierung löschen?'; + + @override + String get deleteVerificationBody => + 'Möchtest du diese Verifizierung wirklich löschen?'; + + @override + String secretQrTokenVerifiedSnackbar(Object username) { + return '$username hat deinen QR-Code gescannt und ist nun verifiziert.'; + } + + @override + String mutualGroupsTitle(num count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count gemeinsame Gruppen', + one: '1 gemeinsame Gruppe', + ); + return '$_temp0'; + } + + @override + String mutualGroupsSentMessages(num count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Nachrichten gesendet', + one: '1 Nachricht gesendet', + ); + return '$_temp0'; + } + @override String chatEntryFlameRestored(Object count) { return '$count Flammen wiederhergestellt'; @@ -1594,6 +1661,12 @@ class AppLocalizationsDe extends AppLocalizations { @override String get userDiscoverySettingsTitle => 'Gemeinsame Freunde'; + @override + String get userDiscoveryWhyThisIsUsed => 'Warum dies verwendet wird'; + + @override + String get userDiscoveryFeatureOffers => 'Dein Nutzen auf einen Blick'; + @override String get userDiscoveryDisabledLearnMore => 'Mehr erfahren'; @@ -1637,6 +1710,43 @@ class AppLocalizationsDe extends AppLocalizations { @override String get friendSuggestionsRequest => 'Anfragen'; + @override + String get friendSuggestionsAskFriend => 'Deine Freunde fragen'; + + @override + String askFriendsDialogTitle(Object username) { + return 'Nach $username fragen'; + } + + @override + String get askFriendsDialogDescription => + 'Wähle die Freunde aus, die du zu diesem Nutzer fragen möchtest:'; + + @override + String get askFriendsDialogConfirm => 'Fragen'; + + @override + String get askFriendsDialogCancel => 'Abbrechen'; + + @override + String get chatAskAFriendReceivedDescription => + 'Dein Freund hat diesen Nutzer als Vorschlag erhalten und möchte wissen, ob er diese Person kennt.'; + + @override + String get chatAskAFriendAddedDescription => + 'Du hast diesen Nutzer zu deinen Kontakten hinzugefügt.'; + + @override + String get chatAskAFriendHide => 'Ausblenden'; + + @override + String get chatAskAFriendRequest => 'Anfragen'; + + @override + String chatAskAFriendUnknownUser(Object userId) { + return 'Nutzer $userId'; + } + @override String contactUserDiscoveryImagesLeft(Object imagesLeft, Object username) { return 'Es fehlen noch $imagesLeft Bilder bis deine Freunde mit $username geteilt werden.'; @@ -1823,4 +1933,59 @@ class AppLocalizationsDe extends AppLocalizations { @override String get dragToZoom => 'Zum Zoomen ziehen'; + + @override + String get showUsername => 'Benutzernamen anzeigen'; + + @override + String get onboardingProfileSelectionTitle => 'Wähle deinen Setup-Weg'; + + @override + String get onboardingProfileSelectionSubtitle => + 'Wähle aus, wie du deine Sicherheits- und Privatsphäre-Einstellungen konfigurieren möchtest.'; + + @override + String get onboardingProfileSelectionDefaultTitle => 'Standard'; + + @override + String get onboardingProfileSelectionDefaultDesc => + 'Wendet sofort die empfohlenen Einstellungen an, damit du die App direkt nutzen kannst.'; + + @override + String get onboardingProfileSelectionDefaultBadge => 'Schnelles Setup'; + + @override + String get onboardingProfileSelectionCustomizeTitle => 'Anpassen'; + + @override + String get onboardingProfileSelectionCustomizeDesc => + 'Schritt-für-Schritt-Einrichtung, damit du selbst entscheiden kannst.'; + + @override + String get onboardingProfileSelectionStrictTitle => 'Erhöhter Schutz'; + + @override + String get onboardingProfileSelectionStrictDesc => + 'Maximaler Schutz vor Phishing. Empfohlen für *Journalisten & Personen des öffentlichen Lebens*.'; + + @override + String get replyFlameRestored => 'Flammen wiederhergestellt'; + + @override + String get replyAskAFriend => 'Einen Freund fragen'; + + @override + String get unverifiedWarningDirectTitle => + 'Identität nicht persönlich verifiziert'; + + @override + String get unverifiedWarningGroupTitle => + 'Nicht alle Mitglieder sind persönlich verifiziert'; + + @override + String get unverifiedWarningBody => + '*Teile keine geheimen Daten*. Jemand könnte sich *als dein Freund ausgeben*.'; + + @override + String get unverifiedWarningButton => 'Jetzt verifizieren'; } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 95a0b491..3cfe0b43 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -288,6 +288,34 @@ class AppLocalizationsEn extends AppLocalizations { return '$len contact(s)'; } + @override + String get settingsPrivacyProfileSelectionTitle => 'Security Profile'; + + @override + String get settingsPrivacyProfileSelectionDesc => + 'Choose your setup path and security configuration'; + + @override + String get securityProfileTitle => 'Security Profile'; + + @override + String get securityProfileSubtitle => + 'Choose the level of protection that fits your daily use. This can be changed at any time in your settings.'; + + @override + String get securityProfileNormalTitle => 'Normal Protection'; + + @override + String get securityProfileNormalDesc => + 'Good balance between a convenient mode without bothering you too much.'; + + @override + String get securityProfileStrictTitle => 'Strict Protection'; + + @override + String get securityProfileStrictDesc => + 'Maximum anti-phishing protection but may be inconvenient.'; + @override String get settingsNotification => 'Notification'; @@ -442,8 +470,9 @@ class AppLocalizationsEn extends AppLocalizations { String get verificationTypeQrScanned => 'You scanned their QR code.'; @override - String get verificationTypeSecretQrToken => - 'The other person scanned your QR code.'; + String verificationTypeSecretQrToken(Object username) { + return '$username has scanned your QR code.'; + } @override String get verificationTypeLink => 'Verified via link.'; @@ -1267,6 +1296,10 @@ class AppLocalizationsEn extends AppLocalizations { @override String get openYourOwnQRcode => 'Open your own QR code'; + @override + String get addContactQrSheetSubtext => + 'Let a friend scan this QR code to add you'; + @override String get finishSetupCardTitle => 'Complete your profile'; @@ -1320,11 +1353,12 @@ class AppLocalizationsEn extends AppLocalizations { 'Be informed about who is requesting'; @override - String get userDiscoverySettingsManualApproval => 'Manual approval'; + String get userDiscoverySettingsManualApproval => + 'Ask every time before sharing'; @override String get userDiscoverySettingsManualApprovalDesc => - 'Before someone is shared, you\'ll be asked first.'; + 'Before one of your friends is shared, you will be asked every time.'; @override String get onboardingUserDiscoveryLetFriendsFindYou => @@ -1486,7 +1520,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String get verificationBadgeGeneralDesc => - 'The checkmark gives you the certainty that you are messaging the right person. Scan the contact\'s QR code to verify it.'; + 'The checkmark gives you the certainty that you are messaging the right person. You can verify contacts at any time by scanning their QR code.'; @override String get verificationBadgeGreenDesc => @@ -1500,6 +1534,40 @@ class AppLocalizationsEn extends AppLocalizations { String get verificationBadgeRedDesc => 'A contact whose identity has *not* yet been verified.'; + @override + String get deleteVerificationTitle => 'Delete verification?'; + + @override + String get deleteVerificationBody => + 'Are you sure you want to delete this verification?'; + + @override + String secretQrTokenVerifiedSnackbar(Object username) { + return '$username has scanned your QR code and is now verified.'; + } + + @override + String mutualGroupsTitle(num count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count mutual groups', + one: '1 mutual group', + ); + return '$_temp0'; + } + + @override + String mutualGroupsSentMessages(num count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count messages sent', + one: '1 message sent', + ); + return '$_temp0'; + } + @override String chatEntryFlameRestored(Object count) { return '$count flames restored'; @@ -1579,6 +1647,12 @@ class AppLocalizationsEn extends AppLocalizations { @override String get userDiscoverySettingsTitle => 'Mutual Friends'; + @override + String get userDiscoveryWhyThisIsUsed => 'Why this is used'; + + @override + String get userDiscoveryFeatureOffers => 'Your benefits at a glance'; + @override String get userDiscoveryDisabledLearnMore => 'Learn more'; @@ -1622,6 +1696,43 @@ class AppLocalizationsEn extends AppLocalizations { @override String get friendSuggestionsRequest => 'Request'; + @override + String get friendSuggestionsAskFriend => 'Ask your friends'; + + @override + String askFriendsDialogTitle(Object username) { + return 'Ask about $username'; + } + + @override + String get askFriendsDialogDescription => + 'Select the friends you want to ask about this user:'; + + @override + String get askFriendsDialogConfirm => 'Ask'; + + @override + String get askFriendsDialogCancel => 'Cancel'; + + @override + String get chatAskAFriendReceivedDescription => + 'Your friend just got this as a suggestion and wants to know if he knows this person.'; + + @override + String get chatAskAFriendAddedDescription => + 'You have added this user to your contacts.'; + + @override + String get chatAskAFriendHide => 'Hide'; + + @override + String get chatAskAFriendRequest => 'Request'; + + @override + String chatAskAFriendUnknownUser(Object userId) { + return 'User $userId'; + } + @override String contactUserDiscoveryImagesLeft(Object imagesLeft, Object username) { return '$imagesLeft more images are needed until your friends are shared with $username.'; @@ -1807,4 +1918,58 @@ class AppLocalizationsEn extends AppLocalizations { @override String get dragToZoom => 'Drag to Zoom'; + + @override + String get showUsername => 'Show username'; + + @override + String get onboardingProfileSelectionTitle => 'Choose your setup path'; + + @override + String get onboardingProfileSelectionSubtitle => + 'Choose how you want to configure your security and privacy settings.'; + + @override + String get onboardingProfileSelectionDefaultTitle => 'Default'; + + @override + String get onboardingProfileSelectionDefaultDesc => + 'Instantly applies recommended settings so you can start using the app.'; + + @override + String get onboardingProfileSelectionDefaultBadge => 'Fast Setup'; + + @override + String get onboardingProfileSelectionCustomizeTitle => 'Customize'; + + @override + String get onboardingProfileSelectionCustomizeDesc => + 'Step-by-step setup so you can decide for yourself.'; + + @override + String get onboardingProfileSelectionStrictTitle => 'Enhanced Protection'; + + @override + String get onboardingProfileSelectionStrictDesc => + 'Maximum anti-phishing defense. Recommended for *journalists & public figures*.'; + + @override + String get replyFlameRestored => 'Flames restored'; + + @override + String get replyAskAFriend => 'Ask a friend'; + + @override + String get unverifiedWarningDirectTitle => 'Identity not verified in person'; + + @override + String get unverifiedWarningGroupTitle => + 'Not all members are verified in person'; + + @override + String get unverifiedWarningBody => + '*Avoid sharing sensitive data*. Risk of *impersonation* without manual verification.'; + + @override + String get unverifiedWarningButton => 'Verify now'; } diff --git a/lib/src/localization/translations b/lib/src/localization/translations index a8c5a355..c33a4c3b 160000 --- a/lib/src/localization/translations +++ b/lib/src/localization/translations @@ -1 +1 @@ -Subproject commit a8c5a355abf95578f1bdbf6a71077c5078b9dd93 +Subproject commit c33a4c3be99b38596abd0cfa91333db3a340dee2 diff --git a/lib/src/model/json/userdata.model.dart b/lib/src/model/json/userdata.model.dart index b18218b9..e3aa59a6 100644 --- a/lib/src/model/json/userdata.model.dart +++ b/lib/src/model/json/userdata.model.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:json_annotation/json_annotation.dart'; +import 'package:twonly/src/services/profile.service.dart'; part 'userdata.model.g.dart'; @JsonSerializable() @@ -10,9 +11,9 @@ class UserData { required this.displayName, required this.subscriptionPlan, required this.currentSetupPage, + required this.appVersion, }); - factory UserData.fromJson(Map json) => - _$UserDataFromJson(json); + factory UserData.fromJson(Map json) => _$UserDataFromJson(json); final int userId; @@ -35,6 +36,12 @@ class UserData { @JsonKey(defaultValue: 0) int deviceId = 0; + @JsonKey(defaultValue: SetupProfile.standard) + SetupProfile setupProfile = SetupProfile.standard; + + @JsonKey(defaultValue: SecurityProfile.normal) + SecurityProfile securityProfile = SecurityProfile.normal; + // --- SUBSCRIPTION DTA --- @JsonKey(defaultValue: 'Free') @@ -179,8 +186,7 @@ class TwonlySafeBackup { required this.backupId, required this.encryptionKey, }); - factory TwonlySafeBackup.fromJson(Map json) => - _$TwonlySafeBackupFromJson(json); + factory TwonlySafeBackup.fromJson(Map json) => _$TwonlySafeBackupFromJson(json); int lastBackupSize = 0; LastBackupUploadState backupUploadState = LastBackupUploadState.none; diff --git a/lib/src/model/json/userdata.model.g.dart b/lib/src/model/json/userdata.model.g.dart index 21165539..4ae054fd 100644 --- a/lib/src/model/json/userdata.model.g.dart +++ b/lib/src/model/json/userdata.model.g.dart @@ -13,13 +13,22 @@ UserData _$UserDataFromJson(Map json) => displayName: json['displayName'] as String, subscriptionPlan: json['subscriptionPlan'] as String? ?? 'Free', currentSetupPage: json['currentSetupPage'] as String?, + appVersion: (json['appVersion'] as num?)?.toInt() ?? 0, ) ..avatarSvg = json['avatarSvg'] as String? ..avatarJson = json['avatarJson'] as String? - ..appVersion = (json['appVersion'] as num?)?.toInt() ?? 0 ..avatarCounter = (json['avatarCounter'] as num?)?.toInt() ?? 0 ..isDeveloper = json['isDeveloper'] as bool? ?? false ..deviceId = (json['deviceId'] as num?)?.toInt() ?? 0 + ..setupProfile = + $enumDecodeNullable(_$SetupProfileEnumMap, json['setupProfile']) ?? + SetupProfile.standard + ..securityProfile = + $enumDecodeNullable( + _$SecurityProfileEnumMap, + json['securityProfile'], + ) ?? + SecurityProfile.normal ..subscriptionPlanIdStore = json['subscriptionPlanIdStore'] as String? ..lastImageSend = json['lastImageSend'] == null ? null @@ -115,6 +124,8 @@ Map _$UserDataToJson(UserData instance) => { 'avatarCounter': instance.avatarCounter, 'isDeveloper': instance.isDeveloper, 'deviceId': instance.deviceId, + 'setupProfile': _$SetupProfileEnumMap[instance.setupProfile]!, + 'securityProfile': _$SecurityProfileEnumMap[instance.securityProfile]!, 'subscriptionPlan': instance.subscriptionPlan, 'subscriptionPlanIdStore': instance.subscriptionPlanIdStore, 'lastImageSend': instance.lastImageSend?.toIso8601String(), @@ -168,6 +179,17 @@ Map _$UserDataToJson(UserData instance) => { 'hasZoomed': instance.hasZoomed, }; +const _$SetupProfileEnumMap = { + SetupProfile.standard: 'standard', + SetupProfile.customized: 'customized', + SetupProfile.maximum: 'maximum', +}; + +const _$SecurityProfileEnumMap = { + SecurityProfile.normal: 'normal', + SecurityProfile.strict: 'strict', +}; + const _$ThemeModeEnumMap = { ThemeMode.system: 'system', ThemeMode.light: 'light', diff --git a/lib/src/model/protobuf/client/data.proto b/lib/src/model/protobuf/client/data.proto index 9af7148c..56cd65a7 100644 --- a/lib/src/model/protobuf/client/data.proto +++ b/lib/src/model/protobuf/client/data.proto @@ -11,10 +11,12 @@ message AdditionalMessageData { LINK = 0; CONTACTS = 1; RESTORED_FLAME_COUNTER = 2; + ASK_ABOUT_USER = 3; } Type type = 1; optional string link = 2; repeated SharedContact contacts = 3; optional int64 restored_flame_counter = 4; + optional int64 ask_about_user_id = 5; } \ No newline at end of file diff --git a/lib/src/model/protobuf/client/generated/data.pb.dart b/lib/src/model/protobuf/client/generated/data.pb.dart index eef79ac2..368474b1 100644 --- a/lib/src/model/protobuf/client/generated/data.pb.dart +++ b/lib/src/model/protobuf/client/generated/data.pb.dart @@ -105,6 +105,7 @@ class AdditionalMessageData extends $pb.GeneratedMessage { $core.String? link, $core.Iterable? contacts, $fixnum.Int64? restoredFlameCounter, + $fixnum.Int64? askAboutUserId, }) { final result = create(); if (type != null) result.type = type; @@ -112,6 +113,7 @@ class AdditionalMessageData extends $pb.GeneratedMessage { if (contacts != null) result.contacts.addAll(contacts); if (restoredFlameCounter != null) result.restoredFlameCounter = restoredFlameCounter; + if (askAboutUserId != null) result.askAboutUserId = askAboutUserId; return result; } @@ -133,6 +135,7 @@ class AdditionalMessageData extends $pb.GeneratedMessage { ..pPM(3, _omitFieldNames ? '' : 'contacts', subBuilder: SharedContact.create) ..aInt64(4, _omitFieldNames ? '' : 'restoredFlameCounter') + ..aInt64(5, _omitFieldNames ? '' : 'askAboutUserId') ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -184,6 +187,15 @@ class AdditionalMessageData extends $pb.GeneratedMessage { $core.bool hasRestoredFlameCounter() => $_has(3); @$pb.TagNumber(4) void clearRestoredFlameCounter() => $_clearField(4); + + @$pb.TagNumber(5) + $fixnum.Int64 get askAboutUserId => $_getI64(4); + @$pb.TagNumber(5) + set askAboutUserId($fixnum.Int64 value) => $_setInt64(4, value); + @$pb.TagNumber(5) + $core.bool hasAskAboutUserId() => $_has(4); + @$pb.TagNumber(5) + void clearAskAboutUserId() => $_clearField(5); } const $core.bool _omitFieldNames = diff --git a/lib/src/model/protobuf/client/generated/data.pbenum.dart b/lib/src/model/protobuf/client/generated/data.pbenum.dart index b579b9a8..5e2fef55 100644 --- a/lib/src/model/protobuf/client/generated/data.pbenum.dart +++ b/lib/src/model/protobuf/client/generated/data.pbenum.dart @@ -22,16 +22,19 @@ class AdditionalMessageData_Type extends $pb.ProtobufEnum { static const AdditionalMessageData_Type RESTORED_FLAME_COUNTER = AdditionalMessageData_Type._( 2, _omitEnumNames ? '' : 'RESTORED_FLAME_COUNTER'); + static const AdditionalMessageData_Type ASK_ABOUT_USER = + AdditionalMessageData_Type._(3, _omitEnumNames ? '' : 'ASK_ABOUT_USER'); static const $core.List values = [ LINK, CONTACTS, RESTORED_FLAME_COUNTER, + ASK_ABOUT_USER, ]; static final $core.List _byValue = - $pb.ProtobufEnum.$_initByValueList(values, 2); + $pb.ProtobufEnum.$_initByValueList(values, 3); static AdditionalMessageData_Type? valueOf($core.int value) => value < 0 || value >= _byValue.length ? null : _byValue[value]; diff --git a/lib/src/model/protobuf/client/generated/data.pbjson.dart b/lib/src/model/protobuf/client/generated/data.pbjson.dart index 399acdee..0c61dc76 100644 --- a/lib/src/model/protobuf/client/generated/data.pbjson.dart +++ b/lib/src/model/protobuf/client/generated/data.pbjson.dart @@ -67,11 +67,21 @@ const AdditionalMessageData$json = { '10': 'restoredFlameCounter', '17': true }, + { + '1': 'ask_about_user_id', + '3': 5, + '4': 1, + '5': 3, + '9': 2, + '10': 'askAboutUserId', + '17': true + }, ], '4': [AdditionalMessageData_Type$json], '8': [ {'1': '_link'}, {'1': '_restored_flame_counter'}, + {'1': '_ask_about_user_id'}, ], }; @@ -82,6 +92,7 @@ const AdditionalMessageData_Type$json = { {'1': 'LINK', '2': 0}, {'1': 'CONTACTS', '2': 1}, {'1': 'RESTORED_FLAME_COUNTER', '2': 2}, + {'1': 'ASK_ABOUT_USER', '2': 3}, ], }; @@ -90,6 +101,7 @@ final $typed_data.Uint8List additionalMessageDataDescriptor = $convert.base64Dec 'ChVBZGRpdGlvbmFsTWVzc2FnZURhdGESLwoEdHlwZRgBIAEoDjIbLkFkZGl0aW9uYWxNZXNzYW' 'dlRGF0YS5UeXBlUgR0eXBlEhcKBGxpbmsYAiABKAlIAFIEbGlua4gBARIqCghjb250YWN0cxgD' 'IAMoCzIOLlNoYXJlZENvbnRhY3RSCGNvbnRhY3RzEjkKFnJlc3RvcmVkX2ZsYW1lX2NvdW50ZX' - 'IYBCABKANIAVIUcmVzdG9yZWRGbGFtZUNvdW50ZXKIAQEiOgoEVHlwZRIICgRMSU5LEAASDAoI' - 'Q09OVEFDVFMQARIaChZSRVNUT1JFRF9GTEFNRV9DT1VOVEVSEAJCBwoFX2xpbmtCGQoXX3Jlc3' - 'RvcmVkX2ZsYW1lX2NvdW50ZXI='); + 'IYBCABKANIAVIUcmVzdG9yZWRGbGFtZUNvdW50ZXKIAQESLgoRYXNrX2Fib3V0X3VzZXJfaWQY' + 'BSABKANIAlIOYXNrQWJvdXRVc2VySWSIAQEiTgoEVHlwZRIICgRMSU5LEAASDAoIQ09OVEFDVF' + 'MQARIaChZSRVNUT1JFRF9GTEFNRV9DT1VOVEVSEAISEgoOQVNLX0FCT1VUX1VTRVIQA0IHCgVf' + 'bGlua0IZChdfcmVzdG9yZWRfZmxhbWVfY291bnRlckIUChJfYXNrX2Fib3V0X3VzZXJfaWQ='); diff --git a/lib/src/model/protobuf/client/generated/qr.pb.dart b/lib/src/model/protobuf/client/generated/qr.pb.dart index 60000fcc..b1c05e2e 100644 --- a/lib/src/model/protobuf/client/generated/qr.pb.dart +++ b/lib/src/model/protobuf/client/generated/qr.pb.dart @@ -97,6 +97,7 @@ class PublicProfile extends $pb.GeneratedMessage { $core.List<$core.int>? signedPrekeySignature, $fixnum.Int64? signedPrekeyId, $core.List<$core.int>? secretVerificationToken, + $fixnum.Int64? timestamp, }) { final result = create(); if (userId != null) result.userId = userId; @@ -109,6 +110,7 @@ class PublicProfile extends $pb.GeneratedMessage { if (signedPrekeyId != null) result.signedPrekeyId = signedPrekeyId; if (secretVerificationToken != null) result.secretVerificationToken = secretVerificationToken; + if (timestamp != null) result.timestamp = timestamp; return result; } @@ -136,6 +138,7 @@ class PublicProfile extends $pb.GeneratedMessage { ..aInt64(7, _omitFieldNames ? '' : 'signedPrekeyId') ..a<$core.List<$core.int>>( 8, _omitFieldNames ? '' : 'secretVerificationToken', $pb.PbFieldType.OY) + ..aInt64(9, _omitFieldNames ? '' : 'timestamp') ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -230,6 +233,15 @@ class PublicProfile extends $pb.GeneratedMessage { $core.bool hasSecretVerificationToken() => $_has(7); @$pb.TagNumber(8) void clearSecretVerificationToken() => $_clearField(8); + + @$pb.TagNumber(9) + $fixnum.Int64 get timestamp => $_getI64(8); + @$pb.TagNumber(9) + set timestamp($fixnum.Int64 value) => $_setInt64(8, value); + @$pb.TagNumber(9) + $core.bool hasTimestamp() => $_has(8); + @$pb.TagNumber(9) + void clearTimestamp() => $_clearField(9); } const $core.bool _omitFieldNames = diff --git a/lib/src/model/protobuf/client/generated/qr.pbjson.dart b/lib/src/model/protobuf/client/generated/qr.pbjson.dart index 16c3c2cd..7d971b0e 100644 --- a/lib/src/model/protobuf/client/generated/qr.pbjson.dart +++ b/lib/src/model/protobuf/client/generated/qr.pbjson.dart @@ -77,9 +77,19 @@ const PublicProfile$json = { '10': 'secretVerificationToken', '17': true }, + { + '1': 'timestamp', + '3': 9, + '4': 1, + '5': 3, + '9': 1, + '10': 'timestamp', + '17': true + }, ], '8': [ {'1': '_secret_verification_token'}, + {'1': '_timestamp'}, ], }; @@ -91,4 +101,5 @@ final $typed_data.Uint8List publicProfileDescriptor = $convert.base64Decode( 'lvbl9pZBgFIAEoA1IOcmVnaXN0cmF0aW9uSWQSNgoXc2lnbmVkX3ByZWtleV9zaWduYXR1cmUY' 'BiABKAxSFXNpZ25lZFByZWtleVNpZ25hdHVyZRIoChBzaWduZWRfcHJla2V5X2lkGAcgASgDUg' '5zaWduZWRQcmVrZXlJZBI/ChlzZWNyZXRfdmVyaWZpY2F0aW9uX3Rva2VuGAggASgMSABSF3Nl' - 'Y3JldFZlcmlmaWNhdGlvblRva2VuiAEBQhwKGl9zZWNyZXRfdmVyaWZpY2F0aW9uX3Rva2Vu'); + 'Y3JldFZlcmlmaWNhdGlvblRva2VuiAEBEiEKCXRpbWVzdGFtcBgJIAEoA0gBUgl0aW1lc3RhbX' + 'CIAQFCHAoaX3NlY3JldF92ZXJpZmljYXRpb25fdG9rZW5CDAoKX3RpbWVzdGFtcA=='); diff --git a/lib/src/model/protobuf/client/qr.proto b/lib/src/model/protobuf/client/qr.proto index 7193a6ac..fa069485 100644 --- a/lib/src/model/protobuf/client/qr.proto +++ b/lib/src/model/protobuf/client/qr.proto @@ -17,4 +17,5 @@ message PublicProfile { bytes signed_prekey_signature = 6; int64 signed_prekey_id = 7; optional bytes secret_verification_token = 8; + optional int64 timestamp = 9; } diff --git a/lib/src/providers/routing.provider.dart b/lib/src/providers/routing.provider.dart index 129ba449..967887d0 100644 --- a/lib/src/providers/routing.provider.dart +++ b/lib/src/providers/routing.provider.dart @@ -1,3 +1,4 @@ +import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:twonly/app.dart'; import 'package:twonly/src/constants/routes.keys.dart'; @@ -38,6 +39,7 @@ import 'package:twonly/src/visual/views/settings/help/help.view.dart'; import 'package:twonly/src/visual/views/settings/notification.view.dart'; import 'package:twonly/src/visual/views/settings/privacy.view.dart'; import 'package:twonly/src/visual/views/settings/privacy/block_users.view.dart'; +import 'package:twonly/src/visual/views/settings/privacy/profile_selection.view.dart'; import 'package:twonly/src/visual/views/settings/privacy/user_discovery.view.dart'; import 'package:twonly/src/visual/views/settings/profile/modify_avatar.view.dart'; import 'package:twonly/src/visual/views/settings/profile/profile.view.dart'; @@ -47,7 +49,10 @@ import 'package:twonly/src/visual/views/settings/subscription/subscription.view. import 'package:twonly/src/visual/views/user_study/user_study_questionnaire.view.dart'; import 'package:twonly/src/visual/views/user_study/user_study_welcome.view.dart'; +final GlobalKey rootNavigatorKey = GlobalKey(); + final routerProvider = GoRouter( + navigatorKey: rootNavigatorKey, routes: [ GoRoute( path: Routes.home, @@ -201,6 +206,10 @@ final routerProvider = GoRouter( path: 'user_discovery', builder: (context, state) => const UserDiscoverySettingsView(), ), + GoRoute( + path: 'profile_selection', + builder: (context, state) => const ProfileSelectionSettingsView(), + ), ], ), GoRoute( diff --git a/lib/src/services/api.service.dart b/lib/src/services/api.service.dart index c81d224d..65cc226b 100644 --- a/lib/src/services/api.service.dart +++ b/lib/src/services/api.service.dart @@ -21,8 +21,7 @@ import 'package:twonly/locator.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pbserver.dart'; import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; -import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart' - as server; +import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart' as server; import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pbserver.dart'; import 'package:twonly/src/services/api/client2client/user_discovery.c2c.dart'; import 'package:twonly/src/services/api/mediafiles/download.api.dart'; @@ -66,15 +65,13 @@ class ApiService { Stream get onPlanUpdated => _planUpdateController.stream; final _connectionStateController = StreamController.broadcast(); - Stream get onConnectionStateUpdated => - _connectionStateController.stream; + Stream get onConnectionStateUpdated => _connectionStateController.stream; final _appOutdatedController = StreamController.broadcast(); Stream get onAppOutdated => _appOutdatedController.stream; final _newDeviceRegisteredController = StreamController.broadcast(); - Stream get onNewDeviceRegistered => - _newDeviceRegisteredController.stream; + Stream get onNewDeviceRegistered => _newDeviceRegisteredController.stream; bool appIsOutdated = false; bool isAuthenticated = false; @@ -83,8 +80,7 @@ class ApiService { Timer? reconnectionTimer; int _reconnectionDelay = 5; - final HashMap> _pendingRequests = - HashMap(); + final HashMap> _pendingRequests = HashMap(); IOWebSocketChannel? _channel; // ignore: cancel_subscriptions StreamSubscription>? _connectivitySubscription; @@ -96,12 +92,20 @@ class ApiService { Uri.parse(apiUrl), pingInterval: const Duration(seconds: 30), ); + + try { + await channel.ready.timeout(const Duration(seconds: 10)); + } catch (e) { + channel.sink.close().ignore(); + rethrow; + } + _channel = channel; _channel!.stream.listen(_onData, onDone: _onDone, onError: _onError); - await _channel!.ready; Log.info('websocket connected to $apiUrl'); return true; - } catch (_) { + } catch (e) { + _channel = null; return false; } } @@ -148,6 +152,7 @@ class ApiService { } Future onClosed() async { + if (_channel == null) return; Log.info('websocket connection closed'); _channel = null; isAuthenticated = false; @@ -179,15 +184,19 @@ class ApiService { _reconnectionDelay = 3; } - Future close(Function callback) async { + Future close(Function? callback) async { Log.info('closing websocket connection'); if (_channel != null) { - await _channel!.sink.close(); + try { + await _channel!.sink.close().timeout(const Duration(seconds: 2)); + } catch (e) { + Log.warn('Timeout or error closing websocket: $e'); + } await onClosed(); - callback(); + callback?.call(); return; } - callback(); + callback?.call(); } Future listenToNetworkChanges() async { @@ -245,7 +254,10 @@ class ApiService { Future _onData(dynamic msgBuffer) async { try { - final msg = server.ServerToClient.fromBuffer(msgBuffer as Uint8List); + if (msgBuffer is! Uint8List) { + msgBuffer = Uint8List.fromList(msgBuffer as List); + } + final msg = server.ServerToClient.fromBuffer(msgBuffer); if (msg.v0.hasResponse()) { final completer = _pendingRequests.remove(msg.v0.seq); if (completer != null && !completer.isCompleted) { @@ -406,9 +418,7 @@ class ApiService { } if (res.error == ErrorCode.UserIdNotFound && contactId != null) { Log.warn('Contact deleted their account $contactId.'); - final contact = await twonlyDB.contactsDao - .getContactByUserId(contactId) - .getSingleOrNull(); + final contact = await twonlyDB.contactsDao.getContactByUserId(contactId).getSingleOrNull(); if (contact != null) { await twonlyDB.contactsDao.updateContact( contactId, @@ -473,8 +483,7 @@ class ApiService { return true; } if (result.isError) { - if (result.error != ErrorCode.AuthTokenNotValid && - result.error != ErrorCode.ForegroundSessionConnected) { + if (result.error != ErrorCode.AuthTokenNotValid && result.error != ErrorCode.ForegroundSessionConnected) { Log.error( 'got error while authenticating to the server: ${result.error}', ); @@ -512,8 +521,7 @@ class ApiService { return true; } if (result.isError) { - if (result.error != ErrorCode.AuthTokenNotValid && - result.error != ErrorCode.ForegroundSessionConnected) { + if (result.error != ErrorCode.AuthTokenNotValid && result.error != ErrorCode.ForegroundSessionConnected) { Log.error( 'got error while authenticating to the server: ${result.error}', ); @@ -545,8 +553,7 @@ class ApiService { return; } - final handshake = Handshake() - ..getAuthChallenge = Handshake_GetAuthChallenge(); + final handshake = Handshake()..getAuthChallenge = Handshake_GetAuthChallenge(); final req = createClientToServerFromHandshake(handshake); final result = await sendRequestSync(req, authenticated: false); @@ -611,9 +618,7 @@ class ApiService { final register = Handshake_Register() ..username = username - ..publicIdentityKey = (await signalStore.getIdentityKeyPair()) - .getPublicKey() - .serialize() + ..publicIdentityKey = (await signalStore.getIdentityKeyPair()).getPublicKey().serialize() ..registrationId = Int64(signalIdentity.registrationId) ..signedPrekey = signedPreKey.getKeyPair().publicKey.serialize() ..signedPrekeySignature = signedPreKey.signature diff --git a/lib/src/services/api/client2client/additional_data.c2c.dart b/lib/src/services/api/client2client/additional_data.c2c.dart index 5a451bb4..e99747db 100644 --- a/lib/src/services/api/client2client/additional_data.c2c.dart +++ b/lib/src/services/api/client2client/additional_data.c2c.dart @@ -10,9 +10,10 @@ Future handleAdditionalDataMessage( int fromUserId, String groupId, EncryptedContent_AdditionalDataMessage message, + String receiptId, ) async { Log.info( - 'Got a additional data message: ${message.senderMessageId} from $groupId', + '[$receiptId] Got a additional data message: ${message.senderMessageId} from $groupId', ); // Prevent message overwrite: reject if a message with this ID already @@ -22,7 +23,7 @@ Future handleAdditionalDataMessage( .getSingleOrNull(); if (existing != null && existing.senderId != fromUserId) { Log.warn( - '$fromUserId tried to overwrite message from ${existing.senderId}. Dropping.', + '[$receiptId] $fromUserId tried to overwrite message from ${existing.senderId}. Dropping.', ); return; } @@ -45,6 +46,6 @@ Future handleAdditionalDataMessage( fromTimestamp(message.timestamp), ); if (msg != null) { - Log.info('Inserted a new text message with ID: ${msg.messageId}'); + Log.info('[$receiptId] Inserted a new text message with ID: ${msg.messageId}'); } } diff --git a/lib/src/services/api/client2client/contact.c2c.dart b/lib/src/services/api/client2client/contact.c2c.dart index af306db3..706f8545 100644 --- a/lib/src/services/api/client2client/contact.c2c.dart +++ b/lib/src/services/api/client2client/contact.c2c.dart @@ -28,7 +28,6 @@ Future handleNewContactRequest(int fromUserId) async { await handleContactAccept(fromUserId); } - // contact was already accepted, so just accept the request in the background. await sendCipherText( contact.userId, EncryptedContent( @@ -36,6 +35,7 @@ Future handleNewContactRequest(int fromUserId) async { type: EncryptedContent_ContactRequest_Type.ACCEPT, ), ), + blocking: false, ); return true; } @@ -88,16 +88,17 @@ Future handleContactAccept(int fromUserId) async { Future handleContactRequest( int fromUserId, EncryptedContent_ContactRequest contactRequest, + String receiptId, ) async { switch (contactRequest.type) { case EncryptedContent_ContactRequest_Type.REQUEST: - Log.info('Got a contact request from $fromUserId'); + Log.info('[$receiptId] Got a contact request from $fromUserId'); return handleNewContactRequest(fromUserId); case EncryptedContent_ContactRequest_Type.ACCEPT: - Log.info('Got a contact accept from $fromUserId'); + Log.info('[$receiptId] Got a contact accept from $fromUserId'); await handleContactAccept(fromUserId); case EncryptedContent_ContactRequest_Type.REJECT: - Log.info('Got a contact reject from $fromUserId'); + Log.info('[$receiptId] Got a contact reject from $fromUserId'); await twonlyDB.contactsDao.updateContact( fromUserId, const ContactsCompanion( @@ -114,14 +115,15 @@ Future handleContactUpdate( int fromUserId, EncryptedContent_ContactUpdate contactUpdate, int? senderProfileCounter, + String receiptId, ) async { switch (contactUpdate.type) { case EncryptedContent_ContactUpdate_Type.REQUEST: - Log.info('Got a contact update request from $fromUserId'); + Log.info('[$receiptId] Got a contact update request from $fromUserId'); await sendContactMyProfileData(fromUserId); case EncryptedContent_ContactUpdate_Type.UPDATE: - Log.info('Got a contact update $fromUserId'); + Log.info('[$receiptId] Got a contact update $fromUserId'); Uint8List? avatarSvgCompressed; if (contactUpdate.hasAvatarSvgCompressed()) { avatarSvgCompressed = Uint8List.fromList( @@ -188,8 +190,9 @@ Future handleContactUpdate( Future handleFlameSync( String groupId, EncryptedContent_FlameSync flameSync, + String receiptId, ) async { - Log.info('Got a flameSync for group $groupId'); + Log.info('[$receiptId] Got a flameSync for group $groupId'); final group = await twonlyDB.groupsDao.getGroup(groupId); if (group == null || group.lastFlameCounterChange == null) return; @@ -235,6 +238,7 @@ Future checkForProfileUpdate( type: EncryptedContent_ContactUpdate_Type.REQUEST, ), ), + blocking: false, ); } } diff --git a/lib/src/services/api/client2client/errors.c2c.dart b/lib/src/services/api/client2client/errors.c2c.dart index bc03adfd..6aa093a2 100644 --- a/lib/src/services/api/client2client/errors.c2c.dart +++ b/lib/src/services/api/client2client/errors.c2c.dart @@ -8,8 +8,9 @@ import 'package:twonly/src/utils/log.dart'; Future handleErrorMessage( int fromUserId, EncryptedContent_ErrorMessages error, + String receiptId, ) async { - Log.error('Got error from $fromUserId: $error'); + Log.error('[$receiptId] Got error from $fromUserId: $error'); switch (error.type) { case EncryptedContent_ErrorMessages_Type diff --git a/lib/src/services/api/client2client/groups.c2c.dart b/lib/src/services/api/client2client/groups.c2c.dart index f9fb9a1e..324af230 100644 --- a/lib/src/services/api/client2client/groups.c2c.dart +++ b/lib/src/services/api/client2client/groups.c2c.dart @@ -15,14 +15,13 @@ Future handleGroupCreate( int fromUserId, String groupId, EncryptedContent_GroupCreate newGroup, + String receiptId, ) async { - final user = await twonlyDB.contactsDao - .getContactByUserId(fromUserId) - .getSingleOrNull(); + final user = await twonlyDB.contactsDao.getContactByUserId(fromUserId).getSingleOrNull(); if (user == null) { // Only contacts can invite other contacts, so this can (via the UI) not happen. Log.error( - 'User is not a contact. Aborting.', + '[$receiptId] User is not a contact. Aborting.', ); return; } @@ -66,7 +65,7 @@ Future handleGroupCreate( if (group == null) { Log.error( - 'Could not create new group. Probably because the group already existed.', + '[$receiptId] Could not create new group. Probably because the group already existed.', ); return; } @@ -108,12 +107,13 @@ Future handleGroupUpdate( int fromUserId, String groupId, EncryptedContent_GroupUpdate update, + String receiptId, ) async { - Log.info('Got group update for $groupId from $fromUserId'); + Log.info('[$receiptId] Got group update for $groupId from $fromUserId'); final actionType = groupActionTypeFromString(update.groupActionType); if (actionType == null) { - Log.error('Group action ${update.groupActionType} is unknown ignoring.'); + Log.error('[$receiptId] Group action ${update.groupActionType} is unknown ignoring.'); return; } @@ -189,10 +189,11 @@ Future handleGroupJoin( int fromUserId, String groupId, EncryptedContent_GroupJoin join, + String receiptId, ) async { if (await twonlyDB.contactsDao.getContactById(fromUserId) == null) { if (!await addNewHiddenContact(fromUserId)) { - Log.error('Got group join, but could not load contact.'); + Log.error('[$receiptId] Got group join, but could not load contact.'); // This can happen in case the group join was received before the group create. // In this case return false, which will cause the receipt to fail and the user // will resend this message. @@ -213,6 +214,7 @@ Future handleResendGroupPublicKey( int fromUserId, String groupId, EncryptedContent_GroupJoin join, + String receiptId, ) async { final group = await twonlyDB.groupsDao.getGroup(groupId); if (group == null || group.myGroupPrivateKey == null) return; @@ -225,6 +227,7 @@ Future handleResendGroupPublicKey( groupPublicKey: keyPair.getPublicKey().serialize(), ), ), + blocking: false, ); } @@ -232,6 +235,7 @@ Future handleTypingIndicator( int fromUserId, String groupId, EncryptedContent_TypingIndicator indicator, + String receiptId, ) async { var lastTypeIndicator = const Value.absent(); diff --git a/lib/src/services/api/client2client/media.c2c.dart b/lib/src/services/api/client2client/media.c2c.dart index 303cd3e2..ca282a71 100644 --- a/lib/src/services/api/client2client/media.c2c.dart +++ b/lib/src/services/api/client2client/media.c2c.dart @@ -18,9 +18,10 @@ Future handleMedia( int fromUserId, String groupId, EncryptedContent_Media media, + String receiptId, ) async { Log.info( - 'Got a media message: ${media.senderMessageId} from $groupId with type ${media.type}', + '[$receiptId] Got a media message: ${media.senderMessageId} from $groupId with type ${media.type}', ); late MediaType mediaType; @@ -33,7 +34,7 @@ Future handleMedia( message.senderId != fromUserId || message.mediaId == null) { Log.warn( - 'Got reupload from $fromUserId for a message that either does not exists (${message == null}) or senderId = ${message?.senderId}', + '[$receiptId] Got reupload for a message that either does not exists (${message == null}) or senderId = ${message?.senderId}', ); return; } @@ -82,13 +83,13 @@ Future handleMedia( if (messageTmp != null) { if (messageTmp.senderId != fromUserId) { Log.warn( - '$fromUserId tried to modify the message from ${messageTmp.senderId}.', + '[$receiptId] $fromUserId tried to modify the message from ${messageTmp.senderId}.', ); return; } if (messageTmp.mediaId == null) { Log.warn( - 'This message already exit without a mediaId. Message is dropped.', + '[$receiptId] This message already exit without a mediaId. Message is dropped.', ); return; } @@ -97,7 +98,7 @@ Future handleMedia( ); if (mediaFile?.downloadState != DownloadState.reuploadRequested) { Log.warn( - 'This message and media file already exit and was not requested again. Dropping it.', + '[$receiptId] This message and media file already exit and was not requested again. Dropping it.', ); return; } @@ -121,7 +122,9 @@ Future handleMedia( MediaFile? mediaFile; Message? message; - Log.info('Starting transaction for media message ${media.senderMessageId}'); + Log.info( + '[$receiptId] Starting transaction for media message ${media.senderMessageId}', + ); await twonlyDB.transaction(() async { mediaFile = await twonlyDB.mediaFilesDao.insertOrUpdateMedia( MediaFilesCompanion( @@ -141,7 +144,7 @@ Future handleMedia( ); if (mediaFile == null) { - Log.error('Could not insert media file into database'); + Log.error('[$receiptId] Could not insert media file into database'); return; } @@ -165,7 +168,7 @@ Future handleMedia( ); }); Log.info( - 'Finished transaction for media message ${media.senderMessageId}. Success: ${message != null}', + '[$receiptId] Finished transaction for media message ${media.senderMessageId}. Success: ${message != null}', ); if (message != null && mediaFile != null) { @@ -173,7 +176,9 @@ Future handleMedia( groupId, fromTimestamp(media.timestamp), ); - Log.info('Inserted a new media message with ID: ${message!.messageId}'); + Log.info( + '[$receiptId] Inserted a new media message with ID: ${message!.messageId}', + ); await incFlameCounter( message!.groupId, true, @@ -184,12 +189,16 @@ Future handleMedia( } else { if (mediaFile == null && message == null) { Log.error( - 'Could not insert new message as both the message and mediaFile are empty.', + '[$receiptId] Could not insert new message as both the message and mediaFile are empty.', ); } else if (mediaFile == null) { - Log.error('Could not insert new message as the mediaFile is empty.'); + Log.error( + '[$receiptId] Could not insert new message as the mediaFile is empty.', + ); } else { - Log.error('Could not insert new message as the message is empty.'); + Log.error( + '[$receiptId] Could not insert new message as the message is empty.', + ); } } } @@ -197,6 +206,7 @@ Future handleMedia( Future handleMediaUpdate( int fromUserId, EncryptedContent_MediaUpdate mediaUpdate, + String receiptId, ) async { final message = await twonlyDB.messagesDao .getMessageById(mediaUpdate.targetMessageId) @@ -204,14 +214,14 @@ Future handleMediaUpdate( if (message == null) { // this can happen, in case the message was already deleted. Log.info( - 'Got media update to message ${mediaUpdate.targetMessageId} but message not found.', + '[$receiptId] Got media update to message ${mediaUpdate.targetMessageId} but message not found.', ); return; } if (message.mediaId == null) { // this can happen, in case the message was already deleted. Log.warn( - 'Got media update for message ${mediaUpdate.targetMessageId} which does not have a mediaId defined.', + '[$receiptId] Got media update for message ${mediaUpdate.targetMessageId} which does not have a mediaId defined.', ); return; } @@ -220,14 +230,14 @@ Future handleMediaUpdate( ); if (mediaFile == null) { Log.info( - 'Got media file update, but media file was not found ${message.mediaId}', + '[$receiptId] Got media file update, but media file was not found ${message.mediaId}', ); return; } switch (mediaUpdate.type) { case EncryptedContent_MediaUpdate_Type.REOPENED: - Log.info('Got media file reopened ${mediaFile.mediaId}'); + Log.info('[$receiptId] Got media file reopened ${mediaFile.mediaId}'); await twonlyDB.messagesDao.updateMessageId( message.messageId, const MessagesCompanion( @@ -235,7 +245,7 @@ Future handleMediaUpdate( ), ); case EncryptedContent_MediaUpdate_Type.STORED: - Log.info('Got media file stored ${mediaFile.mediaId}'); + Log.info('[$receiptId] Got media file stored ${mediaFile.mediaId}'); final mediaService = MediaFileService(mediaFile); await mediaService.storeMediaFile(); await twonlyDB.messagesDao.updateMessageId( @@ -246,7 +256,9 @@ Future handleMediaUpdate( ); case EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR: - Log.info('Got media file decryption error ${mediaFile.mediaId}'); + Log.info( + '[$receiptId] Got media file decryption error ${mediaFile.mediaId}', + ); await reuploadMediaFile(fromUserId, mediaFile, message.messageId); } } diff --git a/lib/src/services/api/client2client/messages.c2c.dart b/lib/src/services/api/client2client/messages.c2c.dart index 4561687f..a8f911fd 100644 --- a/lib/src/services/api/client2client/messages.c2c.dart +++ b/lib/src/services/api/client2client/messages.c2c.dart @@ -7,11 +7,12 @@ import 'package:twonly/src/utils/log.dart'; Future handleMessageUpdate( int contactId, EncryptedContent_MessageUpdate messageUpdate, + String receiptId, ) async { switch (messageUpdate.type) { case EncryptedContent_MessageUpdate_Type.OPENED: Log.info( - 'Opened message ${messageUpdate.multipleTargetMessageIds}', + '[$receiptId] Opened message ${messageUpdate.multipleTargetMessageIds}', ); try { await twonlyDB.messagesDao.handleMessagesOpened( @@ -20,13 +21,13 @@ Future handleMessageUpdate( fromTimestamp(messageUpdate.timestamp), ); } catch (e) { - Log.warn(e); + Log.warn('[$receiptId] Error handling messages opened: $e'); } case EncryptedContent_MessageUpdate_Type.DELETE: - if (!await isSender(contactId, messageUpdate.senderMessageId)) { + if (!await isSender(contactId, messageUpdate.senderMessageId, receiptId)) { return; } - Log.info('Delete message ${messageUpdate.senderMessageId}'); + Log.info('[$receiptId] Delete message ${messageUpdate.senderMessageId}'); try { await twonlyDB.messagesDao.handleMessageDeletion( contactId, @@ -34,13 +35,13 @@ Future handleMessageUpdate( fromTimestamp(messageUpdate.timestamp), ); } catch (e) { - Log.warn(e); + Log.warn('[$receiptId] Error handling message deletion: $e'); } case EncryptedContent_MessageUpdate_Type.EDIT_TEXT: - if (!await isSender(contactId, messageUpdate.senderMessageId)) { + if (!await isSender(contactId, messageUpdate.senderMessageId, receiptId)) { return; } - Log.info('Edit message ${messageUpdate.senderMessageId}'); + Log.info('[$receiptId] Edit message ${messageUpdate.senderMessageId}'); try { await twonlyDB.messagesDao.handleTextEdit( contactId, @@ -49,12 +50,12 @@ Future handleMessageUpdate( fromTimestamp(messageUpdate.timestamp), ); } catch (e) { - Log.warn(e); + Log.warn('[$receiptId] Error handling text edit: $e'); } } } -Future isSender(int fromUserId, String messageId) async { +Future isSender(int fromUserId, String messageId, String receiptId) async { final message = await twonlyDB.messagesDao .getMessageById(messageId) .getSingleOrNull(); @@ -62,6 +63,6 @@ Future isSender(int fromUserId, String messageId) async { if (message.senderId == fromUserId) { return true; } - Log.error('Contact $fromUserId tried to modify the message $messageId'); + Log.error('[$receiptId] Contact $fromUserId tried to modify the message $messageId'); return false; } diff --git a/lib/src/services/api/client2client/pushkeys.c2c.dart b/lib/src/services/api/client2client/pushkeys.c2c.dart index 9f2a7072..8d6bfcfb 100644 --- a/lib/src/services/api/client2client/pushkeys.c2c.dart +++ b/lib/src/services/api/client2client/pushkeys.c2c.dart @@ -10,10 +10,11 @@ DateTime lastPushKeyRequest = clock.now().subtract(const Duration(hours: 1)); Future handlePushKey( int contactId, EncryptedContent_PushKeys pushKeys, + String receiptId, ) async { switch (pushKeys.type) { case EncryptedContent_PushKeys_Type.REQUEST: - Log.info('Got a pushkey request from $contactId'); + Log.info('[$receiptId] Got a pushkey request from $contactId'); if (lastPushKeyRequest.isBefore( clock.now().subtract(const Duration(seconds: 60)), )) { @@ -22,7 +23,7 @@ Future handlePushKey( } case EncryptedContent_PushKeys_Type.UPDATE: - Log.info('Got a pushkey update from $contactId'); + Log.info('[$receiptId] Got a pushkey update from $contactId'); await handleNewPushKey(contactId, pushKeys.keyId.toInt(), pushKeys.key); } } diff --git a/lib/src/services/api/client2client/reaction.c2c.dart b/lib/src/services/api/client2client/reaction.c2c.dart index 77fcdcc3..7988e55b 100644 --- a/lib/src/services/api/client2client/reaction.c2c.dart +++ b/lib/src/services/api/client2client/reaction.c2c.dart @@ -8,8 +8,9 @@ Future handleReaction( int fromUserId, String groupId, EncryptedContent_Reaction reaction, + String receiptId, ) async { - Log.info('Got a reaction from $fromUserId (remove=${reaction.remove})'); + Log.info('[$receiptId] Got a reaction from $fromUserId (remove=${reaction.remove})'); await twonlyDB.reactionsDao.updateReaction( fromUserId, reaction.targetMessageId, diff --git a/lib/src/services/api/client2client/text_message.c2c.dart b/lib/src/services/api/client2client/text_message.c2c.dart index b46e72f2..7c5cc01c 100644 --- a/lib/src/services/api/client2client/text_message.c2c.dart +++ b/lib/src/services/api/client2client/text_message.c2c.dart @@ -11,9 +11,10 @@ Future handleTextMessage( int fromUserId, String groupId, EncryptedContent_TextMessage textMessage, + String receiptId, ) async { Log.info( - 'Got a text message: ${textMessage.senderMessageId} from $groupId', + '[$receiptId] Got a text message: ${textMessage.senderMessageId} from $groupId', ); // Prevent message overwrite: reject if a message with this ID already @@ -23,7 +24,7 @@ Future handleTextMessage( .getSingleOrNull(); if (existing != null && existing.senderId != fromUserId) { Log.warn( - '$fromUserId tried to overwrite message from ${existing.senderId}. Dropping.', + '[$receiptId] $fromUserId tried to overwrite message from ${existing.senderId}. Dropping.', ); return; } @@ -47,6 +48,6 @@ Future handleTextMessage( fromTimestamp(textMessage.timestamp), ); if (message != null) { - Log.info('Inserted a new text message with ID: ${message.messageId}'); + Log.info('[$receiptId] Inserted a new text message with ID: ${message.messageId}'); } } diff --git a/lib/src/services/api/client2client/user_discovery.c2c.dart b/lib/src/services/api/client2client/user_discovery.c2c.dart index b206109d..df0f03ec 100644 --- a/lib/src/services/api/client2client/user_discovery.c2c.dart +++ b/lib/src/services/api/client2client/user_discovery.c2c.dart @@ -15,7 +15,9 @@ void resetUserDiscoveryRequestUpdates() { Future checkForUserDiscoveryChanges( int fromUserId, List receivedVersion, + String receiptId, ) async { + Log.info('[$receiptId] Checking for a new user discovery version.'); final currentVersion = await UserDiscoveryService.shouldRequestNewMessages( fromUserId, receivedVersion, @@ -26,7 +28,7 @@ Future checkForUserDiscoveryChanges( // Only request a new version once per app session return; } - Log.info('Having old version from contact. Requesting new version.'); + Log.info('[$receiptId] Having old version from contact. Requesting new version.'); _requestedUpdates.add(fromUserId); await sendCipherText( fromUserId, @@ -35,6 +37,7 @@ Future checkForUserDiscoveryChanges( currentVersion: currentVersion.toList(), ), ), + blocking: false, ); } } @@ -42,18 +45,19 @@ Future checkForUserDiscoveryChanges( Future handleUserDiscoveryRequest( int fromUserId, EncryptedContent_UserDiscoveryRequest request, + String receiptId, ) async { - Log.info('Got a user discovery request'); + Log.info('[$receiptId] Got a user discovery request'); if (!userService.currentUser.isUserDiscoveryEnabled) { - Log.warn('Got a user discovery request while it is disabled'); + Log.warn('[$receiptId] Got a user discovery request while it is disabled'); return; } final contact = await twonlyDB.contactsDao.getContactById(fromUserId); if (!UserDiscoveryService.isContactAllowed(contact)) { Log.warn( - 'Got a request to update user discovery, but mediaSendCounter (${contact?.mediaSendCounter}) < ${userService.currentUser.requiredSendImages} or user is excluded ${contact?.userDiscoveryExcluded}', + '[$receiptId] Got a request to update user discovery, but mediaSendCounter (${contact?.mediaSendCounter}) < ${userService.currentUser.requiredSendImages} or user is excluded ${contact?.userDiscoveryExcluded}', ); return; } @@ -63,7 +67,7 @@ Future handleUserDiscoveryRequest( request.currentVersion, ); if (newMessages != null && newMessages.isNotEmpty) { - Log.info('Sending ${newMessages.length} user discovery messages'); + Log.info('[$receiptId] Sending ${newMessages.length} user discovery messages'); await sendCipherText( fromUserId, EncryptedContent( @@ -71,21 +75,23 @@ Future handleUserDiscoveryRequest( messages: newMessages, ), ), + blocking: false, ); } else { - Log.info('Got update request, but there are no new updates for the user'); + Log.info('[$receiptId] Got update request, but there are no new updates for the user'); } } Future handleUserDiscoveryUpdate( int fromUserId, EncryptedContent_UserDiscoveryUpdate update, + String receiptId, ) async { if (!userService.currentUser.isUserDiscoveryEnabled) { - Log.warn('Got a user discovery update while it is disabled'); + Log.warn('[$receiptId] Got a user discovery update while it is disabled'); return; } - Log.info('Got ${update.messages.length} user discovery messages'); + Log.info('[$receiptId] Got ${update.messages.length} user discovery messages'); await UserDiscoveryService.handleNewMessages( fromUserId, update.messages.map(Uint8List.fromList).toList(), diff --git a/lib/src/services/api/mediafiles/upload.api.dart b/lib/src/services/api/mediafiles/upload.api.dart index 97c8a1fc..da176777 100644 --- a/lib/src/services/api/mediafiles/upload.api.dart +++ b/lib/src/services/api/mediafiles/upload.api.dart @@ -35,7 +35,6 @@ Future _protectMediaUpload( ) async { final mutex = _uploadMutexes.putIfAbsent(mediaId, Mutex.new); await mutex.protect(action); - _uploadMutexes.remove(mediaId); } Future reuploadMediaFiles() async { diff --git a/lib/src/services/api/messages.api.dart b/lib/src/services/api/messages.api.dart index a1baadda..7d26da32 100644 --- a/lib/src/services/api/messages.api.dart +++ b/lib/src/services/api/messages.api.dart @@ -61,6 +61,8 @@ Future retransmitAllMessages() async { }); } +final Map _tryToSendLocks = {}; + // When the ackByServerAt is set this value is written in the receipted Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({ String? receiptId, @@ -68,15 +70,41 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({ bool onlyReturnEncryptedData = false, bool blocking = true, }) async { + final rId = receiptId ?? receipt?.receiptId; + if (rId == null) { + Log.error( + 'Cannot try to send complete message as both receiptId and receipt are null.', + ); + return null; + } + + final mutex = _tryToSendLocks.putIfAbsent(rId, Mutex.new); + return mutex.protect(() async { + return _tryToSendCompleteMessageInternal( + receiptId: receiptId, + receipt: receipt, + onlyReturnEncryptedData: onlyReturnEncryptedData, + blocking: blocking, + ); + }); +} + +Future<(Uint8List, Uint8List?)?> _tryToSendCompleteMessageInternal({ + String? receiptId, + Receipt? receipt, + bool onlyReturnEncryptedData = false, + bool blocking = true, +}) async { + // this should have a lock for every receiptID, split the function into a _internal withou the lock and a normal with the lock if (apiService.appIsOutdated) return null; + if (receiptId == null && receipt == null) return null; try { - if (receiptId == null && receipt == null) return null; if (receipt == null) { // ignore: parameter_assignments receipt = await twonlyDB.receiptsDao.getReceiptById(receiptId!); if (receipt == null) { - Log.warn('Receipt not found.'); + Log.warn('[$receiptId] Receipt not found.'); return null; } } @@ -120,7 +148,7 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({ message.encryptedContent, ); - Log.info('Uploading ${receipt.receiptId}.'); + Log.info('Uploading message with receiptID ${receipt.receiptId}.'); Uint8List? pushData; if (receipt.retryCount == 0) { @@ -176,7 +204,7 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({ ); if (resp.isError) { - Log.warn('Could not transmit message got ${resp.error}.'); + Log.warn('Could not transmit ${receipt.receiptId} got ${resp.error}.'); if (resp.error == ErrorCode.UserIdNotFound) { await twonlyDB.receiptsDao.deleteReceipt(receipt.receiptId); await twonlyDB.contactsDao.updateContact( @@ -210,7 +238,7 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({ } } } catch (e) { - Log.error('Unknown Error when sending message: $e'); + Log.error('[$receiptId] unknown error when sending message: $e'); if (receipt != null) { await twonlyDB.receiptsDao.deleteReceipt(receipt.receiptId); } @@ -316,6 +344,52 @@ Future insertAndSendContactShareMessage( ); } +Future insertAndSendAskAboutUserMessage( + int contactId, + int askAboutUserId, +) async { + final directChat = await twonlyDB.groupsDao.createOrGetDirectChat(contactId); + if (directChat == null) { + Log.error('Failed to get or create direct chat group for contact $contactId'); + return; + } + + final groupId = directChat.groupId; + + final additionalMessageData = AdditionalMessageData( + type: AdditionalMessageData_Type.ASK_ABOUT_USER, + askAboutUserId: Int64(askAboutUserId), + ); + + final message = await twonlyDB.messagesDao.insertMessage( + MessagesCompanion( + groupId: Value(groupId), + type: Value(MessageType.askAboutUser.name), + additionalMessageData: Value(additionalMessageData.writeToBuffer()), + ), + ); + + if (message == null) { + Log.error('Could not insert message into database'); + return; + } + + final encryptedContent = pb.EncryptedContent( + additionalDataMessage: pb.EncryptedContent_AdditionalDataMessage( + senderMessageId: message.messageId, + additionalMessageData: additionalMessageData.writeToBuffer(), + timestamp: Int64(message.createdAt.millisecondsSinceEpoch), + type: MessageType.askAboutUser.name, + ), + ); + + await sendCipherTextToGroup( + groupId, + encryptedContent, + messageId: message.messageId, + ); +} + Future sendCipherTextToGroup( String groupId, pb.EncryptedContent encryptedContent, { @@ -492,5 +566,5 @@ Future sendContactMyProfileData(int contactId) async { username: userService.currentUser.username, ), ); - await sendCipherText(contactId, encryptedContent); + await sendCipherText(contactId, encryptedContent, blocking: false); } diff --git a/lib/src/services/api/server_messages.api.dart b/lib/src/services/api/server_messages.api.dart index 8ff3aca6..a16b2028 100644 --- a/lib/src/services/api/server_messages.api.dart +++ b/lib/src/services/api/server_messages.api.dart @@ -73,7 +73,7 @@ Future handleServerMessage(server.ServerToClient msg) async { await apiService.sendResponse(ClientToServer()..v0 = v0); AppState.gotMessageFromServer = true; - Log.info('Message from server proccessed.'); + Log.info('All messages from the server proccessed.'); } DateTime lastPushKeyRequest = clock.now().subtract(const Duration(hours: 1)); @@ -86,10 +86,19 @@ Future handleClient2ClientMessage(NewMessage newMessage) async { final receiptId = message.receiptId; final mutex = _messageLocks.putIfAbsent(receiptId, Mutex.new); + if (mutex.isLocked) { + Log.info( + '[$receiptId] Skipping — already being processed by another handler', + ); + return; + } await mutex.protect(() async { - await _handleClient2ClientMessage(newMessage, message); + try { + await _handleClient2ClientMessage(newMessage, message); + } finally { + _messageLocks.remove(receiptId); + } }); - _messageLocks.remove(receiptId); } Future _handleClient2ClientMessage( @@ -103,11 +112,11 @@ Future _handleClient2ClientMessage( return; } - Log.info('Started processing message with receiptId $receiptId'); + Log.info('[$receiptId] Started processing message'); switch (message.type) { case Message_Type.SENDER_DELIVERY_RECEIPT: - Log.info('Got delivery receipt for $receiptId!'); + Log.info('[$receiptId] Got delivery receipt!'); await twonlyDB.receiptsDao.confirmReceipt(receiptId, fromUserId); case Message_Type.PLAINTEXT_CONTENT: @@ -120,13 +129,13 @@ Future _handleClient2ClientMessage( await handleSessionResync(fromUserId); } Log.info( - 'Got decryption error: ${message.plaintextContent.decryptionErrorMessage.type} for $receiptId', + '[$receiptId] Got decryption error: ${message.plaintextContent.decryptionErrorMessage.type}', ); retry = true; } if (message.plaintextContent.hasRetryControlError()) { Log.info( - 'Got access control error for $receiptId. Resending message.', + '[$receiptId] Got access control error. Resending message.', ); retry = true; } @@ -141,7 +150,10 @@ Future _handleClient2ClientMessage( ackByServerAt: const Value(null), ), ); - await tryToSendCompleteMessage(receiptId: newReceiptId); + Log.info( + '[$receiptId] Sending error message to the original sender with receiptId $newReceiptId.', + ); + await tryToSendCompleteMessage(receiptId: newReceiptId, blocking: false); } case Message_Type.CIPHERTEXT: @@ -197,7 +209,6 @@ Future _handleClient2ClientMessage( receiptIdDB = const Value.absent(); } else { // Message was successful processed - // } } @@ -213,9 +224,9 @@ Future _handleClient2ClientMessage( ), ); } catch (e) { - Log.warn(e); + Log.warn('[$receiptId] Error inserting receipt: $e'); } - await tryToSendCompleteMessage(receiptId: receiptId); + await tryToSendCompleteMessage(receiptId: receiptId, blocking: false); } case Message_Type.TEST_NOTIFICATION: break; @@ -223,9 +234,9 @@ Future _handleClient2ClientMessage( try { await twonlyDB.receiptsDao.gotReceipt(receiptId); - Log.info('Got a message with receiptId $receiptId'); + Log.info('[$receiptId] Finished processing'); } catch (e) { - Log.error('Error marking message as received $receiptId: $e'); + Log.error('[$receiptId] Error marking message as received: $e'); } } @@ -235,26 +246,26 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessageRaw( Message_Type messageType, String receiptId, ) async { - final (encryptedContent, decryptionErrorType) = await signalDecryptMessage( + Log.info('[$receiptId] calling signalDecryptMessage'); + var (encryptedContent, decryptionErrorType) = await signalDecryptMessage( fromUserId, encryptedContentRaw, messageType.value, ); if (encryptedContent == null) { - if (decryptionErrorType == null) { - // Duplicate message - return (null, null); - } return ( null, - PlaintextContent() - ..decryptionErrorMessage = (PlaintextContent_DecryptionErrorMessage() - ..type = decryptionErrorType), + PlaintextContent( + decryptionErrorMessage: PlaintextContent_DecryptionErrorMessage( + type: decryptionErrorType ??= + PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN, + ), + ), ); } - Log.info('Calling handleEncryptedMessage for $receiptId'); + Log.info('[$receiptId] Calling handleEncryptedMessage'); final (a, b) = await handleEncryptedMessage( fromUserId, @@ -263,7 +274,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessageRaw( receiptId, ); - Log.info('Finished handleEncryptedMessage for $receiptId'); + Log.info('[$receiptId] Finished handleEncryptedMessage'); if (Platform.isAndroid && a == null && b == null) { // Message was handled without any error -> Show push notification to the user. @@ -294,11 +305,16 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage( await checkForUserDiscoveryChanges( fromUserId, content.senderUserDiscoveryVersion, + receiptId, ); } if (content.hasContactRequest()) { - if (!await handleContactRequest(fromUserId, content.contactRequest)) { + if (!await handleContactRequest( + fromUserId, + content.contactRequest, + receiptId, + )) { return ( null, PlaintextContent() @@ -312,6 +328,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage( await handleErrorMessage( fromUserId, content.errorMessages, + receiptId, ); return (null, null); } @@ -321,6 +338,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage( fromUserId, content.contactUpdate, senderProfileCounter, + receiptId, ); return (null, null); } @@ -329,6 +347,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage( await handleUserDiscoveryRequest( fromUserId, content.userDiscoveryRequest, + receiptId, ); return (null, null); } @@ -337,12 +356,13 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage( await handleUserDiscoveryUpdate( fromUserId, content.userDiscoveryUpdate, + receiptId, ); return (null, null); } if (content.hasPushKeys()) { - await handlePushKey(fromUserId, content.pushKeys); + await handlePushKey(fromUserId, content.pushKeys, receiptId); return (null, null); } @@ -350,6 +370,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage( await handleMessageUpdate( fromUserId, content.messageUpdate, + receiptId, ); return (null, null); } @@ -366,12 +387,13 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage( await handleMediaUpdate( fromUserId, content.mediaUpdate, + receiptId, ); return (null, null); } if (!content.hasGroupId()) { - Log.error('Messages should have a groupId $fromUserId.'); + Log.error('[$receiptId] Messages should have a groupId $fromUserId.'); return (null, null); } @@ -380,6 +402,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage( fromUserId, content.groupId, content.groupCreate, + receiptId, ); return (null, null); } @@ -392,12 +415,12 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage( .getContactByUserId(fromUserId) .getSingleOrNull(); Log.info( - 'Contact exists?: ${contact != null} Is deleted? ${contact?.deletedByUser} Accepted? (${contact?.accepted})', + '[$receiptId] Contact exists?: ${contact != null} Is deleted? ${contact?.deletedByUser} Accepted? (${contact?.accepted})', ); if (contact == null || !contact.accepted || contact.deletedByUser) { await handleNewContactRequest(fromUserId); Log.error( - 'User tries to send message to direct chat while the user does not exists !', + '[$receiptId] User tries to send message to direct chat while the user does not exist!', ); return ( EncryptedContent( @@ -411,7 +434,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage( ); } Log.info( - 'Creating new DirectChat between two users', + '[$receiptId] Creating new DirectChat between two users', ); await twonlyDB.groupsDao.createNewDirectChat( fromUserId, @@ -422,7 +445,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage( } else { if (content.hasGroupJoin()) { Log.error( - 'Got group join message, but group does not exists yet, retry later. As probably the GroupCreate was not yet received.', + '[$receiptId] Got group join message, but group does not exist yet, retry later. As probably the GroupCreate was not yet received.', ); // In case the group join was received before the GroupCreate the sender should send it later again. return ( @@ -432,13 +455,15 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage( ); } - Log.error('User $fromUserId tried to access group ${content.groupId}.'); + Log.error( + '[$receiptId] User $fromUserId tried to access group ${content.groupId}.', + ); return (null, null); } } if (content.hasFlameSync()) { - await handleFlameSync(content.groupId, content.flameSync); + await handleFlameSync(content.groupId, content.flameSync, receiptId); return (null, null); } @@ -447,6 +472,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage( fromUserId, content.groupId, content.groupUpdate, + receiptId, ); return (null, null); } @@ -456,6 +482,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage( fromUserId, content.groupId, content.groupJoin, + receiptId, )) { return ( null, @@ -471,6 +498,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage( fromUserId, content.groupId, content.groupJoin, + receiptId, ); return (null, null); } @@ -480,6 +508,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage( fromUserId, content.groupId, content.additionalDataMessage, + receiptId, ); return (null, null); } @@ -489,6 +518,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage( fromUserId, content.groupId, content.textMessage, + receiptId, ); return (null, null); } @@ -498,6 +528,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage( fromUserId, content.groupId, content.reaction, + receiptId, ); return (null, null); } @@ -507,6 +538,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage( fromUserId, content.groupId, content.media, + receiptId, ); return (null, null); } @@ -516,6 +548,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage( fromUserId, content.groupId, content.typingIndicator, + receiptId, ); } diff --git a/lib/src/services/background/callback_dispatcher.background.dart b/lib/src/services/background/callback_dispatcher.background.dart index 3b689615..a738214c 100644 --- a/lib/src/services/background/callback_dispatcher.background.dart +++ b/lib/src/services/background/callback_dispatcher.background.dart @@ -119,9 +119,15 @@ Future handlePeriodicTask({int lastExecutionInSecondsLimit = 120}) async { if (!shouldBeExecuted) return; Log.info('eu.twonly.periodic_task was called.'); + AppState.gotMessageFromServer = false; final stopwatch = Stopwatch()..start(); + // Issue: Because the background isolate can be reused across multiple periodic tasks, + // the API connection state might be stale or disconnected from a previous run. + // Explicitly close it here to ensure a clean slate before connecting. + await apiService.close(null); + if (!await apiService.connect()) { Log.info('Could not connect to the api. Returning early.'); return; diff --git a/lib/src/services/flame.service.dart b/lib/src/services/flame.service.dart index 2630c419..a0396237 100644 --- a/lib/src/services/flame.service.dart +++ b/lib/src/services/flame.service.dart @@ -65,23 +65,18 @@ Future syncFlameCounters({String? forceForGroup}) async { ({int counter, bool isExpiring}) getFlameCounterFromGroup(Group? group) { const zero = (counter: 0, isExpiring: false); if (group == null) return zero; - if (group.lastMessageSend == null || - group.lastMessageReceived == null || - group.lastFlameCounterChange == null) { + if (group.lastMessageSend == null || group.lastMessageReceived == null || group.lastFlameCounterChange == null) { return zero; } final now = clock.now(); final startOfToday = DateTime(now.year, now.month, now.day); final twoDaysAgo = startOfToday.subtract(const Duration(days: 2)); final oneDayAgo = startOfToday.subtract(const Duration(days: 1)); - if (group.lastMessageSend!.isAfter(twoDaysAgo) && - group.lastMessageReceived!.isAfter(twoDaysAgo) || + if (group.lastMessageSend!.isAfter(twoDaysAgo) && group.lastMessageReceived!.isAfter(twoDaysAgo) || group.lastFlameCounterChange!.isAfter(oneDayAgo)) { // Flame is expiring when today no exchange has happened yet: // both lastMessageSend and lastMessageReceived are before startOfToday. - final isExpiring = - group.lastMessageSend!.isBefore(oneDayAgo) || - group.lastMessageReceived!.isBefore(oneDayAgo); + final isExpiring = group.lastMessageSend!.isBefore(oneDayAgo) || group.lastMessageReceived!.isBefore(oneDayAgo); return (counter: group.flameCounter, isExpiring: isExpiring); } else { return zero; @@ -122,8 +117,7 @@ Future incFlameCounter( final now = clock.now(); final startOfToday = DateTime(now.year, now.month, now.day); final twoDaysAgo = startOfToday.subtract(const Duration(days: 2)); - if (group.lastMessageSend!.isBefore(twoDaysAgo) || - group.lastMessageReceived!.isBefore(twoDaysAgo)) { + if (group.lastMessageSend!.isBefore(twoDaysAgo) || group.lastMessageReceived!.isBefore(twoDaysAgo)) { flameCounter = 0; } } @@ -135,25 +129,21 @@ Future incFlameCounter( final now = clock.now(); final startOfToday = DateTime(now.year, now.month, now.day); - if (group.lastFlameCounterChange == null || - group.lastFlameCounterChange!.isBefore(startOfToday)) { + if (group.lastFlameCounterChange == null || group.lastFlameCounterChange!.isBefore(startOfToday)) { // last flame update was yesterday. check if it can be updated. var updateFlame = false; if (received) { - if (group.lastMessageSend != null && - group.lastMessageSend!.isAfter(startOfToday)) { + if (group.lastMessageSend != null && group.lastMessageSend!.isAfter(startOfToday)) { // today a message was already send -> update flame updateFlame = true; } - } else if (group.lastMessageReceived != null && - group.lastMessageReceived!.isAfter(startOfToday)) { + } else if (group.lastMessageReceived != null && group.lastMessageReceived!.isAfter(startOfToday)) { // today a message was already received -> update flame updateFlame = true; } if (updateFlame) { flameCounter += 1; - if (group.lastFlameCounterChange == null || - group.lastFlameCounterChange!.isBefore(timestamp)) { + if (group.lastFlameCounterChange == null || group.lastFlameCounterChange!.isBefore(timestamp)) { // only update if the timestamp is newer lastFlameCounterChange = Value(timestamp); } @@ -170,13 +160,11 @@ Future incFlameCounter( } if (received) { - if (group.lastMessageReceived == null || - group.lastMessageReceived!.isBefore(timestamp)) { + if (group.lastMessageReceived == null || group.lastMessageReceived!.isBefore(timestamp)) { lastMessageReceived = Value(timestamp); } } else { - if (group.lastMessageSend == null || - group.lastMessageSend!.isBefore(timestamp)) { + if (group.lastMessageSend == null || group.lastMessageSend!.isBefore(timestamp)) { lastMessageSend = Value(timestamp); } } @@ -203,3 +191,18 @@ bool isItPossibleToRestoreFlames(Group group) { clock.now().subtract(const Duration(days: 7)), ); } + +Future restoreFlames(String groupId) async { + final group = await twonlyDB.groupsDao.getGroup(groupId); + if (group == null) return; + final now = clock.now(); + await twonlyDB.groupsDao.updateGroup( + groupId, + GroupsCompanion( + flameCounter: Value(group.maxFlameCounter), + lastFlameCounterChange: Value(now), + lastMessageSend: Value(now), + lastMessageReceived: Value(now), + ), + ); +} diff --git a/lib/src/services/key_verification.service.dart b/lib/src/services/key_verification.service.dart index 410e1b58..f3299ef2 100644 --- a/lib/src/services/key_verification.service.dart +++ b/lib/src/services/key_verification.service.dart @@ -3,14 +3,17 @@ import 'dart:typed_data'; import 'package:collection/collection.dart'; import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:twonly/locator.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/tables/contacts.table.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart' as pb; +import 'package:twonly/src/providers/routing.provider.dart'; import 'package:twonly/src/services/api/messages.api.dart'; import 'package:twonly/src/services/signal/identity.signal.dart'; import 'package:twonly/src/services/signal/session.signal.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/components/snackbar.dart'; class KeyVerificationService { static Future> getNewSecretVerificationToken() async { @@ -70,6 +73,18 @@ class KeyVerificationService { VerificationType.secretQrToken, ); Log.info('Contact was verified via secretQrToken'); + + final contact = await twonlyDB.contactsDao.getContactById(fromUserId); + final context = rootNavigatorKey.currentContext; + if (context != null && context.mounted && contact != null) { + showSnackbar( + context, + context.lang.secretQrTokenVerifiedSnackbar( + getContactDisplayName(contact), + ), + level: SnackbarLevel.success, + ); + } return; } } diff --git a/lib/src/services/mediafiles/mediafile.service.dart b/lib/src/services/mediafiles/mediafile.service.dart index aee30809..f6c1c4b6 100644 --- a/lib/src/services/mediafiles/mediafile.service.dart +++ b/lib/src/services/mediafiles/mediafile.service.dart @@ -84,6 +84,15 @@ class MediaFileService { if (message.openedAt == null) { // Message was not yet opened from all persons, so wait... delete = false; + } else if (message.openedAt!.isAfter( + clock.now().subtract(const Duration(minutes: 3)), + )) { + // When the message was opened in the last two minutes, do not purge. + // Bug: When the user opens an image immediately after starting the app, there is a race condition: + // The message is marked as opened, but then purgeTempFolder is run + // (it is unawaited) and deletes the file. Thi gives a grace period: + // The image must have been opened within the last two minutes, otherwise do not delete it. + delete = false; } else if (mediaFile.requiresAuthentication || mediaFile.displayLimitInMilliseconds != null) { // Message was opened by all persons, and they can not reopen the image. diff --git a/lib/src/services/profile.service.dart b/lib/src/services/profile.service.dart new file mode 100644 index 00000000..5a80f5cb --- /dev/null +++ b/lib/src/services/profile.service.dart @@ -0,0 +1,8 @@ +enum SetupProfile { standard, customized, maximum } + +enum SecurityProfile { normal, strict } + +extension SecurityProfileExtension on SecurityProfile { + bool get showWarningForNonVerifiedContacts => this == SecurityProfile.strict; + bool get showOnlyVerifiedInChatViewList => this == SecurityProfile.normal; +} diff --git a/lib/src/services/signal/encryption.signal.dart b/lib/src/services/signal/encryption.signal.dart index abbb1449..facf5fc3 100644 --- a/lib/src/services/signal/encryption.signal.dart +++ b/lib/src/services/signal/encryption.signal.dart @@ -42,62 +42,72 @@ signalDecryptMessage( ) async { // Hold the lock only for the cryptographic operation, not for network I/O Log.info('Acquiring lockingSignalProtocol for $fromUserId'); - final (decryptedContent, errorType, needsResync) = await lockingSignalProtocol - .protect(() async { - Log.info('Lock acquired for $fromUserId'); - try { - final session = SessionCipher.fromStore( - (await getSignalStore())!, - getSignalAddress(fromUserId), - ); + final ( + decryptedContent, + errorType, + needsResync, + ) = await lockingSignalProtocol.protect(() async { + Log.info('Lock acquired for $fromUserId'); + try { + final session = SessionCipher.fromStore( + (await getSignalStore())!, + getSignalAddress(fromUserId), + ); - Uint8List plaintext; + Uint8List plaintext; - switch (type) { - case CiphertextMessage.prekeyType: - plaintext = await session.decrypt( - PreKeySignalMessage(encryptedContentRaw), - ); - case CiphertextMessage.whisperType: - plaintext = await session.decryptFromSignal( - SignalMessage.fromSerialized(encryptedContentRaw), - ); - default: - Log.error('Unknown Message Decryption Type: $type'); - return ( - null, - PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN, - false, - ); - } - - return (EncryptedContent.fromBuffer(plaintext), null, false); - } on InvalidKeyIdException catch (e) { - Log.warn(e); - return ( - null, - PlaintextContent_DecryptionErrorMessage_Type.PREKEY_UNKNOWN, - false, + switch (type) { + case CiphertextMessage.prekeyType: + plaintext = await session.decrypt( + PreKeySignalMessage(encryptedContentRaw), ); - } on DuplicateMessageException catch (e) { - Log.info(e.toString()); - return (null, null, false); - } on InvalidMessageException catch (e) { - Log.warn(e); - return ( - null, - PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN, - true, + case CiphertextMessage.whisperType: + plaintext = await session.decryptFromSignal( + SignalMessage.fromSerialized(encryptedContentRaw), ); - } catch (e) { - Log.error(e); + default: + Log.error('Unknown Message Decryption Type: $type'); return ( null, PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN, false, ); - } - }); + } + + return (EncryptedContent.fromBuffer(plaintext), null, false); + } on InvalidKeyIdException catch (e) { + Log.warn(e); + return ( + null, + PlaintextContent_DecryptionErrorMessage_Type.PREKEY_UNKNOWN, + false, + ); + } on DuplicateMessageException catch (e) { + // This is normal behavior: This can happen in case a message was decrypted, but before further processing + // the user killed the app. This results in a new transmission from the server, but as the message was already + // decrypted, this error happens. In this case, request the message again. + Log.info(e); + return ( + null, + PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN, + false, + ); + } on InvalidMessageException catch (e) { + Log.warn(e); + return ( + null, + PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN, + true, + ); + } catch (e) { + Log.error(e); + return ( + null, + PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN, + false, + ); + } + }); Log.info('Released lockingSignalProtocol for $fromUserId'); diff --git a/lib/src/services/user_discovery.service.dart b/lib/src/services/user_discovery.service.dart index fba1a4dd..11d41f1f 100644 --- a/lib/src/services/user_discovery.service.dart +++ b/lib/src/services/user_discovery.service.dart @@ -104,7 +104,8 @@ class UserDiscoveryService { static Future getCurrentVersion() async { try { - return await FlutterUserDiscovery.getCurrentVersion(); + return await FlutterUserDiscovery.getCurrentVersion() + .timeout(const Duration(seconds: 5)); } catch (e) { Log.error(e); return null; @@ -140,7 +141,7 @@ class UserDiscoveryService { return await FlutterUserDiscovery.shouldRequestNewMessages( contactId: fromUserId, version: receivedVersion, - ); + ).timeout(const Duration(seconds: 5)); } catch (e) { Log.error(e); return null; @@ -155,7 +156,7 @@ class UserDiscoveryService { return await FlutterUserDiscovery.getNewMessages( contactId: fromUserId, receivedVersion: receivedVersion, - ); + ).timeout(const Duration(seconds: 5)); } catch (e) { Log.error(e); return null; @@ -175,7 +176,7 @@ class UserDiscoveryService { messages: messages, publicKeyVerifiedTimestamp: verifications.lastOrNull?.createdAt.millisecondsSinceEpoch, - ); + ).timeout(const Duration(seconds: 5)); } catch (e) { Log.error(e); } diff --git a/lib/src/utils/log.dart b/lib/src/utils/log.dart index ae44ca31..63a73b36 100644 --- a/lib/src/utils/log.dart +++ b/lib/src/utils/log.dart @@ -18,10 +18,13 @@ class Log { Logger.root.onRecord.listen((record) async { unawaited(_writeLogToFile(record)); if (!kReleaseMode) { - // ignore: avoid_print - print( - '${record.level.name} [${AppState.isInBackgroundTask ? 'b' : 'f'}] [twonly] ${record.loggerName} > ${record.message}', - ); + if (!Platform.environment.containsKey('FLUTTER_TEST') || + record.level >= Level.WARNING) { + // ignore: avoid_print + print( + '${record.level.name} [${AppState.isInBackgroundTask ? 'b' : 'f'}] [twonly] ${record.loggerName} > ${record.message}', + ); + } } }); } @@ -136,7 +139,7 @@ Future cleanLogFile() async { } final lines = await logFile.readAsLines(); - final twoWeekAgo = clock.now().subtract(const Duration(days: 14)); + final twoWeekAgo = clock.now().subtract(const Duration(days: 3)); var keepStartIndex = -1; for (var i = 0; i < lines.length; i += 100) { diff --git a/lib/src/utils/misc.dart b/lib/src/utils/misc.dart index 873bfae3..6d7414bf 100644 --- a/lib/src/utils/misc.dart +++ b/lib/src/utils/misc.dart @@ -293,9 +293,10 @@ Future> sha256File(File file) async { List formattedText( BuildContext context, String input, { + Color? textColor, Color? boldTextColor, }) { - final defaultColor = Theme.of(context).colorScheme.onSurface; + final defaultColor = textColor ?? Theme.of(context).colorScheme.onSurface; final regex = RegExp(r'\*(.*?)\*'); final spans = []; diff --git a/lib/src/utils/qr.utils.dart b/lib/src/utils/qr.utils.dart index c4f6f389..0f847841 100644 --- a/lib/src/utils/qr.utils.dart +++ b/lib/src/utils/qr.utils.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert'; +import 'package:clock/clock.dart'; import 'package:collection/collection.dart' show ListExtensions; import 'package:drift/drift.dart' show Value; import 'package:fixnum/fixnum.dart'; @@ -41,6 +42,7 @@ class QrCodeUtils { signedPrekeySignature: signedPreKey.signature, signedPrekeyId: Int64(signedPreKey.id), secretVerificationToken: secretVerificationToken, + timestamp: Int64(clock.now().millisecondsSinceEpoch), ); final data = publicProfile.writeToBuffer(); @@ -94,7 +96,18 @@ class QrCodeUtils { ); if (verificationOk) { - if (profile.hasSecretVerificationToken()) { + var useSecretVerificationToken = profile.hasSecretVerificationToken(); + if (profile.hasTimestamp()) { + // Only notify the scanned user if the QR code was generated within the last 10 minutes. + final timestamp = DateTime.fromMillisecondsSinceEpoch( + profile.timestamp.toInt(), + ); + final tenMinutesAgo = clock.now().subtract(const Duration(minutes: 10)); + if (timestamp.isBefore(tenMinutesAgo)) { + useSecretVerificationToken = false; + } + } + if (useSecretVerificationToken) { unawaited( KeyVerificationService.handleScannedVerificationToken( contact.userId, diff --git a/lib/src/visual/components/contact_request_badge.comp.dart b/lib/src/visual/components/contact_request_badge.comp.dart new file mode 100644 index 00000000..4a08c5b1 --- /dev/null +++ b/lib/src/visual/components/contact_request_badge.comp.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:go_router/go_router.dart'; +import 'package:twonly/locator.dart'; +import 'package:twonly/src/constants/routes.keys.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/components/notification_badge.comp.dart'; +import 'package:twonly/src/visual/themes/light.dart'; + +class ContactRequestBadgeComp extends StatelessWidget { + const ContactRequestBadgeComp({super.key}); + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: twonlyDB.contactsDao.watchContactsRequestedCount(), + builder: (context, snapshot) { + final count = snapshot.data ?? 0; + if (count == 0) { + return const SizedBox.shrink(); + } + return Stack( + children: [ + Positioned.fill( + child: Center( + child: Container( + width: 40, + height: 40, + decoration: const BoxDecoration( + color: primaryColor, + shape: BoxShape.circle, + ), + ), + ), + ), + Center( + child: NotificationBadgeComp( + backgroundColor: isDarkMode(context) ? Colors.white : Colors.black, + textColor: isDarkMode(context) ? Colors.black : Colors.white, + count: count.toString(), + child: IconButton( + color: Colors.black, + icon: const FaIcon( + FontAwesomeIcons.userPlus, + size: 18, + ), + onPressed: () => context.push(Routes.chatsAddNewUser), + ), + ), + ), + ], + ); + }, + ); + } +} diff --git a/lib/src/visual/components/profile_qr_code.comp.dart b/lib/src/visual/components/profile_qr_code.comp.dart new file mode 100644 index 00000000..86c05c7a --- /dev/null +++ b/lib/src/visual/components/profile_qr_code.comp.dart @@ -0,0 +1,96 @@ +import 'dart:typed_data'; +import 'package:flutter/material.dart'; +import 'package:qr_flutter/qr_flutter.dart'; +import 'package:twonly/src/utils/avatars.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/utils/qr.utils.dart'; + +class ProfileQrCodeComp extends StatefulWidget { + const ProfileQrCodeComp({ + this.size = 250, + this.showAvatar = true, + super.key, + }); + + final double size; + final bool showAvatar; + + @override + State createState() => _ProfileQrCodeCompState(); +} + +class _ProfileQrCodeCompState extends State { + String? _qrCode; + Uint8List? _userAvatar; + bool _isLoading = true; + + @override + void initState() { + super.initState(); + _loadData(); + } + + Future _loadData() async { + final qr = await QrCodeUtils.publicProfileLink(); + final avatar = widget.showAvatar ? await getUserAvatar() : null; + if (mounted) { + setState(() { + _qrCode = qr; + _userAvatar = avatar; + _isLoading = false; + }); + } + } + + @override + Widget build(BuildContext context) { + final loaded = !_isLoading && _qrCode != null; + return SizedBox( + width: widget.size, + height: widget.size, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 150), + child: loaded + ? Container( + key: const ValueKey('qr_code_container'), + // padding: const EdgeInsets.all(3), + decoration: BoxDecoration( + color: context.color.primary, + borderRadius: BorderRadius.circular(12), + boxShadow: const [ + BoxShadow( + color: Colors.black26, + blurRadius: 3, + offset: Offset(0, 2), + ), + ], + ), + child: QrImageView.withQr( + qr: QrCode.fromData( + data: _qrCode!, + errorCorrectLevel: QrErrorCorrectLevel.M, + ), + eyeStyle: QrEyeStyle( + color: isDarkMode(context) ? Colors.black : Colors.white, + borderRadius: 2, + ), + dataModuleStyle: QrDataModuleStyle( + color: isDarkMode(context) ? Colors.black : Colors.white, + borderRadius: 2, + ), + gapless: false, + embeddedImage: (widget.showAvatar && _userAvatar != null) ? MemoryImage(_userAvatar!) : null, + embeddedImageStyle: QrEmbeddedImageStyle( + size: const Size(60, 66), + embeddedImageShape: EmbeddedImageShape.square, + shapeColor: context.color.primary, + safeArea: true, + ), + size: widget.size, + ), + ) + : const SizedBox.shrink(key: ValueKey('qr_code_placeholder')), + ), + ); + } +} diff --git a/lib/src/visual/components/snackbar.dart b/lib/src/visual/components/snackbar.dart index e182fa9a..37f015b3 100644 --- a/lib/src/visual/components/snackbar.dart +++ b/lib/src/visual/components/snackbar.dart @@ -62,7 +62,12 @@ void _showOverlay({ required Duration displayDuration, required void Function(AnimationController) onAnimationControllerInit, }) { - final overlayState = Overlay.maybeOf(context); + var overlayState = Overlay.maybeOf(context); + if (overlayState == null) { + if (context is StatefulElement && context.state is NavigatorState) { + overlayState = (context.state as NavigatorState).overlay; + } + } if (overlayState == null) return; late OverlayEntry overlayEntry; diff --git a/lib/src/visual/components/verification_badge.comp.dart b/lib/src/visual/components/verification_badge.comp.dart index ec655b44..be63acd1 100644 --- a/lib/src/visual/components/verification_badge.comp.dart +++ b/lib/src/visual/components/verification_badge.comp.dart @@ -33,7 +33,7 @@ class VerificationBadgeComp extends StatefulWidget { class _VerificationBadgeCompState extends State { bool _isVerified = false; - bool _isVerifiedByTransferredTrust = false; + int _verifiedByTransferredTrustCount = 0; StreamSubscription? _streamAllVerified; StreamSubscription>? _streamContactVerification; @@ -42,25 +42,40 @@ class _VerificationBadgeCompState extends State { @override void initState() { super.initState(); - if (widget.group != null) { + initAsync(); + } + + Future initAsync() async { + var group = widget.group; + var contact = widget.contact; + + if (group?.isDirectChat == true) { + final members = await twonlyDB.groupsDao.getGroupContact(group!.groupId); + if (members.isNotEmpty) { + contact = members.first; + group = null; + } + } + + if (group != null) { _streamAllVerified = twonlyDB.keyVerificationDao - .watchAllGroupMembersVerified(widget.group!.groupId) + .watchAllGroupMembersVerified(group.groupId) .listen((update) { if (!mounted) return; setState(() { _isVerified = false; - _isVerifiedByTransferredTrust = false; + _verifiedByTransferredTrustCount = 0; if (update == VerificationStatus.trusted) { _isVerified = true; } if (update == VerificationStatus.partialTrusted) { - _isVerifiedByTransferredTrust = true; + _verifiedByTransferredTrustCount = 10; } }); }); - } else if (widget.contact != null) { + } else if (contact != null) { _streamContactVerification = twonlyDB.keyVerificationDao - .watchContactVerification(widget.contact!.userId) + .watchContactVerification(contact.userId) .listen((update) { if (!mounted) return; setState(() { @@ -69,16 +84,16 @@ class _VerificationBadgeCompState extends State { }); _streamTransferredTrust = twonlyDB.keyVerificationDao - .watchTransferredTrustVerifications(widget.contact!.userId) + .watchTransferredTrustVerifications(contact.userId) .listen((update) { if (!mounted) return; setState(() { - _isVerifiedByTransferredTrust = update.isNotEmpty; + _verifiedByTransferredTrustCount = update.length; }); }); } else if (widget.isVerifiedByTransferredTrust != null) { setState(() { - _isVerifiedByTransferredTrust = widget.isVerifiedByTransferredTrust!; + _verifiedByTransferredTrustCount = 10; }); } } @@ -94,7 +109,7 @@ class _VerificationBadgeCompState extends State { @override Widget build(BuildContext context) { if (!_isVerified && - !_isVerifiedByTransferredTrust && + _verifiedByTransferredTrustCount == 0 && widget.showOnlyIfVerified) { return Container(); } @@ -112,10 +127,12 @@ class _VerificationBadgeCompState extends State { bottom: 3, ), child: SvgIcon( - assetPath: (_isVerified || _isVerifiedByTransferredTrust) + assetPath: _isVerified ? SvgIcons.verifiedGreen + : _verifiedByTransferredTrustCount > 0 + ? SvgIcons.verifiedNumeric(_verifiedByTransferredTrustCount) : SvgIcons.verifiedRed, - color: (_isVerifiedByTransferredTrust && !_isVerified) + color: (_verifiedByTransferredTrustCount > 0 && !_isVerified) ? colorVerificationBadgeYellow : null, size: widget.size, diff --git a/lib/src/visual/components/verification_badge_info.comp.dart b/lib/src/visual/components/verification_badge_info.comp.dart index cded90c2..60bfaa07 100644 --- a/lib/src/visual/components/verification_badge_info.comp.dart +++ b/lib/src/visual/components/verification_badge_info.comp.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:twonly/locator.dart'; +import 'package:twonly/src/services/profile.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/elements/svg_icon.element.dart'; import 'package:twonly/src/visual/themes/light.dart'; @@ -23,16 +25,18 @@ class VerificationBadgeInfo extends StatelessWidget { description: context.lang.verificationBadgeGreenDesc, boldTextColor: primaryColor, ), - _buildItem( - context, - icon: const SvgIcon( - assetPath: SvgIcons.verifiedGreen, - size: 40, - color: colorVerificationBadgeYellow, + if (userService.currentUser.securityProfile != SecurityProfile.strict || + userService.currentUser.isUserDiscoveryEnabled) + _buildItem( + context, + icon: const SvgIcon( + assetPath: SvgIcons.verifiedGreen, + size: 40, + color: colorVerificationBadgeYellow, + ), + description: context.lang.verificationBadgeYellowDesc, + boldTextColor: colorVerificationBadgeYellow, ), - description: context.lang.verificationBadgeYellowDesc, - boldTextColor: colorVerificationBadgeYellow, - ), _buildItem( context, icon: const SvgIcon(assetPath: SvgIcons.verifiedRed, size: 40), diff --git a/lib/src/visual/elements/svg_icon.element.dart b/lib/src/visual/elements/svg_icon.element.dart index fd124141..3932a453 100644 --- a/lib/src/visual/elements/svg_icon.element.dart +++ b/lib/src/visual/elements/svg_icon.element.dart @@ -4,6 +4,13 @@ import 'package:flutter_svg/flutter_svg.dart'; class SvgIcons { static const String verifiedGreen = 'assets/icons/verified_badge_green.svg'; static const String verifiedRed = 'assets/icons/verified_badge_red.svg'; + + static String verifiedNumeric(int value) { + if (value >= 4) { + return verifiedGreen; + } + return 'assets/icons/verification_badge_numeric/verified_badge_$value.svg'; + } } class SvgIcon extends StatelessWidget { diff --git a/lib/src/visual/helpers/screenshot.helper.dart b/lib/src/visual/helpers/screenshot.helper.dart index 02c906d9..8605a38e 100644 --- a/lib/src/visual/helpers/screenshot.helper.dart +++ b/lib/src/visual/helpers/screenshot.helper.dart @@ -46,24 +46,43 @@ class ScreenshotController { } late GlobalKey _containerKey; - Future capture({double? pixelRatio}) async { + Future capture({ + double? pixelRatio, + int retries = 20, + }) async { try { final findRenderObject = _containerKey.currentContext?.findRenderObject(); if (findRenderObject == null) { return null; } final boundary = findRenderObject as RenderRepaintBoundary; + final context = _containerKey.currentContext; var tmpPixelRatio = pixelRatio; if (tmpPixelRatio == null) { if (context != null && context.mounted) { - tmpPixelRatio = - tmpPixelRatio ?? MediaQuery.of(context).devicePixelRatio; + tmpPixelRatio = tmpPixelRatio ?? MediaQuery.of(context).devicePixelRatio; } } final image = await boundary.toImage(pixelRatio: tmpPixelRatio ?? 1); return ScreenshotImageHelper(image: image); } catch (e) { + if (retries > 0) { + final completer = Completer(); + WidgetsBinding.instance.addPostFrameCallback((_) async { + final result = await capture( + pixelRatio: pixelRatio, + retries: retries - 1, + ); + completer.complete(result); + }); + Timer(const Duration(milliseconds: 50), () { + if (!completer.isCompleted) { + WidgetsBinding.instance.scheduleFrame(); + } + }); + return completer.future; + } Log.error(e); } return null; diff --git a/lib/src/visual/views/camera/add_new_shortcut.view.dart b/lib/src/visual/views/camera/add_new_shortcut.view.dart index 11a611e4..41291ace 100644 --- a/lib/src/visual/views/camera/add_new_shortcut.view.dart +++ b/lib/src/visual/views/camera/add_new_shortcut.view.dart @@ -203,6 +203,7 @@ class _StartNewChatView extends State { const SizedBox(width: 8), ], ), + floatingActionButtonAnimator: FloatingActionButtonAnimator.noAnimation, floatingActionButton: FilledButton.icon( onPressed: (_selectedGroups.isEmpty || shortcutEmoji == null) ? null diff --git a/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_components/camera_scanned_overlay.dart b/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_components/camera_scanned_overlay.dart index 30de85cf..8e003fcc 100644 --- a/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_components/camera_scanned_overlay.dart +++ b/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_components/camera_scanned_overlay.dart @@ -33,8 +33,7 @@ class CameraScannedOverlay extends StatelessWidget { ...mainController.contactsVerified.values.map( (c) => _buildVerifiedContactTile(context, c), ), - if (mainController.scannedUrl != null) - _buildScannedUrlTile(context, mainController.scannedUrl!), + if (mainController.scannedUrl != null) _buildScannedUrlTile(context, mainController.scannedUrl!), ], ), ), @@ -46,15 +45,14 @@ class CameraScannedOverlay extends StatelessWidget { return GestureDetector( onTap: () async { c.isLoading = true; - mainController.setState(); + mainController.setState?.call(); showSnackbar( context, context.lang.requestedUserToastText(c.profile.username), level: SnackbarLevel.success, ); - if (await addNewContactFromPublicProfile(c.profile) && - context.mounted) { + if (await addNewContactFromPublicProfile(c.profile) && context.mounted) { // showSnackbar( // context, // context.lang.requestedUserToastText(c.profile.username), @@ -113,7 +111,7 @@ class CameraScannedOverlay extends StatelessWidget { ), const SizedBox(width: 10), Text( - getContactDisplayName(c.contact, maxLength: 13), + getContactDisplayName(c.contact, maxLength: 9), ), Expanded(child: Container()), ColoredBox( @@ -121,13 +119,11 @@ class CameraScannedOverlay extends StatelessWidget { child: SizedBox( width: 30, child: Lottie.asset( - c.verificationOk - ? 'assets/animations/success.lottie' - : 'assets/animations/failed.lottie', + c.verificationOk ? 'assets/animations/success.lottie' : 'assets/animations/failed.lottie', repeat: false, onLoaded: (p0) { Future.delayed(const Duration(seconds: 4), () { - mainController.setState(); + mainController.setState?.call(); }); }, ), diff --git a/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart b/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart index 2ccec5ac..1a3e52a3 100644 --- a/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart +++ b/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart @@ -48,7 +48,7 @@ class SelectedCameraDetails { bool cameraLoaded = false; } -class CameraPreviewControllerView extends StatelessWidget { +class CameraPreviewControllerView extends StatefulWidget { const CameraPreviewControllerView({ required this.mainController, required this.isVisible, @@ -62,23 +62,52 @@ class CameraPreviewControllerView extends StatelessWidget { final bool isVisible; final bool hideControllers; + @override + State createState() => _CameraPreviewControllerViewState(); +} + +class _CameraPreviewControllerViewState extends State { + Future? _permissionsFuture; + + @override + void initState() { + super.initState(); + if (!AppState.hasCameraPermissions) { + _permissionsFuture = checkPermissions().then((hasPermission) { + if (hasPermission) { + AppState.hasCameraPermissions = true; + } + return hasPermission; + }); + } + } + @override Widget build(BuildContext context) { - return FutureBuilder( - future: checkPermissions(), + if (AppState.hasCameraPermissions) { + return CameraPreviewView( + sendToGroup: widget.sendToGroup, + mainCameraController: widget.mainController, + isVisible: widget.isVisible, + hideControllers: widget.hideControllers, + ); + } + + return FutureBuilder( + future: _permissionsFuture, builder: (context, snap) { if (snap.hasData) { if (snap.data!) { return CameraPreviewView( - sendToGroup: sendToGroup, - mainCameraController: mainController, - isVisible: isVisible, - hideControllers: hideControllers, + sendToGroup: widget.sendToGroup, + mainCameraController: widget.mainController, + isVisible: widget.isVisible, + hideControllers: widget.hideControllers, ); } else { return PermissionHandlerView( onSuccess: () { - mainController.selectCamera(0, true); + widget.mainController.selectCamera(0, true); }, ); } @@ -210,8 +239,7 @@ class _CameraPreviewViewState extends State { Future initAsync() async { _hasAudioPermission = await Permission.microphone.isGranted; - if (!_hasAudioPermission && - !userService.currentUser.requestedAudioPermission) { + if (!_hasAudioPermission && !userService.currentUser.requestedAudioPermission) { await UserService.update((u) => u.requestedAudioPermission = true); await requestMicrophonePermission(); } @@ -232,8 +260,7 @@ class _CameraPreviewViewState extends State { } Future updateScaleFactor(double newScale) async { - if (mc.selectedCameraDetails.scaleFactor == newScale || - mc.cameraController == null) { + if (mc.selectedCameraDetails.scaleFactor == newScale || mc.cameraController == null) { return; } await mc.cameraController?.setZoomLevel( @@ -316,9 +343,7 @@ class _CameraPreviewViewState extends State { bool sharedFromGallery = false, MediaType? mediaType, }) async { - final type = - mediaType ?? - ((videoFilePath != null) ? MediaType.video : MediaType.image); + final type = mediaType ?? ((videoFilePath != null) ? MediaType.video : MediaType.image); final mediaFileService = await initializeMediaUpload( type, userService.currentUser.defaultShowTime, @@ -359,10 +384,9 @@ class _CameraPreviewViewState extends State { mainCameraController: mc, previewLink: mc.sharedLinkForPreview, ), - transitionsBuilder: - (context, animation, secondaryAnimation, child) { - return child; - }, + transitionsBuilder: (context, animation, secondaryAnimation, child) { + return child; + }, transitionDuration: Duration.zero, reverseTransitionDuration: Duration.zero, ), @@ -392,16 +416,13 @@ class _CameraPreviewViewState extends State { return false; } - bool get isFront => - mc.cameraController?.description.lensDirection == - CameraLensDirection.front; + bool get isFront => mc.cameraController?.description.lensDirection == CameraLensDirection.front; Future onPanUpdate(dynamic details) async { if (details == null) { return; } - if (mc.cameraController == null || - !mc.cameraController!.value.isInitialized) { + if (mc.cameraController == null || !mc.cameraController!.value.isInitialized) { return; } @@ -530,8 +551,7 @@ class _CameraPreviewViewState extends State { } Future startVideoRecording() async { - if (mc.cameraController != null && - mc.cameraController!.value.isRecordingVideo) { + if (mc.cameraController != null && mc.cameraController!.value.isRecordingVideo) { return; } setState(() { @@ -551,8 +571,7 @@ class _CameraPreviewViewState extends State { _currentTime = clock.now(); }); if (_videoRecordingStarted != null && - _currentTime.difference(_videoRecordingStarted!).inSeconds >= - maxVideoRecordingTime) { + _currentTime.difference(_videoRecordingStarted!).inSeconds >= maxVideoRecordingTime) { timer.cancel(); _videoRecordingTimer = null; stopVideoRecording(); @@ -589,8 +608,7 @@ class _CameraPreviewViewState extends State { _videoRecordingLocked = false; }); - if (mc.cameraController == null || - !mc.cameraController!.value.isRecordingVideo) { + if (mc.cameraController == null || !mc.cameraController!.value.isRecordingVideo) { return; } @@ -618,8 +636,7 @@ class _CameraPreviewViewState extends State { @override Widget build(BuildContext context) { - if (mc.selectedCameraDetails.cameraId >= AppEnvironment.cameras.length || - mc.cameraController == null) { + if (mc.selectedCameraDetails.cameraId >= AppEnvironment.cameras.length || mc.cameraController == null) { return Container(); } return StreamBuilder( @@ -643,9 +660,7 @@ class _CameraPreviewViewState extends State { _baseScaleFactor = mc.selectedCameraDetails.scaleFactor; }); // Get the position of the pointer - final renderBox = - keyTriggerButton.currentContext!.findRenderObject()! - as RenderBox; + final renderBox = keyTriggerButton.currentContext!.findRenderObject()! as RenderBox; final localPosition = renderBox.globalToLocal( details.globalPosition, ); @@ -681,24 +696,18 @@ class _CameraPreviewViewState extends State { ), ), ), - if (!mc.isSharePreviewIsShown && - widget.sendToGroup != null && - !mc.isVideoRecording) + if (!mc.isSharePreviewIsShown && widget.sendToGroup != null && !mc.isVideoRecording) ShowTitleText( title: widget.sendToGroup!.groupName, desc: context.lang.cameraPreviewSendTo, ), - if (!mc.isSharePreviewIsShown && - mc.sharedLinkForPreview != null && - !mc.isVideoRecording) + if (!mc.isSharePreviewIsShown && mc.sharedLinkForPreview != null && !mc.isVideoRecording) ShowTitleText( title: mc.sharedLinkForPreview?.host ?? '', desc: 'Link', isLink: true, ), - if (!mc.isSharePreviewIsShown && - !mc.isVideoRecording && - !widget.hideControllers) + if (!mc.isSharePreviewIsShown && !mc.isVideoRecording && !widget.hideControllers) CameraTopActions( selectedCameraDetails: mc.selectedCameraDetails, hasAudioPermission: _hasAudioPermission, @@ -742,8 +751,7 @@ class _CameraPreviewViewState extends State { videoRecordingStarted: _videoRecordingStarted, maxVideoRecordingTime: maxVideoRecordingTime, ), - if (!mc.isSharePreviewIsShown && widget.sendToGroup != null || - widget.hideControllers) + if (!mc.isSharePreviewIsShown && widget.sendToGroup != null || widget.hideControllers) Positioned( left: 5, top: 10, diff --git a/lib/src/visual/views/camera/camera_preview_components/main_camera_controller.dart b/lib/src/visual/views/camera/camera_preview_components/main_camera_controller.dart index df665c84..1a2cae41 100644 --- a/lib/src/visual/views/camera/camera_preview_components/main_camera_controller.dart +++ b/lib/src/visual/views/camera/camera_preview_components/main_camera_controller.dart @@ -40,7 +40,7 @@ class ScannedNewProfile { } class MainCameraController { - late void Function() setState; + void Function()? setState; CameraController? cameraController; ScreenshotController screenshotController = ScreenshotController(); SelectedCameraDetails selectedCameraDetails = SelectedCameraDetails(); @@ -61,12 +61,12 @@ class MainCameraController { void setSharedLinkForPreview(Uri? url) { sharedLinkForPreview = url; - setState(); + setState?.call(); } void onImageSend() { scannedUrl = ''; - setState(); + setState?.call(); } final BarcodeScanner _barcodeScanner = BarcodeScanner(); @@ -85,35 +85,50 @@ class MainCameraController { FaceFilterType _currentFilterType = FaceFilterType.none; FaceFilterType get currentFilterType => _currentFilterType; + Future? _initializeFuture; Future? _pendingDisposal; + int _cameraSessionId = 0; Future closeCamera() async { + _cameraSessionId++; contactsVerified = {}; scannedNewProfiles = {}; scannedUrl = null; - try { - await cameraController?.stopImageStream(); - // ignore: empty_catches - } catch (e) {} final cameraControllerTemp = cameraController; cameraController = null; + final initFutureTemp = _initializeFuture; + _initializeFuture = null; // prevents: CameraException(Disposed CameraController, buildPreview() was called on a disposed CameraController.) _pendingDisposal = Future.delayed( const Duration(milliseconds: 100), () async { + try { + if (initFutureTemp != null) { + await initFutureTemp; + } + // ignore: empty_catches + } catch (e) {} + try { + await cameraControllerTemp?.stopImageStream(); + // ignore: empty_catches + } catch (e) {} await cameraControllerTemp?.dispose(); }, ); initCameraStarted = false; selectedCameraDetails = SelectedCameraDetails(); + setState?.call(); } Future selectCamera(int sCameraId, bool init) async { - await _pendingDisposal; initCameraStarted = true; + final sessionId = ++_cameraSessionId; + await _pendingDisposal; + if (sessionId != _cameraSessionId) return; if (AppEnvironment.cameras.isEmpty) { AppEnvironment.cameras = await availableCameras(); + if (sessionId != _cameraSessionId) return; } var cameraId = sCameraId; @@ -126,8 +141,7 @@ class MainCameraController { if (init) { for (; cameraId < AppEnvironment.cameras.length; cameraId++) { - if (AppEnvironment.cameras[cameraId].lensDirection == - CameraLensDirection.back) { + if (AppEnvironment.cameras[cameraId].lensDirection == CameraLensDirection.back) { break; } } @@ -136,18 +150,23 @@ class MainCameraController { selectedCameraDetails.isZoomAble = false; if (cameraController == null) { + final hasMic = await Permission.microphone.isGranted; + if (sessionId != _cameraSessionId) return; + cameraController = CameraController( AppEnvironment.cameras[cameraId], ResolutionPreset.high, - enableAudio: await Permission.microphone.isGranted, - imageFormatGroup: Platform.isAndroid - ? ImageFormatGroup.nv21 - : ImageFormatGroup.bgra8888, + enableAudio: hasMic, + imageFormatGroup: Platform.isAndroid ? ImageFormatGroup.nv21 : ImageFormatGroup.bgra8888, ); try { - await cameraController?.initialize(); + _initializeFuture = cameraController?.initialize(); + await _initializeFuture; + if (cameraController == null) return; await cameraController?.startImageStream(_processCameraImage); + if (cameraController == null) return; await cameraController?.setZoomLevel(selectedCameraDetails.scaleFactor); + if (cameraController == null) return; if (userService.currentUser.videoStabilizationEnabled && !kDebugMode) { await cameraController?.setVideoStabilizationMode( VideoStabilizationMode.level1, @@ -165,10 +184,13 @@ class MainCameraController { } catch (e) { Log.info(e); } + if (cameraController == null) return; selectedCameraDetails.scaleFactor = 1; await cameraController?.setZoomLevel(1); + if (cameraController == null) return; await cameraController?.setDescription(AppEnvironment.cameras[cameraId]); + if (cameraController == null) return; try { if (!isVideoRecording) { await cameraController?.startImageStream(_processCameraImage); @@ -179,20 +201,19 @@ class MainCameraController { } try { + if (cameraController == null) return; await cameraController?.lockCaptureOrientation( DeviceOrientation.portraitUp, ); + if (cameraController == null) return; await cameraController?.setFlashMode( selectedCameraDetails.isFlashOn ? FlashMode.always : FlashMode.off, ); - selectedCameraDetails.maxAvailableZoom = - await cameraController?.getMaxZoomLevel() ?? 1; - selectedCameraDetails.minAvailableZoom = - await cameraController?.getMinZoomLevel() ?? 1; + if (cameraController == null) return; + selectedCameraDetails.maxAvailableZoom = await cameraController?.getMaxZoomLevel() ?? 1; + selectedCameraDetails.minAvailableZoom = await cameraController?.getMinZoomLevel() ?? 1; selectedCameraDetails - ..isZoomAble = - selectedCameraDetails.maxAvailableZoom != - selectedCameraDetails.minAvailableZoom + ..isZoomAble = selectedCameraDetails.maxAvailableZoom != selectedCameraDetails.minAvailableZoom ..cameraLoaded = true ..cameraId = cameraId; @@ -201,7 +222,7 @@ class MainCameraController { isSelectingFaceFilters = false; setFilter(FaceFilterType.none); zoomButtonKey = GlobalKey(); - setState(); + setState?.call(); } catch (e) { Log.error(e); cameraController = null; @@ -214,8 +235,7 @@ class MainCameraController { } Future onTapDown(TapDownDetails details) async { - final box = - cameraPreviewKey.currentContext?.findRenderObject() as RenderBox?; + final box = cameraPreviewKey.currentContext?.findRenderObject() as RenderBox?; if (box == null) return; final localPosition = box.globalToLocal(details.globalPosition); @@ -224,15 +244,14 @@ class MainCameraController { final dx = (localPosition.dx / box.size.width).clamp(0.0, 1.0); final dy = (localPosition.dy / box.size.height).clamp(0.0, 1.0); - setState(); + setState?.call(); await HapticFeedback.lightImpact(); try { await cameraController?.setFocusPoint(Offset(dx, dy)); await cameraController?.setFocusMode(FocusMode.auto); } catch (e) { - if (e is CameraException && - (e.code == 'setFocusPointFailed' || e.code == 'setFocusModeFailed')) { + if (e is CameraException && (e.code == 'setFocusPointFailed' || e.code == 'setFocusModeFailed')) { Log.info('Focus point or mode not supported on this device'); } else { Log.warn(e); @@ -243,7 +262,7 @@ class MainCameraController { await Future.delayed(const Duration(milliseconds: 500)); focusPointOffset = null; - setState(); + setState?.call(); } void setFilter(FaceFilterType type) { @@ -253,7 +272,7 @@ class MainCameraController { facePaint = null; _isBusyFaces = false; } - setState(); + setState?.call(); } FaceFilterPainter? faceFilterPainter; @@ -273,8 +292,7 @@ class MainCameraController { if (inputImage == null) return; _processBarcode(inputImage); // check if front camera is selected - if (cameraController?.description.lensDirection == - CameraLensDirection.front) { + if (cameraController?.description.lensDirection == CameraLensDirection.front) { if (_currentFilterType != FaceFilterType.none) { _processFaces(inputImage); } @@ -293,16 +311,14 @@ class MainCameraController { if (Platform.isIOS) { rotation = InputImageRotationValue.fromRawValue(sensorOrientation); } else if (Platform.isAndroid) { - var rotationCompensation = - _orientations[cameraController!.value.deviceOrientation]; + var rotationCompensation = _orientations[cameraController!.value.deviceOrientation]; if (rotationCompensation == null) return null; if (camera.lensDirection == CameraLensDirection.front) { // front-facing rotationCompensation = (sensorOrientation + rotationCompensation) % 360; } else { // back-facing - rotationCompensation = - (sensorOrientation - rotationCompensation + 360) % 360; + rotationCompensation = (sensorOrientation - rotationCompensation + 360) % 360; } rotation = InputImageRotationValue.fromRawValue(rotationCompensation); } @@ -344,9 +360,7 @@ class MainCameraController { if (_isBusy) return; _isBusy = true; final barcodes = await _barcodeScanner.processImage(inputImage); - if (inputImage.metadata?.size != null && - inputImage.metadata?.rotation != null && - cameraController != null) { + if (inputImage.metadata?.size != null && inputImage.metadata?.rotation != null && cameraController != null) { final painter = BarcodeDetectorPainter( barcodes, inputImage.metadata!.size, @@ -423,16 +437,14 @@ class MainCameraController { } } _isBusy = false; - setState(); + setState?.call(); } Future _processFaces(InputImage inputImage) async { if (_isBusyFaces) return; _isBusyFaces = true; final faces = await _faceDetector.processImage(inputImage); - if (inputImage.metadata?.size != null && - inputImage.metadata?.rotation != null && - cameraController != null) { + if (inputImage.metadata?.size != null && inputImage.metadata?.rotation != null && cameraController != null) { if (faces.isNotEmpty) { CustomPainter? painter; switch (_currentFilterType) { @@ -471,6 +483,6 @@ class MainCameraController { } } _isBusyFaces = false; - setState(); + setState?.call(); } } diff --git a/lib/src/visual/views/camera/camera_preview_components/zoom_selector.dart b/lib/src/visual/views/camera/camera_preview_components/zoom_selector.dart index b0a22bf5..dc40aec9 100644 --- a/lib/src/visual/views/camera/camera_preview_components/zoom_selector.dart +++ b/lib/src/visual/views/camera/camera_preview_components/zoom_selector.dart @@ -8,7 +8,15 @@ import 'package:flutter/material.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart'; -class CameraZoomButtons extends StatefulWidget { +String beautifulZoomScale(double scale) { + var tmp = scale.toStringAsFixed(1); + if (tmp[0] == '0') { + tmp = tmp.substring(1, tmp.length); + } + return tmp; +} + +class CameraZoomButtons extends StatelessWidget { const CameraZoomButtons({ required this.controller, required this.updateScaleFactor, @@ -25,32 +33,10 @@ class CameraZoomButtons extends StatefulWidget { final Future Function(int sCameraId, bool init) selectCamera; @override - State createState() => _CameraZoomButtonsState(); -} - -String beautifulZoomScale(double scale) { - var tmp = scale.toStringAsFixed(1); - if (tmp[0] == '0') { - tmp = tmp.substring(1, tmp.length); - } - return tmp; -} - -class _CameraZoomButtonsState extends State { - bool showWideAngleZoom = false; - bool showWideAngleZoomIOS = false; - bool _isDisposed = false; - int? _wideCameraIndex; - - @override - void initState() { - super.initState(); - unawaited(initAsync()); - } - - Future initAsync() async { - showWideAngleZoom = (await widget.controller.getMinZoomLevel()) < 1; + Widget build(BuildContext context) { + final showWideAngleZoom = selectedCameraDetails.minAvailableZoom < 1; + int? wideCameraIndex; var index = AppEnvironment.cameras.indexWhere( (t) => t.lensType == CameraLensType.ultraWide, ); @@ -60,33 +46,13 @@ class _CameraZoomButtonsState extends State { ); } if (index != -1) { - _wideCameraIndex = index; + wideCameraIndex = index; } - final isFront = - widget.controller.description.lensDirection == - CameraLensDirection.front; + final isFront = controller.description.lensDirection == CameraLensDirection.front; - if (!showWideAngleZoom && - Platform.isIOS && - _wideCameraIndex != null && - !isFront) { - showWideAngleZoomIOS = true; - } else { - showWideAngleZoomIOS = false; - } - if (_isDisposed) return; - setState(() {}); - } + final showWideAngleZoomIOS = !showWideAngleZoom && Platform.isIOS && wideCameraIndex != null && !isFront; - @override - void dispose() { - _isDisposed = true; // Set the flag to true when disposing - super.dispose(); - } - - @override - Widget build(BuildContext context) { final zoomButtonStyle = TextButton.styleFrom( padding: EdgeInsets.zero, foregroundColor: Colors.white, @@ -97,24 +63,21 @@ class _CameraZoomButtonsState extends State { const zoomTextStyle = TextStyle(fontSize: 13); final isSmallerFocused = - widget.scaleFactor < 1 || - (showWideAngleZoomIOS && - widget.selectedCameraDetails.cameraId == _wideCameraIndex); + scaleFactor < 1 || (showWideAngleZoomIOS && selectedCameraDetails.cameraId == wideCameraIndex); final isMiddleFocused = - widget.scaleFactor >= 1 && - widget.scaleFactor < 2 && - !(showWideAngleZoomIOS && - widget.selectedCameraDetails.cameraId == _wideCameraIndex); + scaleFactor >= 1 && + scaleFactor < 2 && + !(showWideAngleZoomIOS && selectedCameraDetails.cameraId == wideCameraIndex); final maxLevel = max( - min(widget.selectedCameraDetails.maxAvailableZoom, 2), - widget.scaleFactor, + min(selectedCameraDetails.maxAvailableZoom, 2), + scaleFactor, ); final minLevel = beautifulZoomScale( - widget.selectedCameraDetails.minAvailableZoom, + selectedCameraDetails.minAvailableZoom, ); - final currentLevel = beautifulZoomScale(widget.scaleFactor); + final currentLevel = beautifulZoomScale(scaleFactor); return Center( child: ClipRRect( borderRadius: BorderRadius.circular(40), @@ -132,20 +95,18 @@ class _CameraZoomButtonsState extends State { ), onPressed: () async { if (showWideAngleZoomIOS) { - if (_wideCameraIndex != null) { - await widget.selectCamera(_wideCameraIndex!, true); + if (wideCameraIndex != null) { + await selectCamera(wideCameraIndex, true); } } else { - final level = await widget.controller.getMinZoomLevel(); - widget.updateScaleFactor(level); + final level = await controller.getMinZoomLevel(); + updateScaleFactor(level); } }, child: showWideAngleZoomIOS ? const Text('0.5') : Text( - widget.scaleFactor < 1 - ? '${currentLevel}x' - : '${minLevel}x', + scaleFactor < 1 ? '${currentLevel}x' : '${minLevel}x', style: zoomTextStyle, ), ), @@ -156,39 +117,33 @@ class _CameraZoomButtonsState extends State { ), ), onPressed: () async { - if (showWideAngleZoomIOS && - widget.selectedCameraDetails.cameraId == - _wideCameraIndex) { - await widget.selectCamera(0, true); + if (showWideAngleZoomIOS && selectedCameraDetails.cameraId == wideCameraIndex) { + await selectCamera(0, true); } else { - widget.updateScaleFactor(1.0); + updateScaleFactor(1.0); } }, child: Text( - isMiddleFocused - ? '${beautifulZoomScale(widget.scaleFactor)}x' - : '1.0x', + isMiddleFocused ? '${beautifulZoomScale(scaleFactor)}x' : '1.0x', style: zoomTextStyle, ), ), TextButton( style: zoomButtonStyle.copyWith( foregroundColor: WidgetStateProperty.all( - (widget.scaleFactor >= 2) ? Colors.yellow : Colors.white, + (scaleFactor >= 2) ? Colors.yellow : Colors.white, ), ), onPressed: () async { final level = min( - await widget.controller.getMaxZoomLevel(), + await controller.getMaxZoomLevel(), 2, ).toDouble(); - if (showWideAngleZoomIOS && - widget.selectedCameraDetails.cameraId == - _wideCameraIndex) { - await widget.selectCamera(0, true); + if (showWideAngleZoomIOS && selectedCameraDetails.cameraId == wideCameraIndex) { + await selectCamera(0, true); } - widget.updateScaleFactor(level); + updateScaleFactor(level); }, child: Text( '${beautifulZoomScale(maxLevel.toDouble())}x', diff --git a/lib/src/visual/views/camera/camera_qr_scanner.view.dart b/lib/src/visual/views/camera/camera_qr_scanner.view.dart index 1ef5d9e6..87614e4b 100644 --- a/lib/src/visual/views/camera/camera_qr_scanner.view.dart +++ b/lib/src/visual/views/camera/camera_qr_scanner.view.dart @@ -24,6 +24,7 @@ class QrCodeScannerViewState extends State { @override void dispose() { + _mainCameraController.setState = null; _mainCameraController.closeCamera(); super.dispose(); } @@ -31,21 +32,24 @@ class QrCodeScannerViewState extends State { @override Widget build(BuildContext context) { return Scaffold( - body: GestureDetector( - onDoubleTap: _mainCameraController.onDoubleTap, - onTapDown: _mainCameraController.onTapDown, - child: Stack( - children: [ - MainCameraPreview( - mainCameraController: _mainCameraController, + body: Stack( + children: [ + MainCameraPreview( + mainCameraController: _mainCameraController, + ), + Positioned.fill( + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onDoubleTap: _mainCameraController.onDoubleTap, + onTapDown: _mainCameraController.onTapDown, ), - CameraPreviewControllerView( - mainController: _mainCameraController, - hideControllers: true, - isVisible: true, - ), - ], - ), + ), + CameraPreviewControllerView( + mainController: _mainCameraController, + hideControllers: true, + isVisible: true, + ), + ], ), ); } diff --git a/lib/src/visual/views/camera/camera_send_to.view.dart b/lib/src/visual/views/camera/camera_send_to.view.dart index 0f53fe8b..5e063540 100644 --- a/lib/src/visual/views/camera/camera_send_to.view.dart +++ b/lib/src/visual/views/camera/camera_send_to.view.dart @@ -26,6 +26,7 @@ class CameraSendToViewState extends State { @override void dispose() { + _mainCameraController.setState = null; _mainCameraController.closeCamera(); super.dispose(); } @@ -33,21 +34,24 @@ class CameraSendToViewState extends State { @override Widget build(BuildContext context) { return Scaffold( - body: GestureDetector( - onDoubleTap: _mainCameraController.onDoubleTap, - onTapDown: _mainCameraController.onTapDown, - child: Stack( - children: [ - MainCameraPreview( - mainCameraController: _mainCameraController, + body: Stack( + children: [ + MainCameraPreview( + mainCameraController: _mainCameraController, + ), + Positioned.fill( + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onDoubleTap: _mainCameraController.onDoubleTap, + onTapDown: _mainCameraController.onTapDown, ), - CameraPreviewControllerView( - mainController: _mainCameraController, - sendToGroup: widget.sendToGroup, - isVisible: true, - ), - ], - ), + ), + CameraPreviewControllerView( + mainController: _mainCameraController, + sendToGroup: widget.sendToGroup, + isVisible: true, + ), + ], ), ); } diff --git a/lib/src/visual/views/camera/share_image_contact_selection.view.dart b/lib/src/visual/views/camera/share_image_contact_selection.view.dart index 318fd5a6..4a6f7a40 100644 --- a/lib/src/visual/views/camera/share_image_contact_selection.view.dart +++ b/lib/src/visual/views/camera/share_image_contact_selection.view.dart @@ -3,9 +3,7 @@ import 'dart:collection'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:go_router/go_router.dart'; import 'package:twonly/locator.dart'; -import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; @@ -15,6 +13,7 @@ import 'package:twonly/src/services/flame.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/components/avatar_icon.comp.dart'; +import 'package:twonly/src/visual/components/contact_request_badge.comp.dart'; import 'package:twonly/src/visual/components/flame_counter.comp.dart'; import 'package:twonly/src/visual/decorations/input_text.decoration.dart'; import 'package:twonly/src/visual/elements/headline.element.dart'; @@ -22,6 +21,7 @@ import 'package:twonly/src/visual/helpers/screenshot.helper.dart'; import 'package:twonly/src/visual/views/camera/share_image_contact_selection_components/best_friends_selector.dart'; import 'package:twonly/src/visual/views/camera/share_image_contact_selection_components/shortcut_row.comp.dart'; import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/background.layer.dart'; +import 'package:twonly/src/visual/views/chats/chat_list_components/empty_chat_list.comp.dart'; class ShareImageView extends StatefulWidget { const ShareImageView({ @@ -111,9 +111,7 @@ class _ShareImageView extends State { for (final group in groups) { if (group.pinned) continue; - if (!group.archived && - getFlameCounterFromGroup(group).counter > 0 && - bestFriends.length < 6) { + if (!group.archived && getFlameCounterFromGroup(group).counter > 0 && bestFriends.length < 6) { bestFriends.add(group); } else { otherUsers.add(group); @@ -133,10 +131,7 @@ class _ShareImageView extends State { await updateGroups( _allGroups .where( - (x) => - !x.archived || - !hideArchivedUsers || - widget.selectedGroupIds.contains(x.groupId), + (x) => !x.archived || !hideArchivedUsers || widget.selectedGroupIds.contains(x.groupId), ) .toList(), ); @@ -160,31 +155,23 @@ class _ShareImageView extends State { return Scaffold( appBar: AppBar( title: Text(context.lang.shareImageTitle), + actions: const [ + ContactRequestBadgeComp(), + SizedBox(width: 15), + ], ), body: SafeArea( child: Padding( padding: const EdgeInsets.only( bottom: 40, left: 10, - top: 20, right: 10, ), - child: Column( + child: ListView( children: [ if (_allGroups.isEmpty) - Expanded( - child: Center( - child: FilledButton.icon( - icon: const Icon(Icons.person_add), - onPressed: () => context.push(Routes.chatsAddNewUser), - label: Text( - context.lang.chatListViewSearchUserNameBtn, - ), - ), - ), - ), - - if (_allGroups.isNotEmpty) + const EmptyChatListComp() + else ...[ Padding( padding: const EdgeInsets.symmetric(horizontal: 10), child: TextField( @@ -195,163 +182,162 @@ class _ShareImageView extends State { ), ), ), - const SizedBox(height: 10), - ShortcutRowComp( - selectedGroupIds: widget.selectedGroupIds, - updateSelectedGroupIds: updateSelectedGroupIds, - ), - if (_pinnedContacts.isNotEmpty) const SizedBox(height: 10), - BestFriendsSelector( - groups: _pinnedContacts, - selectedGroupIds: widget.selectedGroupIds, - updateSelectedGroupIds: updateSelectedGroupIds, - title: context.lang.shareImagePinnedContacts, - showSelectAll: - !widget.mediaFileService.mediaFile.requiresAuthentication, - ), - const SizedBox(height: 10), - BestFriendsSelector( - groups: _bestFriends, - selectedGroupIds: widget.selectedGroupIds, - updateSelectedGroupIds: updateSelectedGroupIds, - title: context.lang.shareImageBestFriends, - showSelectAll: - !widget.mediaFileService.mediaFile.requiresAuthentication, - ), - const SizedBox(height: 10), - if (_otherUsers.isNotEmpty) - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - HeadLineComp(context.lang.shareImageAllUsers), - if (_allGroups.any((x) => x.archived)) - Row( - children: [ - Text( - context.lang.shareImageShowArchived, - style: const TextStyle(fontSize: 10), - ), - Transform.scale( - scale: 0.75, - child: Checkbox( - value: !hideArchivedUsers, - side: WidgetStateBorderSide.resolveWith( - (states) { - if (states.contains(WidgetState.selected)) { - return const BorderSide(width: 0); - } - return BorderSide( - color: Theme.of( - context, - ).colorScheme.outline, - ); + const SizedBox(height: 10), + ShortcutRowComp( + selectedGroupIds: widget.selectedGroupIds, + updateSelectedGroupIds: updateSelectedGroupIds, + ), + if (_pinnedContacts.isNotEmpty) const SizedBox(height: 10), + BestFriendsSelector( + groups: _pinnedContacts, + selectedGroupIds: widget.selectedGroupIds, + updateSelectedGroupIds: updateSelectedGroupIds, + title: context.lang.shareImagePinnedContacts, + showSelectAll: !widget.mediaFileService.mediaFile.requiresAuthentication, + ), + const SizedBox(height: 10), + BestFriendsSelector( + groups: _bestFriends, + selectedGroupIds: widget.selectedGroupIds, + updateSelectedGroupIds: updateSelectedGroupIds, + title: context.lang.shareImageBestFriends, + showSelectAll: !widget.mediaFileService.mediaFile.requiresAuthentication, + ), + const SizedBox(height: 10), + if (_otherUsers.isNotEmpty) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + HeadLineComp(context.lang.shareImageAllUsers), + if (_allGroups.any((x) => x.archived)) + Row( + children: [ + Text( + context.lang.shareImageShowArchived, + style: const TextStyle(fontSize: 10), + ), + Transform.scale( + scale: 0.75, + child: Checkbox( + value: !hideArchivedUsers, + side: WidgetStateBorderSide.resolveWith( + (states) { + if (states.contains(WidgetState.selected)) { + return const BorderSide(width: 0); + } + return BorderSide( + color: Theme.of( + context, + ).colorScheme.outline, + ); + }, + ), + onChanged: (a) async { + hideArchivedUsers = !hideArchivedUsers; + await _filterUsers(lastQuery); + if (mounted) setState(() {}); }, ), - onChanged: (a) async { - hideArchivedUsers = !hideArchivedUsers; - await _filterUsers(lastQuery); - if (mounted) setState(() {}); - }, ), - ), - ], - ), - ], - ), - if (_otherUsers.isNotEmpty) - Expanded( - child: UserList( + ], + ), + ], + ), + if (_otherUsers.isNotEmpty) + UserList( List.from(_otherUsers), selectedGroupIds: widget.selectedGroupIds, updateSelectedGroupIds: updateSelectedGroupIds, ), - ), + ], ], ), ), ), - floatingActionButton: SizedBox( - height: 168, - child: Padding( - padding: const EdgeInsets.only(bottom: 20, right: 20), - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - if (widget.mediaFileService.mediaFile.type == MediaType.image && - _screenshotImage?.image != null && - userService.currentUser.showShowImagePreviewWhenSending) - SizedBox( - height: 100, - width: 100 * 9 / 16, - child: Container( - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - border: Border.all( - color: context.color.primary, - width: 2, - ), - color: context.color.primary, - borderRadius: BorderRadius.circular(12), - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(12), - child: CustomPaint( - painter: UiImagePainter(_screenshotImage!.image!), - ), - ), - ), - ), - FilledButton.icon( - icon: !mediaStoreFutureReady || sendingImage - ? SizedBox( - height: 12, - width: 12, - child: CircularProgressIndicator( - strokeWidth: 2, - color: Theme.of(context).colorScheme.inversePrimary, + floatingActionButtonAnimator: FloatingActionButtonAnimator.noAnimation, + floatingActionButton: _allGroups.isEmpty + ? null + : SizedBox( + height: 168, + child: Padding( + padding: const EdgeInsets.only(bottom: 20, right: 20), + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (widget.mediaFileService.mediaFile.type == MediaType.image && + _screenshotImage?.image != null && + userService.currentUser.showShowImagePreviewWhenSending) + SizedBox( + height: 100, + width: 100 * 9 / 16, + child: Container( + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + border: Border.all( + color: context.color.primary, + width: 2, + ), + color: context.color.primary, + borderRadius: BorderRadius.circular(12), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: CustomPaint( + painter: UiImagePainter(_screenshotImage!.image!), + ), + ), ), - ) - : const FaIcon(FontAwesomeIcons.solidPaperPlane), - onPressed: () async { - if (!mediaStoreFutureReady || - widget.selectedGroupIds.isEmpty) { - return; - } + ), + FilledButton.icon( + icon: !mediaStoreFutureReady || sendingImage + ? SizedBox( + height: 12, + width: 12, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Theme.of(context).colorScheme.inversePrimary, + ), + ) + : const FaIcon(FontAwesomeIcons.solidPaperPlane), + onPressed: () async { + if (!mediaStoreFutureReady || widget.selectedGroupIds.isEmpty) { + return; + } - setState(() { - sendingImage = true; - }); + setState(() { + sendingImage = true; + }); - // in case mediaStoreFutureReady is ready, the image is stored in the originalPath - await insertMediaFileInMessagesTable( - widget.mediaFileService, - widget.selectedGroupIds.toList(), - additionalData: widget.additionalData, - ); + // in case mediaStoreFutureReady is ready, the image is stored in the originalPath + await insertMediaFileInMessagesTable( + widget.mediaFileService, + widget.selectedGroupIds.toList(), + additionalData: widget.additionalData, + ); - if (context.mounted) { - Navigator.pop(context, true); - } - }, - style: ButtonStyle( - padding: WidgetStateProperty.all( - const EdgeInsets.symmetric(vertical: 10, horizontal: 30), - ), - backgroundColor: WidgetStateProperty.all( - !mediaStoreFutureReady || widget.selectedGroupIds.isEmpty - ? context.color.onSurface - : context.color.primary, - ), - ), - label: Text( - '${context.lang.shareImagedEditorSendImage} (${widget.selectedGroupIds.length})', - style: const TextStyle(fontSize: 17), + if (context.mounted) { + Navigator.pop(context, true); + } + }, + style: ButtonStyle( + padding: WidgetStateProperty.all( + const EdgeInsets.symmetric(vertical: 10, horizontal: 30), + ), + backgroundColor: WidgetStateProperty.all( + !mediaStoreFutureReady || widget.selectedGroupIds.isEmpty + ? context.color.onSurface + : context.color.primary, + ), + ), + label: Text( + '${context.lang.shareImagedEditorSendImage} (${widget.selectedGroupIds.length})', + style: const TextStyle(fontSize: 17), + ), + ), + ], ), ), - ], - ), - ), - ), + ), ); } } @@ -375,6 +361,8 @@ class UserList extends StatelessWidget { ); return ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), restorationId: 'new_message_users_list', itemCount: groups.length, itemBuilder: (context, i) { diff --git a/lib/src/visual/views/camera/share_image_contact_selection_components/best_friends_selector.dart b/lib/src/visual/views/camera/share_image_contact_selection_components/best_friends_selector.dart index 78c87516..91f8a70b 100644 --- a/lib/src/visual/views/camera/share_image_contact_selection_components/best_friends_selector.dart +++ b/lib/src/visual/views/camera/share_image_contact_selection_components/best_friends_selector.dart @@ -156,7 +156,7 @@ class UserCheckbox extends StatelessWidget { Row( children: [ Text( - substringBy(group.groupName, 12), + substringBy(group.groupName, 11), overflow: TextOverflow.ellipsis, ), ], diff --git a/lib/src/visual/views/camera/share_image_editor.view.dart b/lib/src/visual/views/camera/share_image_editor.view.dart index d2b29bc7..d09ad5e5 100644 --- a/lib/src/visual/views/camera/share_image_editor.view.dart +++ b/lib/src/visual/views/camera/share_image_editor.view.dart @@ -214,8 +214,7 @@ class _ShareImageEditorView extends State { List get actionsAtTheRight { if (layers.isNotEmpty && - (layers.first.isEditing || - (layers.last.isEditing && layers.last.hasCustomActionButtons))) { + (layers.first.isEditing || (layers.last.isEditing && layers.last.hasCustomActionButtons))) { return []; } return [ @@ -291,13 +290,9 @@ class _ShareImageEditorView extends State { if (media.type == MediaType.video) ...[ const SizedBox(height: 8), ActionButton( - (mediaService.removeAudio) - ? Icons.volume_off_rounded - : Icons.volume_up_rounded, + (mediaService.removeAudio) ? Icons.volume_off_rounded : Icons.volume_up_rounded, tooltipText: 'Enable Audio in Video', - color: (mediaService.removeAudio) - ? Colors.white.withAlpha(160) - : Colors.white, + color: (mediaService.removeAudio) ? Colors.white.withAlpha(160) : Colors.white, onPressed: () async { await mediaService.toggleRemoveAudio(); if (mediaService.removeAudio) { @@ -335,9 +330,7 @@ class _ShareImageEditorView extends State { ActionButton( FontAwesomeIcons.shieldHeart, tooltipText: context.lang.protectAsARealTwonly, - color: media.requiresAuthentication - ? Theme.of(context).colorScheme.primary - : Colors.white, + color: media.requiresAuthentication ? Theme.of(context).colorScheme.primary : Colors.white, onPressed: () async { await mediaService.setRequiresAuth(!media.requiresAuthentication); selectedGroupIds = HashSet(); @@ -383,8 +376,7 @@ class _ShareImageEditorView extends State { List get actionsAtTheTop { if (layers.isNotEmpty && - (layers.first.isEditing || - (layers.last.isEditing && layers.last.hasCustomActionButtons))) { + (layers.first.isEditing || (layers.last.isEditing && layers.last.hasCustomActionButtons))) { return []; } return [ @@ -474,6 +466,14 @@ class _ShareImageEditorView extends State { return (layers.first as BackgroundLayerData).image.image; } } + if (layers.length == 2) { + final filterLayer = layers[1]; + if (layers.first is BackgroundLayerData && filterLayer is FilterLayerData) { + if (filterLayer.page == 1) { + return (layers.first as BackgroundLayerData).image.image; + } + } + } for (final x in layers) { x.showCustomButtons = false; @@ -513,15 +513,15 @@ class _ShareImageEditorView extends State { } } ScreenshotImageHelper? image; - var bytes = await widget.screenshotImage?.getBytes(); if (media.type == MediaType.gif) { + final bytes = await widget.screenshotImage?.getBytes(); if (bytes != null) { mediaService.originalPath.writeAsBytesSync(bytes.toList()); } } else { image = await getEditedImageBytes(); if (image == null) return null; - bytes = await image.getBytes(); + final bytes = await image.getBytes(); if (bytes == null) { Log.error('imageBytes are empty'); return null; @@ -657,9 +657,7 @@ class _ShareImageEditorView extends State { await askToCloseThenClose(); }, child: Scaffold( - backgroundColor: widget.sharedFromGallery - ? null - : Colors.white.withAlpha(0), + backgroundColor: widget.sharedFromGallery ? null : Colors.white.withAlpha(0), resizeToAvoidBottomInset: false, body: Stack( fit: StackFit.expand, diff --git a/lib/src/visual/views/chats/chat_list.view.dart b/lib/src/visual/views/chats/chat_list.view.dart index 8a889073..4feb177f 100644 --- a/lib/src/visual/views/chats/chat_list.view.dart +++ b/lib/src/visual/views/chats/chat_list.view.dart @@ -18,6 +18,7 @@ import 'package:twonly/src/visual/components/avatar_icon.comp.dart'; import 'package:twonly/src/visual/components/connection_status.comp.dart'; import 'package:twonly/src/visual/components/notification_badge.comp.dart'; import 'package:twonly/src/visual/themes/light.dart'; +import 'package:twonly/src/visual/views/chats/chat_list_components/empty_chat_list.comp.dart'; import 'package:twonly/src/visual/views/chats/chat_list_components/feedback_btn.comp.dart'; import 'package:twonly/src/visual/views/chats/chat_list_components/group_list_item.comp.dart'; import 'package:twonly/src/visual/views/onboarding/setup/components/finish_setup.comp.dart'; @@ -31,11 +32,16 @@ class ChatListView extends StatefulWidget { class _ChatListViewState extends State { StreamSubscription? _userSub; - late StreamSubscription> _contactsSub; + StreamSubscription>? _contactsSub; + StreamSubscription>? _contactsCountSub; List _groupsNotPinned = []; List _groupsPinned = []; List _groupsArchived = []; + bool _hasContacts = false; + bool _loading = true; + bool get _hasOpenGroup => _groupsNotPinned.isNotEmpty || _groupsArchived.isNotEmpty || _groupsPinned.isNotEmpty; + GlobalKey searchForOtherUsers = GlobalKey(); bool showFeedbackShortcut = false; @@ -58,33 +64,35 @@ class _ChatListViewState extends State { _contactsSub = stream.listen((groups) { if (!mounted) return; setState(() { - _groupsNotPinned = groups - .where((x) => !x.pinned && !x.archived) - .toList(); + _groupsNotPinned = groups.where((x) => !x.pinned && !x.archived).toList(); _groupsPinned = groups.where((x) => x.pinned && !x.archived).toList(); _groupsArchived = groups.where((x) => x.archived).toList(); + _loading = false; }); }); - _countContactRequestStream = twonlyDB.contactsDao - .watchContactsRequestedCount() - .listen((update) { - if (update != null) { - if (!mounted) return; - setState(() { - _countContactRequest = update; - }); - } - }); + _contactsCountSub = twonlyDB.contactsDao.watchAllAcceptedContacts().listen((contacts) { + if (!mounted) return; + setState(() { + _hasContacts = contacts.isNotEmpty; + }); + }); - _countAnnouncedStream = twonlyDB.userDiscoveryDao - .watchNewAnnouncementsWithDataCount() - .listen((update) { - if (!mounted) return; - setState(() { - _countAnnouncedUsers = update; - }); + _countContactRequestStream = twonlyDB.contactsDao.watchContactsRequestedCount().listen((update) { + if (update != null) { + if (!mounted) return; + setState(() { + _countContactRequest = update; }); + } + }); + + _countAnnouncedStream = twonlyDB.userDiscoveryDao.watchNewAnnouncementsWithDataCount().listen((update) { + if (!mounted) return; + setState(() { + _countAnnouncedUsers = update; + }); + }); WidgetsBinding.instance.addPostFrameCallback((_) async { final changeLog = await rootBundle.loadString('CHANGELOG.md'); @@ -93,8 +101,7 @@ class _ChatListViewState extends State { changeLog.codeUnits, )).bytes; if (!userService.currentUser.hideChangeLog && - userService.currentUser.lastChangeLogHash.toString() != - changeLogHash.toString()) { + userService.currentUser.lastChangeLogHash.toString() != changeLogHash.toString()) { await UserService.update((u) { u.lastChangeLogHash = changeLogHash; }); @@ -113,7 +120,8 @@ class _ChatListViewState extends State { @override void dispose() { - _contactsSub.cancel(); + _contactsSub?.cancel(); + _contactsCountSub?.cancel(); _countContactRequestStream.cancel(); _countAnnouncedStream.cancel(); _userSub?.cancel(); @@ -182,16 +190,11 @@ class _ChatListViewState extends State { ), Center( child: NotificationBadgeComp( - backgroundColor: isDarkMode(context) - ? Colors.white - : Colors.black, + backgroundColor: isDarkMode(context) ? Colors.white : Colors.black, textColor: isDarkMode(context) ? Colors.black : Colors.white, - count: (_countAnnouncedUsers + _countContactRequest) - .toString(), + count: (_countAnnouncedUsers + _countContactRequest).toString(), child: IconButton( - color: (_countAnnouncedUsers + _countContactRequest > 0) - ? Colors.black - : null, + color: (_countAnnouncedUsers + _countContactRequest > 0) ? Colors.black : null, key: searchForOtherUsers, icon: const FaIcon(FontAwesomeIcons.userPlus, size: 18), onPressed: () => context.push(Routes.chatsAddNewUser), @@ -217,21 +220,15 @@ class _ChatListViewState extends State { children: [ const FinishSetupComp(), const MissingBackupComp(), - if (_groupsNotPinned.isEmpty && - _groupsPinned.isEmpty && - _groupsArchived.isEmpty) + if (_loading) + const Expanded( + child: SizedBox.shrink(), + ) + else if (!_hasOpenGroup) Expanded( - child: Center( - child: Padding( - padding: const EdgeInsets.all(10), - child: FilledButton.icon( - icon: const Icon(Icons.person_add), - onPressed: () => context.push(Routes.chatsAddNewUser), - label: Text( - context.lang.chatListViewSearchUserNameBtn, - ), - ), - ), + child: ListView( + physics: const AlwaysScrollableScrollPhysics(), + children: const [EmptyChatListComp()], ), ) else @@ -243,10 +240,7 @@ class _ChatListViewState extends State { _groupsNotPinned.length + (_groupsArchived.isNotEmpty ? 1 : 0), itemBuilder: (context, index) { - if (index >= - _groupsNotPinned.length + - _groupsPinned.length + - (_groupsPinned.isNotEmpty ? 1 : 0)) { + if (index >= _groupsNotPinned.length + _groupsPinned.length + (_groupsPinned.isNotEmpty ? 1 : 0)) { if (_groupsArchived.isEmpty) return Container(); return ListTile( title: Text( @@ -289,42 +283,45 @@ class _ChatListViewState extends State { ], ), ), - floatingActionButton: Padding( - padding: const EdgeInsets.only(bottom: 30), - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Material( - elevation: 3, - shape: const CircleBorder(), - color: context.color.primary, - child: InkWell( - borderRadius: BorderRadius.circular(12), - onTap: () => context.push(Routes.settingsPublicProfile), - child: SizedBox( - width: 45, - height: 45, - child: Center( + floatingActionButtonAnimator: FloatingActionButtonAnimator.noAnimation, + floatingActionButton: !_hasContacts + ? null + : Padding( + padding: const EdgeInsets.only(bottom: 30), + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Material( + elevation: 3, + shape: const CircleBorder(), + color: context.color.primary, + child: InkWell( + borderRadius: BorderRadius.circular(12), + onTap: () => context.push(Routes.settingsPublicProfile), + child: SizedBox( + width: 45, + height: 45, + child: Center( + child: FaIcon( + FontAwesomeIcons.qrcode, + color: isDarkMode(context) ? Colors.black : Colors.white, + ), + ), + ), + ), + ), + const SizedBox(height: 12), + FloatingActionButton( + backgroundColor: context.color.primary, + onPressed: () => context.push(Routes.chatsStartNewChat), child: FaIcon( - FontAwesomeIcons.qrcode, + FontAwesomeIcons.penToSquare, color: isDarkMode(context) ? Colors.black : Colors.white, ), ), - ), + ], ), ), - const SizedBox(height: 12), - FloatingActionButton( - backgroundColor: context.color.primary, - onPressed: () => context.push(Routes.chatsStartNewChat), - child: FaIcon( - FontAwesomeIcons.penToSquare, - color: isDarkMode(context) ? Colors.black : Colors.white, - ), - ), - ], - ), - ), ); } } diff --git a/lib/src/visual/views/chats/chat_list_components/empty_chat_list.comp.dart b/lib/src/visual/views/chats/chat_list_components/empty_chat_list.comp.dart new file mode 100644 index 00000000..78118ba0 --- /dev/null +++ b/lib/src/visual/views/chats/chat_list_components/empty_chat_list.comp.dart @@ -0,0 +1,119 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart' show FaIcon, FontAwesomeIcons; +import 'package:go_router/go_router.dart'; +import 'package:share_plus/share_plus.dart'; +import 'package:twonly/locator.dart'; +import 'package:twonly/src/constants/routes.keys.dart'; +import 'package:twonly/src/services/signal/identity.signal.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/components/profile_qr_code.comp.dart'; +import 'package:twonly/src/visual/themes/light.dart'; + +class EmptyChatListComp extends StatelessWidget { + const EmptyChatListComp({super.key}); + + Future _shareProfile(BuildContext context) async { + try { + final pubKey = await getUserPublicKey(); + final params = ShareParams( + text: 'https://me.twonly.eu/${userService.currentUser.username}#${base64Url.encode(pubKey)}', + ); + await SharePlus.instance.share(params); + } catch (e) { + if (context.mounted) { + await context.push(Routes.chatsAddNewUser); + } + } + } + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + children: [ + const SizedBox( + height: 24, + width: double.infinity, + ), + const Text( + 'Find your first friend', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 28, + ), + ), + const SizedBox(height: 8), + Text( + 'Let friends scan your QR code, or share them your profile.', + style: TextStyle( + fontSize: 14, + color: context.color.onSurface.withValues(alpha: 0.6), + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 36), + const Center(child: ProfileQrCodeComp()), + const SizedBox(height: 36), + // 3. Action Buttons + // Button 1: Share Profile (Full Width) + FilledButton.icon( + style: primaryColorButtonStyle, + onPressed: () => _shareProfile(context), + icon: const FaIcon(FontAwesomeIcons.shareNodes, size: 20), + label: const Text( + 'Share your profile', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(height: 12), + // Button Row: Scan QR Code & Enter Username + Row( + children: [ + Expanded( + child: FilledButton.icon( + style: secondaryGreyButtonStyle(context), + onPressed: () => context.push(Routes.cameraQRScanner), + icon: const Icon(Icons.qr_code_scanner_rounded, size: 20), + label: const FittedBox( + fit: BoxFit.scaleDown, + child: Text( + 'Scan QR Code', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: FilledButton.icon( + style: secondaryGreyButtonStyle(context), + onPressed: () => context.push(Routes.chatsAddNewUser), + icon: const Icon(Icons.person_add_rounded, size: 20), + label: const FittedBox( + fit: BoxFit.scaleDown, + child: Text( + 'Add by Username', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ), + ], + ), + const SizedBox(height: 50), + ], + ), + ); + } +} diff --git a/lib/src/visual/views/chats/chat_list_components/group_list_item.comp.dart b/lib/src/visual/views/chats/chat_list_components/group_list_item.comp.dart index 225a3195..77a19625 100644 --- a/lib/src/visual/views/chats/chat_list_components/group_list_item.comp.dart +++ b/lib/src/visual/views/chats/chat_list_components/group_list_item.comp.dart @@ -11,6 +11,7 @@ import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/messages.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/services/api/mediafiles/download.api.dart'; +import 'package:twonly/src/services/profile.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/components/avatar_icon.comp.dart'; import 'package:twonly/src/visual/components/flame_counter.comp.dart'; @@ -73,37 +74,31 @@ class _UserListItem extends State { }); }); - _lastReactionStream = twonlyDB.reactionsDao - .watchLastReactions(widget.group.groupId) - .listen((update) { - if (!mounted) return; - setState(() { - _lastReaction = update; - }); - }); + _lastReactionStream = twonlyDB.reactionsDao.watchLastReactions(widget.group.groupId).listen((update) { + if (!mounted) return; + setState(() { + _lastReaction = update; + }); + }); - _messagesNotOpenedStream = twonlyDB.messagesDao - .watchMessageNotOpened(widget.group.groupId) - .listen((update) { - protectUpdateState.protect(() async { - await updateState(_lastMessage, update); - }); - }); + _messagesNotOpenedStream = twonlyDB.messagesDao.watchMessageNotOpened(widget.group.groupId).listen((update) { + protectUpdateState.protect(() async { + await updateState(_lastMessage, update); + }); + }); - _lastMediaFilesStream = twonlyDB.mediaFilesDao - .watchNewestMediaFiles() - .listen((mediaFiles) { - if (!mounted) return; - for (final mediaFile in mediaFiles) { - final index = _previewMediaFiles.indexWhere( - (t) => t.mediaId == mediaFile.mediaId, - ); - if (index >= 0) { - _previewMediaFiles[index] = mediaFile; - } - } - setState(() {}); - }); + _lastMediaFilesStream = twonlyDB.mediaFilesDao.watchNewestMediaFiles().listen((mediaFiles) { + if (!mounted) return; + for (final mediaFile in mediaFiles) { + final index = _previewMediaFiles.indexWhere( + (t) => t.mediaId == mediaFile.mediaId, + ); + if (index >= 0) { + _previewMediaFiles[index] = mediaFile; + } + } + setState(() {}); + }); final groupContacts = await twonlyDB.groupsDao.getGroupContact( widget.group.groupId, @@ -125,9 +120,7 @@ class _UserListItem extends State { _previewMessages = []; } else if (newMessagesNotOpened.isNotEmpty) { // Filter for the preview non opened messages. First messages which where send but not yet opened by the other side. - final receivedMessages = newMessagesNotOpened - .where((x) => x.senderId != null) - .toList(); + final receivedMessages = newMessagesNotOpened.where((x) => x.senderId != null).toList(); if (receivedMessages.isNotEmpty) { _previewMessages = receivedMessages; @@ -151,9 +144,7 @@ class _UserListItem extends State { } } - final msgs = _previewMessages - .where((x) => x.type == MessageType.media.name) - .toList(); + final msgs = _previewMessages.where((x) => x.type == MessageType.media.name).toList(); if (msgs.isNotEmpty && msgs.first.type == MessageType.media.name && !msgs.first.isDeletedFromSender && @@ -165,8 +156,7 @@ class _UserListItem extends State { } for (final message in _previewMessages) { - if (message.mediaId != null && - !_previewMediaFiles.any((t) => t.mediaId == message.mediaId)) { + if (message.mediaId != null && !_previewMediaFiles.any((t) => t.mediaId == message.mediaId)) { final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById( message.mediaId!, ); @@ -191,9 +181,7 @@ class _UserListItem extends State { } if (_hasNonOpenedMediaFile) { - final msgs = _previewMessages - .where((x) => x.type == MessageType.media.name) - .toList(); + final msgs = _previewMessages.where((x) => x.type == MessageType.media.name).toList(); final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById( msgs.first.mediaId!, ); @@ -219,97 +207,99 @@ class _UserListItem extends State { @override Widget build(BuildContext context) { - return GroupContextMenu( - group: widget.group, - child: ListTile( - title: Row( - children: [ - Text( - substringBy(widget.group.groupName, 30), - ), - const SizedBox(width: 3), - VerificationBadgeComp( - group: widget.group, - showOnlyIfVerified: true, - clickable: false, - size: 12, - ), - ], - ), - subtitle: _receiverDeletedAccount - ? Text(context.lang.userDeletedAccount) - : (_currentMessage == null) - ? (widget.group.totalMediaCounter == 0) - ? Text(context.lang.chatsTapToSend) - : Row( - children: [ - LastMessageTimeComp( - dateTime: widget.group.lastMessageExchange, - ), - FlameCounterWidget( - groupId: widget.group.groupId, - prefix: true, - ), - ], - ) - : Row( - children: [ - TypingIndicatorSubtitleComp( - groupId: widget.group.groupId, - ), - MessageSendStateIcon( - _previewMessages, - _previewMediaFiles, - lastReaction: _lastReaction, - group: widget.group, - ), - const Text('•'), - const SizedBox(width: 5), - if (_currentMessage != null) - LastMessageTimeComp(message: _currentMessage), - FlameCounterWidget( - groupId: widget.group.groupId, - prefix: true, - ), - ], - ), - leading: GestureDetector( - onTap: () async { - if (widget.group.isDirectChat) { - final contacts = await twonlyDB.groupsDao.getGroupContact( - widget.group.groupId, - ); - if (!context.mounted) return; - await context.push(Routes.profileContact(contacts.first.userId)); - return; - } else { - await context.push(Routes.profileGroup(widget.group.groupId)); - } - }, - child: AvatarIcon(group: widget.group), - ), - trailing: (widget.group.leftGroup || _receiverDeletedAccount) - ? null - : IconButton( - onPressed: () { - if (_hasNonOpenedMediaFile) { - context.push(Routes.chatsMessages(widget.group.groupId)); - } else { - context.push( - Routes.chatsCameraSendTo, - extra: widget.group, - ); - } - }, - icon: FaIcon( - _hasNonOpenedMediaFile - ? FontAwesomeIcons.solidComments - : FontAwesomeIcons.camera, - color: context.color.outline.withAlpha(150), + return StreamBuilder( + stream: userService.onUserUpdated, + builder: (context, snapshot) { + return GroupContextMenu( + group: widget.group, + child: ListTile( + title: Row( + children: [ + Text( + substringBy(widget.group.groupName, 30), ), - ), - onTap: onTap, - ), + const SizedBox(width: 3), + VerificationBadgeComp( + group: widget.group, + showOnlyIfVerified: userService.currentUser.securityProfile.showOnlyVerifiedInChatViewList, + clickable: false, + size: 12, + ), + ], + ), + subtitle: _receiverDeletedAccount + ? Text(context.lang.userDeletedAccount) + : (_currentMessage == null) + ? (widget.group.totalMediaCounter == 0) + ? Text(context.lang.chatsTapToSend) + : Row( + children: [ + LastMessageTimeComp( + dateTime: widget.group.lastMessageExchange, + ), + FlameCounterWidget( + groupId: widget.group.groupId, + prefix: true, + ), + ], + ) + : Row( + children: [ + TypingIndicatorSubtitleComp( + groupId: widget.group.groupId, + ), + MessageSendStateIcon( + _previewMessages, + _previewMediaFiles, + lastReaction: _lastReaction, + group: widget.group, + ), + const Text('•'), + const SizedBox(width: 5), + if (_currentMessage != null) LastMessageTimeComp(message: _currentMessage), + FlameCounterWidget( + groupId: widget.group.groupId, + prefix: true, + ), + ], + ), + leading: GestureDetector( + onTap: () async { + if (widget.group.isDirectChat) { + final contacts = await twonlyDB.groupsDao.getGroupContact( + widget.group.groupId, + ); + if (!context.mounted) return; + await context.push(Routes.profileContact(contacts.first.userId)); + return; + } else { + await context.push(Routes.profileGroup(widget.group.groupId)); + } + }, + child: AvatarIcon(group: widget.group), + ), + trailing: (widget.group.leftGroup || _receiverDeletedAccount) + ? null + : IconButton( + onPressed: () { + if (_hasNonOpenedMediaFile) { + context.push(Routes.chatsMessages(widget.group.groupId)); + } else { + context.push( + Routes.chatsCameraSendTo, + extra: widget.group, + ); + } + }, + icon: FaIcon( + _hasNonOpenedMediaFile ? FontAwesomeIcons.solidComments : FontAwesomeIcons.camera, + color: context.color.outline.withAlpha(150), + ), + ), + onTap: onTap, + ), + ); + }, ); } } diff --git a/lib/src/visual/views/chats/chat_messages.view.dart b/lib/src/visual/views/chats/chat_messages.view.dart index 0e4c856e..46077800 100644 --- a/lib/src/visual/views/chats/chat_messages.view.dart +++ b/lib/src/visual/views/chats/chat_messages.view.dart @@ -146,7 +146,9 @@ class _ChatMessagesViewState extends State _nextTypingIndicator = Timer.periodic(const Duration(seconds: 2), ( _, ) async { - await sendTypingIndication(widget.groupId, false); + if (_isViewActive()) { + await sendTypingIndication(widget.groupId, false); + } }); } } diff --git a/lib/src/visual/views/chats/chat_messages_components/chat_list_entry.dart b/lib/src/visual/views/chats/chat_messages_components/chat_list_entry.dart index 76135e60..d076c4c5 100644 --- a/lib/src/visual/views/chats/chat_messages_components/chat_list_entry.dart +++ b/lib/src/visual/views/chats/chat_messages_components/chat_list_entry.dart @@ -13,6 +13,7 @@ import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/visual/components/avatar_icon.comp.dart'; import 'package:twonly/src/visual/views/chats/chat_messages_components/chat_reaction_row.dart'; +import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/chat_ask_a_friend.entry.dart'; import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/chat_audio_entry.dart'; import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/chat_contacts.entry.dart'; import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/chat_flame_restored.entry.dart'; @@ -137,12 +138,24 @@ class _ChatListEntryState extends State { if (widget.message.type == MessageType.contacts.name) { return ChatContactsEntry( message: widget.message, + borderRadius: borderRadius, + info: info, ); } if (widget.message.type == MessageType.restoreFlameCounter.name) { return ChatFlameRestoredEntry( message: widget.message, + borderRadius: borderRadius, + info: info, + ); + } + + if (widget.message.type == MessageType.askAboutUser.name) { + return ChatAskAFriendEntry( + message: widget.message, + borderRadius: borderRadius, + info: info, ); } diff --git a/lib/src/visual/views/chats/chat_messages_components/entries/chat_ask_a_friend.entry.dart b/lib/src/visual/views/chats/chat_messages_components/entries/chat_ask_a_friend.entry.dart new file mode 100644 index 00000000..d50655e9 --- /dev/null +++ b/lib/src/visual/views/chats/chat_messages_components/entries/chat_ask_a_friend.entry.dart @@ -0,0 +1,324 @@ +import 'dart:convert'; + +import 'package:drift/drift.dart' show Value; +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:go_router/go_router.dart'; +import 'package:twonly/locator.dart'; +import 'package:twonly/src/constants/routes.keys.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/protobuf/client/generated/data.pb.dart'; +import 'package:twonly/src/services/api/utils.api.dart'; +import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/components/avatar_icon.comp.dart'; +import 'package:twonly/src/visual/themes/light.dart'; +import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/common.dart'; + +class ChatAskAFriendEntry extends StatefulWidget { + const ChatAskAFriendEntry({ + required this.message, + required this.borderRadius, + required this.info, + super.key, + }); + + final Message message; + final BorderRadiusGeometry borderRadius; + final BubbleInfo info; + + @override + State createState() => _ChatAskAFriendEntryState(); +} + +class _ChatAskAFriendEntryState extends State { + bool _isLoading = false; + String? _username; + bool _isSent = false; + AdditionalMessageData? _data; + + @override + void initState() { + super.initState(); + _isSent = widget.message.senderId == null; + if (widget.message.additionalMessageData != null) { + try { + _data = AdditionalMessageData.fromBuffer( + widget.message.additionalMessageData!, + ); + } catch (e) { + _data = null; + } + } + _loadUser(); + } + + Future _loadUser() async { + if (_data == null || !_data!.hasAskAboutUserId()) return; + final userId = _data!.askAboutUserId.toInt(); + setState(() { + _isLoading = true; + }); + + try { + if (_isSent) { + // Try getting from contacts + final contact = await twonlyDB.contactsDao.getContactById(userId); + if (contact != null) { + _username = contact.displayName ?? contact.username; + } else { + // Try getting from announced users + final announced = await twonlyDB.userDiscoveryDao + .getAnnouncedUserById(userId); + if (announced != null && announced.username != null) { + _username = announced.username; + } + } + } else { + // Receiver side: try contacts first + final contact = await twonlyDB.contactsDao.getContactById(userId); + if (contact != null) { + _username = contact.displayName ?? contact.username; + } else { + // Fetch from API + final userdata = await apiService.getUserById(userId); + if (userdata != null) { + _username = utf8.decode(userdata.username); + } + } + } + } catch (e) { + Log.error(e); + } finally { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } + } + + Future _hideUser() async { + if (_data == null || !_data!.hasAskAboutUserId()) return; + await twonlyDB.userDiscoveryDao.updateAnnouncedUser( + _data!.askAboutUserId.toInt(), + const UserDiscoveryAnnouncedUsersCompanion( + isHidden: Value(true), + ), + ); + } + + Future _requestUser() async { + if (_data == null || !_data!.hasAskAboutUserId()) return; + setState(() { + _isLoading = true; + }); + try { + final userId = _data!.askAboutUserId.toInt(); + final userdata = await apiService.getUserById(userId); + if (userdata != null) { + await twonlyDB.contactsDao.insertOnConflictUpdate( + ContactsCompanion( + username: Value(utf8.decode(userdata.username)), + userId: Value(userdata.userId.toInt()), + requested: const Value(false), + blocked: const Value(false), + deletedByUser: const Value(false), + ), + ); + await importSignalContactAndCreateRequest(userdata); + } + } catch (e) { + Log.error(e); + } finally { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } + } + + @override + Widget build(BuildContext context) { + if (_data == null || !_data!.hasAskAboutUserId()) { + return const SizedBox.shrink(); + } + + final userId = _data!.askAboutUserId.toInt(); + + return Container( + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width * 0.8, + ), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + decoration: BoxDecoration( + color: widget.info.color, + borderRadius: widget.borderRadius, + ), + child: IntrinsicWidth( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + StreamBuilder( + stream: twonlyDB.contactsDao.watchContact(userId), + builder: (context, snapshot) { + final contactInDb = snapshot.data; + return GestureDetector( + onTap: () { + if (contactInDb != null) { + context.push(Routes.profileContact(userId)); + } + }, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 6, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + AvatarIcon( + contactId: userId, + fontSize: 12, + ), + const SizedBox(width: 8), + if (_isLoading && _username == null) + const Padding( + padding: EdgeInsets.only(right: 8), + child: SizedBox( + width: 14, + height: 14, + child: CircularProgressIndicator( + strokeWidth: 2, + ), + ), + ) + else if (_username != null) + Flexible( + child: Padding( + padding: const EdgeInsets.only(right: 4), + child: Text( + _username!, + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 14, + color: widget.info.textColor, + ), + ), + ), + ) + else + Flexible( + child: Padding( + padding: const EdgeInsets.only(right: 4), + child: Text( + context.lang.chatAskAFriendUnknownUser( + userId.toString(), + ), + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 14, + color: widget.info.textColor, + ), + ), + ), + ), + if (contactInDb != null) ...[ + Opacity( + opacity: 0.5, + child: FaIcon( + FontAwesomeIcons.chevronRight, + size: 10, + color: widget.info.textColor, + ), + ), + const SizedBox(width: 8), + ] else ...[ + const SizedBox(width: 4), + ], + ], + ), + ), + ); + }, + ), + if (!_isSent) ...[ + const SizedBox(height: 12), + Text( + context.lang.chatAskAFriendReceivedDescription, + style: TextStyle( + fontSize: 12, + color: widget.info.textColor, + ), + ), + ] else ...[ + StreamBuilder( + stream: twonlyDB.contactsDao.watchContact(userId), + builder: (context, snapshot) { + final contactInDb = snapshot.data; + if (contactInDb != null) { + return Text( + context.lang.chatAskAFriendAddedDescription, + style: TextStyle( + fontSize: 12, + color: widget.info.textColor, + ), + ); + } + + return Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: _isLoading ? null : _hideUser, + style: TextButton.styleFrom( + minimumSize: Size.zero, + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + ), + child: Text( + context.lang.chatAskAFriendHide, + style: TextStyle( + fontSize: 12, + color: widget.info.textColor, + ), + ), + ), + const SizedBox(width: 8), + FilledButton( + style: FilledButton.styleFrom( + minimumSize: Size.zero, + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + ).merge(secondaryGreyButtonStyle(context)), + onPressed: _isLoading ? null : _requestUser, + child: _isLoading + ? const SizedBox( + width: 12, + height: 12, + child: CircularProgressIndicator( + strokeWidth: 2, + ), + ) + : Text( + context.lang.chatAskAFriendRequest, + style: const TextStyle(fontSize: 12), + ), + ), + ], + ); + }, + ), + ], + ], + ), + ), + ); + } +} diff --git a/lib/src/visual/views/chats/chat_messages_components/entries/chat_contacts.entry.dart b/lib/src/visual/views/chats/chat_messages_components/entries/chat_contacts.entry.dart index e8c69569..6d02afb0 100644 --- a/lib/src/visual/views/chats/chat_messages_components/entries/chat_contacts.entry.dart +++ b/lib/src/visual/views/chats/chat_messages_components/entries/chat_contacts.entry.dart @@ -18,10 +18,14 @@ import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/c class ChatContactsEntry extends StatefulWidget { const ChatContactsEntry({ required this.message, + required this.borderRadius, + required this.info, super.key, }); final Message message; + final BorderRadiusGeometry borderRadius; + final BubbleInfo info; @override State createState() => _ChatContactsEntryState(); @@ -46,23 +50,14 @@ class _ChatContactsEntryState extends State { return const SizedBox.shrink(); } - final info = getBubbleInfo( - context, - widget.message, - null, - null, - null, - 0, - ); - return Container( constraints: BoxConstraints( maxWidth: MediaQuery.of(context).size.width * 0.8, ), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( - color: info.color, - borderRadius: BorderRadius.circular(12), + color: widget.info.color, + borderRadius: widget.borderRadius, ), child: IntrinsicWidth( child: Column( diff --git a/lib/src/visual/views/chats/chat_messages_components/entries/chat_flame_restored.entry.dart b/lib/src/visual/views/chats/chat_messages_components/entries/chat_flame_restored.entry.dart index 9ecf6e86..0b28e1a0 100644 --- a/lib/src/visual/views/chats/chat_messages_components/entries/chat_flame_restored.entry.dart +++ b/lib/src/visual/views/chats/chat_messages_components/entries/chat_flame_restored.entry.dart @@ -2,15 +2,22 @@ import 'package:flutter/material.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/protobuf/client/generated/data.pb.dart'; import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/components/animate_icon.comp.dart'; import 'package:twonly/src/visual/elements/better_text.element.dart'; +import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/common.dart'; + class ChatFlameRestoredEntry extends StatelessWidget { const ChatFlameRestoredEntry({ required this.message, + required this.borderRadius, + required this.info, super.key, }); final Message message; + final BorderRadiusGeometry borderRadius; + final BubbleInfo info; @override Widget build(BuildContext context) { @@ -34,16 +41,29 @@ class ChatFlameRestoredEntry extends StatelessWidget { constraints: BoxConstraints( maxWidth: MediaQuery.of(context).size.width * 0.8, ), - padding: const EdgeInsets.only(left: 10, top: 6, bottom: 6, right: 10), + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( - color: Colors.orange, - borderRadius: BorderRadius.circular(12), + color: info.color, + borderRadius: borderRadius, ), - child: BetterText( - text: context.lang.chatEntryFlameRestored( - data.restoredFlameCounter.toInt(), - ), - textColor: isDarkMode(context) ? Colors.black : Colors.black, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox( + width: 24, + height: 24, + child: EmojiAnimationComp(emoji: '🔥'), + ), + const SizedBox(width: 8), + Flexible( + child: BetterText( + text: context.lang.chatEntryFlameRestored( + data.restoredFlameCounter.toInt(), + ), + textColor: isDarkMode(context) ? Colors.black : Colors.black, + ), + ), + ], ), ); } diff --git a/lib/src/visual/views/chats/chat_messages_components/message_input.dart b/lib/src/visual/views/chats/chat_messages_components/message_input.dart index cc54579c..915a7d55 100644 --- a/lib/src/visual/views/chats/chat_messages_components/message_input.dart +++ b/lib/src/visual/views/chats/chat_messages_components/message_input.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'package:audio_waveforms/audio_waveforms.dart'; +import 'package:clock/clock.dart'; import 'package:drift/drift.dart' show Value; import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; import 'package:flutter/material.dart'; @@ -18,7 +19,9 @@ import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/views/camera/camera_send_to.view.dart'; import 'package:twonly/src/visual/views/chats/chat_messages_components/bottom_sheets/share_additional.bottom_sheet.dart'; import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/chat_audio_entry.dart'; +import 'package:twonly/src/visual/views/chats/chat_messages_components/unverified_contact_warning.comp.dart'; import 'package:twonly/src/visual/views/chats/chat_messages_components/user_discovery_manual_approval.comp.dart'; +import 'package:twonly/src/visual/views/contact/contact_components/restore_flame.comp.dart'; class MessageInput extends StatefulWidget { const MessageInput({ @@ -51,6 +54,8 @@ class _MessageInputState extends State { Offset _recordingOffset = Offset.zero; RecordingState _recordingState = RecordingState.none; Timer? _nextTypingIndicator; + DateTime? _lastTextChangeTime; + int? _contactId; Future _sendMessage() async { if (_textFieldController.text == '') return; @@ -70,6 +75,7 @@ class _MessageInputState extends State { void initState() { super.initState(); _textFieldController = TextEditingController(); + _textFieldController.addListener(_handleTextChange); if (widget.group.draftMessage != null) { _textFieldController.text = widget.group.draftMessage!; } @@ -78,16 +84,20 @@ class _MessageInputState extends State { _nextTypingIndicator = Timer.periodic(const Duration(seconds: 1), ( _, ) async { - if (widget.textFieldFocus.hasFocus) { + if (widget.textFieldFocus.hasFocus && + _lastTextChangeTime != null && + DateTime.now().difference(_lastTextChangeTime!) <= const Duration(seconds: 6)) { await sendTypingIndication(widget.group.groupId, true); } }); } _initializeControllers(); + _loadContactId(); } @override void dispose() { + _textFieldController.removeListener(_handleTextChange); widget.textFieldFocus.removeListener(_handleTextFocusChange); widget.textFieldFocus.dispose(); recorderController.dispose(); @@ -105,6 +115,10 @@ class _MessageInputState extends State { }); } + void _handleTextChange() { + _lastTextChangeTime = clock.now(); + } + void _handleTextFocusChange() { if (widget.textFieldFocus.hasFocus) { setState(() { @@ -194,316 +208,319 @@ class _MessageInputState extends State { ); } + Future _loadContactId() async { + if (widget.group.isDirectChat) { + final members = await twonlyDB.groupsDao.getGroupContact(widget.group.groupId); + if (members.isNotEmpty && mounted) { + setState(() { + _contactId = members.first.userId; + }); + } + } + } + @override Widget build(BuildContext context) { return Column( children: [ UserDiscoveryManualApprovalComp(group: widget.group), - Padding( - padding: const EdgeInsets.only( - bottom: 10, - left: 10, - top: 10, + if (_contactId != null) + Container( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: context.color.surfaceContainerHighest, + borderRadius: BorderRadius.circular(16), + ), + clipBehavior: Clip.antiAlias, + child: RestoreFlameComp( + contactId: _contactId!, + flameOnRightSide: true, + ), ), - child: Row( - children: [ - Expanded( - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 3, - ), - decoration: BoxDecoration( - color: context.color.surfaceContainer, - borderRadius: BorderRadius.circular(20), - ), - child: Row( - children: [ - if (_recordingState != RecordingState.recording) - GestureDetector( - onTap: () { - setState(() { - _emojiShowing = !_emojiShowing; - if (_emojiShowing) { - widget.textFieldFocus.unfocus(); - } else { - widget.textFieldFocus.requestFocus(); - } - }); - }, - child: ColoredBox( - color: Colors.transparent, - child: Padding( - padding: const EdgeInsets.only( - top: 8, - bottom: 8, - left: 12, - right: 8, - ), - child: FaIcon( - size: 20, - _emojiShowing - ? FontAwesomeIcons.keyboard - : FontAwesomeIcons.faceSmile, + UnverifiedContactWarningComp( + group: widget.group, + child: Padding( + padding: const EdgeInsets.only( + bottom: 10, + left: 10, + top: 5, + ), + child: Row( + children: [ + Expanded( + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 3, + ), + decoration: BoxDecoration( + color: context.color.surfaceContainer, + borderRadius: BorderRadius.circular(20), + ), + child: Row( + children: [ + if (_recordingState != RecordingState.recording) + GestureDetector( + onTap: () { + setState(() { + _emojiShowing = !_emojiShowing; + if (_emojiShowing) { + widget.textFieldFocus.unfocus(); + } else { + widget.textFieldFocus.requestFocus(); + } + }); + }, + child: ColoredBox( + color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.only( + top: 8, + bottom: 8, + left: 12, + right: 8, + ), + child: FaIcon( + size: 20, + _emojiShowing ? FontAwesomeIcons.keyboard : FontAwesomeIcons.faceSmile, + ), ), ), ), - ), - Expanded( - child: Stack( - children: [ - TextField( - controller: _textFieldController, - focusNode: widget.textFieldFocus, - keyboardType: TextInputType.multiline, - showCursor: - _recordingState != RecordingState.recording, - maxLines: 4, - minLines: 1, - onChanged: (value) async { - setState(() {}); - await twonlyDB.groupsDao.updateGroup( - widget.group.groupId, - GroupsCompanion( - draftMessage: Value( - _textFieldController.text, + Expanded( + child: Stack( + children: [ + TextField( + controller: _textFieldController, + focusNode: widget.textFieldFocus, + keyboardType: TextInputType.multiline, + showCursor: _recordingState != RecordingState.recording, + maxLines: 4, + minLines: 1, + onChanged: (value) async { + setState(() {}); + await twonlyDB.groupsDao.updateGroup( + widget.group.groupId, + GroupsCompanion( + draftMessage: Value( + _textFieldController.text, + ), ), - ), - ); - }, - onSubmitted: (_) { - _sendMessage(); - }, - style: const TextStyle(fontSize: 17), - decoration: InputDecoration( - hintText: context.lang.chatListDetailInput, - contentPadding: EdgeInsets.zero, - border: InputBorder.none, - ), - ), - if (_recordingState == RecordingState.recording) - Container( - decoration: BoxDecoration( - color: context.color.surfaceContainer, - borderRadius: BorderRadius.circular(20), + ); + }, + onSubmitted: (_) { + _sendMessage(); + }, + style: const TextStyle(fontSize: 17), + decoration: InputDecoration( + hintText: context.lang.chatListDetailInput, + contentPadding: EdgeInsets.zero, + border: InputBorder.none, ), - child: Row( - children: [ - const Padding( - padding: EdgeInsets.only( - top: 14, - bottom: 14, - left: 12, - right: 8, - ), - child: FaIcon( - FontAwesomeIcons.microphone, - size: 20, - color: Colors.red, - ), - ), - const SizedBox(width: 10), - Text( - formatMsToMinSec( - _currentDuration, - ), - style: TextStyle( - color: isDarkMode(context) - ? Colors.white - : Colors.black, - fontSize: 12, - ), - ), - if (!_audioRecordingLock) ...[ - SizedBox( - width: (100 - _cancelSlideOffset) % 101, + ), + if (_recordingState == RecordingState.recording) + Container( + decoration: BoxDecoration( + color: context.color.surfaceContainer, + borderRadius: BorderRadius.circular(20), + ), + child: Row( + children: [ + const Padding( + padding: EdgeInsets.only( + top: 14, + bottom: 14, + left: 12, + right: 8, + ), + child: FaIcon( + FontAwesomeIcons.microphone, + size: 20, + color: Colors.red, + ), ), + const SizedBox(width: 10), Text( - context.lang.voiceMessageSlideToCancel, + formatMsToMinSec( + _currentDuration, + ), + style: TextStyle( + color: isDarkMode(context) ? Colors.white : Colors.black, + fontSize: 12, + ), ), - ] else ...[ - Expanded( - child: Container(), - ), - GestureDetector( - onTap: _cancelAudioRecording, - child: Text( - context.lang.voiceMessageCancel, - style: const TextStyle( - color: Colors.red, + if (!_audioRecordingLock) ...[ + SizedBox( + width: (100 - _cancelSlideOffset) % 101, + ), + Text( + context.lang.voiceMessageSlideToCancel, + ), + ] else ...[ + Expanded( + child: Container(), + ), + GestureDetector( + onTap: _cancelAudioRecording, + child: Text( + context.lang.voiceMessageCancel, + style: const TextStyle( + color: Colors.red, + ), ), ), - ), - const SizedBox(width: 20), + const SizedBox(width: 20), + ], ], - ], - ), - ), - ], - ), - ), - if (_textFieldController.text == '') - IconButton( - icon: const FaIcon(FontAwesomeIcons.camera), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return CameraSendToView(widget.group); - }, - ), - ); - }, - ), - if (_textFieldController.text == '') - GestureDetector( - onLongPressMoveUpdate: (details) { - if (_audioRecordingLock) return; - if (_recordingOffset.dy - - details.localPosition.dy >= - 100) { - HapticFeedback.heavyImpact(); - setState(() { - _audioRecordingLock = true; - }); - } - if (_recordingOffset.dx - - details.localPosition.dx >= - 90 && - _recordingState == RecordingState.recording) { - _recordingState = RecordingState.none; - HapticFeedback.heavyImpact(); - _cancelAudioRecording(); - } - - setState(() { - final a = - _recordingOffset.dx - - details.localPosition.dx; - if (a > 0 && a <= 90) { - _cancelSlideOffset = - _recordingOffset.dx - - details.localPosition.dx; - } - }); - }, - onLongPressStart: (a) { - _recordingOffset = a.localPosition; - _startAudioRecording(); - }, - onLongPressCancel: _cancelAudioRecording, - onLongPressEnd: (a) { - if (_recordingState != RecordingState.recording) { - return; - } - if (!_audioRecordingLock) { - _stopAudioRecording(); - } - }, - child: Stack( - clipBehavior: Clip.none, - children: [ - if (_recordingState == RecordingState.recording && - !_audioRecordingLock) - Positioned.fill( - top: -120, - left: -5, - child: Align( - alignment: AlignmentGeometry.topCenter, - child: Container( - padding: const EdgeInsets.only(top: 13), - height: 60, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(90), - color: isDarkMode(context) - ? Colors.black - : Colors.white, - ), - child: const Center( - child: Column( - children: [ - FaIcon( - FontAwesomeIcons.lock, - size: 16, - ), - SizedBox(height: 5), - FaIcon( - FontAwesomeIcons.angleUp, - size: 16, - ), - ], - ), - ), - ), - ), - ), - if (_recordingState == RecordingState.recording && - !_audioRecordingLock) - Positioned.fill( - top: -20, - left: -25, - bottom: -20, - right: -20, - child: Container( - decoration: BoxDecoration( - color: Colors.red, - borderRadius: BorderRadius.circular(90), - ), - width: 60, - height: 60, - ), - ), - if (!_audioRecordingLock) - ColoredBox( - color: Colors.transparent, - child: Padding( - padding: const EdgeInsets.only( - top: 8, - bottom: 8, - left: 8, - right: 12, - ), - child: FaIcon( - size: 20, - color: - (_recordingState == - RecordingState.recording) - ? Colors.white - : null, - (_recordingState == RecordingState.none) - ? FontAwesomeIcons.microphone - : (_recordingState == - RecordingState.recording) - ? FontAwesomeIcons.stop - : FontAwesomeIcons.play, - ), ), ), ], ), ), - ], + if (_textFieldController.text == '') + IconButton( + icon: const FaIcon(FontAwesomeIcons.camera), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return CameraSendToView(widget.group); + }, + ), + ); + }, + ), + if (_textFieldController.text == '') + GestureDetector( + onLongPressMoveUpdate: (details) { + if (_audioRecordingLock) return; + if (_recordingOffset.dy - details.localPosition.dy >= 100) { + HapticFeedback.heavyImpact(); + setState(() { + _audioRecordingLock = true; + }); + } + if (_recordingOffset.dx - details.localPosition.dx >= 90 && + _recordingState == RecordingState.recording) { + _recordingState = RecordingState.none; + HapticFeedback.heavyImpact(); + _cancelAudioRecording(); + } + + setState(() { + final a = _recordingOffset.dx - details.localPosition.dx; + if (a > 0 && a <= 90) { + _cancelSlideOffset = _recordingOffset.dx - details.localPosition.dx; + } + }); + }, + onLongPressStart: (a) { + _recordingOffset = a.localPosition; + _startAudioRecording(); + }, + onLongPressCancel: _cancelAudioRecording, + onLongPressEnd: (a) { + if (_recordingState != RecordingState.recording) { + return; + } + if (!_audioRecordingLock) { + _stopAudioRecording(); + } + }, + child: Stack( + clipBehavior: Clip.none, + children: [ + if (_recordingState == RecordingState.recording && !_audioRecordingLock) + Positioned.fill( + top: -120, + left: -5, + child: Align( + alignment: AlignmentGeometry.topCenter, + child: Container( + padding: const EdgeInsets.only(top: 13), + height: 60, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(90), + color: isDarkMode(context) ? Colors.black : Colors.white, + ), + child: const Center( + child: Column( + children: [ + FaIcon( + FontAwesomeIcons.lock, + size: 16, + ), + SizedBox(height: 5), + FaIcon( + FontAwesomeIcons.angleUp, + size: 16, + ), + ], + ), + ), + ), + ), + ), + if (_recordingState == RecordingState.recording && !_audioRecordingLock) + Positioned.fill( + top: -20, + left: -25, + bottom: -20, + right: -20, + child: Container( + decoration: BoxDecoration( + color: Colors.red, + borderRadius: BorderRadius.circular(90), + ), + width: 60, + height: 60, + ), + ), + if (!_audioRecordingLock) + ColoredBox( + color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.only( + top: 8, + bottom: 8, + left: 8, + right: 12, + ), + child: FaIcon( + size: 20, + color: (_recordingState == RecordingState.recording) ? Colors.white : null, + (_recordingState == RecordingState.none) + ? FontAwesomeIcons.microphone + : (_recordingState == RecordingState.recording) + ? FontAwesomeIcons.stop + : FontAwesomeIcons.play, + ), + ), + ), + ], + ), + ), + ], + ), ), ), - ), - if (_textFieldController.text != '' || _audioRecordingLock) - IconButton( - padding: const EdgeInsets.all(15), - icon: FaIcon( - color: context.color.primary, - FontAwesomeIcons.solidPaperPlane, + if (_textFieldController.text != '' || _audioRecordingLock) + IconButton( + padding: const EdgeInsets.all(15), + icon: FaIcon( + color: context.color.primary, + FontAwesomeIcons.solidPaperPlane, + ), + onPressed: _audioRecordingLock ? _stopAudioRecording : _sendMessage, + ) + else + IconButton( + icon: const FaIcon(FontAwesomeIcons.plus), + padding: const EdgeInsets.all(15), + onPressed: () => _showAdditionalShareModal(context), ), - onPressed: _audioRecordingLock - ? _stopAudioRecording - : _sendMessage, - ) - else - IconButton( - icon: const FaIcon(FontAwesomeIcons.plus), - padding: const EdgeInsets.all(15), - onPressed: () => _showAdditionalShareModal(context), - ), - ], + ], + ), ), ), Offstage( diff --git a/lib/src/visual/views/chats/chat_messages_components/response_container.dart b/lib/src/visual/views/chats/chat_messages_components/response_container.dart index c5029355..d0d8d58d 100644 --- a/lib/src/visual/views/chats/chat_messages_components/response_container.dart +++ b/lib/src/visual/views/chats/chat_messages_components/response_container.dart @@ -4,6 +4,7 @@ import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/messages.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/protobuf/client/generated/data.pb.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/views/chats/chat_messages.view.dart'; @@ -39,10 +40,8 @@ class _ResponseContainerState extends State { void didChangeDependencies() { super.didChangeDependencies(); WidgetsBinding.instance.addPostFrameCallback((_) { - final messageBox = - _message.currentContext?.findRenderObject() as RenderBox?; - final previewBox = - _preview.currentContext?.findRenderObject() as RenderBox?; + final messageBox = _message.currentContext?.findRenderObject() as RenderBox?; + final previewBox = _preview.currentContext?.findRenderObject() as RenderBox?; if (messageBox == null || previewBox == null) { return; } @@ -65,9 +64,7 @@ class _ResponseContainerState extends State { return widget.child!; } return GestureDetector( - onTap: widget.scrollToMessage == null - ? null - : () => widget.scrollToMessage!(widget.msg.quotesMessageId!), + onTap: widget.scrollToMessage == null ? null : () => widget.scrollToMessage!(widget.msg.quotesMessageId!), child: Container( constraints: BoxConstraints( maxWidth: MediaQuery.of(context).size.width * 0.8, @@ -143,16 +140,12 @@ class _ResponsePreviewState extends State { } Future initAsync() async { - _message ??= await twonlyDB.messagesDao - .getMessageById(widget.messageId!) - .getSingleOrNull(); + _message ??= await twonlyDB.messagesDao.getMessageById(widget.messageId!).getSingleOrNull(); if (_message?.mediaId != null) { _mediaService = await MediaFileService.fromMediaId(_message!.mediaId!); } if (_message?.senderId != null) { - final contact = await twonlyDB.contactsDao - .getContactByUserId(_message!.senderId!) - .getSingleOrNull(); + final contact = await twonlyDB.contactsDao.getContactByUserId(_message!.senderId!).getSingleOrNull(); if (contact != null) { _username = getContactDisplayName(contact); } @@ -186,6 +179,28 @@ class _ResponsePreviewState extends State { subtitle = 'Audio'; } } + if (_message!.type == MessageType.contacts.name) { + subtitle = context.lang.contacts; + } + if (_message!.type == MessageType.restoreFlameCounter.name) { + if (_message!.additionalMessageData != null) { + try { + final data = AdditionalMessageData.fromBuffer( + _message!.additionalMessageData!, + ); + subtitle = context.lang.chatEntryFlameRestored( + data.restoredFlameCounter.toInt(), + ); + } catch (e) { + subtitle = context.lang.replyFlameRestored; + } + } else { + subtitle = context.lang.replyFlameRestored; + } + } + if (_message!.type == MessageType.askAboutUser.name) { + subtitle = context.lang.replyAskAFriend; + } if (_message!.senderId == null) { _username = context.lang.you; @@ -248,8 +263,7 @@ class _ResponsePreviewState extends State { ], ), ), - if (_mediaService != null && - _mediaService!.mediaFile.type != MediaType.audio) + if (_mediaService != null && _mediaService!.mediaFile.type != MediaType.audio) SizedBox( height: widget.showBorder ? 100 : 210, child: Image.file( diff --git a/lib/src/visual/views/chats/chat_messages_components/unverified_contact_warning.comp.dart b/lib/src/visual/views/chats/chat_messages_components/unverified_contact_warning.comp.dart new file mode 100644 index 00000000..789c5763 --- /dev/null +++ b/lib/src/visual/views/chats/chat_messages_components/unverified_contact_warning.comp.dart @@ -0,0 +1,121 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:twonly/locator.dart'; +import 'package:twonly/src/constants/routes.keys.dart'; +import 'package:twonly/src/database/daos/key_verification.dao.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/services/profile.service.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/components/verification_badge.comp.dart'; + +class UnverifiedContactWarningComp extends StatelessWidget { + const UnverifiedContactWarningComp({ + required this.group, + required this.child, + super.key, + }); + + final Group group; + final Widget child; + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: userService.onUserUpdated, + builder: (context, _) { + if (!userService.currentUser.securityProfile.showWarningForNonVerifiedContacts) { + return child; + } + return StreamBuilder( + stream: twonlyDB.keyVerificationDao.watchAllGroupMembersVerified(group.groupId), + builder: (context, snapshot) { + final status = snapshot.data; + if (status == null || status == VerificationStatus.trusted) { + return child; + } + + return Container( + margin: const EdgeInsets.only(top: 8), + decoration: BoxDecoration( + color: context.color.errorContainer.withValues(alpha: 0.5), + borderRadius: BorderRadius.circular(24), + border: Border.all(color: context.color.error.withValues(alpha: 0.5)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(12, 10, 12, 8), + child: Row( + children: [ + VerificationBadgeComp( + group: group, + size: 24, + clickable: false, + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + group.isDirectChat + ? context.lang.unverifiedWarningDirectTitle + : context.lang.unverifiedWarningGroupTitle, + style: TextStyle( + color: context.color.onErrorContainer, + fontWeight: FontWeight.bold, + fontSize: 13, + height: 1.2, + ), + ), + const SizedBox(height: 4), + RichText( + text: TextSpan( + style: TextStyle( + color: context.color.onErrorContainer, + fontSize: 11, + ), + children: formattedText( + context, + context.lang.unverifiedWarningBody, + textColor: context.color.onErrorContainer, + ), + ), + ), + ], + ), + ), + const SizedBox(width: 10), + SizedBox( + height: 30, + child: FilledButton.tonal( + style: FilledButton.styleFrom( + backgroundColor: context.color.onErrorContainer, + foregroundColor: context.color.errorContainer, + padding: const EdgeInsets.symmetric(horizontal: 10), + textStyle: const TextStyle(fontSize: 11, fontWeight: FontWeight.bold), + ), + onPressed: () async { + if (group.isDirectChat) { + await context.push(Routes.settingsHelpFaqVerifyBadge); + } else { + await context.push(Routes.profileGroup(group.groupId)); + } + }, + child: Text(context.lang.unverifiedWarningButton), + ), + ), + ], + ), + ), + child, + ], + ), + ); + }, + ); + }, + ); + } +} diff --git a/lib/src/visual/views/contact/add_new_contact.view.dart b/lib/src/visual/views/contact/add_new_contact.view.dart index 6808ce0c..5c631673 100644 --- a/lib/src/visual/views/contact/add_new_contact.view.dart +++ b/lib/src/visual/views/contact/add_new_contact.view.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:convert'; import 'package:collection/collection.dart'; import 'package:drift/drift.dart' hide Column; @@ -6,14 +7,18 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:go_router/go_router.dart'; +import 'package:share_plus/share_plus.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/database/daos/user_discovery.dao.dart'; import 'package:twonly/src/database/tables/contacts.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/services/api/utils.api.dart'; +import 'package:twonly/src/services/signal/identity.signal.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/components/alert.dialog.dart'; +import 'package:twonly/src/visual/components/profile_qr_code.comp.dart'; +import 'package:twonly/src/visual/themes/light.dart'; import 'package:twonly/src/visual/views/contact/add_new_contact_components/friend_suggestions.comp.dart'; import 'package:twonly/src/visual/views/contact/add_new_contact_components/open_requests_list.comp.dart'; @@ -57,24 +62,21 @@ class _SearchUsernameView extends State { } }, ); - _newAnnouncedUsersStream = twonlyDB.userDiscoveryDao - .watchNewAnnouncedUsersWithRelations() - .listen((update) { - if (mounted) { - setState(() { - _newAnnouncedUsers = update; - }); - } + + _newAnnouncedUsersStream = twonlyDB.userDiscoveryDao.watchNewAnnouncedUsersWithRelations().listen((update) { + if (mounted) { + setState(() { + _newAnnouncedUsers = update; }); - _allAnnouncedUsersStream = twonlyDB.userDiscoveryDao - .watchAllAnnouncedUsersWithRelations() - .listen((update) { - if (mounted) { - setState(() { - _allAnnouncedUsers = update; - }); - } + } + }); + _allAnnouncedUsersStream = twonlyDB.userDiscoveryDao.watchAllAnnouncedUsersWithRelations().listen((update) { + if (mounted) { + setState(() { + _allAnnouncedUsers = update; }); + } + }); if (widget.username != null) { _usernameController.text = widget.username!; @@ -85,6 +87,60 @@ class _SearchUsernameView extends State { twonlyDB.userDiscoveryDao.markAllValidAnnouncedUsersAsShown(); } + Future _shareProfile() async { + final pubKey = await getUserPublicKey(); + final params = ShareParams( + text: 'https://me.twonly.eu/${userService.currentUser.username}#${base64Url.encode(pubKey)}', + ); + await SharePlus.instance.share(params); + } + + void _showMyQrCode() { + // ignore: inference_failure_on_function_invocation + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) { + return Container( + decoration: BoxDecoration( + color: context.color.surface, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(24), + ), + ), + padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(width: double.infinity), + Container( + width: 40, + height: 4, + decoration: BoxDecoration( + color: context.color.onSurface.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(2), + ), + ), + const SizedBox(height: 24), + const ProfileQrCodeComp(), + const SizedBox(height: 24), + Text( + context.lang.addContactQrSheetSubtext, + style: TextStyle( + fontSize: 14, + color: context.color.onSurface.withValues(alpha: 0.6), + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + ], + ), + ); + }, + ); + } + @override void dispose() { _contactsStream.cancel(); @@ -134,9 +190,7 @@ class _SearchUsernameView extends State { ), ); - if (widget.publicKey != null && - mounted && - widget.publicKey!.equals(userdata.publicIdentityKey)) { + if (widget.publicKey != null && mounted && widget.publicKey!.equals(userdata.publicIdentityKey)) { final markAsVerified = await showAlertDialog( context, context.lang.linkFromUsername(username), @@ -154,21 +208,6 @@ class _SearchUsernameView extends State { if (added > 0) await importSignalContactAndCreateRequest(userdata); } - InputDecoration _getInputDecoration(String hintText) { - return InputDecoration( - hintText: hintText, - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(9), - borderSide: BorderSide(color: context.color.primary), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide(color: context.color.outline), - ), - contentPadding: const EdgeInsets.symmetric(vertical: 15, horizontal: 20), - ); - } - @override Widget build(BuildContext context) { return Scaffold( @@ -176,77 +215,157 @@ class _SearchUsernameView extends State { title: Text(context.lang.addFriendTitle), ), body: SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 10), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: Row( - children: [ - Expanded( - child: TextField( - onSubmitted: _requestNewUserByUsername, - onChanged: (value) { - _usernameController.text = value.toLowerCase(); - _usernameController.selection = - TextSelection.fromPosition( - TextPosition( - offset: _usernameController.text.length, - ), - ); - setState(() {}); - }, - inputFormatters: [ - LengthLimitingTextInputFormatter(12), - FilteringTextInputFormatter.allow( - RegExp('[a-z0-9A-Z._]'), - ), - ], - controller: _usernameController, - decoration: _getInputDecoration( - context.lang.searchUsernameInput, + child: ListView( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + child: SearchBar( + controller: _usernameController, + hintText: context.lang.searchUsernameInput, + elevation: const WidgetStatePropertyAll(0), + backgroundColor: WidgetStatePropertyAll( + context.color.surfaceContainerHighest.withValues(alpha: 0.3), + ), + shape: WidgetStatePropertyAll( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + padding: const WidgetStatePropertyAll( + EdgeInsets.symmetric(horizontal: 8), + ), + leading: const Icon(Icons.search, size: 20, color: Colors.grey), + trailing: [ + if (_usernameController.text.isNotEmpty) ...[ + IconButton( + icon: const Icon(Icons.clear, size: 20), + onPressed: () { + _usernameController.clear(); + setState(() {}); + }, + ), + if (_isLoading) + const Padding( + padding: EdgeInsets.symmetric(horizontal: 8), + child: SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator(strokeWidth: 2), + ), + ) + else + IconButton( + icon: FaIcon( + FontAwesomeIcons.magnifyingGlassPlus, + size: 20, + color: context.color.primary, + ), + onPressed: () => _requestNewUserByUsername( + _usernameController.text, ), ), - ), - ], - ), - ), - const SizedBox( - height: 20, - ), - Expanded( - child: ListView( - children: [ - Center( - child: OutlinedButton.icon( - onPressed: () => - context.push(Routes.settingsPublicProfile), - icon: const FaIcon(FontAwesomeIcons.qrcode), - label: Text(context.lang.scanQrOrShow), + ] else ...[ + IconButton( + icon: FaIcon( + FontAwesomeIcons.camera, + size: 20, + color: context.color.primary, ), + onPressed: () => context.push(Routes.cameraQRScanner), + tooltip: context.lang.scanOtherProfile, ), - OpenRequestsListComp( - contacts: _openRequestsContacts, - relations: _allAnnouncedUsers, - ), - FriendSuggestionsComp(_newAnnouncedUsers), ], - ), + ], + onSubmitted: _requestNewUserByUsername, + onChanged: (value) { + _usernameController.text = value.toLowerCase(); + _usernameController.selection = TextSelection.fromPosition( + TextPosition(offset: _usernameController.text.length), + ); + setState(() {}); + }, ), - ], - ), - ), - ), - floatingActionButton: Padding( - padding: const EdgeInsets.only(bottom: 30), - child: FloatingActionButton( - onPressed: _isLoading || _usernameController.text.isEmpty - ? null - : () => _requestNewUserByUsername(_usernameController.text), - child: _isLoading - ? const Center(child: CircularProgressIndicator()) - : const FaIcon(FontAwesomeIcons.magnifyingGlassPlus), + ), + const SizedBox( + height: 10, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Expanded( + child: FilledButton.icon( + style: FilledButton.styleFrom( + backgroundColor: primaryColor, + foregroundColor: Colors.black87, + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 10, + ), + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + onPressed: _shareProfile, + icon: const FaIcon( + FontAwesomeIcons.shareNodes, + size: 14, + ), + label: Text( + context.lang.shareYourProfile, + style: const TextStyle(fontSize: 13), + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: FilledButton.icon( + style: FilledButton.styleFrom( + backgroundColor: context.color.secondaryContainer, + foregroundColor: context.color.onSecondaryContainer, + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 10, + ), + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + onPressed: _showMyQrCode, + icon: const FaIcon( + FontAwesomeIcons.qrcode, + size: 14, + ), + label: Text( + context.lang.openYourOwnQRcode, + style: const TextStyle(fontSize: 13), + ), + ), + ), + ], + ), + ], + ), + ), + const SizedBox(height: 15), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Column( + children: [ + OpenRequestsListComp( + contacts: _openRequestsContacts, + relations: _allAnnouncedUsers, + ), + FriendSuggestionsComp(_newAnnouncedUsers), + ], + ), + ), + ], ), ), ); diff --git a/lib/src/visual/views/contact/add_new_contact_components/friend_suggestions.comp.dart b/lib/src/visual/views/contact/add_new_contact_components/friend_suggestions.comp.dart index 73725388..992d6cf5 100644 --- a/lib/src/visual/views/contact/add_new_contact_components/friend_suggestions.comp.dart +++ b/lib/src/visual/views/contact/add_new_contact_components/friend_suggestions.comp.dart @@ -5,6 +5,7 @@ import 'package:twonly/locator.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/daos/user_discovery.dao.dart'; import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/services/api/messages.api.dart'; import 'package:twonly/src/services/api/utils.api.dart'; import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/utils/log.dart'; @@ -82,6 +83,87 @@ class FriendSuggestionsComp extends StatelessWidget { ); } + Future _askFriends( + BuildContext context, + UserDiscoveryAnnouncedUser user, + List<(Contact, DateTime?)> friends, + ) async { + Log.info('Asking friends about user: ${user.announcedUserId}'); + final selectedFriends = {}; + final username = user.username ?? ''; + + final result = await showDialog( + context: context, + builder: (context) { + return StatefulBuilder( + builder: (context, setState) { + return AlertDialog( + title: Text(context.lang.askFriendsDialogTitle(username)), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(context.lang.askFriendsDialogDescription), + const SizedBox(height: 10), + Flexible( + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: friends.map((f) { + final contact = f.$1; + final isSelected = + selectedFriends.contains(contact.userId); + return CheckboxListTile( + contentPadding: EdgeInsets.zero, + title: Text(contact.displayName ?? contact.username), + value: isSelected, + onChanged: (val) { + setState(() { + if (val == true) { + selectedFriends.add(contact.userId); + } else { + selectedFriends.remove(contact.userId); + } + }); + }, + ); + }).toList(), + ), + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: Text(context.lang.askFriendsDialogCancel), + ), + TextButton( + onPressed: selectedFriends.isEmpty + ? null + : () => Navigator.pop(context, true), + child: Text(context.lang.askFriendsDialogConfirm), + ), + ], + ); + }, + ); + }, + ); + + if (result == true && selectedFriends.isNotEmpty) { + for (final contactId in selectedFriends) { + await insertAndSendAskAboutUserMessage(contactId, user.announcedUserId); + } + + await twonlyDB.userDiscoveryDao.updateAnnouncedUser( + user.announcedUserId, + const UserDiscoveryAnnouncedUsersCompanion( + wasAskedFriends: Value(true), + ), + ); + } + } + @override Widget build(BuildContext context) { if (announcedUsers.isEmpty) return Container(); @@ -99,71 +181,146 @@ class FriendSuggestionsComp extends StatelessWidget { final friendsList = buildFriendsListText(context, friends); - return ListTile( + return Padding( key: ValueKey(user.announcedUserId), - contentPadding: EdgeInsets.zero, - title: Text(substringBy(user.username!, 25)), - subtitle: StreamBuilder( - stream: twonlyDB.groupsDao.watchNonDirectGroupsForMember( - user.announcedUserId, - ), - builder: (context, snapshot) { - var text = friendsList; - if (snapshot.hasData && snapshot.data!.isNotEmpty) { - text += formattedText( - context, - context.lang.friendSuggestionsGroupMemberIn( - joinWithAnd( - snapshot.data!.map((g) => '*${g.groupName}*').toList(), - context.lang.andWord, - ), - ), - ); - } - return RichText( - text: TextSpan( - children: text, - style: const TextStyle(fontSize: 11), - ), - ); - }, - ), - - leading: const AvatarIcon( - fontSize: 17, - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( children: [ - SizedBox( - height: 26, - child: FilledButton( - style: FilledButton.styleFrom( - padding: const EdgeInsets.only(right: 8, left: 4), - ).merge(secondaryGreyButtonStyle(context)), - child: Row( - children: [ - const Padding( - padding: EdgeInsets.symmetric(horizontal: 6), - child: FaIcon(FontAwesomeIcons.userPlus, size: 12), + const AvatarIcon( + fontSize: 17, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + substringBy(user.username!, 25), + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, ), - Text( - context.lang.friendSuggestionsRequest, - style: const TextStyle(fontSize: 10), + ), + const SizedBox(height: 4), + StreamBuilder>( + stream: twonlyDB.groupsDao + .watchNonDirectGroupsForMember( + user.announcedUserId, + ), + builder: (context, snapshot) { + var text = friendsList; + if (snapshot.hasData && snapshot.data!.isNotEmpty) { + text += formattedText( + context, + context.lang.friendSuggestionsGroupMemberIn( + joinWithAnd( + snapshot.data! + .map((g) => '*${g.groupName}*') + .toList(), + context.lang.andWord, + ), + ), + ); + } + return RichText( + text: TextSpan( + children: text, + style: TextStyle( + fontSize: 11, + color: context.color.onSurfaceVariant, + ), + ), + ); + }, + ), + ], + ), + ), + const SizedBox(width: 8), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + if (!user.wasAskedFriends) ...[ + SizedBox( + height: 28, + child: FilledButton( + style: FilledButton.styleFrom( + padding: const EdgeInsets.only( + right: 8, + left: 4, + ), + ).merge(secondaryGreyButtonStyle(context)), + onPressed: () => _askFriends(context, user, friends), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Padding( + padding: EdgeInsets.symmetric( + horizontal: 6, + ), + child: FaIcon( + FontAwesomeIcons.circleQuestion, + size: 12, + ), + ), + Text( + context.lang.friendSuggestionsAskFriend, + style: const TextStyle(fontSize: 10), + ), + ], + ), + ), + ), + const SizedBox(height: 6), + ], + SizedBox( + height: 26, + child: FilledButton( + style: FilledButton.styleFrom( + padding: const EdgeInsets.only( + right: 8, + left: 4, + ), + ).merge(secondaryGreyButtonStyle(context)), + onPressed: () => + _requestAnnouncedUser(context, user), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Padding( + padding: EdgeInsets.symmetric( + horizontal: 6, + ), + child: FaIcon( + FontAwesomeIcons.userPlus, + size: 12, + ), + ), + Text( + context.lang.friendSuggestionsRequest, + style: const TextStyle(fontSize: 10), + ), + ], + ), + ), ), ], ), - onPressed: () => _requestAnnouncedUser(context, user), - ), - ), - IconButton( - style: IconButton.styleFrom( - padding: const EdgeInsets.symmetric(horizontal: 8), - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - constraints: const BoxConstraints(), - icon: const Icon(Icons.close, size: 18), - onPressed: () => _hideAnnouncedUser(user.announcedUserId), + IconButton( + style: IconButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 8), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + constraints: const BoxConstraints(), + icon: const Icon(Icons.close, size: 18), + onPressed: () => _hideAnnouncedUser(user.announcedUserId), + ), + ], ), ], ), diff --git a/lib/src/visual/views/contact/add_new_contact_components/open_requests_list.comp.dart b/lib/src/visual/views/contact/add_new_contact_components/open_requests_list.comp.dart index 53a3a3c0..6f665b63 100644 --- a/lib/src/visual/views/contact/add_new_contact_components/open_requests_list.comp.dart +++ b/lib/src/visual/views/contact/add_new_contact_components/open_requests_list.comp.dart @@ -7,6 +7,7 @@ import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/services/api/messages.api.dart'; import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/components/alert.dialog.dart'; import 'package:twonly/src/visual/components/avatar_icon.comp.dart'; import 'package:twonly/src/visual/components/verification_badge.comp.dart'; import 'package:twonly/src/visual/elements/headline.element.dart'; @@ -63,8 +64,17 @@ class OpenRequestsListComp extends StatelessWidget { ], ), onPressed: () async { - const update = ContactsCompanion(blocked: Value(true)); - await twonlyDB.contactsDao.updateContact(contact.userId, update); + final block = await showAlertDialog( + context, + context.lang.contactBlockTitle(getContactDisplayName(contact)), + context.lang.contactBlockBody, + ); + if (block) { + const update = ContactsCompanion(blocked: Value(true)); + if (context.mounted) { + await twonlyDB.contactsDao.updateContact(contact.userId, update); + } + } }, ), ), @@ -179,9 +189,7 @@ class OpenRequestsListComp extends StatelessWidget { ), trailing: Row( mainAxisSize: MainAxisSize.min, - children: contact.requested - ? requestedActions(context, contact) - : sendRequestActions(context, contact), + children: contact.requested ? requestedActions(context, contact) : sendRequestActions(context, contact), ), ); }), diff --git a/lib/src/visual/views/contact/contact.view.dart b/lib/src/visual/views/contact/contact.view.dart index 4d6b5ad1..aa0343c2 100644 --- a/lib/src/visual/views/contact/contact.view.dart +++ b/lib/src/visual/views/contact/contact.view.dart @@ -4,13 +4,10 @@ import 'package:drift/drift.dart' hide Column; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:go_router/go_router.dart'; -import 'package:intl/intl.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; -import 'package:twonly/src/database/tables/contacts.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/services/user_discovery.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/components/alert.dialog.dart'; import 'package:twonly/src/visual/components/avatar_icon.comp.dart'; @@ -19,9 +16,11 @@ import 'package:twonly/src/visual/components/select_chat_deletion_time.comp.dart import 'package:twonly/src/visual/components/snackbar.dart'; import 'package:twonly/src/visual/components/verification_badge.comp.dart'; import 'package:twonly/src/visual/elements/better_list_title.element.dart'; +import 'package:twonly/src/visual/views/contact/contact_components/mutual_groups_expansion_tile.comp.dart'; import 'package:twonly/src/visual/views/contact/contact_components/restore_flame.comp.dart'; +import 'package:twonly/src/visual/views/contact/contact_components/user_discovery_contact_settings.comp.dart'; +import 'package:twonly/src/visual/views/contact/contact_components/verification_expansion_tile.comp.dart'; import 'package:twonly/src/visual/views/groups/group.view.dart'; -import 'package:twonly/src/visual/views/settings/privacy/user_discovery.view.dart'; class ContactView extends StatefulWidget { const ContactView(this.userId, {super.key}); @@ -35,13 +34,9 @@ class ContactView extends StatefulWidget { class _ContactViewState extends State { Contact? _contact; List _memberOfGroups = []; - List _keyVerifications = []; - List<(Contact, DateTime)> _transferredTrust = []; late StreamSubscription _streamContact; late StreamSubscription> _streamMemberOfGroups; - late StreamSubscription> _streamKeyVerifications; - late StreamSubscription> _streamTransferredTrust; @override void initState() { @@ -63,30 +58,12 @@ class _ContactViewState extends State { _memberOfGroups = groups; }); }); - _streamKeyVerifications = twonlyDB.keyVerificationDao - .watchContactVerification(widget.userId) - .listen((update) { - if (!mounted) return; - setState(() { - _keyVerifications = update; - }); - }); - _streamTransferredTrust = twonlyDB.keyVerificationDao - .watchTransferredTrustVerifications(widget.userId) - .listen((update) { - if (!mounted) return; - setState(() { - _transferredTrust = update; - }); - }); } @override void dispose() { _streamContact.cancel(); _streamMemberOfGroups.cancel(); - _streamKeyVerifications.cancel(); - _streamTransferredTrust.cancel(); super.dispose(); } @@ -116,14 +93,6 @@ class _ContactViewState extends State { ); if (remove) { await twonlyDB.contactsDao.deleteContactByUserId(contact.userId); - // await twonlyDB.contactsDao.updateContact( - // contact.userId, - // const ContactsCompanion( - // accepted: Value(false), - // requested: Value(false), - // deletedByUser: Value(true), - // ), - // ); if (mounted) { Navigator.popUntil(context, (route) => route.isFirst); } @@ -260,131 +229,16 @@ class _ContactViewState extends State { RestoreFlameComp( contactId: widget.userId, ), - if (_keyVerifications.isEmpty && _transferredTrust.isEmpty) - BetterListTile( - leading: VerificationBadgeComp( - contact: contact, - size: 20, - ), - text: context.lang.contactVerifyNumberTitle, - onTap: () async { - await context.push(Routes.settingsHelpFaqVerifyBadge); - setState(() {}); - }, - ), - if (_keyVerifications.isNotEmpty || _transferredTrust.isNotEmpty) - ExpansionTile( - shape: const RoundedRectangleBorder(), - backgroundColor: context.color.surfaceContainer, - collapsedShape: const RoundedRectangleBorder(), - leading: Padding( - padding: const EdgeInsetsGeometry.only(left: 12, right: 12), - child: VerificationBadgeComp( - contact: contact, - size: 20, - ), - ), - title: Text(context.lang.userVerifiedTitle), - children: [ - ..._keyVerifications.map( - (kv) => ListTile( - dense: true, - title: Text(_verificationTypeLabel(context, kv.type)), - trailing: Text( - DateFormat.yMd( - Localizations.localeOf(context).toString(), - ).format(kv.createdAt), - style: TextStyle( - color: context.color.onSurfaceVariant, - fontSize: 13, - ), - ), - ), - ), - ..._transferredTrust.map( - (tt) => ListTile( - dense: true, - title: Row( - children: [ - Text( - context.lang.contactVerifiedBy( - getContactDisplayName(tt.$1), - ), - ), - VerificationBadgeComp( - contact: tt.$1, - ), - ], - ), - trailing: Text( - DateFormat.yMd( - Localizations.localeOf(context).toString(), - ).format(tt.$2), - style: TextStyle( - color: context.color.onSurfaceVariant, - fontSize: 13, - ), - ), - ), - ), - ], - ), - if (userService.currentUser.isUserDiscoveryEnabled) - if (userService.currentUser.userDiscoveryRequiresManualApproval && - contact.userDiscoveryManualApproved != true) - BetterListTile( - icon: FontAwesomeIcons.usersViewfinder, - text: context.lang.userDiscoverySettingsTitle, - subtitle: Text( - context.lang.contactUserDiscoveryManualApprovalPending, - style: const TextStyle(fontSize: 10), - ), - trailing: TextButton( - onPressed: () async { - await twonlyDB.contactsDao.updateContact( - contact.userId, - const ContactsCompanion( - userDiscoveryManualApproved: Value(true), - ), - ); - }, - child: Text( - context.lang.contactUserDiscoveryManualApprovalApprove, - ), - ), - ) - else - BetterListTile( - icon: FontAwesomeIcons.usersViewfinder, - text: context.lang.userDiscoverySettingsTitle, - onTap: () => context.navPush(const UserDiscoverySettingsView()), - subtitle: - !contact.userDiscoveryExcluded && - contact.mediaSendCounter < - userService.currentUser.requiredSendImages - ? Text( - context.lang.contactUserDiscoveryImagesLeft( - userService.currentUser.requiredSendImages - - contact.mediaSendCounter, - getContactDisplayName(contact), - ), - style: const TextStyle(fontSize: 9), - ) - : null, - trailing: Transform.scale( - scale: 0.8, - child: Switch( - value: !contact.userDiscoveryExcluded, - onChanged: (a) async { - await UserDiscoveryService.changeExclusionForContact( - contact.userId, - !a, - ); - }, - ), - ), - ), - + VerificationExpansionTileComp( + contact: contact, + ), + MutualGroupsExpansionTileComp( + contact: contact, + ), + UserDiscoveryContactSettingsComp( + contact: contact, + ), + const Divider(), BetterListTile( icon: FontAwesomeIcons.flag, text: context.lang.reportUser, @@ -408,19 +262,6 @@ class _ContactViewState extends State { } } -String _verificationTypeLabel(BuildContext context, VerificationType type) { - return switch (type) { - VerificationType.qrScanned => context.lang.verificationTypeQrScanned, - VerificationType.secretQrToken => - context.lang.verificationTypeSecretQrToken, - VerificationType.link => context.lang.verificationTypeLink, - VerificationType.contactSharedByVerified => - context.lang.verificationTypeContactSharedByVerified, - VerificationType.migratedFromOldVersion => - context.lang.verificationTypeMigratedFromOldVersion, - }; -} - Future showNicknameChangeDialog( BuildContext context, Contact contact, diff --git a/lib/src/visual/views/contact/contact_components/mutual_groups_expansion_tile.comp.dart b/lib/src/visual/views/contact/contact_components/mutual_groups_expansion_tile.comp.dart new file mode 100644 index 00000000..8f56eea9 --- /dev/null +++ b/lib/src/visual/views/contact/contact_components/mutual_groups_expansion_tile.comp.dart @@ -0,0 +1,112 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:go_router/go_router.dart'; +import 'package:twonly/locator.dart'; +import 'package:twonly/src/constants/routes.keys.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/components/avatar_icon.comp.dart'; + +class MutualGroupsExpansionTileComp extends StatefulWidget { + const MutualGroupsExpansionTileComp({ + required this.contact, + super.key, + }); + + final Contact contact; + + @override + State createState() => + _MutualGroupsExpansionTileCompState(); +} + +class _MutualGroupsExpansionTileCompState + extends State { + List _groups = []; + late StreamSubscription> _streamGroups; + bool? _hasInitializedExpanded; + + @override + void initState() { + super.initState(); + _streamGroups = twonlyDB.groupsDao + .watchNonDirectGroupsForMember(widget.contact.userId) + .listen((groupsList) { + if (!mounted) return; + setState(() { + _groups = groupsList; + _groups.sort((a, b) { + return b.totalMediaCounter.compareTo(a.totalMediaCounter); + }); + _hasInitializedExpanded ??= true; + }); + }); + } + + @override + void dispose() { + _streamGroups.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (_hasInitializedExpanded == null || _groups.isEmpty) { + return const SizedBox.shrink(); + } + + return ExpansionTile( + key: PageStorageKey('mutual_groups_${widget.contact.userId}'), + shape: const RoundedRectangleBorder(), + backgroundColor: context.color.surfaceContainer, + collapsedShape: const RoundedRectangleBorder(), + initiallyExpanded: _groups.length < 5, + onExpansionChanged: (expanded) { + setState(() {}); + }, + leading: Padding( + padding: const EdgeInsets.only(left: 14, right: 14), + child: SizedBox( + width: 20, + height: 20, + child: Icon( + FontAwesomeIcons.userGroup, + size: 16, + color: context.color.onSurfaceVariant, + ), + ), + ), + title: Text( + context.lang.mutualGroupsTitle(_groups.length), + style: TextStyle( + color: context.color.onSurface, + ), + ), + children: _groups.map((group) { + return ListTile( + dense: true, + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 4, + ), + leading: AvatarIcon( + group: group, + fontSize: 14, + ), + title: Text( + group.groupName, + style: TextStyle( + color: context.color.onSurface, + fontWeight: FontWeight.w400, + ), + ), + onTap: () { + context.push(Routes.chatsMessages(group.groupId)); + }, + ); + }).toList(), + ); + } +} diff --git a/lib/src/visual/views/contact/contact_components/restore_flame.comp.dart b/lib/src/visual/views/contact/contact_components/restore_flame.comp.dart index 4b2852af..d512eefd 100644 --- a/lib/src/visual/views/contact/contact_components/restore_flame.comp.dart +++ b/lib/src/visual/views/contact/contact_components/restore_flame.comp.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:clock/clock.dart'; import 'package:drift/drift.dart' show Value; import 'package:fixnum/fixnum.dart'; import 'package:flutter/foundation.dart'; @@ -11,8 +10,7 @@ import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/database/tables/messages.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/protobuf/client/generated/data.pb.dart'; -import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart' - as pb; +import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart' as pb; import 'package:twonly/src/services/api/messages.api.dart'; import 'package:twonly/src/services/flame.service.dart'; import 'package:twonly/src/services/subscription.service.dart'; @@ -24,9 +22,11 @@ import 'package:twonly/src/visual/elements/better_list_title.element.dart'; class RestoreFlameComp extends StatefulWidget { const RestoreFlameComp({ required this.contactId, + this.flameOnRightSide = false, super.key, }); final int contactId; + final bool flameOnRightSide; @override State createState() => _RestoreFlameCompState(); @@ -60,21 +60,15 @@ class _RestoreFlameCompState extends State { final currentPlan = planFromString( userService.currentUser.subscriptionPlan, ); - if (!isUserAllowed(currentPlan, PremiumFeatures.RestoreFlames) && - kReleaseMode) { + if (!isUserAllowed(currentPlan, PremiumFeatures.RestoreFlames) && kReleaseMode) { await context.push(Routes.settingsSubscription); return; } Log.info( 'Restoring flames from ${_group!.flameCounter} to ${_group!.maxFlameCounter}', ); - await twonlyDB.groupsDao.updateGroup( - _groupId, - GroupsCompanion( - flameCounter: Value(_group!.maxFlameCounter), - lastFlameCounterChange: Value(clock.now()), - ), - ); + + await restoreFlames(_groupId); final addData = AdditionalMessageData( type: AdditionalMessageData_Type.RESTORED_FLAME_COUNTER, @@ -116,6 +110,22 @@ class _RestoreFlameCompState extends State { if (_group == null || !isItPossibleToRestoreFlames(_group!)) { return Container(); } + if (widget.flameOnRightSide) { + return ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 16), + onTap: _restoreFlames, + title: Text( + 'Restore your ${_group!.maxFlameCounter} lost flames', + style: const TextStyle(fontWeight: FontWeight.w500), + ), + trailing: const SizedBox( + width: 24, + child: EmojiAnimationComp( + emoji: '🔥', + ), + ), + ); + } return BetterListTile( onTap: _restoreFlames, leading: const SizedBox( diff --git a/lib/src/visual/views/contact/contact_components/user_discovery_contact_settings.comp.dart b/lib/src/visual/views/contact/contact_components/user_discovery_contact_settings.comp.dart new file mode 100644 index 00000000..c60d19c7 --- /dev/null +++ b/lib/src/visual/views/contact/contact_components/user_discovery_contact_settings.comp.dart @@ -0,0 +1,81 @@ +import 'package:drift/drift.dart' hide Column; +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:twonly/locator.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/services/user_discovery.service.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/elements/better_list_title.element.dart'; +import 'package:twonly/src/visual/views/settings/privacy/user_discovery.view.dart'; + +class UserDiscoveryContactSettingsComp extends StatelessWidget { + const UserDiscoveryContactSettingsComp({ + required this.contact, + super.key, + }); + + final Contact contact; + + @override + Widget build(BuildContext context) { + if (!userService.currentUser.isUserDiscoveryEnabled) { + return const SizedBox.shrink(); + } + + if (userService.currentUser.userDiscoveryRequiresManualApproval && + contact.userDiscoveryManualApproved != true) { + return BetterListTile( + icon: FontAwesomeIcons.usersViewfinder, + text: context.lang.userDiscoverySettingsTitle, + subtitle: Text( + context.lang.contactUserDiscoveryManualApprovalPending, + style: const TextStyle(fontSize: 10), + ), + trailing: TextButton( + onPressed: () async { + await twonlyDB.contactsDao.updateContact( + contact.userId, + const ContactsCompanion( + userDiscoveryManualApproved: Value(true), + ), + ); + }, + child: Text( + context.lang.contactUserDiscoveryManualApprovalApprove, + ), + ), + ); + } + + return BetterListTile( + icon: FontAwesomeIcons.usersViewfinder, + text: context.lang.userDiscoverySettingsTitle, + onTap: () => context.navPush(const UserDiscoverySettingsView()), + subtitle: !contact.userDiscoveryExcluded && + contact.mediaSendCounter < + userService.currentUser.requiredSendImages + ? Text( + context.lang.contactUserDiscoveryImagesLeft( + userService.currentUser.requiredSendImages - + contact.mediaSendCounter, + getContactDisplayName(contact), + ), + style: const TextStyle(fontSize: 9), + ) + : null, + trailing: Transform.scale( + scale: 0.8, + child: Switch( + value: !contact.userDiscoveryExcluded, + onChanged: (a) async { + await UserDiscoveryService.changeExclusionForContact( + contact.userId, + !a, + ); + }, + ), + ), + ); + } +} diff --git a/lib/src/visual/views/contact/contact_components/verification_expansion_tile.comp.dart b/lib/src/visual/views/contact/contact_components/verification_expansion_tile.comp.dart new file mode 100644 index 00000000..cf63479b --- /dev/null +++ b/lib/src/visual/views/contact/contact_components/verification_expansion_tile.comp.dart @@ -0,0 +1,184 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:go_router/go_router.dart'; +import 'package:intl/intl.dart'; +import 'package:twonly/locator.dart'; +import 'package:twonly/src/constants/routes.keys.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/database/tables/contacts.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/components/alert.dialog.dart'; +import 'package:twonly/src/visual/components/verification_badge.comp.dart'; +import 'package:twonly/src/visual/elements/better_list_title.element.dart'; + +class VerificationExpansionTileComp extends StatefulWidget { + const VerificationExpansionTileComp({ + required this.contact, + super.key, + }); + + final Contact contact; + + @override + State createState() => + _VerificationExpansionTileCompState(); +} + +class _VerificationExpansionTileCompState + extends State { + List _keyVerifications = []; + List<(Contact, DateTime)> _transferredTrust = []; + + late StreamSubscription> _streamKeyVerifications; + late StreamSubscription> _streamTransferredTrust; + + @override + void initState() { + super.initState(); + _streamKeyVerifications = twonlyDB.keyVerificationDao + .watchContactVerification(widget.contact.userId) + .listen((update) { + if (!mounted) return; + setState(() { + _keyVerifications = update; + }); + }); + _streamTransferredTrust = twonlyDB.keyVerificationDao + .watchTransferredTrustVerifications(widget.contact.userId) + .listen((update) { + if (!mounted) return; + setState(() { + _transferredTrust = update; + }); + }); + } + + @override + void dispose() { + _streamKeyVerifications.cancel(); + _streamTransferredTrust.cancel(); + super.dispose(); + } + + String _verificationTypeLabel(BuildContext context, VerificationType type) { + return switch (type) { + VerificationType.qrScanned => context.lang.verificationTypeQrScanned, + VerificationType.secretQrToken => + context.lang.verificationTypeSecretQrToken( + getContactDisplayName(widget.contact), + ), + VerificationType.link => context.lang.verificationTypeLink, + VerificationType.contactSharedByVerified => + context.lang.verificationTypeContactSharedByVerified, + VerificationType.migratedFromOldVersion => + context.lang.verificationTypeMigratedFromOldVersion, + }; + } + + @override + Widget build(BuildContext context) { + if (_keyVerifications.isEmpty && _transferredTrust.isEmpty) { + return BetterListTile( + leading: VerificationBadgeComp( + contact: widget.contact, + size: 20, + ), + text: context.lang.contactVerifyNumberTitle, + onTap: () async { + await context.push(Routes.settingsHelpFaqVerifyBadge); + if (mounted) setState(() {}); + }, + ); + } + + return ExpansionTile( + shape: const RoundedRectangleBorder(), + backgroundColor: context.color.surfaceContainer, + collapsedShape: const RoundedRectangleBorder(), + leading: Padding( + padding: const EdgeInsetsGeometry.only(left: 12, right: 12), + child: VerificationBadgeComp( + contact: widget.contact, + size: 20, + ), + ), + title: Text(context.lang.userVerifiedTitle), + children: [ + ..._keyVerifications.map( + (kv) => ListTile( + dense: true, + contentPadding: const EdgeInsets.only(left: 16), + title: Text(_verificationTypeLabel(context, kv.type)), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + DateFormat.yMd( + Localizations.localeOf(context).toString(), + ).format(kv.createdAt), + style: TextStyle( + color: context.color.onSurfaceVariant, + fontSize: 13, + ), + ), + IconButton( + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + iconSize: 8, + icon: Icon( + FontAwesomeIcons.trash, + size: 8, + color: context.color.onSurfaceVariant, + ), + onPressed: () async { + final confirm = await showAlertDialog( + context, + context.lang.deleteVerificationTitle, + context.lang.deleteVerificationBody, + ); + if (confirm) { + await twonlyDB.keyVerificationDao + .deleteKeyVerificationById( + kv.verificationId, + widget.contact.userId, + ); + } + }, + ), + ], + ), + ), + ), + ..._transferredTrust.map( + (tt) => ListTile( + dense: true, + title: Row( + children: [ + Text( + context.lang.contactVerifiedBy( + getContactDisplayName(tt.$1), + ), + ), + VerificationBadgeComp( + contact: tt.$1, + ), + ], + ), + trailing: Text( + DateFormat.yMd( + Localizations.localeOf(context).toString(), + ).format(tt.$2), + style: TextStyle( + color: context.color.onSurfaceVariant, + fontSize: 13, + ), + ), + ), + ), + ], + ); + } +} diff --git a/lib/src/visual/views/groups/group_create_select_group_name.view.dart b/lib/src/visual/views/groups/group_create_select_group_name.view.dart index 974e6fc8..1bfc7132 100644 --- a/lib/src/visual/views/groups/group_create_select_group_name.view.dart +++ b/lib/src/visual/views/groups/group_create_select_group_name.view.dart @@ -58,6 +58,7 @@ class _GroupCreateSelectGroupNameViewState appBar: AppBar( title: Text(context.lang.selectGroupName), ), + floatingActionButtonAnimator: FloatingActionButtonAnimator.noAnimation, floatingActionButton: FilledButton.icon( onPressed: (textFieldGroupName.text.isEmpty || _isLoading) ? null diff --git a/lib/src/visual/views/groups/group_create_select_members.view.dart b/lib/src/visual/views/groups/group_create_select_members.view.dart index 5c6a0d0d..42930b06 100644 --- a/lib/src/visual/views/groups/group_create_select_members.view.dart +++ b/lib/src/visual/views/groups/group_create_select_members.view.dart @@ -129,6 +129,7 @@ class _StartNewChatView extends State { : context.lang.addMember, ), ), + floatingActionButtonAnimator: FloatingActionButtonAnimator.noAnimation, floatingActionButton: FilledButton.icon( onPressed: selectedUsers.isEmpty ? null : submitChanges, label: Text( diff --git a/lib/src/visual/views/home.view.dart b/lib/src/visual/views/home.view.dart index aa955f89..791106e2 100644 --- a/lib/src/visual/views/home.view.dart +++ b/lib/src/visual/views/home.view.dart @@ -78,9 +78,7 @@ class HomeViewState extends State { _selectNotificationSub = selectNotificationStream.stream.listen(( response, ) async { - if (response.payload != null && - response.payload!.startsWith(Routes.chats) && - response.payload! != Routes.chats) { + if (response.payload != null && response.payload!.startsWith(Routes.chats) && response.payload! != Routes.chats) { await routerProvider.push(response.payload!); } streamHomeViewPageIndex.add(0); @@ -116,40 +114,31 @@ class HomeViewState extends State { ); WidgetsBinding.instance.addPostFrameCallback((_) { - if (widget.initialPage == 1 && - !userService.currentUser.startWithCameraOpen || - widget.initialPage == 0) { + if (widget.initialPage == 1 && !userService.currentUser.startWithCameraOpen || widget.initialPage == 0) { streamHomeViewPageIndex.add(0); } }); } Future _initAsync() async { - final notificationAppLaunchDetails = await flutterLocalNotificationsPlugin - .getNotificationAppLaunchDetails(); + final notificationAppLaunchDetails = await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails(); RemoteMessage? initialRemoteMessage; try { - initialRemoteMessage = await FirebaseMessaging.instance - .getInitialMessage(); + initialRemoteMessage = await FirebaseMessaging.instance.getInitialMessage(); } catch (e) { Log.error('Could not get initial Firebase message: $e'); } if (widget.initialPage == 0 || initialRemoteMessage != null || - (notificationAppLaunchDetails != null && - notificationAppLaunchDetails.didNotificationLaunchApp)) { + (notificationAppLaunchDetails != null && notificationAppLaunchDetails.didNotificationLaunchApp)) { if (initialRemoteMessage != null) { Log.info('App launched from iOS/Remote push notification tap.'); streamHomeViewPageIndex.add(0); - } else if (notificationAppLaunchDetails?.didNotificationLaunchApp ?? - false) { - final payload = - notificationAppLaunchDetails?.notificationResponse?.payload; - if (payload != null && - payload.startsWith(Routes.chats) && - payload != Routes.chats) { + } else if (notificationAppLaunchDetails?.didNotificationLaunchApp ?? false) { + final payload = notificationAppLaunchDetails?.notificationResponse?.payload; + if (payload != null && payload.startsWith(Routes.chats) && payload != Routes.chats) { await routerProvider.push(payload); streamHomeViewPageIndex.add(0); } @@ -180,6 +169,7 @@ class HomeViewState extends State { _homeViewPageIndexSub?.cancel(); _selectNotificationSub?.cancel(); _disableCameraTimer?.cancel(); + _mainCameraController.setState = null; _mainCameraController.closeCamera(); _intentStreamSub?.cancel(); _deepLinkSub?.cancel(); @@ -190,25 +180,30 @@ class HomeViewState extends State { _disableCameraTimer?.cancel(); if (notification.depth > 0 && notification.metrics.axis == Axis.vertical) { - if (_activePageIdx == 2 && - notification.metrics.pixels < 100 && - !_isBottomNavVisible) { - setState(() { - _isBottomNavVisible = true; - }); - } else if (notification is ScrollUpdateNotification) { - final delta = notification.scrollDelta ?? 0; - if (delta > 5 && - _isBottomNavVisible && - (_activePageIdx != 2 || notification.metrics.pixels >= 100)) { - setState(() { - _isBottomNavVisible = false; - }); - } else if (delta < -5 && !_isBottomNavVisible) { + final canScroll = notification.metrics.maxScrollExtent > notification.metrics.minScrollExtent; + if (!canScroll) { + if (!_isBottomNavVisible) { setState(() { _isBottomNavVisible = true; }); } + } else { + if (_activePageIdx == 2 && notification.metrics.pixels < 100 && !_isBottomNavVisible) { + setState(() { + _isBottomNavVisible = true; + }); + } else if (notification is ScrollUpdateNotification) { + final delta = notification.scrollDelta ?? 0; + if (delta > 5 && _isBottomNavVisible && (_activePageIdx != 2 || notification.metrics.pixels >= 100)) { + setState(() { + _isBottomNavVisible = false; + }); + } else if (delta < -5 && !_isBottomNavVisible) { + setState(() { + _isBottomNavVisible = true; + }); + } + } } } @@ -243,59 +238,59 @@ class HomeViewState extends State { @override Widget build(BuildContext context) { return Scaffold( - body: GestureDetector( - onDoubleTap: _offsetRatio == 0 - ? _mainCameraController.onDoubleTap - : null, - onTapDown: _offsetRatio == 0 ? _mainCameraController.onTapDown : null, - child: Stack( - children: [ - MainCameraPreview(mainCameraController: _mainCameraController), - Positioned.fill( - child: Opacity( - opacity: _offsetRatio, - child: Container( - color: context.color.surface, - ), + body: Stack( + children: [ + MainCameraPreview(mainCameraController: _mainCameraController), + Positioned.fill( + child: Opacity( + opacity: _offsetRatio, + child: Container( + color: context.color.surface, ), ), - NotificationListener( - onNotification: _onPageView, - child: Positioned.fill( - child: PageView( - controller: _homeViewPageController, - onPageChanged: (index) { - setState(() { - _activePageIdx = index; - }); - }, - children: [ - const ChatListView(), - Container(), - const MemoriesView(), - ], - ), + ), + NotificationListener( + onNotification: _onPageView, + child: Positioned.fill( + child: PageView( + controller: _homeViewPageController, + onPageChanged: (index) { + setState(() { + _activePageIdx = index; + }); + }, + children: [ + const ChatListView(), + Container(), + const MemoriesView(), + ], ), ), - Positioned( - left: 0, - top: 0, - right: 0, - bottom: (_offsetRatio > 0.25) - ? MediaQuery.sizeOf(context).height * 2 - : 0, - child: Opacity( - opacity: 1 - (_offsetRatio * 4) % 1, - child: CameraPreviewControllerView( - mainController: _mainCameraController, - isVisible: - ((1 - (_offsetRatio * 4) % 1) == 1) && - _activePageIdx == 1, - ), + ), + Positioned.fill( + child: _offsetRatio == 0 + ? GestureDetector( + behavior: HitTestBehavior.translucent, + onDoubleTap: _mainCameraController.onDoubleTap, + onTapDown: _mainCameraController.onTapDown, + ) + : const SizedBox.shrink(), + ), + Positioned( + key: const ValueKey('camera_controls'), + left: 0, + top: 0, + right: 0, + bottom: (_offsetRatio > 0.25) ? MediaQuery.sizeOf(context).height * 2 : 0, + child: Opacity( + opacity: 1 - (_offsetRatio * 4) % 1, + child: CameraPreviewControllerView( + mainController: _mainCameraController, + isVisible: ((1 - (_offsetRatio * 4) % 1) == 1) && _activePageIdx == 1, ), ), - ], - ), + ), + ], ), bottomNavigationBar: AnimatedSize( duration: const Duration(milliseconds: 250), diff --git a/lib/src/visual/views/onboarding/register.view.dart b/lib/src/visual/views/onboarding/register.view.dart index 23668693..570456c7 100644 --- a/lib/src/visual/views/onboarding/register.view.dart +++ b/lib/src/visual/views/onboarding/register.view.dart @@ -140,7 +140,8 @@ class _RegisterViewState extends State { displayName: username, subscriptionPlan: 'Free', currentSetupPage: SetupPages.profile.name, - )..appVersion = AppState.latestAppVersionId; + appVersion: AppState.latestAppVersionId, + ); await UserService.save(userData); @@ -153,21 +154,19 @@ class _RegisterViewState extends State { final isDark = isDarkMode(context); final cardColor = isDark ? const Color(0xFF1E293B) : Colors.white; final inputColor = isDark ? const Color(0xFF0F172A) : Colors.grey[100]; - final sloganColor = isDark - ? Colors.white.withValues(alpha: 0.9) - : Colors.grey[800]; + final sloganColor = isDark ? Colors.white.withValues(alpha: 0.9) : Colors.grey[800]; final secondaryButtonColor = isDark ? Colors.grey[400] : Colors.grey[600]; return OnboardingWrapper( children: [ - const SizedBox(height: 40), + const SizedBox(height: 30), Center( child: Container( - padding: const EdgeInsets.all(20), + padding: const EdgeInsets.all(10), child: const LinkLogoAnimation(), ), ), - const SizedBox(height: 16), + const SizedBox(height: 12), Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Text( @@ -180,7 +179,7 @@ class _RegisterViewState extends State { ), ), ), - const SizedBox(height: 48), + const SizedBox(height: 30), Container( padding: const EdgeInsets.all(24), decoration: BoxDecoration( @@ -188,9 +187,7 @@ class _RegisterViewState extends State { borderRadius: BorderRadius.circular(32), boxShadow: [ BoxShadow( - color: isDark - ? Colors.black.withValues(alpha: 0.3) - : Colors.black.withValues(alpha: 0.1), + color: isDark ? Colors.black.withValues(alpha: 0.3) : Colors.black.withValues(alpha: 0.1), blurRadius: 20, offset: const Offset(0, 10), ), @@ -262,8 +259,7 @@ class _RegisterViewState extends State { ), ), ), - if (_showUserNameError && - usernameController.text.length < 3) ...[ + if (_showUserNameError && usernameController.text.length < 3) ...[ const SizedBox(height: 8), Text( context.lang.registerUsernameLimits, diff --git a/lib/src/visual/views/onboarding/setup.view.dart b/lib/src/visual/views/onboarding/setup.view.dart index 847cdc50..80b1760d 100644 --- a/lib/src/visual/views/onboarding/setup.view.dart +++ b/lib/src/visual/views/onboarding/setup.view.dart @@ -2,12 +2,14 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:twonly/locator.dart'; +import 'package:twonly/src/services/profile.service.dart'; import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/visual/views/onboarding/setup/add_new_contacts.setup.dart'; import 'package:twonly/src/visual/views/onboarding/setup/backup.setup.dart'; import 'package:twonly/src/visual/views/onboarding/setup/let_your_friends_find_you.setup.dart'; import 'package:twonly/src/visual/views/onboarding/setup/profile.setup.dart'; +import 'package:twonly/src/visual/views/onboarding/setup/profile_selection.setup.dart'; +import 'package:twonly/src/visual/views/onboarding/setup/security_profile.setup.dart'; import 'package:twonly/src/visual/views/onboarding/setup/share_your_friends.setup.dart'; import 'package:twonly/src/visual/views/onboarding/setup/verification_badge.setup.dart'; import 'package:twonly/src/visual/views/settings/privacy/user_discovery/components/user_discovery_setup.comp.dart'; @@ -15,7 +17,8 @@ import 'package:twonly/src/visual/views/settings/privacy/user_discovery/componen enum SetupPages { profile, backup, - addNewContact, + profileSelection, + securityProfile, verificationBadge, shareYourFriends, letYourFriendsFindYou, @@ -29,25 +32,62 @@ extension SetupPagesExtension on SetupPages { ); } - int get pageNumber => index + 1; - int get totalPages => SetupPages.values.length; + static List get activePages { + final setupProfile = userService.currentUser.setupProfile; + switch (setupProfile) { + case SetupProfile.standard: + return [ + SetupPages.profile, + SetupPages.backup, + SetupPages.profileSelection, + ]; + case SetupProfile.maximum: + return [ + SetupPages.profile, + SetupPages.backup, + SetupPages.profileSelection, + SetupPages.verificationBadge, + ]; + case SetupProfile.customized: + return [ + SetupPages.profile, + SetupPages.backup, + SetupPages.profileSelection, + SetupPages.securityProfile, + SetupPages.verificationBadge, + SetupPages.shareYourFriends, + SetupPages.letYourFriendsFindYou, + ]; + } + } + + int get pageNumber { + final idx = activePages.indexOf(this); + return idx != -1 ? idx + 1 : 1; + } + + int get totalPages => activePages.length; int get progressPercentage => ((pageNumber - 1) / totalPages * 100).round(); String get progressText => '$pageNumber / $totalPages'; - bool get isLast => index == SetupPages.values.length - 1; + bool get isLast { + return activePages.isNotEmpty && activePages.last == this; + } SetupPages? next() { - final nextIndex = index + 1; - if (nextIndex < SetupPages.values.length) { - return SetupPages.values[nextIndex]; + final pages = activePages; + final idx = pages.indexOf(this); + if (idx != -1 && idx + 1 < pages.length) { + return pages[idx + 1]; } return null; } SetupPages? previous() { - final prevIndex = index - 1; - if (prevIndex >= 0) { - return SetupPages.values[prevIndex]; + final pages = activePages; + final idx = pages.indexOf(this); + if (idx > 0) { + return pages[idx - 1]; } return null; } @@ -112,9 +152,7 @@ class _SetupViewState extends State { right: index == currentPage.totalPages - 1 ? 0 : 8, ), decoration: BoxDecoration( - color: isFinished - ? context.color.primary - : context.color.surfaceContainer, + color: isFinished ? context.color.primary : context.color.surfaceContainer, borderRadius: BorderRadius.circular(10), ), ), @@ -151,8 +189,7 @@ class _SetupViewState extends State { ), ), ), - if (currentPage.index > 0 && !currentPage.isLast) - const SizedBox(width: 24), + if (currentPage.index > 0 && !currentPage.isLast) const SizedBox(width: 24), if (!currentPage.isLast) TextButton( onPressed: () async { @@ -185,8 +222,10 @@ class _SetupViewState extends State { return const ProfileSetupPage(); case SetupPages.backup: return const BackupSetupPage(); - case SetupPages.addNewContact: - return const AddNewContactsPage(); + case SetupPages.profileSelection: + return const ProfileSelectionSetup(); + case SetupPages.securityProfile: + return const SecurityProfileSetup(); case SetupPages.verificationBadge: return const VerificationBadgeSetupPage(); case SetupPages.shareYourFriends: diff --git a/lib/src/visual/views/onboarding/setup/components/mock_contact_request_actions.comp.dart b/lib/src/visual/views/onboarding/setup/components/mock_contact_request_actions.comp.dart index 155fbf54..b13ffce5 100644 --- a/lib/src/visual/views/onboarding/setup/components/mock_contact_request_actions.comp.dart +++ b/lib/src/visual/views/onboarding/setup/components/mock_contact_request_actions.comp.dart @@ -10,58 +10,64 @@ class MockContactRequestActionsComp extends StatelessWidget { Widget build(BuildContext context) { return IgnorePointer( child: SizedBox( - // width: 125, child: Row( mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, children: [ + Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + SizedBox( + height: 20, + child: FilledButton( + style: FilledButton.styleFrom( + padding: const EdgeInsets.only(right: 2, left: 4), + backgroundColor: context.color.surfaceContainerHigh, + foregroundColor: context.color.onSurface, + ), + onPressed: () {}, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.person_off_rounded, + color: Color.fromARGB(164, 244, 67, 54), + size: 12, + ), + Text( + context.lang.contactActionBlock, + style: const TextStyle(fontSize: 8), + ), + ], + ), + ), + ), + const SizedBox(height: 4), + SizedBox( + height: 20, + child: FilledButton( + style: FilledButton.styleFrom( + padding: const EdgeInsets.only(right: 2, left: 4), + backgroundColor: context.color.surfaceContainerHigh, + foregroundColor: context.color.onSurface, + ), + onPressed: () {}, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.check, color: Colors.green, size: 12), + Text( + context.lang.contactActionAccept, + style: const TextStyle(fontSize: 8), + ), + ], + ), + ), + ), + ], + ), const SizedBox(width: 4), - SizedBox( - height: 20, - // width: 45, - child: FilledButton( - style: FilledButton.styleFrom( - padding: const EdgeInsets.only(right: 2, left: 4), - backgroundColor: context.color.surfaceContainerHigh, - foregroundColor: context.color.onSurface, - ), - onPressed: () {}, - child: Row( - children: [ - const Icon( - Icons.person_off_rounded, - color: Color.fromARGB(164, 244, 67, 54), - size: 12, - ), - Text( - context.lang.contactActionBlock, - style: const TextStyle(fontSize: 8), - ), - ], - ), - ), - ), - const SizedBox(width: 6), - SizedBox( - height: 20, - // width: 50, - child: FilledButton( - style: FilledButton.styleFrom( - padding: const EdgeInsets.only(right: 2, left: 4), - backgroundColor: context.color.surfaceContainerHigh, - foregroundColor: context.color.onSurface, - ), - onPressed: () {}, - child: Row( - children: [ - const Icon(Icons.check, color: Colors.green, size: 12), - Text( - context.lang.contactActionAccept, - style: const TextStyle(fontSize: 8), - ), - ], - ), - ), - ), IconButton( style: IconButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 2), @@ -86,28 +92,59 @@ class MockContactSuggestedActionsComp extends StatelessWidget { return IgnorePointer( child: Row( mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, children: [ const SizedBox(width: 4), - SizedBox( - height: 20, - child: FilledButton( - style: FilledButton.styleFrom( - padding: const EdgeInsets.only(right: 8, left: 4), - ).merge(secondaryGreyButtonStyle(context)), - onPressed: () {}, - child: Row( - children: [ - const Padding( - padding: EdgeInsets.symmetric(horizontal: 6), - child: FaIcon(FontAwesomeIcons.userPlus, size: 10), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + SizedBox( + height: 20, + child: FilledButton( + style: FilledButton.styleFrom( + padding: const EdgeInsets.only(right: 8, left: 4), + ).merge(secondaryGreyButtonStyle(context)), + onPressed: () {}, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Padding( + padding: EdgeInsets.symmetric(horizontal: 6), + child: FaIcon(FontAwesomeIcons.circleQuestion, size: 10), + ), + Text( + context.lang.friendSuggestionsAskFriend, + style: const TextStyle(fontSize: 8), + ), + ], ), - Text( - context.lang.friendSuggestionsRequest, - style: const TextStyle(fontSize: 8), - ), - ], + ), ), - ), + const SizedBox(height: 4), + SizedBox( + height: 20, + child: FilledButton( + style: FilledButton.styleFrom( + padding: const EdgeInsets.only(right: 8, left: 4), + ).merge(secondaryGreyButtonStyle(context)), + onPressed: () {}, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Padding( + padding: EdgeInsets.symmetric(horizontal: 6), + child: FaIcon(FontAwesomeIcons.userPlus, size: 10), + ), + Text( + context.lang.friendSuggestionsRequest, + style: const TextStyle(fontSize: 8), + ), + ], + ), + ), + ), + ], ), IconButton( style: IconButton.styleFrom( diff --git a/lib/src/visual/views/onboarding/setup/components/next_button.comp.dart b/lib/src/visual/views/onboarding/setup/components/next_button.comp.dart index fbf9eb80..36962bc9 100644 --- a/lib/src/visual/views/onboarding/setup/components/next_button.comp.dart +++ b/lib/src/visual/views/onboarding/setup/components/next_button.comp.dart @@ -18,43 +18,48 @@ class NextButtonComp extends StatelessWidget { @override Widget build(BuildContext context) { - final currentPage = SetupPagesExtension.fromStr( - userService.currentUser.currentSetupPage, - ); - return ElevatedButton( - onPressed: (canSubmit && !isLoading) - ? () async { - if (onPressed != null) { - final error = await onPressed?.call(); - if (error == true) return; - } - await UserService.update((user) { - user.currentSetupPage = currentPage.next()?.name; - }); - } - : null, - style: ElevatedButton.styleFrom( - minimumSize: const Size(double.infinity, 56), - backgroundColor: context.color.primary, - foregroundColor: context.color.onPrimary, - elevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - ), - child: isLoading - ? const SizedBox( - height: 24, - width: 24, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation(Colors.white), - ), - ) - : Text( - currentPage.isLast ? context.lang.finishSetup : context.lang.next, - style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + return StreamBuilder( + stream: userService.onUserUpdated, + builder: (context, snapshot) { + final currentPage = SetupPagesExtension.fromStr( + userService.currentUser.currentSetupPage, + ); + return ElevatedButton( + onPressed: (canSubmit && !isLoading) + ? () async { + if (onPressed != null) { + final error = await onPressed?.call(); + if (error == true) return; + } + await UserService.update((user) { + user.currentSetupPage = currentPage.next()?.name; + }); + } + : null, + style: ElevatedButton.styleFrom( + minimumSize: const Size(double.infinity, 56), + backgroundColor: context.color.primary, + foregroundColor: context.color.onPrimary, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), ), + ), + child: isLoading + ? const SizedBox( + height: 24, + width: 24, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ) + : Text( + currentPage.isLast ? context.lang.finishSetup : context.lang.next, + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + ); + }, ); } } diff --git a/lib/src/visual/views/onboarding/setup/components/profile_card.comp.dart b/lib/src/visual/views/onboarding/setup/components/profile_card.comp.dart new file mode 100644 index 00000000..9f1206cc --- /dev/null +++ b/lib/src/visual/views/onboarding/setup/components/profile_card.comp.dart @@ -0,0 +1,192 @@ +import 'package:flutter/material.dart'; +import 'package:twonly/src/services/profile.service.dart'; +import 'package:twonly/src/utils/misc.dart'; + +class SafetyProfileCard extends StatelessWidget { + const SafetyProfileCard({ + required this.profile, + required this.isSelected, + required this.onTap, + this.isHovered = false, + this.onHover, + super.key, + }); + + final Object profile; + final bool isSelected; + final VoidCallback onTap; + final bool isHovered; + final ValueChanged? onHover; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + final bodyMediumStyle = theme.textTheme.bodyMedium?.copyWith( + color: context.color.onSurfaceVariant, + height: 1.35, + ); + + final String title; + final Widget subtitle; + final IconData icon; + final String? badgeText; + + if (profile is SetupProfile) { + switch (profile as SetupProfile) { + case SetupProfile.standard: + title = context.lang.onboardingProfileSelectionDefaultTitle; + subtitle = Text( + context.lang.onboardingProfileSelectionDefaultDesc, + style: bodyMediumStyle, + ); + icon = Icons.bolt_rounded; + badgeText = context.lang.onboardingProfileSelectionDefaultBadge; + case SetupProfile.customized: + title = context.lang.onboardingProfileSelectionCustomizeTitle; + subtitle = Text( + context.lang.onboardingProfileSelectionCustomizeDesc, + style: bodyMediumStyle, + ); + icon = Icons.tune_rounded; + badgeText = null; + case SetupProfile.maximum: + title = context.lang.onboardingProfileSelectionStrictTitle; + subtitle = RichText( + text: TextSpan( + style: bodyMediumStyle, + children: formattedText( + context, + context.lang.onboardingProfileSelectionStrictDesc, + boldTextColor: context.color.onSurface, + ), + ), + ); + icon = Icons.lock_outline_rounded; + badgeText = null; + } + } else if (profile is SecurityProfile) { + switch (profile as SecurityProfile) { + case SecurityProfile.normal: + title = context.lang.securityProfileNormalTitle; + subtitle = Text( + context.lang.securityProfileNormalDesc, + style: bodyMediumStyle, + ); + icon = Icons.shield_outlined; + badgeText = null; + case SecurityProfile.strict: + title = context.lang.securityProfileStrictTitle; + subtitle = Text( + context.lang.securityProfileStrictDesc, + style: bodyMediumStyle, + ); + icon = Icons.verified_user_outlined; + badgeText = null; + } + } else { + throw ArgumentError('Invalid profile type: $profile'); + } + + return MouseRegion( + onEnter: (_) => onHover?.call(true), + onExit: (_) => onHover?.call(false), + child: GestureDetector( + onTap: onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 150), + curve: Curves.easeInOut, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: isSelected + ? context.color.primaryContainer.withValues(alpha: 0.12) + : context.color.surfaceContainerLow, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: isSelected + ? context.color.primary + : (isHovered + ? context.color.onSurfaceVariant.withValues(alpha: 0.3) + : context.color.outlineVariant.withValues(alpha: 0.4)), + width: isSelected ? 2 : 1, + ), + boxShadow: isSelected + ? [ + BoxShadow( + color: context.color.primary.withValues(alpha: 0.06), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ] + : [], + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: isSelected + ? context.color.primary.withValues(alpha: 0.1) + : context.color.surfaceContainerHigh, + shape: BoxShape.circle, + ), + child: Icon( + icon, + color: isSelected + ? context.color.primary + : context.color.onSurfaceVariant, + size: 24, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + title, + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + color: isSelected + ? context.color.primary + : theme.textTheme.titleMedium?.color, + ), + ), + ), + if (badgeText != null) ...[ + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 2, + ), + decoration: BoxDecoration( + color: context.color.primary, + borderRadius: BorderRadius.circular(8), + ), + child: Text( + badgeText, + style: theme.textTheme.labelSmall?.copyWith( + color: context.color.onPrimary, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ], + ), + const SizedBox(height: 6), + subtitle, + ], + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/src/visual/views/onboarding/setup/profile_selection.setup.dart b/lib/src/visual/views/onboarding/setup/profile_selection.setup.dart new file mode 100644 index 00000000..abe4f776 --- /dev/null +++ b/lib/src/visual/views/onboarding/setup/profile_selection.setup.dart @@ -0,0 +1,111 @@ +import 'package:flutter/material.dart'; +import 'package:twonly/locator.dart'; +import 'package:twonly/src/services/profile.service.dart'; +import 'package:twonly/src/services/user.service.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/views/onboarding/setup/components/next_button.comp.dart'; +import 'package:twonly/src/visual/views/onboarding/setup/components/profile_card.comp.dart'; +import 'package:twonly/src/visual/views/settings/privacy/user_discovery/components/user_discovery_setup.comp.dart'; + +class ProfileSelectionSetup extends StatefulWidget { + const ProfileSelectionSetup({super.key}); + + @override + State createState() => _ProfileSelectionSetupState(); +} + +class _ProfileSelectionSetupState extends State { + SetupProfile? _hoveredProfile; + bool _isLoading = false; + + Future _onProfileTapped(SetupProfile profile) async { + await UserService.update((user) { + user.setupProfile = profile; + if (profile == SetupProfile.standard) { + user.securityProfile = SecurityProfile.normal; + } else if (profile == SetupProfile.maximum) { + user.securityProfile = SecurityProfile.strict; + } + }); + } + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: userService.onUserUpdated, + builder: (context, snapshot) { + final user = userService.currentUser; + final selectedProfile = user.setupProfile; + + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + context.lang.onboardingProfileSelectionTitle, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Text( + context.lang.onboardingProfileSelectionSubtitle, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: context.color.onSurfaceVariant, + ), + ), + const SizedBox(height: 32), + SafetyProfileCard( + profile: SetupProfile.standard, + isSelected: selectedProfile == SetupProfile.standard, + isHovered: _hoveredProfile == SetupProfile.standard, + onHover: (hovered) => setState(() { + _hoveredProfile = hovered ? SetupProfile.standard : null; + }), + onTap: () => _onProfileTapped(SetupProfile.standard), + ), + const SizedBox(height: 16), + SafetyProfileCard( + profile: SetupProfile.customized, + isSelected: selectedProfile == SetupProfile.customized, + isHovered: _hoveredProfile == SetupProfile.customized, + onHover: (hovered) => setState(() { + _hoveredProfile = hovered ? SetupProfile.customized : null; + }), + onTap: () => _onProfileTapped(SetupProfile.customized), + ), + const SizedBox(height: 16), + SafetyProfileCard( + profile: SetupProfile.maximum, + isSelected: selectedProfile == SetupProfile.maximum, + isHovered: _hoveredProfile == SetupProfile.maximum, + onHover: (hovered) => setState(() { + _hoveredProfile = hovered ? SetupProfile.maximum : null; + }), + onTap: () => _onProfileTapped(SetupProfile.maximum), + ), + const SizedBox(height: 40), + NextButtonComp( + key: ValueKey(selectedProfile), + isLoading: _isLoading, + onPressed: () async { + if (selectedProfile == SetupProfile.standard) { + setState(() { + _isLoading = true; + }); + await UserDiscoverySetupState().initializeOrUpdate(); + setState(() { + _isLoading = false; + }); + } + return false; + }, + ), + ], + ); + }, + ); + } +} diff --git a/lib/src/visual/views/onboarding/setup/security_profile.setup.dart b/lib/src/visual/views/onboarding/setup/security_profile.setup.dart new file mode 100644 index 00000000..0bde4049 --- /dev/null +++ b/lib/src/visual/views/onboarding/setup/security_profile.setup.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import 'package:twonly/locator.dart'; +import 'package:twonly/src/services/profile.service.dart'; +import 'package:twonly/src/services/user.service.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/views/onboarding/setup/components/next_button.comp.dart'; +import 'package:twonly/src/visual/views/onboarding/setup/components/profile_card.comp.dart'; + +class SecurityProfileSetup extends StatefulWidget { + const SecurityProfileSetup({super.key}); + + @override + State createState() => _SecurityProfileSetupState(); +} + +class _SecurityProfileSetupState extends State { + SecurityProfile? _hoveredProfile; + + Future _onProfileTapped(SecurityProfile profile) async { + await UserService.update((user) { + user.securityProfile = profile; + }); + } + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: userService.onUserUpdated, + builder: (context, snapshot) { + final user = userService.currentUser; + final selectedProfile = user.securityProfile; + + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + context.lang.securityProfileTitle, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Text( + context.lang.securityProfileSubtitle, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: context.color.onSurfaceVariant, + ), + ), + const SizedBox(height: 32), + SafetyProfileCard( + profile: SecurityProfile.normal, + isSelected: selectedProfile == SecurityProfile.normal, + isHovered: _hoveredProfile == SecurityProfile.normal, + onHover: (hovered) => setState(() { + _hoveredProfile = hovered ? SecurityProfile.normal : null; + }), + onTap: () => _onProfileTapped(SecurityProfile.normal), + ), + const SizedBox(height: 16), + SafetyProfileCard( + profile: SecurityProfile.strict, + isSelected: selectedProfile == SecurityProfile.strict, + isHovered: _hoveredProfile == SecurityProfile.strict, + onHover: (hovered) => setState(() { + _hoveredProfile = hovered ? SecurityProfile.strict : null; + }), + onTap: () => _onProfileTapped(SecurityProfile.strict), + ), + const SizedBox(height: 40), + const NextButtonComp(), + ], + ); + }, + ); + } +} diff --git a/lib/src/visual/views/public_profile.view.dart b/lib/src/visual/views/public_profile.view.dart index 693be045..cda31a15 100644 --- a/lib/src/visual/views/public_profile.view.dart +++ b/lib/src/visual/views/public_profile.view.dart @@ -5,17 +5,14 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:go_router/go_router.dart'; -import 'package:qr_flutter/qr_flutter.dart'; import 'package:share_plus/share_plus.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/services/signal/identity.signal.dart'; -import 'package:twonly/src/utils/avatars.dart'; import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/utils/qr.utils.dart'; -import 'package:twonly/src/visual/components/notification_badge.comp.dart'; +import 'package:twonly/src/visual/components/contact_request_badge.comp.dart'; +import 'package:twonly/src/visual/components/profile_qr_code.comp.dart'; import 'package:twonly/src/visual/elements/better_list_title.element.dart'; -import 'package:twonly/src/visual/themes/light.dart'; class PublicProfileView extends StatefulWidget { const PublicProfileView({super.key}); @@ -25,11 +22,7 @@ class PublicProfileView extends StatefulWidget { } class _PublicProfileViewState extends State { - String? _qrCode; - Uint8List? _userAvatar; Uint8List? _publicKey; - int _countContactRequest = 0; - late StreamSubscription _countContactRequestStream; @override void initState() { @@ -38,77 +31,20 @@ class _PublicProfileViewState extends State { } Future initAsync() async { - _qrCode = await QrCodeUtils.publicProfileLink(); - _userAvatar = await getUserAvatar(); _publicKey = await getUserPublicKey(); if (mounted) setState(() {}); - - _countContactRequestStream = twonlyDB.contactsDao - .watchContactsRequestedCount() - .listen((update) { - if (update != null) { - if (!mounted) return; - setState(() { - _countContactRequest = update; - }); - } - }); - } - - @override - void dispose() { - _countContactRequestStream.cancel(); - super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - actions: [ - Stack( - children: (_countContactRequest == 0) - ? [] - : [ - Positioned.fill( - child: Center( - child: Container( - width: 40, - height: 40, - decoration: const BoxDecoration( - color: primaryColor, - shape: BoxShape.circle, - ), - ), - ), - ), - Center( - child: NotificationBadgeComp( - backgroundColor: isDarkMode(context) - ? Colors.white - : Colors.black, - textColor: isDarkMode(context) - ? Colors.black - : Colors.white, - count: (_countContactRequest).toString(), - child: IconButton( - color: (_countContactRequest > 0) - ? Colors.black - : null, - icon: const FaIcon( - FontAwesomeIcons.userPlus, - size: 18, - ), - onPressed: () => context.push(Routes.chatsAddNewUser), - ), - ), - ), - ], - ), - const SizedBox(width: 15), + actions: const [ + ContactRequestBadgeComp(), + SizedBox(width: 15), ], ), - body: Column( + body: ListView( children: [ Container(width: double.infinity), const SizedBox(height: 10), @@ -134,47 +70,12 @@ class _PublicProfileViewState extends State { ), ), const SizedBox(height: 20), - if (_qrCode != null && _userAvatar != null) - Container( - decoration: BoxDecoration( - color: context.color.primary, - borderRadius: BorderRadius.circular(12), - boxShadow: const [ - BoxShadow( - color: Colors.black26, - blurRadius: 6, - offset: Offset(0, 2), - ), - ], - ), - child: QrImageView.withQr( - qr: QrCode.fromData( - data: _qrCode!, - errorCorrectLevel: QrErrorCorrectLevel.M, - ), - eyeStyle: QrEyeStyle( - color: isDarkMode(context) ? Colors.black : Colors.white, - borderRadius: 2, - ), - dataModuleStyle: QrDataModuleStyle( - color: isDarkMode(context) ? Colors.black : Colors.white, - borderRadius: 2, - ), - gapless: false, - embeddedImage: MemoryImage(_userAvatar!), - embeddedImageStyle: QrEmbeddedImageStyle( - size: const Size(60, 66), - embeddedImageShape: EmbeddedImageShape.square, - shapeColor: context.color.primary, - safeArea: true, - ), - size: 250, - ), - ), + const ProfileQrCodeComp(), const SizedBox(height: 20), Text( userService.currentUser.username, style: const TextStyle(fontSize: 24), + textAlign: TextAlign.center, ), const SizedBox(height: 20), const Divider(), @@ -197,8 +98,7 @@ class _PublicProfileViewState extends State { ), onTap: () { final params = ShareParams( - text: - 'https://me.twonly.eu/${userService.currentUser.username}#${base64Url.encode(_publicKey!)}', + text: 'https://me.twonly.eu/${userService.currentUser.username}#${base64Url.encode(_publicKey!)}', ); SharePlus.instance.share(params); }, diff --git a/lib/src/visual/views/settings/developer/developer.view.dart b/lib/src/visual/views/settings/developer/developer.view.dart index 8329941a..fcef4c8d 100644 --- a/lib/src/visual/views/settings/developer/developer.view.dart +++ b/lib/src/visual/views/settings/developer/developer.view.dart @@ -369,9 +369,7 @@ class _DeveloperSettingsViewState extends State { title: const Text('Reopen Setup'), onTap: () async { await UserService.update((u) { - u - ..currentSetupPage = SetupPages.profile.name - ..isUserDiscoveryEnabled = false; + u.currentSetupPage = SetupPages.profile.name; }); }, ), diff --git a/lib/src/visual/views/settings/privacy.view.dart b/lib/src/visual/views/settings/privacy.view.dart index 2f6561f3..88d43c01 100644 --- a/lib/src/visual/views/settings/privacy.view.dart +++ b/lib/src/visual/views/settings/privacy.view.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/constants/routes.keys.dart'; +import 'package:twonly/src/services/profile.service.dart'; import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/utils/misc.dart'; @@ -78,6 +79,18 @@ class _PrivacyViewState extends State { setState(() {}); }, ), + ListTile( + title: Text(context.lang.settingsPrivacyProfileSelectionTitle), + subtitle: Text( + userService.currentUser.securityProfile == SecurityProfile.strict + ? context.lang.securityProfileStrictTitle + : context.lang.securityProfileNormalTitle, + ), + onTap: () async { + await context.push(Routes.settingsPrivacyProfileSelection); + setState(() {}); + }, + ), const Divider(), ListTile( title: Text(context.lang.settingsTypingIndication), diff --git a/lib/src/visual/views/settings/privacy/profile_selection.view.dart b/lib/src/visual/views/settings/privacy/profile_selection.view.dart new file mode 100644 index 00000000..dfd76c86 --- /dev/null +++ b/lib/src/visual/views/settings/privacy/profile_selection.view.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import 'package:twonly/locator.dart'; +import 'package:twonly/src/services/profile.service.dart'; +import 'package:twonly/src/services/user.service.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/views/onboarding/setup/components/profile_card.comp.dart'; + +class ProfileSelectionSettingsView extends StatefulWidget { + const ProfileSelectionSettingsView({super.key}); + + @override + State createState() => + _ProfileSelectionSettingsViewState(); +} + +class _ProfileSelectionSettingsViewState + extends State { + SecurityProfile? _hoveredProfile; + + Future _onProfileTapped(SecurityProfile profile) async { + await UserService.update((user) { + user.securityProfile = profile; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(context.lang.securityProfileTitle), + ), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(24), + child: StreamBuilder( + stream: userService.onUserUpdated, + builder: (context, snapshot) { + final user = userService.currentUser; + final selectedProfile = user.securityProfile; + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + context.lang.securityProfileSubtitle, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: context.color.onSurfaceVariant, + ), + ), + const SizedBox(height: 32), + SafetyProfileCard( + profile: SecurityProfile.normal, + isSelected: selectedProfile == SecurityProfile.normal, + isHovered: _hoveredProfile == SecurityProfile.normal, + onHover: (hovered) => setState(() { + _hoveredProfile = hovered ? SecurityProfile.normal : null; + }), + onTap: () => _onProfileTapped(SecurityProfile.normal), + ), + const SizedBox(height: 16), + SafetyProfileCard( + profile: SecurityProfile.strict, + isSelected: selectedProfile == SecurityProfile.strict, + isHovered: _hoveredProfile == SecurityProfile.strict, + onHover: (hovered) => setState(() { + _hoveredProfile = hovered ? SecurityProfile.strict : null; + }), + onTap: () => _onProfileTapped(SecurityProfile.strict), + ), + ], + ); + }, + ), + ), + ), + ); + } +} diff --git a/lib/src/visual/views/settings/privacy/user_discovery/components/user_discovery_setup.comp.dart b/lib/src/visual/views/settings/privacy/user_discovery/components/user_discovery_setup.comp.dart index 5c214df0..cb474d59 100644 --- a/lib/src/visual/views/settings/privacy/user_discovery/components/user_discovery_setup.comp.dart +++ b/lib/src/visual/views/settings/privacy/user_discovery/components/user_discovery_setup.comp.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/services/user.service.dart'; @@ -8,7 +9,6 @@ import 'package:twonly/src/visual/components/avatar_icon.comp.dart'; import 'package:twonly/src/visual/components/verification_badge.comp.dart'; import 'package:twonly/src/visual/views/contact/add_new_contact_components/friend_suggestions.comp.dart'; import 'package:twonly/src/visual/views/onboarding/setup/components/mock_contact_request_actions.comp.dart'; -import 'package:twonly/src/visual/views/onboarding/setup/components/setup_switch_card.comp.dart'; List getExampleUsers(BuildContext context) => [ context.lang.exampleUserName1, @@ -26,12 +26,12 @@ List getExampleUsers(BuildContext context) => [ class UserDiscoverySetupState { UserDiscoverySetupState({ - required this.setState, + this.setState, this.isUserDiscoveryEnabled = true, this.sharePromotion = true, this.isManualApprovalEnabled = false, this.threshold = 3, - this.requiredSendImages = 4, + this.requiredSendImages = kReleaseMode ? 4 : 0, }); bool wasChanged = false; @@ -43,13 +43,17 @@ class UserDiscoverySetupState { bool isManualApprovalEnabled; int requiredSendImages; - void Function(void Function()) setState; + void Function(void Function())? setState; void update(void Function() update) { update(); - setState(() { + if (setState != null) { + setState!(() { + wasChanged = true; + }); + } else { wasChanged = true; - }); + } } Future initializeOrUpdate() async { @@ -103,18 +107,24 @@ class UserDiscoverySetupComp extends StatelessWidget { @override Widget build(BuildContext context) { + final showShareYourFriends = + showOnlySpecificPage == UserDiscoveryPages.all || showOnlySpecificPage == UserDiscoveryPages.shareYourFriends; + final showLetYourFriendsFindYou = + showOnlySpecificPage == UserDiscoveryPages.all || + showOnlySpecificPage == UserDiscoveryPages.letYourFriendsFindYou; + return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - if (showOnlySpecificPage == UserDiscoveryPages.all || - showOnlySpecificPage == UserDiscoveryPages.shareYourFriends) ...[ + if (showShareYourFriends) ...[ Text( context.lang.onboardingUserDiscoveryShareFriends, style: Theme.of(context).textTheme.headlineSmall, textAlign: TextAlign.center, ), - const SizedBox(height: 32), + const SizedBox(height: 24), RichText( text: TextSpan( @@ -122,72 +132,66 @@ class UserDiscoverySetupComp extends StatelessWidget { context, context.lang.onboardingUserDiscoveryShareFriendsDesc, ), + style: TextStyle( + color: context.color.onSurface, + fontSize: 13, + height: 1.4, + ), ), textAlign: TextAlign.center, ), - const SizedBox(height: 24), - SetupSwitchCard( - value: state.isUserDiscoveryEnabled, - onChanged: (val) => state.update(() { - state.isUserDiscoveryEnabled = val; - }), - title: context.lang.onboardingUserDiscoveryShareFriends, - expandedChild: Column( + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: context.color.surfaceContainerLow, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: context.color.outlineVariant.withValues(alpha: 0.5), + ), + ), + child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - SwitchListTile( - value: state.isManualApprovalEnabled, - onChanged: (val) => state.update( - () => state.isManualApprovalEnabled = val, - ), - title: Text( - context.lang.userDiscoverySettingsManualApproval, - style: const TextStyle(fontSize: 13), - ), - subtitle: Text( - context.lang.userDiscoverySettingsManualApprovalDesc, - style: const TextStyle(fontSize: 10), - ), - tileColor: context.color.surfaceContainerLow, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, + Text( + context.lang.userDiscoveryFeatureOffers, + style: TextStyle( + fontSize: 16, + color: context.color.primary, ), + textAlign: TextAlign.center, ), - const Padding( - padding: EdgeInsets.only(bottom: 8), - child: Divider(), - ), - const _ExampleLabel(), + const SizedBox(height: 12), Text( context.lang.onboardingUserDiscoveryWhoIsRequesting, style: TextStyle( color: context.color.onSurfaceVariant, fontSize: 12, + fontWeight: FontWeight.w500, ), textAlign: TextAlign.center, ), - const SizedBox(height: 16), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), + const SizedBox(height: 8), + Center( child: Container( padding: const EdgeInsets.symmetric( - horizontal: 6, - vertical: 3, + horizontal: 12, + vertical: 8, ), decoration: BoxDecoration( - border: Border.all(color: Colors.grey, width: 0.5), + color: context.color.surface, borderRadius: BorderRadius.circular(12), + border: Border.all( + color: context.color.outlineVariant.withValues( + alpha: 0.3, + ), + ), ), child: Row( - mainAxisAlignment: MainAxisAlignment.center, children: [ const AvatarIcon(fontSize: 14), - const SizedBox(width: 5), + const SizedBox(width: 8), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -208,7 +212,10 @@ class UserDiscoverySetupComp extends StatelessWidget { context.lang.exampleUserName1, ], ), - style: const TextStyle(fontSize: 10), + style: TextStyle( + fontSize: 10, + color: context.color.onSurfaceVariant, + ), ), ), ], @@ -219,37 +226,46 @@ class UserDiscoverySetupComp extends StatelessWidget { ), ), ), - - const SizedBox(height: 24), + const SizedBox(height: 16), Text( context.lang.onboardingUserDiscoveryContactsVerifiedBadge, style: TextStyle( color: context.color.onSurfaceVariant, fontSize: 12, + fontWeight: FontWeight.w500, ), textAlign: TextAlign.center, ), - const SizedBox(height: 16), + const SizedBox(height: 8), Center( child: Container( - width: 100, - height: 40, + constraints: const BoxConstraints(maxWidth: 320), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), decoration: BoxDecoration( - border: Border.all(color: Colors.grey, width: 0.5), + color: context.color.surface, borderRadius: BorderRadius.circular(12), + border: Border.all( + color: context.color.outlineVariant.withValues( + alpha: 0.3, + ), + ), ), child: Row( - mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, children: [ const AvatarIcon(fontSize: 12), - const SizedBox(width: 5), + const SizedBox(width: 8), Text( context.lang.exampleJane, style: const TextStyle( fontWeight: FontWeight.bold, + fontSize: 13, ), ), - const SizedBox(width: 5), + const SizedBox(width: 6), const VerificationBadgeComp( isVerifiedByTransferredTrust: true, size: 14, @@ -259,25 +275,96 @@ class UserDiscoverySetupComp extends StatelessWidget { ), ), ), - - const SizedBox(height: 16), ], ), ), + const SizedBox(height: 24), - if (showOnlySpecificPage == UserDiscoveryPages.all) - const SizedBox(height: 80), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: context.color.surfaceContainerLow, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: context.color.outlineVariant.withValues(alpha: 0.5), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SwitchListTile( + value: state.isUserDiscoveryEnabled, + onChanged: (val) => state.update(() { + state.isUserDiscoveryEnabled = val; + }), + title: Text( + context.lang.onboardingUserDiscoveryShareFriends, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + ), + ), + tileColor: Colors.transparent, + contentPadding: const EdgeInsets.symmetric(horizontal: 8), + ), + AnimatedCrossFade( + firstChild: const SizedBox( + width: double.infinity, + height: 0, + ), + secondChild: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Divider( + color: context.color.outlineVariant.withValues( + alpha: 0.3, + ), + ), + ), + SwitchListTile( + value: state.isManualApprovalEnabled, + onChanged: (val) => state.update( + () => state.isManualApprovalEnabled = val, + ), + title: Text( + context.lang.userDiscoverySettingsManualApproval, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + ), + ), + subtitle: Text( + context.lang.userDiscoverySettingsManualApprovalDesc, + style: TextStyle( + fontSize: 11, + color: context.color.onSurfaceVariant, + ), + ), + tileColor: Colors.transparent, + contentPadding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + ), + ], + ), + crossFadeState: state.isUserDiscoveryEnabled ? CrossFadeState.showSecond : CrossFadeState.showFirst, + duration: const Duration(milliseconds: 300), + ), + ], + ), + ), + if (showOnlySpecificPage == UserDiscoveryPages.all) const SizedBox(height: 48), ], - - if (showOnlySpecificPage == UserDiscoveryPages.all || - showOnlySpecificPage == - UserDiscoveryPages.letYourFriendsFindYou) ...[ + if (showLetYourFriendsFindYou) ...[ Text( context.lang.onboardingUserDiscoveryLetFriendsFindYou, style: Theme.of(context).textTheme.headlineSmall, textAlign: TextAlign.center, ), - const SizedBox(height: 32), + const SizedBox(height: 24), RichText( text: TextSpan( @@ -285,87 +372,66 @@ class UserDiscoverySetupComp extends StatelessWidget { context, context.lang.userDiscoveryDisabledIntro, ), + style: TextStyle( + color: context.color.onSurface, + fontSize: 13, + height: 1.4, + ), ), textAlign: TextAlign.center, ), + const SizedBox(height: 24), - const SizedBox(height: 32), - - SetupSwitchCard( - value: state.sharePromotion, - onChanged: (val) => state.update(() { - state.sharePromotion = val; - }), - title: context.lang.onboardingUserDiscoveryBeRecommended, - expandedChild: Column( + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: context.color.surfaceContainerLow, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: context.color.outlineVariant.withValues(alpha: 0.5), + ), + ), + child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Row( - children: [ - Expanded( - child: Text( - context.lang.userDiscoverySettingsMutualFriends, - style: const TextStyle(fontSize: 12), - ), - ), - const SizedBox(width: 12), - DropdownButtonHideUnderline( - child: DropdownButton( - value: state.threshold, - items: List.generate( - 9, - (index) { - final value = index + 2; - return DropdownMenuItem( - value: value, - child: Text('$value'), - ); - }, - ), - onChanged: (newValue) { - if (newValue != null) { - state.update(() { - state.threshold = newValue; - }); - } - }, - ), - ), - ], + Text( + context.lang.userDiscoveryFeatureOffers, + style: TextStyle( + fontSize: 16, + color: context.color.primary, ), + textAlign: TextAlign.center, ), - const Padding( - padding: EdgeInsets.only(bottom: 8, top: 8), - child: Divider(), - ), - const _ExampleLabel(), + const SizedBox(height: 12), Text( context.lang.onboardingUserDiscoveryWhatOthersSee, style: TextStyle( color: context.color.onSurfaceVariant, fontSize: 12, + fontWeight: FontWeight.w500, ), textAlign: TextAlign.center, ), - const SizedBox(height: 16), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), + const SizedBox(height: 8), + Center( child: Container( padding: const EdgeInsets.symmetric( - horizontal: 6, - vertical: 3, + horizontal: 12, + vertical: 8, ), decoration: BoxDecoration( - border: Border.all(color: Colors.grey, width: 0.5), + color: context.color.surface, borderRadius: BorderRadius.circular(12), + border: Border.all( + color: context.color.outlineVariant.withValues( + alpha: 0.3, + ), + ), ), child: Row( - mainAxisAlignment: MainAxisAlignment.center, children: [ const AvatarIcon(fontSize: 14), - const SizedBox(width: 5), + const SizedBox(width: 8), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -374,6 +440,7 @@ class UserDiscoverySetupComp extends StatelessWidget { userService.currentUser.username, style: const TextStyle( fontWeight: FontWeight.bold, + fontSize: 13, ), ), RichText( @@ -385,7 +452,10 @@ class UserDiscoverySetupComp extends StatelessWidget { state.threshold, ), ), - style: const TextStyle(fontSize: 11), + style: TextStyle( + fontSize: 11, + color: context.color.onSurfaceVariant, + ), ), ), ], @@ -402,28 +472,30 @@ class UserDiscoverySetupComp extends StatelessWidget { style: TextStyle( color: context.color.onSurfaceVariant, fontSize: 12, + fontWeight: FontWeight.w500, ), textAlign: TextAlign.center, ), - const SizedBox(height: 16), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - ), + const SizedBox(height: 8), + Center( child: Container( padding: const EdgeInsets.symmetric( - horizontal: 6, - vertical: 3, + horizontal: 12, + vertical: 8, ), decoration: BoxDecoration( - border: Border.all(color: Colors.grey, width: 0.5), + color: context.color.surface, borderRadius: BorderRadius.circular(12), + border: Border.all( + color: context.color.outlineVariant.withValues( + alpha: 0.3, + ), + ), ), child: Row( - mainAxisAlignment: MainAxisAlignment.center, children: [ const AvatarIcon(fontSize: 14), - const SizedBox(width: 5), + const SizedBox(width: 8), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -444,7 +516,10 @@ class UserDiscoverySetupComp extends StatelessWidget { state.threshold, ), ), - style: const TextStyle(fontSize: 10), + style: TextStyle( + fontSize: 10, + color: context.color.onSurfaceVariant, + ), ), ), ], @@ -455,7 +530,119 @@ class UserDiscoverySetupComp extends StatelessWidget { ), ), ), - const SizedBox(height: 16), + ], + ), + ), + const SizedBox(height: 24), + + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: context.color.surfaceContainerLow, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: context.color.outlineVariant.withValues(alpha: 0.5), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SwitchListTile( + value: state.sharePromotion, + onChanged: (val) => state.update(() { + state.sharePromotion = val; + }), + title: Text( + context.lang.onboardingUserDiscoveryBeRecommended, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + ), + ), + tileColor: Colors.transparent, + contentPadding: const EdgeInsets.symmetric(horizontal: 8), + ), + AnimatedCrossFade( + firstChild: const SizedBox( + width: double.infinity, + height: 0, + ), + secondChild: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Divider( + color: context.color.outlineVariant.withValues( + alpha: 0.3, + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 8, + ), + child: Row( + children: [ + Expanded( + child: Text( + context.lang.userDiscoverySettingsMutualFriends, + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + ), + const SizedBox(width: 12), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + ), + decoration: BoxDecoration( + color: context.color.surface, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: context.color.outlineVariant.withValues( + alpha: 0.5, + ), + ), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + value: state.threshold, + style: TextStyle( + color: context.color.onSurface, + fontWeight: FontWeight.bold, + ), + items: List.generate( + 9, + (index) { + final value = index + 2; + return DropdownMenuItem( + value: value, + child: Text('$value'), + ); + }, + ), + onChanged: (newValue) { + if (newValue != null) { + state.update(() { + state.threshold = newValue; + }); + } + }, + ), + ), + ), + ], + ), + ), + ], + ), + crossFadeState: state.sharePromotion ? CrossFadeState.showSecond : CrossFadeState.showFirst, + duration: const Duration(milliseconds: 300), + ), ], ), ), @@ -465,28 +652,3 @@ class UserDiscoverySetupComp extends StatelessWidget { ); } } - -class _ExampleLabel extends StatelessWidget { - const _ExampleLabel(); - - @override - Widget build(BuildContext context) { - return Align( - alignment: Alignment.centerRight, - child: Padding( - padding: const EdgeInsets.only(right: 12), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - decoration: BoxDecoration( - border: Border.all(color: Colors.grey, width: 0.5), - borderRadius: BorderRadius.circular(12), - ), - child: Text( - context.lang.onboardingExampleLabel, - style: const TextStyle(fontSize: 10), - ), - ), - ), - ); - } -} diff --git a/lib/src/visual/views/settings/subscription/select_additional_users.view.dart b/lib/src/visual/views/settings/subscription/select_additional_users.view.dart index 21252dcd..051d4982 100644 --- a/lib/src/visual/views/settings/subscription/select_additional_users.view.dart +++ b/lib/src/visual/views/settings/subscription/select_additional_users.view.dart @@ -99,6 +99,7 @@ class _SelectAdditionalUsers extends State { appBar: AppBar( title: Text(context.lang.additionalUserSelectTitle), ), + floatingActionButtonAnimator: FloatingActionButtonAnimator.noAnimation, floatingActionButton: FilledButton.icon( onPressed: selectedUsers.isEmpty ? null diff --git a/lib/src/visual/views/shared/select_contacts.view.dart b/lib/src/visual/views/shared/select_contacts.view.dart index 53684c33..80e87310 100644 --- a/lib/src/visual/views/shared/select_contacts.view.dart +++ b/lib/src/visual/views/shared/select_contacts.view.dart @@ -112,6 +112,7 @@ class _SelectAdditionalUsers extends State { appBar: AppBar( title: Text(widget.text.title), ), + floatingActionButtonAnimator: FloatingActionButtonAnimator.noAnimation, floatingActionButton: FilledButton.icon( onPressed: selectedUsers.isEmpty ? null diff --git a/pubspec.lock b/pubspec.lock index 8b6957f8..fa2fde95 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -2185,7 +2185,7 @@ packages: source: hosted version: "0.9.1+2" workmanager_platform_interface: - dependency: transitive + dependency: "direct dev" description: name: workmanager_platform_interface sha256: f40422f10b970c67abb84230b44da22b075147637532ac501729256fcea10a47 diff --git a/pubspec.yaml b/pubspec.yaml index dcf2d3af..bbb9847b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec publish_to: 'none' -version: 0.2.15+124 +version: 0.2.20+129 environment: sdk: ^3.11.0 @@ -207,6 +207,7 @@ flutter: # Add assets from the images directory to the application. - assets/images/ - assets/icons/ + - assets/icons/verification_badge_numeric/ - assets/animated_icons/ - assets/animations/ - assets/filters/ diff --git a/rust/src/log.rs b/rust/src/log.rs index 884aa04f..854a5351 100644 --- a/rust/src/log.rs +++ b/rust/src/log.rs @@ -34,10 +34,16 @@ pub(crate) async fn init_tracing(logs_dir: &std::path::Path, is_dart_available: // Replace stdout with our new DartWriter! + let default_filter = if std::env::var("FLUTTER_TEST").is_ok() { + "info,refinery_core=warn,refinery=warn" + } else { + "debug,refinery_core=warn,refinery=warn" + }; + let registry = Registry::default() .with( EnvFilter::try_from_default_env() - .unwrap_or_else(|_| EnvFilter::new("debug,refinery_core=warn,refinery=warn")), + .unwrap_or_else(|_| EnvFilter::new(default_filter)), ) .with(stdout_layer); diff --git a/rust_dependencies/protocols/src/user_discovery/mod.rs b/rust_dependencies/protocols/src/user_discovery/mod.rs index 68bccd63..cfe0d2ac 100644 --- a/rust_dependencies/protocols/src/user_discovery/mod.rs +++ b/rust_dependencies/protocols/src/user_discovery/mod.rs @@ -10,7 +10,7 @@ use std::u8; use blahaj::{Share, Sharks}; use prost::Message; use serde::{Deserialize, Serialize}; -use tokio::sync::{Mutex, MutexGuard}; +use tokio::sync::Mutex; use crate::user_discovery::error::{Result, UserDiscoveryError}; use crate::user_discovery::traits::{AnnouncedUser, OtherPromotion, UserDiscoveryUtils}; use crate::user_discovery::user_discovery_message::{UserDiscoveryAnnouncement, UserDiscoveryPromotion}; @@ -56,7 +56,7 @@ where { store: Store, utils: Utils, - config_lock: Arc>, + config_lock: Arc>, } impl UserDiscovery { @@ -101,6 +101,7 @@ impl UserDiscovery UserDiscoveryConfig { threshold, user_id, + total_number_of_shares: 255, ..Default::default() }, }; @@ -126,24 +127,27 @@ impl UserDiscovery serde_json::from_str(&c)?, - Err(_) => UserDiscoveryConfig { - threshold, - user_id, - ..Default::default() - }, - }; - final_config.public_id = public_id; - final_config.announcement_version += 1; - final_config.verification_shares = verification_shares; - final_config.share_promotion = share_promotion; - final_config.threshold = threshold; + { + let mut final_config = match self.store.get_config().await { + Ok(c) => serde_json::from_str(&c)?, + Err(_) => UserDiscoveryConfig { + threshold, + user_id, + ..Default::default() + }, + }; - self.update_config(final_config, config_lock).await?; + final_config.public_id = public_id; + final_config.announcement_version += 1; + final_config.verification_shares = verification_shares; + final_config.share_promotion = share_promotion; + final_config.threshold = threshold; + + self.store + .update_config(serde_json::to_string_pretty(&final_config)?) + .await?; + } tracing::info!("Protocols: initialize_or_update finished"); Ok(()) @@ -164,7 +168,7 @@ impl UserDiscovery Result> { - let (config, _) = self.get_config().await?; + let config = self.get_config_snapshot().await?; Ok(UserDiscoveryVersion { announcement: config.announcement_version, promotion: config.promotion_version, @@ -207,7 +211,7 @@ impl UserDiscovery UserDiscovery, ) -> Result<()> { - let (mut config, config_lock) = self.get_config().await?; - config.promotion_version += 1; - let Some(current_promotion) = self.store.get_contact_promotion(contact_id).await? else { // User does not participate... return Ok(()); @@ -376,10 +377,19 @@ impl UserDiscovery UserDiscovery UserDiscovery Result<(UserDiscoveryConfig, MutexGuard<'_, bool>)> { - let mut lock = self.config_lock.lock().await; - *lock = true; - Ok((serde_json::from_str(&self.store.get_config().await?)?, lock)) + /// Reads the config from the store without holding any lock. + /// Use this for read-only access to the config. + async fn get_config_snapshot(&self) -> Result { + Ok(serde_json::from_str(&self.store.get_config().await?)?) } - async fn update_config( - &self, - config: UserDiscoveryConfig, - mut config_lock: MutexGuard<'_, bool>, - ) -> Result<()> { + /// Atomically reads the config, applies the mutation, and writes it back. + /// The config_lock is only held during the read-modify-write cycle, + /// NOT across any async Dart callbacks from the caller. + async fn read_modify_write_config(&self, mutate: F) -> Result<()> + where + F: FnOnce(&mut UserDiscoveryConfig), + { + let _lock = tokio::time::timeout( + std::time::Duration::from_secs(10), + self.config_lock.lock(), + ) + .await + .ok(); + let mut config: UserDiscoveryConfig = + serde_json::from_str(&self.store.get_config().await?)?; + mutate(&mut config); self.store .update_config(serde_json::to_string_pretty(&config)?) .await?; - *config_lock = false; Ok(()) } @@ -542,13 +560,19 @@ impl UserDiscovery UserDiscovery UserDiscovery Self { Self { threshold: 2, - total_number_of_shares: u8::MAX, + total_number_of_shares: 255, announcement_version: 0, promotion_version: 0, verification_shares: vec![], @@ -768,3 +790,4 @@ impl Default for UserDiscoveryConfig { } } } + diff --git a/test/drift/twonly_db/generated/schema.dart b/test/drift/twonly_db/generated/schema.dart index 7e839796..cd869b3c 100644 --- a/test/drift/twonly_db/generated/schema.dart +++ b/test/drift/twonly_db/generated/schema.dart @@ -20,6 +20,7 @@ import 'schema_v13.dart' as v13; import 'schema_v14.dart' as v14; import 'schema_v15.dart' as v15; import 'schema_v16.dart' as v16; +import 'schema_v17.dart' as v17; class GeneratedHelper implements SchemaInstantiationHelper { @override @@ -57,6 +58,8 @@ class GeneratedHelper implements SchemaInstantiationHelper { return v15.DatabaseAtV15(db); case 16: return v16.DatabaseAtV16(db); + case 17: + return v17.DatabaseAtV17(db); default: throw MissingSchemaException(version, versions); } @@ -79,5 +82,6 @@ class GeneratedHelper implements SchemaInstantiationHelper { 14, 15, 16, + 17, ]; } diff --git a/test/drift/twonly_db/generated/schema_v17.dart b/test/drift/twonly_db/generated/schema_v17.dart new file mode 100644 index 00000000..3d02ee34 --- /dev/null +++ b/test/drift/twonly_db/generated/schema_v17.dart @@ -0,0 +1,10624 @@ +// dart format width=80 +import 'dart:typed_data' as i2; +// GENERATED BY drift_dev, DO NOT MODIFY. +// ignore_for_file: type=lint,unused_import +// +import 'package:drift/drift.dart'; + +class Contacts extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Contacts(this.attachedDatabase, [this._alias]); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn username = GeneratedColumn( + 'username', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn displayName = GeneratedColumn( + 'display_name', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn nickName = GeneratedColumn( + 'nick_name', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn avatarSvgCompressed = + GeneratedColumn( + 'avatar_svg_compressed', + aliasedName, + true, + type: DriftSqlType.blob, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn senderProfileCounter = GeneratedColumn( + 'sender_profile_counter', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn accepted = GeneratedColumn( + 'accepted', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (accepted IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn deletedByUser = GeneratedColumn( + 'deleted_by_user', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (deleted_by_user IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn requested = GeneratedColumn( + 'requested', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (requested IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn blocked = GeneratedColumn( + 'blocked', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (blocked IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn verified = GeneratedColumn( + 'verified', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (verified IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn accountDeleted = GeneratedColumn( + 'account_deleted', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (account_deleted IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT (CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER))', + defaultValue: const CustomExpression( + 'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)', + ), + ); + late final GeneratedColumn userDiscoveryVersion = + GeneratedColumn( + 'user_discovery_version', + aliasedName, + true, + type: DriftSqlType.blob, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn userDiscoveryExcluded = GeneratedColumn( + 'user_discovery_excluded', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT 0 CHECK (user_discovery_excluded IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn userDiscoveryManualApproved = + GeneratedColumn( + 'user_discovery_manual_approved', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NULL DEFAULT 0 CHECK (user_discovery_manual_approved IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn mediaSendCounter = GeneratedColumn( + 'media_send_counter', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn mediaReceivedCounter = GeneratedColumn( + 'media_received_counter', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0', + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [ + userId, + username, + displayName, + nickName, + avatarSvgCompressed, + senderProfileCounter, + accepted, + deletedByUser, + requested, + blocked, + verified, + accountDeleted, + createdAt, + userDiscoveryVersion, + userDiscoveryExcluded, + userDiscoveryManualApproved, + mediaSendCounter, + mediaReceivedCounter, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'contacts'; + @override + Set get $primaryKey => {userId}; + @override + ContactsData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ContactsData( + userId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}user_id'], + )!, + username: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}username'], + )!, + displayName: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}display_name'], + ), + nickName: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}nick_name'], + ), + avatarSvgCompressed: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}avatar_svg_compressed'], + ), + senderProfileCounter: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}sender_profile_counter'], + )!, + accepted: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}accepted'], + )!, + deletedByUser: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}deleted_by_user'], + )!, + requested: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}requested'], + )!, + blocked: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}blocked'], + )!, + verified: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}verified'], + )!, + accountDeleted: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}account_deleted'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}created_at'], + )!, + userDiscoveryVersion: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}user_discovery_version'], + ), + userDiscoveryExcluded: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}user_discovery_excluded'], + )!, + userDiscoveryManualApproved: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}user_discovery_manual_approved'], + ), + mediaSendCounter: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}media_send_counter'], + )!, + mediaReceivedCounter: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}media_received_counter'], + )!, + ); + } + + @override + Contacts createAlias(String alias) { + return Contacts(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(user_id)']; + @override + bool get dontWriteConstraints => true; +} + +class ContactsData extends DataClass implements Insertable { + final int userId; + final String username; + final String? displayName; + final String? nickName; + final i2.Uint8List? avatarSvgCompressed; + final int senderProfileCounter; + final int accepted; + final int deletedByUser; + final int requested; + final int blocked; + final int verified; + final int accountDeleted; + final int createdAt; + final i2.Uint8List? userDiscoveryVersion; + final int userDiscoveryExcluded; + final int? userDiscoveryManualApproved; + final int mediaSendCounter; + final int mediaReceivedCounter; + const ContactsData({ + required this.userId, + required this.username, + this.displayName, + this.nickName, + this.avatarSvgCompressed, + required this.senderProfileCounter, + required this.accepted, + required this.deletedByUser, + required this.requested, + required this.blocked, + required this.verified, + required this.accountDeleted, + required this.createdAt, + this.userDiscoveryVersion, + required this.userDiscoveryExcluded, + this.userDiscoveryManualApproved, + required this.mediaSendCounter, + required this.mediaReceivedCounter, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['user_id'] = Variable(userId); + map['username'] = Variable(username); + if (!nullToAbsent || displayName != null) { + map['display_name'] = Variable(displayName); + } + if (!nullToAbsent || nickName != null) { + map['nick_name'] = Variable(nickName); + } + if (!nullToAbsent || avatarSvgCompressed != null) { + map['avatar_svg_compressed'] = Variable( + avatarSvgCompressed, + ); + } + map['sender_profile_counter'] = Variable(senderProfileCounter); + map['accepted'] = Variable(accepted); + map['deleted_by_user'] = Variable(deletedByUser); + map['requested'] = Variable(requested); + map['blocked'] = Variable(blocked); + map['verified'] = Variable(verified); + map['account_deleted'] = Variable(accountDeleted); + map['created_at'] = Variable(createdAt); + if (!nullToAbsent || userDiscoveryVersion != null) { + map['user_discovery_version'] = Variable( + userDiscoveryVersion, + ); + } + map['user_discovery_excluded'] = Variable(userDiscoveryExcluded); + if (!nullToAbsent || userDiscoveryManualApproved != null) { + map['user_discovery_manual_approved'] = Variable( + userDiscoveryManualApproved, + ); + } + map['media_send_counter'] = Variable(mediaSendCounter); + map['media_received_counter'] = Variable(mediaReceivedCounter); + return map; + } + + ContactsCompanion toCompanion(bool nullToAbsent) { + return ContactsCompanion( + userId: Value(userId), + username: Value(username), + displayName: displayName == null && nullToAbsent + ? const Value.absent() + : Value(displayName), + nickName: nickName == null && nullToAbsent + ? const Value.absent() + : Value(nickName), + avatarSvgCompressed: avatarSvgCompressed == null && nullToAbsent + ? const Value.absent() + : Value(avatarSvgCompressed), + senderProfileCounter: Value(senderProfileCounter), + accepted: Value(accepted), + deletedByUser: Value(deletedByUser), + requested: Value(requested), + blocked: Value(blocked), + verified: Value(verified), + accountDeleted: Value(accountDeleted), + createdAt: Value(createdAt), + userDiscoveryVersion: userDiscoveryVersion == null && nullToAbsent + ? const Value.absent() + : Value(userDiscoveryVersion), + userDiscoveryExcluded: Value(userDiscoveryExcluded), + userDiscoveryManualApproved: + userDiscoveryManualApproved == null && nullToAbsent + ? const Value.absent() + : Value(userDiscoveryManualApproved), + mediaSendCounter: Value(mediaSendCounter), + mediaReceivedCounter: Value(mediaReceivedCounter), + ); + } + + factory ContactsData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ContactsData( + userId: serializer.fromJson(json['userId']), + username: serializer.fromJson(json['username']), + displayName: serializer.fromJson(json['displayName']), + nickName: serializer.fromJson(json['nickName']), + avatarSvgCompressed: serializer.fromJson( + json['avatarSvgCompressed'], + ), + senderProfileCounter: serializer.fromJson( + json['senderProfileCounter'], + ), + accepted: serializer.fromJson(json['accepted']), + deletedByUser: serializer.fromJson(json['deletedByUser']), + requested: serializer.fromJson(json['requested']), + blocked: serializer.fromJson(json['blocked']), + verified: serializer.fromJson(json['verified']), + accountDeleted: serializer.fromJson(json['accountDeleted']), + createdAt: serializer.fromJson(json['createdAt']), + userDiscoveryVersion: serializer.fromJson( + json['userDiscoveryVersion'], + ), + userDiscoveryExcluded: serializer.fromJson( + json['userDiscoveryExcluded'], + ), + userDiscoveryManualApproved: serializer.fromJson( + json['userDiscoveryManualApproved'], + ), + mediaSendCounter: serializer.fromJson(json['mediaSendCounter']), + mediaReceivedCounter: serializer.fromJson( + json['mediaReceivedCounter'], + ), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'userId': serializer.toJson(userId), + 'username': serializer.toJson(username), + 'displayName': serializer.toJson(displayName), + 'nickName': serializer.toJson(nickName), + 'avatarSvgCompressed': serializer.toJson( + avatarSvgCompressed, + ), + 'senderProfileCounter': serializer.toJson(senderProfileCounter), + 'accepted': serializer.toJson(accepted), + 'deletedByUser': serializer.toJson(deletedByUser), + 'requested': serializer.toJson(requested), + 'blocked': serializer.toJson(blocked), + 'verified': serializer.toJson(verified), + 'accountDeleted': serializer.toJson(accountDeleted), + 'createdAt': serializer.toJson(createdAt), + 'userDiscoveryVersion': serializer.toJson( + userDiscoveryVersion, + ), + 'userDiscoveryExcluded': serializer.toJson(userDiscoveryExcluded), + 'userDiscoveryManualApproved': serializer.toJson( + userDiscoveryManualApproved, + ), + 'mediaSendCounter': serializer.toJson(mediaSendCounter), + 'mediaReceivedCounter': serializer.toJson(mediaReceivedCounter), + }; + } + + ContactsData copyWith({ + int? userId, + String? username, + Value displayName = const Value.absent(), + Value nickName = const Value.absent(), + Value avatarSvgCompressed = const Value.absent(), + int? senderProfileCounter, + int? accepted, + int? deletedByUser, + int? requested, + int? blocked, + int? verified, + int? accountDeleted, + int? createdAt, + Value userDiscoveryVersion = const Value.absent(), + int? userDiscoveryExcluded, + Value userDiscoveryManualApproved = const Value.absent(), + int? mediaSendCounter, + int? mediaReceivedCounter, + }) => ContactsData( + userId: userId ?? this.userId, + username: username ?? this.username, + displayName: displayName.present ? displayName.value : this.displayName, + nickName: nickName.present ? nickName.value : this.nickName, + avatarSvgCompressed: avatarSvgCompressed.present + ? avatarSvgCompressed.value + : this.avatarSvgCompressed, + senderProfileCounter: senderProfileCounter ?? this.senderProfileCounter, + accepted: accepted ?? this.accepted, + deletedByUser: deletedByUser ?? this.deletedByUser, + requested: requested ?? this.requested, + blocked: blocked ?? this.blocked, + verified: verified ?? this.verified, + accountDeleted: accountDeleted ?? this.accountDeleted, + createdAt: createdAt ?? this.createdAt, + userDiscoveryVersion: userDiscoveryVersion.present + ? userDiscoveryVersion.value + : this.userDiscoveryVersion, + userDiscoveryExcluded: userDiscoveryExcluded ?? this.userDiscoveryExcluded, + userDiscoveryManualApproved: userDiscoveryManualApproved.present + ? userDiscoveryManualApproved.value + : this.userDiscoveryManualApproved, + mediaSendCounter: mediaSendCounter ?? this.mediaSendCounter, + mediaReceivedCounter: mediaReceivedCounter ?? this.mediaReceivedCounter, + ); + ContactsData copyWithCompanion(ContactsCompanion data) { + return ContactsData( + userId: data.userId.present ? data.userId.value : this.userId, + username: data.username.present ? data.username.value : this.username, + displayName: data.displayName.present + ? data.displayName.value + : this.displayName, + nickName: data.nickName.present ? data.nickName.value : this.nickName, + avatarSvgCompressed: data.avatarSvgCompressed.present + ? data.avatarSvgCompressed.value + : this.avatarSvgCompressed, + senderProfileCounter: data.senderProfileCounter.present + ? data.senderProfileCounter.value + : this.senderProfileCounter, + accepted: data.accepted.present ? data.accepted.value : this.accepted, + deletedByUser: data.deletedByUser.present + ? data.deletedByUser.value + : this.deletedByUser, + requested: data.requested.present ? data.requested.value : this.requested, + blocked: data.blocked.present ? data.blocked.value : this.blocked, + verified: data.verified.present ? data.verified.value : this.verified, + accountDeleted: data.accountDeleted.present + ? data.accountDeleted.value + : this.accountDeleted, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + userDiscoveryVersion: data.userDiscoveryVersion.present + ? data.userDiscoveryVersion.value + : this.userDiscoveryVersion, + userDiscoveryExcluded: data.userDiscoveryExcluded.present + ? data.userDiscoveryExcluded.value + : this.userDiscoveryExcluded, + userDiscoveryManualApproved: data.userDiscoveryManualApproved.present + ? data.userDiscoveryManualApproved.value + : this.userDiscoveryManualApproved, + mediaSendCounter: data.mediaSendCounter.present + ? data.mediaSendCounter.value + : this.mediaSendCounter, + mediaReceivedCounter: data.mediaReceivedCounter.present + ? data.mediaReceivedCounter.value + : this.mediaReceivedCounter, + ); + } + + @override + String toString() { + return (StringBuffer('ContactsData(') + ..write('userId: $userId, ') + ..write('username: $username, ') + ..write('displayName: $displayName, ') + ..write('nickName: $nickName, ') + ..write('avatarSvgCompressed: $avatarSvgCompressed, ') + ..write('senderProfileCounter: $senderProfileCounter, ') + ..write('accepted: $accepted, ') + ..write('deletedByUser: $deletedByUser, ') + ..write('requested: $requested, ') + ..write('blocked: $blocked, ') + ..write('verified: $verified, ') + ..write('accountDeleted: $accountDeleted, ') + ..write('createdAt: $createdAt, ') + ..write('userDiscoveryVersion: $userDiscoveryVersion, ') + ..write('userDiscoveryExcluded: $userDiscoveryExcluded, ') + ..write('userDiscoveryManualApproved: $userDiscoveryManualApproved, ') + ..write('mediaSendCounter: $mediaSendCounter, ') + ..write('mediaReceivedCounter: $mediaReceivedCounter') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + userId, + username, + displayName, + nickName, + $driftBlobEquality.hash(avatarSvgCompressed), + senderProfileCounter, + accepted, + deletedByUser, + requested, + blocked, + verified, + accountDeleted, + createdAt, + $driftBlobEquality.hash(userDiscoveryVersion), + userDiscoveryExcluded, + userDiscoveryManualApproved, + mediaSendCounter, + mediaReceivedCounter, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ContactsData && + other.userId == this.userId && + other.username == this.username && + other.displayName == this.displayName && + other.nickName == this.nickName && + $driftBlobEquality.equals( + other.avatarSvgCompressed, + this.avatarSvgCompressed, + ) && + other.senderProfileCounter == this.senderProfileCounter && + other.accepted == this.accepted && + other.deletedByUser == this.deletedByUser && + other.requested == this.requested && + other.blocked == this.blocked && + other.verified == this.verified && + other.accountDeleted == this.accountDeleted && + other.createdAt == this.createdAt && + $driftBlobEquality.equals( + other.userDiscoveryVersion, + this.userDiscoveryVersion, + ) && + other.userDiscoveryExcluded == this.userDiscoveryExcluded && + other.userDiscoveryManualApproved == + this.userDiscoveryManualApproved && + other.mediaSendCounter == this.mediaSendCounter && + other.mediaReceivedCounter == this.mediaReceivedCounter); +} + +class ContactsCompanion extends UpdateCompanion { + final Value userId; + final Value username; + final Value displayName; + final Value nickName; + final Value avatarSvgCompressed; + final Value senderProfileCounter; + final Value accepted; + final Value deletedByUser; + final Value requested; + final Value blocked; + final Value verified; + final Value accountDeleted; + final Value createdAt; + final Value userDiscoveryVersion; + final Value userDiscoveryExcluded; + final Value userDiscoveryManualApproved; + final Value mediaSendCounter; + final Value mediaReceivedCounter; + const ContactsCompanion({ + this.userId = const Value.absent(), + this.username = const Value.absent(), + this.displayName = const Value.absent(), + this.nickName = const Value.absent(), + this.avatarSvgCompressed = const Value.absent(), + this.senderProfileCounter = const Value.absent(), + this.accepted = const Value.absent(), + this.deletedByUser = const Value.absent(), + this.requested = const Value.absent(), + this.blocked = const Value.absent(), + this.verified = const Value.absent(), + this.accountDeleted = const Value.absent(), + this.createdAt = const Value.absent(), + this.userDiscoveryVersion = const Value.absent(), + this.userDiscoveryExcluded = const Value.absent(), + this.userDiscoveryManualApproved = const Value.absent(), + this.mediaSendCounter = const Value.absent(), + this.mediaReceivedCounter = const Value.absent(), + }); + ContactsCompanion.insert({ + this.userId = const Value.absent(), + required String username, + this.displayName = const Value.absent(), + this.nickName = const Value.absent(), + this.avatarSvgCompressed = const Value.absent(), + this.senderProfileCounter = const Value.absent(), + this.accepted = const Value.absent(), + this.deletedByUser = const Value.absent(), + this.requested = const Value.absent(), + this.blocked = const Value.absent(), + this.verified = const Value.absent(), + this.accountDeleted = const Value.absent(), + this.createdAt = const Value.absent(), + this.userDiscoveryVersion = const Value.absent(), + this.userDiscoveryExcluded = const Value.absent(), + this.userDiscoveryManualApproved = const Value.absent(), + this.mediaSendCounter = const Value.absent(), + this.mediaReceivedCounter = const Value.absent(), + }) : username = Value(username); + static Insertable custom({ + Expression? userId, + Expression? username, + Expression? displayName, + Expression? nickName, + Expression? avatarSvgCompressed, + Expression? senderProfileCounter, + Expression? accepted, + Expression? deletedByUser, + Expression? requested, + Expression? blocked, + Expression? verified, + Expression? accountDeleted, + Expression? createdAt, + Expression? userDiscoveryVersion, + Expression? userDiscoveryExcluded, + Expression? userDiscoveryManualApproved, + Expression? mediaSendCounter, + Expression? mediaReceivedCounter, + }) { + return RawValuesInsertable({ + if (userId != null) 'user_id': userId, + if (username != null) 'username': username, + if (displayName != null) 'display_name': displayName, + if (nickName != null) 'nick_name': nickName, + if (avatarSvgCompressed != null) + 'avatar_svg_compressed': avatarSvgCompressed, + if (senderProfileCounter != null) + 'sender_profile_counter': senderProfileCounter, + if (accepted != null) 'accepted': accepted, + if (deletedByUser != null) 'deleted_by_user': deletedByUser, + if (requested != null) 'requested': requested, + if (blocked != null) 'blocked': blocked, + if (verified != null) 'verified': verified, + if (accountDeleted != null) 'account_deleted': accountDeleted, + if (createdAt != null) 'created_at': createdAt, + if (userDiscoveryVersion != null) + 'user_discovery_version': userDiscoveryVersion, + if (userDiscoveryExcluded != null) + 'user_discovery_excluded': userDiscoveryExcluded, + if (userDiscoveryManualApproved != null) + 'user_discovery_manual_approved': userDiscoveryManualApproved, + if (mediaSendCounter != null) 'media_send_counter': mediaSendCounter, + if (mediaReceivedCounter != null) + 'media_received_counter': mediaReceivedCounter, + }); + } + + ContactsCompanion copyWith({ + Value? userId, + Value? username, + Value? displayName, + Value? nickName, + Value? avatarSvgCompressed, + Value? senderProfileCounter, + Value? accepted, + Value? deletedByUser, + Value? requested, + Value? blocked, + Value? verified, + Value? accountDeleted, + Value? createdAt, + Value? userDiscoveryVersion, + Value? userDiscoveryExcluded, + Value? userDiscoveryManualApproved, + Value? mediaSendCounter, + Value? mediaReceivedCounter, + }) { + return ContactsCompanion( + userId: userId ?? this.userId, + username: username ?? this.username, + displayName: displayName ?? this.displayName, + nickName: nickName ?? this.nickName, + avatarSvgCompressed: avatarSvgCompressed ?? this.avatarSvgCompressed, + senderProfileCounter: senderProfileCounter ?? this.senderProfileCounter, + accepted: accepted ?? this.accepted, + deletedByUser: deletedByUser ?? this.deletedByUser, + requested: requested ?? this.requested, + blocked: blocked ?? this.blocked, + verified: verified ?? this.verified, + accountDeleted: accountDeleted ?? this.accountDeleted, + createdAt: createdAt ?? this.createdAt, + userDiscoveryVersion: userDiscoveryVersion ?? this.userDiscoveryVersion, + userDiscoveryExcluded: + userDiscoveryExcluded ?? this.userDiscoveryExcluded, + userDiscoveryManualApproved: + userDiscoveryManualApproved ?? this.userDiscoveryManualApproved, + mediaSendCounter: mediaSendCounter ?? this.mediaSendCounter, + mediaReceivedCounter: mediaReceivedCounter ?? this.mediaReceivedCounter, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (username.present) { + map['username'] = Variable(username.value); + } + if (displayName.present) { + map['display_name'] = Variable(displayName.value); + } + if (nickName.present) { + map['nick_name'] = Variable(nickName.value); + } + if (avatarSvgCompressed.present) { + map['avatar_svg_compressed'] = Variable( + avatarSvgCompressed.value, + ); + } + if (senderProfileCounter.present) { + map['sender_profile_counter'] = Variable(senderProfileCounter.value); + } + if (accepted.present) { + map['accepted'] = Variable(accepted.value); + } + if (deletedByUser.present) { + map['deleted_by_user'] = Variable(deletedByUser.value); + } + if (requested.present) { + map['requested'] = Variable(requested.value); + } + if (blocked.present) { + map['blocked'] = Variable(blocked.value); + } + if (verified.present) { + map['verified'] = Variable(verified.value); + } + if (accountDeleted.present) { + map['account_deleted'] = Variable(accountDeleted.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (userDiscoveryVersion.present) { + map['user_discovery_version'] = Variable( + userDiscoveryVersion.value, + ); + } + if (userDiscoveryExcluded.present) { + map['user_discovery_excluded'] = Variable( + userDiscoveryExcluded.value, + ); + } + if (userDiscoveryManualApproved.present) { + map['user_discovery_manual_approved'] = Variable( + userDiscoveryManualApproved.value, + ); + } + if (mediaSendCounter.present) { + map['media_send_counter'] = Variable(mediaSendCounter.value); + } + if (mediaReceivedCounter.present) { + map['media_received_counter'] = Variable(mediaReceivedCounter.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ContactsCompanion(') + ..write('userId: $userId, ') + ..write('username: $username, ') + ..write('displayName: $displayName, ') + ..write('nickName: $nickName, ') + ..write('avatarSvgCompressed: $avatarSvgCompressed, ') + ..write('senderProfileCounter: $senderProfileCounter, ') + ..write('accepted: $accepted, ') + ..write('deletedByUser: $deletedByUser, ') + ..write('requested: $requested, ') + ..write('blocked: $blocked, ') + ..write('verified: $verified, ') + ..write('accountDeleted: $accountDeleted, ') + ..write('createdAt: $createdAt, ') + ..write('userDiscoveryVersion: $userDiscoveryVersion, ') + ..write('userDiscoveryExcluded: $userDiscoveryExcluded, ') + ..write('userDiscoveryManualApproved: $userDiscoveryManualApproved, ') + ..write('mediaSendCounter: $mediaSendCounter, ') + ..write('mediaReceivedCounter: $mediaReceivedCounter') + ..write(')')) + .toString(); + } +} + +class Groups extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Groups(this.attachedDatabase, [this._alias]); + late final GeneratedColumn groupId = GeneratedColumn( + 'group_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn isGroupAdmin = GeneratedColumn( + 'is_group_admin', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_group_admin IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn isDirectChat = GeneratedColumn( + 'is_direct_chat', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_direct_chat IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn pinned = GeneratedColumn( + 'pinned', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (pinned IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn archived = GeneratedColumn( + 'archived', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (archived IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn joinedGroup = GeneratedColumn( + 'joined_group', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (joined_group IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn leftGroup = GeneratedColumn( + 'left_group', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (left_group IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn deletedContent = GeneratedColumn( + 'deleted_content', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (deleted_content IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn stateVersionId = GeneratedColumn( + 'state_version_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn stateEncryptionKey = + GeneratedColumn( + 'state_encryption_key', + aliasedName, + true, + type: DriftSqlType.blob, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn myGroupPrivateKey = + GeneratedColumn( + 'my_group_private_key', + aliasedName, + true, + type: DriftSqlType.blob, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn groupName = GeneratedColumn( + 'group_name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn draftMessage = GeneratedColumn( + 'draft_message', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn totalMediaCounter = GeneratedColumn( + 'total_media_counter', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn alsoBestFriend = GeneratedColumn( + 'also_best_friend', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (also_best_friend IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn deleteMessagesAfterMilliseconds = + GeneratedColumn( + 'delete_messages_after_milliseconds', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 86400000', + defaultValue: const CustomExpression('86400000'), + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT (CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER))', + defaultValue: const CustomExpression( + 'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)', + ), + ); + late final GeneratedColumn lastMessageSend = GeneratedColumn( + 'last_message_send', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn lastMessageReceived = GeneratedColumn( + 'last_message_received', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn lastFlameCounterChange = GeneratedColumn( + 'last_flame_counter_change', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn lastFlameSync = GeneratedColumn( + 'last_flame_sync', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn flameCounter = GeneratedColumn( + 'flame_counter', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn maxFlameCounter = GeneratedColumn( + 'max_flame_counter', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn maxFlameCounterFrom = GeneratedColumn( + 'max_flame_counter_from', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn lastMessageExchange = GeneratedColumn( + 'last_message_exchange', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT (CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER))', + defaultValue: const CustomExpression( + 'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)', + ), + ); + @override + List get $columns => [ + groupId, + isGroupAdmin, + isDirectChat, + pinned, + archived, + joinedGroup, + leftGroup, + deletedContent, + stateVersionId, + stateEncryptionKey, + myGroupPrivateKey, + groupName, + draftMessage, + totalMediaCounter, + alsoBestFriend, + deleteMessagesAfterMilliseconds, + createdAt, + lastMessageSend, + lastMessageReceived, + lastFlameCounterChange, + lastFlameSync, + flameCounter, + maxFlameCounter, + maxFlameCounterFrom, + lastMessageExchange, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'groups'; + @override + Set get $primaryKey => {groupId}; + @override + GroupsData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return GroupsData( + groupId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}group_id'], + )!, + isGroupAdmin: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}is_group_admin'], + )!, + isDirectChat: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}is_direct_chat'], + )!, + pinned: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}pinned'], + )!, + archived: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}archived'], + )!, + joinedGroup: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}joined_group'], + )!, + leftGroup: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}left_group'], + )!, + deletedContent: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}deleted_content'], + )!, + stateVersionId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}state_version_id'], + )!, + stateEncryptionKey: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}state_encryption_key'], + ), + myGroupPrivateKey: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}my_group_private_key'], + ), + groupName: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}group_name'], + )!, + draftMessage: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}draft_message'], + ), + totalMediaCounter: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}total_media_counter'], + )!, + alsoBestFriend: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}also_best_friend'], + )!, + deleteMessagesAfterMilliseconds: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}delete_messages_after_milliseconds'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}created_at'], + )!, + lastMessageSend: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}last_message_send'], + ), + lastMessageReceived: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}last_message_received'], + ), + lastFlameCounterChange: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}last_flame_counter_change'], + ), + lastFlameSync: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}last_flame_sync'], + ), + flameCounter: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}flame_counter'], + )!, + maxFlameCounter: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}max_flame_counter'], + )!, + maxFlameCounterFrom: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}max_flame_counter_from'], + ), + lastMessageExchange: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}last_message_exchange'], + )!, + ); + } + + @override + Groups createAlias(String alias) { + return Groups(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(group_id)']; + @override + bool get dontWriteConstraints => true; +} + +class GroupsData extends DataClass implements Insertable { + final String groupId; + final int isGroupAdmin; + final int isDirectChat; + final int pinned; + final int archived; + final int joinedGroup; + final int leftGroup; + final int deletedContent; + final int stateVersionId; + final i2.Uint8List? stateEncryptionKey; + final i2.Uint8List? myGroupPrivateKey; + final String groupName; + final String? draftMessage; + final int totalMediaCounter; + final int alsoBestFriend; + final int deleteMessagesAfterMilliseconds; + final int createdAt; + final int? lastMessageSend; + final int? lastMessageReceived; + final int? lastFlameCounterChange; + final int? lastFlameSync; + final int flameCounter; + final int maxFlameCounter; + final int? maxFlameCounterFrom; + final int lastMessageExchange; + const GroupsData({ + required this.groupId, + required this.isGroupAdmin, + required this.isDirectChat, + required this.pinned, + required this.archived, + required this.joinedGroup, + required this.leftGroup, + required this.deletedContent, + required this.stateVersionId, + this.stateEncryptionKey, + this.myGroupPrivateKey, + required this.groupName, + this.draftMessage, + required this.totalMediaCounter, + required this.alsoBestFriend, + required this.deleteMessagesAfterMilliseconds, + required this.createdAt, + this.lastMessageSend, + this.lastMessageReceived, + this.lastFlameCounterChange, + this.lastFlameSync, + required this.flameCounter, + required this.maxFlameCounter, + this.maxFlameCounterFrom, + required this.lastMessageExchange, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['group_id'] = Variable(groupId); + map['is_group_admin'] = Variable(isGroupAdmin); + map['is_direct_chat'] = Variable(isDirectChat); + map['pinned'] = Variable(pinned); + map['archived'] = Variable(archived); + map['joined_group'] = Variable(joinedGroup); + map['left_group'] = Variable(leftGroup); + map['deleted_content'] = Variable(deletedContent); + map['state_version_id'] = Variable(stateVersionId); + if (!nullToAbsent || stateEncryptionKey != null) { + map['state_encryption_key'] = Variable(stateEncryptionKey); + } + if (!nullToAbsent || myGroupPrivateKey != null) { + map['my_group_private_key'] = Variable(myGroupPrivateKey); + } + map['group_name'] = Variable(groupName); + if (!nullToAbsent || draftMessage != null) { + map['draft_message'] = Variable(draftMessage); + } + map['total_media_counter'] = Variable(totalMediaCounter); + map['also_best_friend'] = Variable(alsoBestFriend); + map['delete_messages_after_milliseconds'] = Variable( + deleteMessagesAfterMilliseconds, + ); + map['created_at'] = Variable(createdAt); + if (!nullToAbsent || lastMessageSend != null) { + map['last_message_send'] = Variable(lastMessageSend); + } + if (!nullToAbsent || lastMessageReceived != null) { + map['last_message_received'] = Variable(lastMessageReceived); + } + if (!nullToAbsent || lastFlameCounterChange != null) { + map['last_flame_counter_change'] = Variable(lastFlameCounterChange); + } + if (!nullToAbsent || lastFlameSync != null) { + map['last_flame_sync'] = Variable(lastFlameSync); + } + map['flame_counter'] = Variable(flameCounter); + map['max_flame_counter'] = Variable(maxFlameCounter); + if (!nullToAbsent || maxFlameCounterFrom != null) { + map['max_flame_counter_from'] = Variable(maxFlameCounterFrom); + } + map['last_message_exchange'] = Variable(lastMessageExchange); + return map; + } + + GroupsCompanion toCompanion(bool nullToAbsent) { + return GroupsCompanion( + groupId: Value(groupId), + isGroupAdmin: Value(isGroupAdmin), + isDirectChat: Value(isDirectChat), + pinned: Value(pinned), + archived: Value(archived), + joinedGroup: Value(joinedGroup), + leftGroup: Value(leftGroup), + deletedContent: Value(deletedContent), + stateVersionId: Value(stateVersionId), + stateEncryptionKey: stateEncryptionKey == null && nullToAbsent + ? const Value.absent() + : Value(stateEncryptionKey), + myGroupPrivateKey: myGroupPrivateKey == null && nullToAbsent + ? const Value.absent() + : Value(myGroupPrivateKey), + groupName: Value(groupName), + draftMessage: draftMessage == null && nullToAbsent + ? const Value.absent() + : Value(draftMessage), + totalMediaCounter: Value(totalMediaCounter), + alsoBestFriend: Value(alsoBestFriend), + deleteMessagesAfterMilliseconds: Value(deleteMessagesAfterMilliseconds), + createdAt: Value(createdAt), + lastMessageSend: lastMessageSend == null && nullToAbsent + ? const Value.absent() + : Value(lastMessageSend), + lastMessageReceived: lastMessageReceived == null && nullToAbsent + ? const Value.absent() + : Value(lastMessageReceived), + lastFlameCounterChange: lastFlameCounterChange == null && nullToAbsent + ? const Value.absent() + : Value(lastFlameCounterChange), + lastFlameSync: lastFlameSync == null && nullToAbsent + ? const Value.absent() + : Value(lastFlameSync), + flameCounter: Value(flameCounter), + maxFlameCounter: Value(maxFlameCounter), + maxFlameCounterFrom: maxFlameCounterFrom == null && nullToAbsent + ? const Value.absent() + : Value(maxFlameCounterFrom), + lastMessageExchange: Value(lastMessageExchange), + ); + } + + factory GroupsData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return GroupsData( + groupId: serializer.fromJson(json['groupId']), + isGroupAdmin: serializer.fromJson(json['isGroupAdmin']), + isDirectChat: serializer.fromJson(json['isDirectChat']), + pinned: serializer.fromJson(json['pinned']), + archived: serializer.fromJson(json['archived']), + joinedGroup: serializer.fromJson(json['joinedGroup']), + leftGroup: serializer.fromJson(json['leftGroup']), + deletedContent: serializer.fromJson(json['deletedContent']), + stateVersionId: serializer.fromJson(json['stateVersionId']), + stateEncryptionKey: serializer.fromJson( + json['stateEncryptionKey'], + ), + myGroupPrivateKey: serializer.fromJson( + json['myGroupPrivateKey'], + ), + groupName: serializer.fromJson(json['groupName']), + draftMessage: serializer.fromJson(json['draftMessage']), + totalMediaCounter: serializer.fromJson(json['totalMediaCounter']), + alsoBestFriend: serializer.fromJson(json['alsoBestFriend']), + deleteMessagesAfterMilliseconds: serializer.fromJson( + json['deleteMessagesAfterMilliseconds'], + ), + createdAt: serializer.fromJson(json['createdAt']), + lastMessageSend: serializer.fromJson(json['lastMessageSend']), + lastMessageReceived: serializer.fromJson( + json['lastMessageReceived'], + ), + lastFlameCounterChange: serializer.fromJson( + json['lastFlameCounterChange'], + ), + lastFlameSync: serializer.fromJson(json['lastFlameSync']), + flameCounter: serializer.fromJson(json['flameCounter']), + maxFlameCounter: serializer.fromJson(json['maxFlameCounter']), + maxFlameCounterFrom: serializer.fromJson( + json['maxFlameCounterFrom'], + ), + lastMessageExchange: serializer.fromJson( + json['lastMessageExchange'], + ), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'groupId': serializer.toJson(groupId), + 'isGroupAdmin': serializer.toJson(isGroupAdmin), + 'isDirectChat': serializer.toJson(isDirectChat), + 'pinned': serializer.toJson(pinned), + 'archived': serializer.toJson(archived), + 'joinedGroup': serializer.toJson(joinedGroup), + 'leftGroup': serializer.toJson(leftGroup), + 'deletedContent': serializer.toJson(deletedContent), + 'stateVersionId': serializer.toJson(stateVersionId), + 'stateEncryptionKey': serializer.toJson( + stateEncryptionKey, + ), + 'myGroupPrivateKey': serializer.toJson(myGroupPrivateKey), + 'groupName': serializer.toJson(groupName), + 'draftMessage': serializer.toJson(draftMessage), + 'totalMediaCounter': serializer.toJson(totalMediaCounter), + 'alsoBestFriend': serializer.toJson(alsoBestFriend), + 'deleteMessagesAfterMilliseconds': serializer.toJson( + deleteMessagesAfterMilliseconds, + ), + 'createdAt': serializer.toJson(createdAt), + 'lastMessageSend': serializer.toJson(lastMessageSend), + 'lastMessageReceived': serializer.toJson(lastMessageReceived), + 'lastFlameCounterChange': serializer.toJson(lastFlameCounterChange), + 'lastFlameSync': serializer.toJson(lastFlameSync), + 'flameCounter': serializer.toJson(flameCounter), + 'maxFlameCounter': serializer.toJson(maxFlameCounter), + 'maxFlameCounterFrom': serializer.toJson(maxFlameCounterFrom), + 'lastMessageExchange': serializer.toJson(lastMessageExchange), + }; + } + + GroupsData copyWith({ + String? groupId, + int? isGroupAdmin, + int? isDirectChat, + int? pinned, + int? archived, + int? joinedGroup, + int? leftGroup, + int? deletedContent, + int? stateVersionId, + Value stateEncryptionKey = const Value.absent(), + Value myGroupPrivateKey = const Value.absent(), + String? groupName, + Value draftMessage = const Value.absent(), + int? totalMediaCounter, + int? alsoBestFriend, + int? deleteMessagesAfterMilliseconds, + int? createdAt, + Value lastMessageSend = const Value.absent(), + Value lastMessageReceived = const Value.absent(), + Value lastFlameCounterChange = const Value.absent(), + Value lastFlameSync = const Value.absent(), + int? flameCounter, + int? maxFlameCounter, + Value maxFlameCounterFrom = const Value.absent(), + int? lastMessageExchange, + }) => GroupsData( + groupId: groupId ?? this.groupId, + isGroupAdmin: isGroupAdmin ?? this.isGroupAdmin, + isDirectChat: isDirectChat ?? this.isDirectChat, + pinned: pinned ?? this.pinned, + archived: archived ?? this.archived, + joinedGroup: joinedGroup ?? this.joinedGroup, + leftGroup: leftGroup ?? this.leftGroup, + deletedContent: deletedContent ?? this.deletedContent, + stateVersionId: stateVersionId ?? this.stateVersionId, + stateEncryptionKey: stateEncryptionKey.present + ? stateEncryptionKey.value + : this.stateEncryptionKey, + myGroupPrivateKey: myGroupPrivateKey.present + ? myGroupPrivateKey.value + : this.myGroupPrivateKey, + groupName: groupName ?? this.groupName, + draftMessage: draftMessage.present ? draftMessage.value : this.draftMessage, + totalMediaCounter: totalMediaCounter ?? this.totalMediaCounter, + alsoBestFriend: alsoBestFriend ?? this.alsoBestFriend, + deleteMessagesAfterMilliseconds: + deleteMessagesAfterMilliseconds ?? this.deleteMessagesAfterMilliseconds, + createdAt: createdAt ?? this.createdAt, + lastMessageSend: lastMessageSend.present + ? lastMessageSend.value + : this.lastMessageSend, + lastMessageReceived: lastMessageReceived.present + ? lastMessageReceived.value + : this.lastMessageReceived, + lastFlameCounterChange: lastFlameCounterChange.present + ? lastFlameCounterChange.value + : this.lastFlameCounterChange, + lastFlameSync: lastFlameSync.present + ? lastFlameSync.value + : this.lastFlameSync, + flameCounter: flameCounter ?? this.flameCounter, + maxFlameCounter: maxFlameCounter ?? this.maxFlameCounter, + maxFlameCounterFrom: maxFlameCounterFrom.present + ? maxFlameCounterFrom.value + : this.maxFlameCounterFrom, + lastMessageExchange: lastMessageExchange ?? this.lastMessageExchange, + ); + GroupsData copyWithCompanion(GroupsCompanion data) { + return GroupsData( + groupId: data.groupId.present ? data.groupId.value : this.groupId, + isGroupAdmin: data.isGroupAdmin.present + ? data.isGroupAdmin.value + : this.isGroupAdmin, + isDirectChat: data.isDirectChat.present + ? data.isDirectChat.value + : this.isDirectChat, + pinned: data.pinned.present ? data.pinned.value : this.pinned, + archived: data.archived.present ? data.archived.value : this.archived, + joinedGroup: data.joinedGroup.present + ? data.joinedGroup.value + : this.joinedGroup, + leftGroup: data.leftGroup.present ? data.leftGroup.value : this.leftGroup, + deletedContent: data.deletedContent.present + ? data.deletedContent.value + : this.deletedContent, + stateVersionId: data.stateVersionId.present + ? data.stateVersionId.value + : this.stateVersionId, + stateEncryptionKey: data.stateEncryptionKey.present + ? data.stateEncryptionKey.value + : this.stateEncryptionKey, + myGroupPrivateKey: data.myGroupPrivateKey.present + ? data.myGroupPrivateKey.value + : this.myGroupPrivateKey, + groupName: data.groupName.present ? data.groupName.value : this.groupName, + draftMessage: data.draftMessage.present + ? data.draftMessage.value + : this.draftMessage, + totalMediaCounter: data.totalMediaCounter.present + ? data.totalMediaCounter.value + : this.totalMediaCounter, + alsoBestFriend: data.alsoBestFriend.present + ? data.alsoBestFriend.value + : this.alsoBestFriend, + deleteMessagesAfterMilliseconds: + data.deleteMessagesAfterMilliseconds.present + ? data.deleteMessagesAfterMilliseconds.value + : this.deleteMessagesAfterMilliseconds, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + lastMessageSend: data.lastMessageSend.present + ? data.lastMessageSend.value + : this.lastMessageSend, + lastMessageReceived: data.lastMessageReceived.present + ? data.lastMessageReceived.value + : this.lastMessageReceived, + lastFlameCounterChange: data.lastFlameCounterChange.present + ? data.lastFlameCounterChange.value + : this.lastFlameCounterChange, + lastFlameSync: data.lastFlameSync.present + ? data.lastFlameSync.value + : this.lastFlameSync, + flameCounter: data.flameCounter.present + ? data.flameCounter.value + : this.flameCounter, + maxFlameCounter: data.maxFlameCounter.present + ? data.maxFlameCounter.value + : this.maxFlameCounter, + maxFlameCounterFrom: data.maxFlameCounterFrom.present + ? data.maxFlameCounterFrom.value + : this.maxFlameCounterFrom, + lastMessageExchange: data.lastMessageExchange.present + ? data.lastMessageExchange.value + : this.lastMessageExchange, + ); + } + + @override + String toString() { + return (StringBuffer('GroupsData(') + ..write('groupId: $groupId, ') + ..write('isGroupAdmin: $isGroupAdmin, ') + ..write('isDirectChat: $isDirectChat, ') + ..write('pinned: $pinned, ') + ..write('archived: $archived, ') + ..write('joinedGroup: $joinedGroup, ') + ..write('leftGroup: $leftGroup, ') + ..write('deletedContent: $deletedContent, ') + ..write('stateVersionId: $stateVersionId, ') + ..write('stateEncryptionKey: $stateEncryptionKey, ') + ..write('myGroupPrivateKey: $myGroupPrivateKey, ') + ..write('groupName: $groupName, ') + ..write('draftMessage: $draftMessage, ') + ..write('totalMediaCounter: $totalMediaCounter, ') + ..write('alsoBestFriend: $alsoBestFriend, ') + ..write( + 'deleteMessagesAfterMilliseconds: $deleteMessagesAfterMilliseconds, ', + ) + ..write('createdAt: $createdAt, ') + ..write('lastMessageSend: $lastMessageSend, ') + ..write('lastMessageReceived: $lastMessageReceived, ') + ..write('lastFlameCounterChange: $lastFlameCounterChange, ') + ..write('lastFlameSync: $lastFlameSync, ') + ..write('flameCounter: $flameCounter, ') + ..write('maxFlameCounter: $maxFlameCounter, ') + ..write('maxFlameCounterFrom: $maxFlameCounterFrom, ') + ..write('lastMessageExchange: $lastMessageExchange') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hashAll([ + groupId, + isGroupAdmin, + isDirectChat, + pinned, + archived, + joinedGroup, + leftGroup, + deletedContent, + stateVersionId, + $driftBlobEquality.hash(stateEncryptionKey), + $driftBlobEquality.hash(myGroupPrivateKey), + groupName, + draftMessage, + totalMediaCounter, + alsoBestFriend, + deleteMessagesAfterMilliseconds, + createdAt, + lastMessageSend, + lastMessageReceived, + lastFlameCounterChange, + lastFlameSync, + flameCounter, + maxFlameCounter, + maxFlameCounterFrom, + lastMessageExchange, + ]); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is GroupsData && + other.groupId == this.groupId && + other.isGroupAdmin == this.isGroupAdmin && + other.isDirectChat == this.isDirectChat && + other.pinned == this.pinned && + other.archived == this.archived && + other.joinedGroup == this.joinedGroup && + other.leftGroup == this.leftGroup && + other.deletedContent == this.deletedContent && + other.stateVersionId == this.stateVersionId && + $driftBlobEquality.equals( + other.stateEncryptionKey, + this.stateEncryptionKey, + ) && + $driftBlobEquality.equals( + other.myGroupPrivateKey, + this.myGroupPrivateKey, + ) && + other.groupName == this.groupName && + other.draftMessage == this.draftMessage && + other.totalMediaCounter == this.totalMediaCounter && + other.alsoBestFriend == this.alsoBestFriend && + other.deleteMessagesAfterMilliseconds == + this.deleteMessagesAfterMilliseconds && + other.createdAt == this.createdAt && + other.lastMessageSend == this.lastMessageSend && + other.lastMessageReceived == this.lastMessageReceived && + other.lastFlameCounterChange == this.lastFlameCounterChange && + other.lastFlameSync == this.lastFlameSync && + other.flameCounter == this.flameCounter && + other.maxFlameCounter == this.maxFlameCounter && + other.maxFlameCounterFrom == this.maxFlameCounterFrom && + other.lastMessageExchange == this.lastMessageExchange); +} + +class GroupsCompanion extends UpdateCompanion { + final Value groupId; + final Value isGroupAdmin; + final Value isDirectChat; + final Value pinned; + final Value archived; + final Value joinedGroup; + final Value leftGroup; + final Value deletedContent; + final Value stateVersionId; + final Value stateEncryptionKey; + final Value myGroupPrivateKey; + final Value groupName; + final Value draftMessage; + final Value totalMediaCounter; + final Value alsoBestFriend; + final Value deleteMessagesAfterMilliseconds; + final Value createdAt; + final Value lastMessageSend; + final Value lastMessageReceived; + final Value lastFlameCounterChange; + final Value lastFlameSync; + final Value flameCounter; + final Value maxFlameCounter; + final Value maxFlameCounterFrom; + final Value lastMessageExchange; + final Value rowid; + const GroupsCompanion({ + this.groupId = const Value.absent(), + this.isGroupAdmin = const Value.absent(), + this.isDirectChat = const Value.absent(), + this.pinned = const Value.absent(), + this.archived = const Value.absent(), + this.joinedGroup = const Value.absent(), + this.leftGroup = const Value.absent(), + this.deletedContent = const Value.absent(), + this.stateVersionId = const Value.absent(), + this.stateEncryptionKey = const Value.absent(), + this.myGroupPrivateKey = const Value.absent(), + this.groupName = const Value.absent(), + this.draftMessage = const Value.absent(), + this.totalMediaCounter = const Value.absent(), + this.alsoBestFriend = const Value.absent(), + this.deleteMessagesAfterMilliseconds = const Value.absent(), + this.createdAt = const Value.absent(), + this.lastMessageSend = const Value.absent(), + this.lastMessageReceived = const Value.absent(), + this.lastFlameCounterChange = const Value.absent(), + this.lastFlameSync = const Value.absent(), + this.flameCounter = const Value.absent(), + this.maxFlameCounter = const Value.absent(), + this.maxFlameCounterFrom = const Value.absent(), + this.lastMessageExchange = const Value.absent(), + this.rowid = const Value.absent(), + }); + GroupsCompanion.insert({ + required String groupId, + this.isGroupAdmin = const Value.absent(), + this.isDirectChat = const Value.absent(), + this.pinned = const Value.absent(), + this.archived = const Value.absent(), + this.joinedGroup = const Value.absent(), + this.leftGroup = const Value.absent(), + this.deletedContent = const Value.absent(), + this.stateVersionId = const Value.absent(), + this.stateEncryptionKey = const Value.absent(), + this.myGroupPrivateKey = const Value.absent(), + required String groupName, + this.draftMessage = const Value.absent(), + this.totalMediaCounter = const Value.absent(), + this.alsoBestFriend = const Value.absent(), + this.deleteMessagesAfterMilliseconds = const Value.absent(), + this.createdAt = const Value.absent(), + this.lastMessageSend = const Value.absent(), + this.lastMessageReceived = const Value.absent(), + this.lastFlameCounterChange = const Value.absent(), + this.lastFlameSync = const Value.absent(), + this.flameCounter = const Value.absent(), + this.maxFlameCounter = const Value.absent(), + this.maxFlameCounterFrom = const Value.absent(), + this.lastMessageExchange = const Value.absent(), + this.rowid = const Value.absent(), + }) : groupId = Value(groupId), + groupName = Value(groupName); + static Insertable custom({ + Expression? groupId, + Expression? isGroupAdmin, + Expression? isDirectChat, + Expression? pinned, + Expression? archived, + Expression? joinedGroup, + Expression? leftGroup, + Expression? deletedContent, + Expression? stateVersionId, + Expression? stateEncryptionKey, + Expression? myGroupPrivateKey, + Expression? groupName, + Expression? draftMessage, + Expression? totalMediaCounter, + Expression? alsoBestFriend, + Expression? deleteMessagesAfterMilliseconds, + Expression? createdAt, + Expression? lastMessageSend, + Expression? lastMessageReceived, + Expression? lastFlameCounterChange, + Expression? lastFlameSync, + Expression? flameCounter, + Expression? maxFlameCounter, + Expression? maxFlameCounterFrom, + Expression? lastMessageExchange, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (groupId != null) 'group_id': groupId, + if (isGroupAdmin != null) 'is_group_admin': isGroupAdmin, + if (isDirectChat != null) 'is_direct_chat': isDirectChat, + if (pinned != null) 'pinned': pinned, + if (archived != null) 'archived': archived, + if (joinedGroup != null) 'joined_group': joinedGroup, + if (leftGroup != null) 'left_group': leftGroup, + if (deletedContent != null) 'deleted_content': deletedContent, + if (stateVersionId != null) 'state_version_id': stateVersionId, + if (stateEncryptionKey != null) + 'state_encryption_key': stateEncryptionKey, + if (myGroupPrivateKey != null) 'my_group_private_key': myGroupPrivateKey, + if (groupName != null) 'group_name': groupName, + if (draftMessage != null) 'draft_message': draftMessage, + if (totalMediaCounter != null) 'total_media_counter': totalMediaCounter, + if (alsoBestFriend != null) 'also_best_friend': alsoBestFriend, + if (deleteMessagesAfterMilliseconds != null) + 'delete_messages_after_milliseconds': deleteMessagesAfterMilliseconds, + if (createdAt != null) 'created_at': createdAt, + if (lastMessageSend != null) 'last_message_send': lastMessageSend, + if (lastMessageReceived != null) + 'last_message_received': lastMessageReceived, + if (lastFlameCounterChange != null) + 'last_flame_counter_change': lastFlameCounterChange, + if (lastFlameSync != null) 'last_flame_sync': lastFlameSync, + if (flameCounter != null) 'flame_counter': flameCounter, + if (maxFlameCounter != null) 'max_flame_counter': maxFlameCounter, + if (maxFlameCounterFrom != null) + 'max_flame_counter_from': maxFlameCounterFrom, + if (lastMessageExchange != null) + 'last_message_exchange': lastMessageExchange, + if (rowid != null) 'rowid': rowid, + }); + } + + GroupsCompanion copyWith({ + Value? groupId, + Value? isGroupAdmin, + Value? isDirectChat, + Value? pinned, + Value? archived, + Value? joinedGroup, + Value? leftGroup, + Value? deletedContent, + Value? stateVersionId, + Value? stateEncryptionKey, + Value? myGroupPrivateKey, + Value? groupName, + Value? draftMessage, + Value? totalMediaCounter, + Value? alsoBestFriend, + Value? deleteMessagesAfterMilliseconds, + Value? createdAt, + Value? lastMessageSend, + Value? lastMessageReceived, + Value? lastFlameCounterChange, + Value? lastFlameSync, + Value? flameCounter, + Value? maxFlameCounter, + Value? maxFlameCounterFrom, + Value? lastMessageExchange, + Value? rowid, + }) { + return GroupsCompanion( + groupId: groupId ?? this.groupId, + isGroupAdmin: isGroupAdmin ?? this.isGroupAdmin, + isDirectChat: isDirectChat ?? this.isDirectChat, + pinned: pinned ?? this.pinned, + archived: archived ?? this.archived, + joinedGroup: joinedGroup ?? this.joinedGroup, + leftGroup: leftGroup ?? this.leftGroup, + deletedContent: deletedContent ?? this.deletedContent, + stateVersionId: stateVersionId ?? this.stateVersionId, + stateEncryptionKey: stateEncryptionKey ?? this.stateEncryptionKey, + myGroupPrivateKey: myGroupPrivateKey ?? this.myGroupPrivateKey, + groupName: groupName ?? this.groupName, + draftMessage: draftMessage ?? this.draftMessage, + totalMediaCounter: totalMediaCounter ?? this.totalMediaCounter, + alsoBestFriend: alsoBestFriend ?? this.alsoBestFriend, + deleteMessagesAfterMilliseconds: + deleteMessagesAfterMilliseconds ?? + this.deleteMessagesAfterMilliseconds, + createdAt: createdAt ?? this.createdAt, + lastMessageSend: lastMessageSend ?? this.lastMessageSend, + lastMessageReceived: lastMessageReceived ?? this.lastMessageReceived, + lastFlameCounterChange: + lastFlameCounterChange ?? this.lastFlameCounterChange, + lastFlameSync: lastFlameSync ?? this.lastFlameSync, + flameCounter: flameCounter ?? this.flameCounter, + maxFlameCounter: maxFlameCounter ?? this.maxFlameCounter, + maxFlameCounterFrom: maxFlameCounterFrom ?? this.maxFlameCounterFrom, + lastMessageExchange: lastMessageExchange ?? this.lastMessageExchange, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (groupId.present) { + map['group_id'] = Variable(groupId.value); + } + if (isGroupAdmin.present) { + map['is_group_admin'] = Variable(isGroupAdmin.value); + } + if (isDirectChat.present) { + map['is_direct_chat'] = Variable(isDirectChat.value); + } + if (pinned.present) { + map['pinned'] = Variable(pinned.value); + } + if (archived.present) { + map['archived'] = Variable(archived.value); + } + if (joinedGroup.present) { + map['joined_group'] = Variable(joinedGroup.value); + } + if (leftGroup.present) { + map['left_group'] = Variable(leftGroup.value); + } + if (deletedContent.present) { + map['deleted_content'] = Variable(deletedContent.value); + } + if (stateVersionId.present) { + map['state_version_id'] = Variable(stateVersionId.value); + } + if (stateEncryptionKey.present) { + map['state_encryption_key'] = Variable( + stateEncryptionKey.value, + ); + } + if (myGroupPrivateKey.present) { + map['my_group_private_key'] = Variable( + myGroupPrivateKey.value, + ); + } + if (groupName.present) { + map['group_name'] = Variable(groupName.value); + } + if (draftMessage.present) { + map['draft_message'] = Variable(draftMessage.value); + } + if (totalMediaCounter.present) { + map['total_media_counter'] = Variable(totalMediaCounter.value); + } + if (alsoBestFriend.present) { + map['also_best_friend'] = Variable(alsoBestFriend.value); + } + if (deleteMessagesAfterMilliseconds.present) { + map['delete_messages_after_milliseconds'] = Variable( + deleteMessagesAfterMilliseconds.value, + ); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (lastMessageSend.present) { + map['last_message_send'] = Variable(lastMessageSend.value); + } + if (lastMessageReceived.present) { + map['last_message_received'] = Variable(lastMessageReceived.value); + } + if (lastFlameCounterChange.present) { + map['last_flame_counter_change'] = Variable( + lastFlameCounterChange.value, + ); + } + if (lastFlameSync.present) { + map['last_flame_sync'] = Variable(lastFlameSync.value); + } + if (flameCounter.present) { + map['flame_counter'] = Variable(flameCounter.value); + } + if (maxFlameCounter.present) { + map['max_flame_counter'] = Variable(maxFlameCounter.value); + } + if (maxFlameCounterFrom.present) { + map['max_flame_counter_from'] = Variable(maxFlameCounterFrom.value); + } + if (lastMessageExchange.present) { + map['last_message_exchange'] = Variable(lastMessageExchange.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('GroupsCompanion(') + ..write('groupId: $groupId, ') + ..write('isGroupAdmin: $isGroupAdmin, ') + ..write('isDirectChat: $isDirectChat, ') + ..write('pinned: $pinned, ') + ..write('archived: $archived, ') + ..write('joinedGroup: $joinedGroup, ') + ..write('leftGroup: $leftGroup, ') + ..write('deletedContent: $deletedContent, ') + ..write('stateVersionId: $stateVersionId, ') + ..write('stateEncryptionKey: $stateEncryptionKey, ') + ..write('myGroupPrivateKey: $myGroupPrivateKey, ') + ..write('groupName: $groupName, ') + ..write('draftMessage: $draftMessage, ') + ..write('totalMediaCounter: $totalMediaCounter, ') + ..write('alsoBestFriend: $alsoBestFriend, ') + ..write( + 'deleteMessagesAfterMilliseconds: $deleteMessagesAfterMilliseconds, ', + ) + ..write('createdAt: $createdAt, ') + ..write('lastMessageSend: $lastMessageSend, ') + ..write('lastMessageReceived: $lastMessageReceived, ') + ..write('lastFlameCounterChange: $lastFlameCounterChange, ') + ..write('lastFlameSync: $lastFlameSync, ') + ..write('flameCounter: $flameCounter, ') + ..write('maxFlameCounter: $maxFlameCounter, ') + ..write('maxFlameCounterFrom: $maxFlameCounterFrom, ') + ..write('lastMessageExchange: $lastMessageExchange, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class MediaFiles extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + MediaFiles(this.attachedDatabase, [this._alias]); + late final GeneratedColumn mediaId = GeneratedColumn( + 'media_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn uploadState = GeneratedColumn( + 'upload_state', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn downloadState = GeneratedColumn( + 'download_state', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn requiresAuthentication = GeneratedColumn( + 'requires_authentication', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT 0 CHECK (requires_authentication IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn stored = GeneratedColumn( + 'stored', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK ("stored" IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn isDraftMedia = GeneratedColumn( + 'is_draft_media', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_draft_media IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_favorite IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn hasCropAnalyzed = GeneratedColumn( + 'has_crop_analyzed', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT 0 CHECK (has_crop_analyzed IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn preProgressingProcess = GeneratedColumn( + 'pre_progressing_process', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn reuploadRequestedBy = + GeneratedColumn( + 'reupload_requested_by', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn displayLimitInMilliseconds = + GeneratedColumn( + 'display_limit_in_milliseconds', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn removeAudio = GeneratedColumn( + 'remove_audio', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL CHECK (remove_audio IN (0, 1))', + ); + late final GeneratedColumn downloadToken = + GeneratedColumn( + 'download_token', + aliasedName, + true, + type: DriftSqlType.blob, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn encryptionKey = + GeneratedColumn( + 'encryption_key', + aliasedName, + true, + type: DriftSqlType.blob, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn encryptionMac = + GeneratedColumn( + 'encryption_mac', + aliasedName, + true, + type: DriftSqlType.blob, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn encryptionNonce = + GeneratedColumn( + 'encryption_nonce', + aliasedName, + true, + type: DriftSqlType.blob, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn storedFileHash = + GeneratedColumn( + 'stored_file_hash', + aliasedName, + true, + type: DriftSqlType.blob, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn hasThumbnail = GeneratedColumn( + 'has_thumbnail', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (has_thumbnail IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn sizeInBytes = GeneratedColumn( + 'size_in_bytes', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT (CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER))', + defaultValue: const CustomExpression( + 'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)', + ), + ); + late final GeneratedColumn createdAtMonth = GeneratedColumn( + 'created_at_month', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [ + mediaId, + type, + uploadState, + downloadState, + requiresAuthentication, + stored, + isDraftMedia, + isFavorite, + hasCropAnalyzed, + preProgressingProcess, + reuploadRequestedBy, + displayLimitInMilliseconds, + removeAudio, + downloadToken, + encryptionKey, + encryptionMac, + encryptionNonce, + storedFileHash, + hasThumbnail, + sizeInBytes, + createdAt, + createdAtMonth, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'media_files'; + @override + Set get $primaryKey => {mediaId}; + @override + MediaFilesData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MediaFilesData( + mediaId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}media_id'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}type'], + )!, + uploadState: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}upload_state'], + ), + downloadState: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}download_state'], + ), + requiresAuthentication: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}requires_authentication'], + )!, + stored: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}stored'], + )!, + isDraftMedia: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}is_draft_media'], + )!, + isFavorite: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}is_favorite'], + )!, + hasCropAnalyzed: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}has_crop_analyzed'], + )!, + preProgressingProcess: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}pre_progressing_process'], + ), + reuploadRequestedBy: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}reupload_requested_by'], + ), + displayLimitInMilliseconds: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}display_limit_in_milliseconds'], + ), + removeAudio: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}remove_audio'], + ), + downloadToken: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}download_token'], + ), + encryptionKey: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}encryption_key'], + ), + encryptionMac: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}encryption_mac'], + ), + encryptionNonce: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}encryption_nonce'], + ), + storedFileHash: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}stored_file_hash'], + ), + hasThumbnail: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}has_thumbnail'], + )!, + sizeInBytes: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}size_in_bytes'], + ), + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}created_at'], + )!, + createdAtMonth: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}created_at_month'], + ), + ); + } + + @override + MediaFiles createAlias(String alias) { + return MediaFiles(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(media_id)']; + @override + bool get dontWriteConstraints => true; +} + +class MediaFilesData extends DataClass implements Insertable { + final String mediaId; + final String type; + final String? uploadState; + final String? downloadState; + final int requiresAuthentication; + final int stored; + final int isDraftMedia; + final int isFavorite; + final int hasCropAnalyzed; + final int? preProgressingProcess; + final String? reuploadRequestedBy; + final int? displayLimitInMilliseconds; + final int? removeAudio; + final i2.Uint8List? downloadToken; + final i2.Uint8List? encryptionKey; + final i2.Uint8List? encryptionMac; + final i2.Uint8List? encryptionNonce; + final i2.Uint8List? storedFileHash; + final int hasThumbnail; + final int? sizeInBytes; + final int createdAt; + final String? createdAtMonth; + const MediaFilesData({ + required this.mediaId, + required this.type, + this.uploadState, + this.downloadState, + required this.requiresAuthentication, + required this.stored, + required this.isDraftMedia, + required this.isFavorite, + required this.hasCropAnalyzed, + this.preProgressingProcess, + this.reuploadRequestedBy, + this.displayLimitInMilliseconds, + this.removeAudio, + this.downloadToken, + this.encryptionKey, + this.encryptionMac, + this.encryptionNonce, + this.storedFileHash, + required this.hasThumbnail, + this.sizeInBytes, + required this.createdAt, + this.createdAtMonth, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['media_id'] = Variable(mediaId); + map['type'] = Variable(type); + if (!nullToAbsent || uploadState != null) { + map['upload_state'] = Variable(uploadState); + } + if (!nullToAbsent || downloadState != null) { + map['download_state'] = Variable(downloadState); + } + map['requires_authentication'] = Variable(requiresAuthentication); + map['stored'] = Variable(stored); + map['is_draft_media'] = Variable(isDraftMedia); + map['is_favorite'] = Variable(isFavorite); + map['has_crop_analyzed'] = Variable(hasCropAnalyzed); + if (!nullToAbsent || preProgressingProcess != null) { + map['pre_progressing_process'] = Variable(preProgressingProcess); + } + if (!nullToAbsent || reuploadRequestedBy != null) { + map['reupload_requested_by'] = Variable(reuploadRequestedBy); + } + if (!nullToAbsent || displayLimitInMilliseconds != null) { + map['display_limit_in_milliseconds'] = Variable( + displayLimitInMilliseconds, + ); + } + if (!nullToAbsent || removeAudio != null) { + map['remove_audio'] = Variable(removeAudio); + } + if (!nullToAbsent || downloadToken != null) { + map['download_token'] = Variable(downloadToken); + } + if (!nullToAbsent || encryptionKey != null) { + map['encryption_key'] = Variable(encryptionKey); + } + if (!nullToAbsent || encryptionMac != null) { + map['encryption_mac'] = Variable(encryptionMac); + } + if (!nullToAbsent || encryptionNonce != null) { + map['encryption_nonce'] = Variable(encryptionNonce); + } + if (!nullToAbsent || storedFileHash != null) { + map['stored_file_hash'] = Variable(storedFileHash); + } + map['has_thumbnail'] = Variable(hasThumbnail); + if (!nullToAbsent || sizeInBytes != null) { + map['size_in_bytes'] = Variable(sizeInBytes); + } + map['created_at'] = Variable(createdAt); + if (!nullToAbsent || createdAtMonth != null) { + map['created_at_month'] = Variable(createdAtMonth); + } + return map; + } + + MediaFilesCompanion toCompanion(bool nullToAbsent) { + return MediaFilesCompanion( + mediaId: Value(mediaId), + type: Value(type), + uploadState: uploadState == null && nullToAbsent + ? const Value.absent() + : Value(uploadState), + downloadState: downloadState == null && nullToAbsent + ? const Value.absent() + : Value(downloadState), + requiresAuthentication: Value(requiresAuthentication), + stored: Value(stored), + isDraftMedia: Value(isDraftMedia), + isFavorite: Value(isFavorite), + hasCropAnalyzed: Value(hasCropAnalyzed), + preProgressingProcess: preProgressingProcess == null && nullToAbsent + ? const Value.absent() + : Value(preProgressingProcess), + reuploadRequestedBy: reuploadRequestedBy == null && nullToAbsent + ? const Value.absent() + : Value(reuploadRequestedBy), + displayLimitInMilliseconds: + displayLimitInMilliseconds == null && nullToAbsent + ? const Value.absent() + : Value(displayLimitInMilliseconds), + removeAudio: removeAudio == null && nullToAbsent + ? const Value.absent() + : Value(removeAudio), + downloadToken: downloadToken == null && nullToAbsent + ? const Value.absent() + : Value(downloadToken), + encryptionKey: encryptionKey == null && nullToAbsent + ? const Value.absent() + : Value(encryptionKey), + encryptionMac: encryptionMac == null && nullToAbsent + ? const Value.absent() + : Value(encryptionMac), + encryptionNonce: encryptionNonce == null && nullToAbsent + ? const Value.absent() + : Value(encryptionNonce), + storedFileHash: storedFileHash == null && nullToAbsent + ? const Value.absent() + : Value(storedFileHash), + hasThumbnail: Value(hasThumbnail), + sizeInBytes: sizeInBytes == null && nullToAbsent + ? const Value.absent() + : Value(sizeInBytes), + createdAt: Value(createdAt), + createdAtMonth: createdAtMonth == null && nullToAbsent + ? const Value.absent() + : Value(createdAtMonth), + ); + } + + factory MediaFilesData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MediaFilesData( + mediaId: serializer.fromJson(json['mediaId']), + type: serializer.fromJson(json['type']), + uploadState: serializer.fromJson(json['uploadState']), + downloadState: serializer.fromJson(json['downloadState']), + requiresAuthentication: serializer.fromJson( + json['requiresAuthentication'], + ), + stored: serializer.fromJson(json['stored']), + isDraftMedia: serializer.fromJson(json['isDraftMedia']), + isFavorite: serializer.fromJson(json['isFavorite']), + hasCropAnalyzed: serializer.fromJson(json['hasCropAnalyzed']), + preProgressingProcess: serializer.fromJson( + json['preProgressingProcess'], + ), + reuploadRequestedBy: serializer.fromJson( + json['reuploadRequestedBy'], + ), + displayLimitInMilliseconds: serializer.fromJson( + json['displayLimitInMilliseconds'], + ), + removeAudio: serializer.fromJson(json['removeAudio']), + downloadToken: serializer.fromJson(json['downloadToken']), + encryptionKey: serializer.fromJson(json['encryptionKey']), + encryptionMac: serializer.fromJson(json['encryptionMac']), + encryptionNonce: serializer.fromJson( + json['encryptionNonce'], + ), + storedFileHash: serializer.fromJson( + json['storedFileHash'], + ), + hasThumbnail: serializer.fromJson(json['hasThumbnail']), + sizeInBytes: serializer.fromJson(json['sizeInBytes']), + createdAt: serializer.fromJson(json['createdAt']), + createdAtMonth: serializer.fromJson(json['createdAtMonth']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'mediaId': serializer.toJson(mediaId), + 'type': serializer.toJson(type), + 'uploadState': serializer.toJson(uploadState), + 'downloadState': serializer.toJson(downloadState), + 'requiresAuthentication': serializer.toJson(requiresAuthentication), + 'stored': serializer.toJson(stored), + 'isDraftMedia': serializer.toJson(isDraftMedia), + 'isFavorite': serializer.toJson(isFavorite), + 'hasCropAnalyzed': serializer.toJson(hasCropAnalyzed), + 'preProgressingProcess': serializer.toJson(preProgressingProcess), + 'reuploadRequestedBy': serializer.toJson(reuploadRequestedBy), + 'displayLimitInMilliseconds': serializer.toJson( + displayLimitInMilliseconds, + ), + 'removeAudio': serializer.toJson(removeAudio), + 'downloadToken': serializer.toJson(downloadToken), + 'encryptionKey': serializer.toJson(encryptionKey), + 'encryptionMac': serializer.toJson(encryptionMac), + 'encryptionNonce': serializer.toJson(encryptionNonce), + 'storedFileHash': serializer.toJson(storedFileHash), + 'hasThumbnail': serializer.toJson(hasThumbnail), + 'sizeInBytes': serializer.toJson(sizeInBytes), + 'createdAt': serializer.toJson(createdAt), + 'createdAtMonth': serializer.toJson(createdAtMonth), + }; + } + + MediaFilesData copyWith({ + String? mediaId, + String? type, + Value uploadState = const Value.absent(), + Value downloadState = const Value.absent(), + int? requiresAuthentication, + int? stored, + int? isDraftMedia, + int? isFavorite, + int? hasCropAnalyzed, + Value preProgressingProcess = const Value.absent(), + Value reuploadRequestedBy = const Value.absent(), + Value displayLimitInMilliseconds = const Value.absent(), + Value removeAudio = const Value.absent(), + Value downloadToken = const Value.absent(), + Value encryptionKey = const Value.absent(), + Value encryptionMac = const Value.absent(), + Value encryptionNonce = const Value.absent(), + Value storedFileHash = const Value.absent(), + int? hasThumbnail, + Value sizeInBytes = const Value.absent(), + int? createdAt, + Value createdAtMonth = const Value.absent(), + }) => MediaFilesData( + mediaId: mediaId ?? this.mediaId, + type: type ?? this.type, + uploadState: uploadState.present ? uploadState.value : this.uploadState, + downloadState: downloadState.present + ? downloadState.value + : this.downloadState, + requiresAuthentication: + requiresAuthentication ?? this.requiresAuthentication, + stored: stored ?? this.stored, + isDraftMedia: isDraftMedia ?? this.isDraftMedia, + isFavorite: isFavorite ?? this.isFavorite, + hasCropAnalyzed: hasCropAnalyzed ?? this.hasCropAnalyzed, + preProgressingProcess: preProgressingProcess.present + ? preProgressingProcess.value + : this.preProgressingProcess, + reuploadRequestedBy: reuploadRequestedBy.present + ? reuploadRequestedBy.value + : this.reuploadRequestedBy, + displayLimitInMilliseconds: displayLimitInMilliseconds.present + ? displayLimitInMilliseconds.value + : this.displayLimitInMilliseconds, + removeAudio: removeAudio.present ? removeAudio.value : this.removeAudio, + downloadToken: downloadToken.present + ? downloadToken.value + : this.downloadToken, + encryptionKey: encryptionKey.present + ? encryptionKey.value + : this.encryptionKey, + encryptionMac: encryptionMac.present + ? encryptionMac.value + : this.encryptionMac, + encryptionNonce: encryptionNonce.present + ? encryptionNonce.value + : this.encryptionNonce, + storedFileHash: storedFileHash.present + ? storedFileHash.value + : this.storedFileHash, + hasThumbnail: hasThumbnail ?? this.hasThumbnail, + sizeInBytes: sizeInBytes.present ? sizeInBytes.value : this.sizeInBytes, + createdAt: createdAt ?? this.createdAt, + createdAtMonth: createdAtMonth.present + ? createdAtMonth.value + : this.createdAtMonth, + ); + MediaFilesData copyWithCompanion(MediaFilesCompanion data) { + return MediaFilesData( + mediaId: data.mediaId.present ? data.mediaId.value : this.mediaId, + type: data.type.present ? data.type.value : this.type, + uploadState: data.uploadState.present + ? data.uploadState.value + : this.uploadState, + downloadState: data.downloadState.present + ? data.downloadState.value + : this.downloadState, + requiresAuthentication: data.requiresAuthentication.present + ? data.requiresAuthentication.value + : this.requiresAuthentication, + stored: data.stored.present ? data.stored.value : this.stored, + isDraftMedia: data.isDraftMedia.present + ? data.isDraftMedia.value + : this.isDraftMedia, + isFavorite: data.isFavorite.present + ? data.isFavorite.value + : this.isFavorite, + hasCropAnalyzed: data.hasCropAnalyzed.present + ? data.hasCropAnalyzed.value + : this.hasCropAnalyzed, + preProgressingProcess: data.preProgressingProcess.present + ? data.preProgressingProcess.value + : this.preProgressingProcess, + reuploadRequestedBy: data.reuploadRequestedBy.present + ? data.reuploadRequestedBy.value + : this.reuploadRequestedBy, + displayLimitInMilliseconds: data.displayLimitInMilliseconds.present + ? data.displayLimitInMilliseconds.value + : this.displayLimitInMilliseconds, + removeAudio: data.removeAudio.present + ? data.removeAudio.value + : this.removeAudio, + downloadToken: data.downloadToken.present + ? data.downloadToken.value + : this.downloadToken, + encryptionKey: data.encryptionKey.present + ? data.encryptionKey.value + : this.encryptionKey, + encryptionMac: data.encryptionMac.present + ? data.encryptionMac.value + : this.encryptionMac, + encryptionNonce: data.encryptionNonce.present + ? data.encryptionNonce.value + : this.encryptionNonce, + storedFileHash: data.storedFileHash.present + ? data.storedFileHash.value + : this.storedFileHash, + hasThumbnail: data.hasThumbnail.present + ? data.hasThumbnail.value + : this.hasThumbnail, + sizeInBytes: data.sizeInBytes.present + ? data.sizeInBytes.value + : this.sizeInBytes, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + createdAtMonth: data.createdAtMonth.present + ? data.createdAtMonth.value + : this.createdAtMonth, + ); + } + + @override + String toString() { + return (StringBuffer('MediaFilesData(') + ..write('mediaId: $mediaId, ') + ..write('type: $type, ') + ..write('uploadState: $uploadState, ') + ..write('downloadState: $downloadState, ') + ..write('requiresAuthentication: $requiresAuthentication, ') + ..write('stored: $stored, ') + ..write('isDraftMedia: $isDraftMedia, ') + ..write('isFavorite: $isFavorite, ') + ..write('hasCropAnalyzed: $hasCropAnalyzed, ') + ..write('preProgressingProcess: $preProgressingProcess, ') + ..write('reuploadRequestedBy: $reuploadRequestedBy, ') + ..write('displayLimitInMilliseconds: $displayLimitInMilliseconds, ') + ..write('removeAudio: $removeAudio, ') + ..write('downloadToken: $downloadToken, ') + ..write('encryptionKey: $encryptionKey, ') + ..write('encryptionMac: $encryptionMac, ') + ..write('encryptionNonce: $encryptionNonce, ') + ..write('storedFileHash: $storedFileHash, ') + ..write('hasThumbnail: $hasThumbnail, ') + ..write('sizeInBytes: $sizeInBytes, ') + ..write('createdAt: $createdAt, ') + ..write('createdAtMonth: $createdAtMonth') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hashAll([ + mediaId, + type, + uploadState, + downloadState, + requiresAuthentication, + stored, + isDraftMedia, + isFavorite, + hasCropAnalyzed, + preProgressingProcess, + reuploadRequestedBy, + displayLimitInMilliseconds, + removeAudio, + $driftBlobEquality.hash(downloadToken), + $driftBlobEquality.hash(encryptionKey), + $driftBlobEquality.hash(encryptionMac), + $driftBlobEquality.hash(encryptionNonce), + $driftBlobEquality.hash(storedFileHash), + hasThumbnail, + sizeInBytes, + createdAt, + createdAtMonth, + ]); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MediaFilesData && + other.mediaId == this.mediaId && + other.type == this.type && + other.uploadState == this.uploadState && + other.downloadState == this.downloadState && + other.requiresAuthentication == this.requiresAuthentication && + other.stored == this.stored && + other.isDraftMedia == this.isDraftMedia && + other.isFavorite == this.isFavorite && + other.hasCropAnalyzed == this.hasCropAnalyzed && + other.preProgressingProcess == this.preProgressingProcess && + other.reuploadRequestedBy == this.reuploadRequestedBy && + other.displayLimitInMilliseconds == this.displayLimitInMilliseconds && + other.removeAudio == this.removeAudio && + $driftBlobEquality.equals(other.downloadToken, this.downloadToken) && + $driftBlobEquality.equals(other.encryptionKey, this.encryptionKey) && + $driftBlobEquality.equals(other.encryptionMac, this.encryptionMac) && + $driftBlobEquality.equals( + other.encryptionNonce, + this.encryptionNonce, + ) && + $driftBlobEquality.equals( + other.storedFileHash, + this.storedFileHash, + ) && + other.hasThumbnail == this.hasThumbnail && + other.sizeInBytes == this.sizeInBytes && + other.createdAt == this.createdAt && + other.createdAtMonth == this.createdAtMonth); +} + +class MediaFilesCompanion extends UpdateCompanion { + final Value mediaId; + final Value type; + final Value uploadState; + final Value downloadState; + final Value requiresAuthentication; + final Value stored; + final Value isDraftMedia; + final Value isFavorite; + final Value hasCropAnalyzed; + final Value preProgressingProcess; + final Value reuploadRequestedBy; + final Value displayLimitInMilliseconds; + final Value removeAudio; + final Value downloadToken; + final Value encryptionKey; + final Value encryptionMac; + final Value encryptionNonce; + final Value storedFileHash; + final Value hasThumbnail; + final Value sizeInBytes; + final Value createdAt; + final Value createdAtMonth; + final Value rowid; + const MediaFilesCompanion({ + this.mediaId = const Value.absent(), + this.type = const Value.absent(), + this.uploadState = const Value.absent(), + this.downloadState = const Value.absent(), + this.requiresAuthentication = const Value.absent(), + this.stored = const Value.absent(), + this.isDraftMedia = const Value.absent(), + this.isFavorite = const Value.absent(), + this.hasCropAnalyzed = const Value.absent(), + this.preProgressingProcess = const Value.absent(), + this.reuploadRequestedBy = const Value.absent(), + this.displayLimitInMilliseconds = const Value.absent(), + this.removeAudio = const Value.absent(), + this.downloadToken = const Value.absent(), + this.encryptionKey = const Value.absent(), + this.encryptionMac = const Value.absent(), + this.encryptionNonce = const Value.absent(), + this.storedFileHash = const Value.absent(), + this.hasThumbnail = const Value.absent(), + this.sizeInBytes = const Value.absent(), + this.createdAt = const Value.absent(), + this.createdAtMonth = const Value.absent(), + this.rowid = const Value.absent(), + }); + MediaFilesCompanion.insert({ + required String mediaId, + required String type, + this.uploadState = const Value.absent(), + this.downloadState = const Value.absent(), + this.requiresAuthentication = const Value.absent(), + this.stored = const Value.absent(), + this.isDraftMedia = const Value.absent(), + this.isFavorite = const Value.absent(), + this.hasCropAnalyzed = const Value.absent(), + this.preProgressingProcess = const Value.absent(), + this.reuploadRequestedBy = const Value.absent(), + this.displayLimitInMilliseconds = const Value.absent(), + this.removeAudio = const Value.absent(), + this.downloadToken = const Value.absent(), + this.encryptionKey = const Value.absent(), + this.encryptionMac = const Value.absent(), + this.encryptionNonce = const Value.absent(), + this.storedFileHash = const Value.absent(), + this.hasThumbnail = const Value.absent(), + this.sizeInBytes = const Value.absent(), + this.createdAt = const Value.absent(), + this.createdAtMonth = const Value.absent(), + this.rowid = const Value.absent(), + }) : mediaId = Value(mediaId), + type = Value(type); + static Insertable custom({ + Expression? mediaId, + Expression? type, + Expression? uploadState, + Expression? downloadState, + Expression? requiresAuthentication, + Expression? stored, + Expression? isDraftMedia, + Expression? isFavorite, + Expression? hasCropAnalyzed, + Expression? preProgressingProcess, + Expression? reuploadRequestedBy, + Expression? displayLimitInMilliseconds, + Expression? removeAudio, + Expression? downloadToken, + Expression? encryptionKey, + Expression? encryptionMac, + Expression? encryptionNonce, + Expression? storedFileHash, + Expression? hasThumbnail, + Expression? sizeInBytes, + Expression? createdAt, + Expression? createdAtMonth, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (mediaId != null) 'media_id': mediaId, + if (type != null) 'type': type, + if (uploadState != null) 'upload_state': uploadState, + if (downloadState != null) 'download_state': downloadState, + if (requiresAuthentication != null) + 'requires_authentication': requiresAuthentication, + if (stored != null) 'stored': stored, + if (isDraftMedia != null) 'is_draft_media': isDraftMedia, + if (isFavorite != null) 'is_favorite': isFavorite, + if (hasCropAnalyzed != null) 'has_crop_analyzed': hasCropAnalyzed, + if (preProgressingProcess != null) + 'pre_progressing_process': preProgressingProcess, + if (reuploadRequestedBy != null) + 'reupload_requested_by': reuploadRequestedBy, + if (displayLimitInMilliseconds != null) + 'display_limit_in_milliseconds': displayLimitInMilliseconds, + if (removeAudio != null) 'remove_audio': removeAudio, + if (downloadToken != null) 'download_token': downloadToken, + if (encryptionKey != null) 'encryption_key': encryptionKey, + if (encryptionMac != null) 'encryption_mac': encryptionMac, + if (encryptionNonce != null) 'encryption_nonce': encryptionNonce, + if (storedFileHash != null) 'stored_file_hash': storedFileHash, + if (hasThumbnail != null) 'has_thumbnail': hasThumbnail, + if (sizeInBytes != null) 'size_in_bytes': sizeInBytes, + if (createdAt != null) 'created_at': createdAt, + if (createdAtMonth != null) 'created_at_month': createdAtMonth, + if (rowid != null) 'rowid': rowid, + }); + } + + MediaFilesCompanion copyWith({ + Value? mediaId, + Value? type, + Value? uploadState, + Value? downloadState, + Value? requiresAuthentication, + Value? stored, + Value? isDraftMedia, + Value? isFavorite, + Value? hasCropAnalyzed, + Value? preProgressingProcess, + Value? reuploadRequestedBy, + Value? displayLimitInMilliseconds, + Value? removeAudio, + Value? downloadToken, + Value? encryptionKey, + Value? encryptionMac, + Value? encryptionNonce, + Value? storedFileHash, + Value? hasThumbnail, + Value? sizeInBytes, + Value? createdAt, + Value? createdAtMonth, + Value? rowid, + }) { + return MediaFilesCompanion( + mediaId: mediaId ?? this.mediaId, + type: type ?? this.type, + uploadState: uploadState ?? this.uploadState, + downloadState: downloadState ?? this.downloadState, + requiresAuthentication: + requiresAuthentication ?? this.requiresAuthentication, + stored: stored ?? this.stored, + isDraftMedia: isDraftMedia ?? this.isDraftMedia, + isFavorite: isFavorite ?? this.isFavorite, + hasCropAnalyzed: hasCropAnalyzed ?? this.hasCropAnalyzed, + preProgressingProcess: + preProgressingProcess ?? this.preProgressingProcess, + reuploadRequestedBy: reuploadRequestedBy ?? this.reuploadRequestedBy, + displayLimitInMilliseconds: + displayLimitInMilliseconds ?? this.displayLimitInMilliseconds, + removeAudio: removeAudio ?? this.removeAudio, + downloadToken: downloadToken ?? this.downloadToken, + encryptionKey: encryptionKey ?? this.encryptionKey, + encryptionMac: encryptionMac ?? this.encryptionMac, + encryptionNonce: encryptionNonce ?? this.encryptionNonce, + storedFileHash: storedFileHash ?? this.storedFileHash, + hasThumbnail: hasThumbnail ?? this.hasThumbnail, + sizeInBytes: sizeInBytes ?? this.sizeInBytes, + createdAt: createdAt ?? this.createdAt, + createdAtMonth: createdAtMonth ?? this.createdAtMonth, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (mediaId.present) { + map['media_id'] = Variable(mediaId.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (uploadState.present) { + map['upload_state'] = Variable(uploadState.value); + } + if (downloadState.present) { + map['download_state'] = Variable(downloadState.value); + } + if (requiresAuthentication.present) { + map['requires_authentication'] = Variable( + requiresAuthentication.value, + ); + } + if (stored.present) { + map['stored'] = Variable(stored.value); + } + if (isDraftMedia.present) { + map['is_draft_media'] = Variable(isDraftMedia.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (hasCropAnalyzed.present) { + map['has_crop_analyzed'] = Variable(hasCropAnalyzed.value); + } + if (preProgressingProcess.present) { + map['pre_progressing_process'] = Variable( + preProgressingProcess.value, + ); + } + if (reuploadRequestedBy.present) { + map['reupload_requested_by'] = Variable( + reuploadRequestedBy.value, + ); + } + if (displayLimitInMilliseconds.present) { + map['display_limit_in_milliseconds'] = Variable( + displayLimitInMilliseconds.value, + ); + } + if (removeAudio.present) { + map['remove_audio'] = Variable(removeAudio.value); + } + if (downloadToken.present) { + map['download_token'] = Variable(downloadToken.value); + } + if (encryptionKey.present) { + map['encryption_key'] = Variable(encryptionKey.value); + } + if (encryptionMac.present) { + map['encryption_mac'] = Variable(encryptionMac.value); + } + if (encryptionNonce.present) { + map['encryption_nonce'] = Variable(encryptionNonce.value); + } + if (storedFileHash.present) { + map['stored_file_hash'] = Variable(storedFileHash.value); + } + if (hasThumbnail.present) { + map['has_thumbnail'] = Variable(hasThumbnail.value); + } + if (sizeInBytes.present) { + map['size_in_bytes'] = Variable(sizeInBytes.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (createdAtMonth.present) { + map['created_at_month'] = Variable(createdAtMonth.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MediaFilesCompanion(') + ..write('mediaId: $mediaId, ') + ..write('type: $type, ') + ..write('uploadState: $uploadState, ') + ..write('downloadState: $downloadState, ') + ..write('requiresAuthentication: $requiresAuthentication, ') + ..write('stored: $stored, ') + ..write('isDraftMedia: $isDraftMedia, ') + ..write('isFavorite: $isFavorite, ') + ..write('hasCropAnalyzed: $hasCropAnalyzed, ') + ..write('preProgressingProcess: $preProgressingProcess, ') + ..write('reuploadRequestedBy: $reuploadRequestedBy, ') + ..write('displayLimitInMilliseconds: $displayLimitInMilliseconds, ') + ..write('removeAudio: $removeAudio, ') + ..write('downloadToken: $downloadToken, ') + ..write('encryptionKey: $encryptionKey, ') + ..write('encryptionMac: $encryptionMac, ') + ..write('encryptionNonce: $encryptionNonce, ') + ..write('storedFileHash: $storedFileHash, ') + ..write('hasThumbnail: $hasThumbnail, ') + ..write('sizeInBytes: $sizeInBytes, ') + ..write('createdAt: $createdAt, ') + ..write('createdAtMonth: $createdAtMonth, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class Messages extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Messages(this.attachedDatabase, [this._alias]); + late final GeneratedColumn groupId = GeneratedColumn( + 'group_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: + 'NOT NULL REFERENCES "groups"(group_id)ON DELETE CASCADE', + ); + late final GeneratedColumn messageId = GeneratedColumn( + 'message_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn senderId = GeneratedColumn( + 'sender_id', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL REFERENCES contacts(user_id)', + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn content = GeneratedColumn( + 'content', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn mediaId = GeneratedColumn( + 'media_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: + 'NULL REFERENCES media_files(media_id)ON DELETE SET NULL', + ); + late final GeneratedColumn additionalMessageData = + GeneratedColumn( + 'additional_message_data', + aliasedName, + true, + type: DriftSqlType.blob, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn mediaStored = GeneratedColumn( + 'media_stored', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (media_stored IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn mediaReopened = GeneratedColumn( + 'media_reopened', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (media_reopened IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn downloadToken = + GeneratedColumn( + 'download_token', + aliasedName, + true, + type: DriftSqlType.blob, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn quotesMessageId = GeneratedColumn( + 'quotes_message_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn isDeletedFromSender = GeneratedColumn( + 'is_deleted_from_sender', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT 0 CHECK (is_deleted_from_sender IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn openedAt = GeneratedColumn( + 'opened_at', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn openedByAll = GeneratedColumn( + 'opened_by_all', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT (CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER))', + defaultValue: const CustomExpression( + 'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)', + ), + ); + late final GeneratedColumn modifiedAt = GeneratedColumn( + 'modified_at', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn ackByUser = GeneratedColumn( + 'ack_by_user', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn ackByServer = GeneratedColumn( + 'ack_by_server', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [ + groupId, + messageId, + senderId, + type, + content, + mediaId, + additionalMessageData, + mediaStored, + mediaReopened, + downloadToken, + quotesMessageId, + isDeletedFromSender, + openedAt, + openedByAll, + createdAt, + modifiedAt, + ackByUser, + ackByServer, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'messages'; + @override + Set get $primaryKey => {messageId}; + @override + MessagesData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MessagesData( + groupId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}group_id'], + )!, + messageId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}message_id'], + )!, + senderId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}sender_id'], + ), + type: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}type'], + )!, + content: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}content'], + ), + mediaId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}media_id'], + ), + additionalMessageData: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}additional_message_data'], + ), + mediaStored: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}media_stored'], + )!, + mediaReopened: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}media_reopened'], + )!, + downloadToken: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}download_token'], + ), + quotesMessageId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}quotes_message_id'], + ), + isDeletedFromSender: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}is_deleted_from_sender'], + )!, + openedAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}opened_at'], + ), + openedByAll: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}opened_by_all'], + ), + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}created_at'], + )!, + modifiedAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}modified_at'], + ), + ackByUser: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}ack_by_user'], + ), + ackByServer: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}ack_by_server'], + ), + ); + } + + @override + Messages createAlias(String alias) { + return Messages(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(message_id)']; + @override + bool get dontWriteConstraints => true; +} + +class MessagesData extends DataClass implements Insertable { + final String groupId; + final String messageId; + final int? senderId; + final String type; + final String? content; + final String? mediaId; + final i2.Uint8List? additionalMessageData; + final int mediaStored; + final int mediaReopened; + final i2.Uint8List? downloadToken; + final String? quotesMessageId; + final int isDeletedFromSender; + final int? openedAt; + final int? openedByAll; + final int createdAt; + final int? modifiedAt; + final int? ackByUser; + final int? ackByServer; + const MessagesData({ + required this.groupId, + required this.messageId, + this.senderId, + required this.type, + this.content, + this.mediaId, + this.additionalMessageData, + required this.mediaStored, + required this.mediaReopened, + this.downloadToken, + this.quotesMessageId, + required this.isDeletedFromSender, + this.openedAt, + this.openedByAll, + required this.createdAt, + this.modifiedAt, + this.ackByUser, + this.ackByServer, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['group_id'] = Variable(groupId); + map['message_id'] = Variable(messageId); + if (!nullToAbsent || senderId != null) { + map['sender_id'] = Variable(senderId); + } + map['type'] = Variable(type); + if (!nullToAbsent || content != null) { + map['content'] = Variable(content); + } + if (!nullToAbsent || mediaId != null) { + map['media_id'] = Variable(mediaId); + } + if (!nullToAbsent || additionalMessageData != null) { + map['additional_message_data'] = Variable( + additionalMessageData, + ); + } + map['media_stored'] = Variable(mediaStored); + map['media_reopened'] = Variable(mediaReopened); + if (!nullToAbsent || downloadToken != null) { + map['download_token'] = Variable(downloadToken); + } + if (!nullToAbsent || quotesMessageId != null) { + map['quotes_message_id'] = Variable(quotesMessageId); + } + map['is_deleted_from_sender'] = Variable(isDeletedFromSender); + if (!nullToAbsent || openedAt != null) { + map['opened_at'] = Variable(openedAt); + } + if (!nullToAbsent || openedByAll != null) { + map['opened_by_all'] = Variable(openedByAll); + } + map['created_at'] = Variable(createdAt); + if (!nullToAbsent || modifiedAt != null) { + map['modified_at'] = Variable(modifiedAt); + } + if (!nullToAbsent || ackByUser != null) { + map['ack_by_user'] = Variable(ackByUser); + } + if (!nullToAbsent || ackByServer != null) { + map['ack_by_server'] = Variable(ackByServer); + } + return map; + } + + MessagesCompanion toCompanion(bool nullToAbsent) { + return MessagesCompanion( + groupId: Value(groupId), + messageId: Value(messageId), + senderId: senderId == null && nullToAbsent + ? const Value.absent() + : Value(senderId), + type: Value(type), + content: content == null && nullToAbsent + ? const Value.absent() + : Value(content), + mediaId: mediaId == null && nullToAbsent + ? const Value.absent() + : Value(mediaId), + additionalMessageData: additionalMessageData == null && nullToAbsent + ? const Value.absent() + : Value(additionalMessageData), + mediaStored: Value(mediaStored), + mediaReopened: Value(mediaReopened), + downloadToken: downloadToken == null && nullToAbsent + ? const Value.absent() + : Value(downloadToken), + quotesMessageId: quotesMessageId == null && nullToAbsent + ? const Value.absent() + : Value(quotesMessageId), + isDeletedFromSender: Value(isDeletedFromSender), + openedAt: openedAt == null && nullToAbsent + ? const Value.absent() + : Value(openedAt), + openedByAll: openedByAll == null && nullToAbsent + ? const Value.absent() + : Value(openedByAll), + createdAt: Value(createdAt), + modifiedAt: modifiedAt == null && nullToAbsent + ? const Value.absent() + : Value(modifiedAt), + ackByUser: ackByUser == null && nullToAbsent + ? const Value.absent() + : Value(ackByUser), + ackByServer: ackByServer == null && nullToAbsent + ? const Value.absent() + : Value(ackByServer), + ); + } + + factory MessagesData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MessagesData( + groupId: serializer.fromJson(json['groupId']), + messageId: serializer.fromJson(json['messageId']), + senderId: serializer.fromJson(json['senderId']), + type: serializer.fromJson(json['type']), + content: serializer.fromJson(json['content']), + mediaId: serializer.fromJson(json['mediaId']), + additionalMessageData: serializer.fromJson( + json['additionalMessageData'], + ), + mediaStored: serializer.fromJson(json['mediaStored']), + mediaReopened: serializer.fromJson(json['mediaReopened']), + downloadToken: serializer.fromJson(json['downloadToken']), + quotesMessageId: serializer.fromJson(json['quotesMessageId']), + isDeletedFromSender: serializer.fromJson( + json['isDeletedFromSender'], + ), + openedAt: serializer.fromJson(json['openedAt']), + openedByAll: serializer.fromJson(json['openedByAll']), + createdAt: serializer.fromJson(json['createdAt']), + modifiedAt: serializer.fromJson(json['modifiedAt']), + ackByUser: serializer.fromJson(json['ackByUser']), + ackByServer: serializer.fromJson(json['ackByServer']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'groupId': serializer.toJson(groupId), + 'messageId': serializer.toJson(messageId), + 'senderId': serializer.toJson(senderId), + 'type': serializer.toJson(type), + 'content': serializer.toJson(content), + 'mediaId': serializer.toJson(mediaId), + 'additionalMessageData': serializer.toJson( + additionalMessageData, + ), + 'mediaStored': serializer.toJson(mediaStored), + 'mediaReopened': serializer.toJson(mediaReopened), + 'downloadToken': serializer.toJson(downloadToken), + 'quotesMessageId': serializer.toJson(quotesMessageId), + 'isDeletedFromSender': serializer.toJson(isDeletedFromSender), + 'openedAt': serializer.toJson(openedAt), + 'openedByAll': serializer.toJson(openedByAll), + 'createdAt': serializer.toJson(createdAt), + 'modifiedAt': serializer.toJson(modifiedAt), + 'ackByUser': serializer.toJson(ackByUser), + 'ackByServer': serializer.toJson(ackByServer), + }; + } + + MessagesData copyWith({ + String? groupId, + String? messageId, + Value senderId = const Value.absent(), + String? type, + Value content = const Value.absent(), + Value mediaId = const Value.absent(), + Value additionalMessageData = const Value.absent(), + int? mediaStored, + int? mediaReopened, + Value downloadToken = const Value.absent(), + Value quotesMessageId = const Value.absent(), + int? isDeletedFromSender, + Value openedAt = const Value.absent(), + Value openedByAll = const Value.absent(), + int? createdAt, + Value modifiedAt = const Value.absent(), + Value ackByUser = const Value.absent(), + Value ackByServer = const Value.absent(), + }) => MessagesData( + groupId: groupId ?? this.groupId, + messageId: messageId ?? this.messageId, + senderId: senderId.present ? senderId.value : this.senderId, + type: type ?? this.type, + content: content.present ? content.value : this.content, + mediaId: mediaId.present ? mediaId.value : this.mediaId, + additionalMessageData: additionalMessageData.present + ? additionalMessageData.value + : this.additionalMessageData, + mediaStored: mediaStored ?? this.mediaStored, + mediaReopened: mediaReopened ?? this.mediaReopened, + downloadToken: downloadToken.present + ? downloadToken.value + : this.downloadToken, + quotesMessageId: quotesMessageId.present + ? quotesMessageId.value + : this.quotesMessageId, + isDeletedFromSender: isDeletedFromSender ?? this.isDeletedFromSender, + openedAt: openedAt.present ? openedAt.value : this.openedAt, + openedByAll: openedByAll.present ? openedByAll.value : this.openedByAll, + createdAt: createdAt ?? this.createdAt, + modifiedAt: modifiedAt.present ? modifiedAt.value : this.modifiedAt, + ackByUser: ackByUser.present ? ackByUser.value : this.ackByUser, + ackByServer: ackByServer.present ? ackByServer.value : this.ackByServer, + ); + MessagesData copyWithCompanion(MessagesCompanion data) { + return MessagesData( + groupId: data.groupId.present ? data.groupId.value : this.groupId, + messageId: data.messageId.present ? data.messageId.value : this.messageId, + senderId: data.senderId.present ? data.senderId.value : this.senderId, + type: data.type.present ? data.type.value : this.type, + content: data.content.present ? data.content.value : this.content, + mediaId: data.mediaId.present ? data.mediaId.value : this.mediaId, + additionalMessageData: data.additionalMessageData.present + ? data.additionalMessageData.value + : this.additionalMessageData, + mediaStored: data.mediaStored.present + ? data.mediaStored.value + : this.mediaStored, + mediaReopened: data.mediaReopened.present + ? data.mediaReopened.value + : this.mediaReopened, + downloadToken: data.downloadToken.present + ? data.downloadToken.value + : this.downloadToken, + quotesMessageId: data.quotesMessageId.present + ? data.quotesMessageId.value + : this.quotesMessageId, + isDeletedFromSender: data.isDeletedFromSender.present + ? data.isDeletedFromSender.value + : this.isDeletedFromSender, + openedAt: data.openedAt.present ? data.openedAt.value : this.openedAt, + openedByAll: data.openedByAll.present + ? data.openedByAll.value + : this.openedByAll, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + modifiedAt: data.modifiedAt.present + ? data.modifiedAt.value + : this.modifiedAt, + ackByUser: data.ackByUser.present ? data.ackByUser.value : this.ackByUser, + ackByServer: data.ackByServer.present + ? data.ackByServer.value + : this.ackByServer, + ); + } + + @override + String toString() { + return (StringBuffer('MessagesData(') + ..write('groupId: $groupId, ') + ..write('messageId: $messageId, ') + ..write('senderId: $senderId, ') + ..write('type: $type, ') + ..write('content: $content, ') + ..write('mediaId: $mediaId, ') + ..write('additionalMessageData: $additionalMessageData, ') + ..write('mediaStored: $mediaStored, ') + ..write('mediaReopened: $mediaReopened, ') + ..write('downloadToken: $downloadToken, ') + ..write('quotesMessageId: $quotesMessageId, ') + ..write('isDeletedFromSender: $isDeletedFromSender, ') + ..write('openedAt: $openedAt, ') + ..write('openedByAll: $openedByAll, ') + ..write('createdAt: $createdAt, ') + ..write('modifiedAt: $modifiedAt, ') + ..write('ackByUser: $ackByUser, ') + ..write('ackByServer: $ackByServer') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + groupId, + messageId, + senderId, + type, + content, + mediaId, + $driftBlobEquality.hash(additionalMessageData), + mediaStored, + mediaReopened, + $driftBlobEquality.hash(downloadToken), + quotesMessageId, + isDeletedFromSender, + openedAt, + openedByAll, + createdAt, + modifiedAt, + ackByUser, + ackByServer, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MessagesData && + other.groupId == this.groupId && + other.messageId == this.messageId && + other.senderId == this.senderId && + other.type == this.type && + other.content == this.content && + other.mediaId == this.mediaId && + $driftBlobEquality.equals( + other.additionalMessageData, + this.additionalMessageData, + ) && + other.mediaStored == this.mediaStored && + other.mediaReopened == this.mediaReopened && + $driftBlobEquality.equals(other.downloadToken, this.downloadToken) && + other.quotesMessageId == this.quotesMessageId && + other.isDeletedFromSender == this.isDeletedFromSender && + other.openedAt == this.openedAt && + other.openedByAll == this.openedByAll && + other.createdAt == this.createdAt && + other.modifiedAt == this.modifiedAt && + other.ackByUser == this.ackByUser && + other.ackByServer == this.ackByServer); +} + +class MessagesCompanion extends UpdateCompanion { + final Value groupId; + final Value messageId; + final Value senderId; + final Value type; + final Value content; + final Value mediaId; + final Value additionalMessageData; + final Value mediaStored; + final Value mediaReopened; + final Value downloadToken; + final Value quotesMessageId; + final Value isDeletedFromSender; + final Value openedAt; + final Value openedByAll; + final Value createdAt; + final Value modifiedAt; + final Value ackByUser; + final Value ackByServer; + final Value rowid; + const MessagesCompanion({ + this.groupId = const Value.absent(), + this.messageId = const Value.absent(), + this.senderId = const Value.absent(), + this.type = const Value.absent(), + this.content = const Value.absent(), + this.mediaId = const Value.absent(), + this.additionalMessageData = const Value.absent(), + this.mediaStored = const Value.absent(), + this.mediaReopened = const Value.absent(), + this.downloadToken = const Value.absent(), + this.quotesMessageId = const Value.absent(), + this.isDeletedFromSender = const Value.absent(), + this.openedAt = const Value.absent(), + this.openedByAll = const Value.absent(), + this.createdAt = const Value.absent(), + this.modifiedAt = const Value.absent(), + this.ackByUser = const Value.absent(), + this.ackByServer = const Value.absent(), + this.rowid = const Value.absent(), + }); + MessagesCompanion.insert({ + required String groupId, + required String messageId, + this.senderId = const Value.absent(), + required String type, + this.content = const Value.absent(), + this.mediaId = const Value.absent(), + this.additionalMessageData = const Value.absent(), + this.mediaStored = const Value.absent(), + this.mediaReopened = const Value.absent(), + this.downloadToken = const Value.absent(), + this.quotesMessageId = const Value.absent(), + this.isDeletedFromSender = const Value.absent(), + this.openedAt = const Value.absent(), + this.openedByAll = const Value.absent(), + this.createdAt = const Value.absent(), + this.modifiedAt = const Value.absent(), + this.ackByUser = const Value.absent(), + this.ackByServer = const Value.absent(), + this.rowid = const Value.absent(), + }) : groupId = Value(groupId), + messageId = Value(messageId), + type = Value(type); + static Insertable custom({ + Expression? groupId, + Expression? messageId, + Expression? senderId, + Expression? type, + Expression? content, + Expression? mediaId, + Expression? additionalMessageData, + Expression? mediaStored, + Expression? mediaReopened, + Expression? downloadToken, + Expression? quotesMessageId, + Expression? isDeletedFromSender, + Expression? openedAt, + Expression? openedByAll, + Expression? createdAt, + Expression? modifiedAt, + Expression? ackByUser, + Expression? ackByServer, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (groupId != null) 'group_id': groupId, + if (messageId != null) 'message_id': messageId, + if (senderId != null) 'sender_id': senderId, + if (type != null) 'type': type, + if (content != null) 'content': content, + if (mediaId != null) 'media_id': mediaId, + if (additionalMessageData != null) + 'additional_message_data': additionalMessageData, + if (mediaStored != null) 'media_stored': mediaStored, + if (mediaReopened != null) 'media_reopened': mediaReopened, + if (downloadToken != null) 'download_token': downloadToken, + if (quotesMessageId != null) 'quotes_message_id': quotesMessageId, + if (isDeletedFromSender != null) + 'is_deleted_from_sender': isDeletedFromSender, + if (openedAt != null) 'opened_at': openedAt, + if (openedByAll != null) 'opened_by_all': openedByAll, + if (createdAt != null) 'created_at': createdAt, + if (modifiedAt != null) 'modified_at': modifiedAt, + if (ackByUser != null) 'ack_by_user': ackByUser, + if (ackByServer != null) 'ack_by_server': ackByServer, + if (rowid != null) 'rowid': rowid, + }); + } + + MessagesCompanion copyWith({ + Value? groupId, + Value? messageId, + Value? senderId, + Value? type, + Value? content, + Value? mediaId, + Value? additionalMessageData, + Value? mediaStored, + Value? mediaReopened, + Value? downloadToken, + Value? quotesMessageId, + Value? isDeletedFromSender, + Value? openedAt, + Value? openedByAll, + Value? createdAt, + Value? modifiedAt, + Value? ackByUser, + Value? ackByServer, + Value? rowid, + }) { + return MessagesCompanion( + groupId: groupId ?? this.groupId, + messageId: messageId ?? this.messageId, + senderId: senderId ?? this.senderId, + type: type ?? this.type, + content: content ?? this.content, + mediaId: mediaId ?? this.mediaId, + additionalMessageData: + additionalMessageData ?? this.additionalMessageData, + mediaStored: mediaStored ?? this.mediaStored, + mediaReopened: mediaReopened ?? this.mediaReopened, + downloadToken: downloadToken ?? this.downloadToken, + quotesMessageId: quotesMessageId ?? this.quotesMessageId, + isDeletedFromSender: isDeletedFromSender ?? this.isDeletedFromSender, + openedAt: openedAt ?? this.openedAt, + openedByAll: openedByAll ?? this.openedByAll, + createdAt: createdAt ?? this.createdAt, + modifiedAt: modifiedAt ?? this.modifiedAt, + ackByUser: ackByUser ?? this.ackByUser, + ackByServer: ackByServer ?? this.ackByServer, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (groupId.present) { + map['group_id'] = Variable(groupId.value); + } + if (messageId.present) { + map['message_id'] = Variable(messageId.value); + } + if (senderId.present) { + map['sender_id'] = Variable(senderId.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (content.present) { + map['content'] = Variable(content.value); + } + if (mediaId.present) { + map['media_id'] = Variable(mediaId.value); + } + if (additionalMessageData.present) { + map['additional_message_data'] = Variable( + additionalMessageData.value, + ); + } + if (mediaStored.present) { + map['media_stored'] = Variable(mediaStored.value); + } + if (mediaReopened.present) { + map['media_reopened'] = Variable(mediaReopened.value); + } + if (downloadToken.present) { + map['download_token'] = Variable(downloadToken.value); + } + if (quotesMessageId.present) { + map['quotes_message_id'] = Variable(quotesMessageId.value); + } + if (isDeletedFromSender.present) { + map['is_deleted_from_sender'] = Variable(isDeletedFromSender.value); + } + if (openedAt.present) { + map['opened_at'] = Variable(openedAt.value); + } + if (openedByAll.present) { + map['opened_by_all'] = Variable(openedByAll.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (modifiedAt.present) { + map['modified_at'] = Variable(modifiedAt.value); + } + if (ackByUser.present) { + map['ack_by_user'] = Variable(ackByUser.value); + } + if (ackByServer.present) { + map['ack_by_server'] = Variable(ackByServer.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MessagesCompanion(') + ..write('groupId: $groupId, ') + ..write('messageId: $messageId, ') + ..write('senderId: $senderId, ') + ..write('type: $type, ') + ..write('content: $content, ') + ..write('mediaId: $mediaId, ') + ..write('additionalMessageData: $additionalMessageData, ') + ..write('mediaStored: $mediaStored, ') + ..write('mediaReopened: $mediaReopened, ') + ..write('downloadToken: $downloadToken, ') + ..write('quotesMessageId: $quotesMessageId, ') + ..write('isDeletedFromSender: $isDeletedFromSender, ') + ..write('openedAt: $openedAt, ') + ..write('openedByAll: $openedByAll, ') + ..write('createdAt: $createdAt, ') + ..write('modifiedAt: $modifiedAt, ') + ..write('ackByUser: $ackByUser, ') + ..write('ackByServer: $ackByServer, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class MessageHistories extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + MessageHistories(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL PRIMARY KEY AUTOINCREMENT', + ); + late final GeneratedColumn messageId = GeneratedColumn( + 'message_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: + 'NOT NULL REFERENCES messages(message_id)ON DELETE CASCADE', + ); + late final GeneratedColumn contactId = GeneratedColumn( + 'contact_id', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL REFERENCES contacts(user_id)ON DELETE CASCADE', + ); + late final GeneratedColumn content = GeneratedColumn( + 'content', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT (CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER))', + defaultValue: const CustomExpression( + 'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)', + ), + ); + @override + List get $columns => [ + id, + messageId, + contactId, + content, + createdAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'message_histories'; + @override + Set get $primaryKey => {id}; + @override + MessageHistoriesData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MessageHistoriesData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}id'], + )!, + messageId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}message_id'], + )!, + contactId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}contact_id'], + ), + content: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}content'], + ), + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}created_at'], + )!, + ); + } + + @override + MessageHistories createAlias(String alias) { + return MessageHistories(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; +} + +class MessageHistoriesData extends DataClass + implements Insertable { + final int id; + final String messageId; + final int? contactId; + final String? content; + final int createdAt; + const MessageHistoriesData({ + required this.id, + required this.messageId, + this.contactId, + this.content, + required this.createdAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['message_id'] = Variable(messageId); + if (!nullToAbsent || contactId != null) { + map['contact_id'] = Variable(contactId); + } + if (!nullToAbsent || content != null) { + map['content'] = Variable(content); + } + map['created_at'] = Variable(createdAt); + return map; + } + + MessageHistoriesCompanion toCompanion(bool nullToAbsent) { + return MessageHistoriesCompanion( + id: Value(id), + messageId: Value(messageId), + contactId: contactId == null && nullToAbsent + ? const Value.absent() + : Value(contactId), + content: content == null && nullToAbsent + ? const Value.absent() + : Value(content), + createdAt: Value(createdAt), + ); + } + + factory MessageHistoriesData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MessageHistoriesData( + id: serializer.fromJson(json['id']), + messageId: serializer.fromJson(json['messageId']), + contactId: serializer.fromJson(json['contactId']), + content: serializer.fromJson(json['content']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'messageId': serializer.toJson(messageId), + 'contactId': serializer.toJson(contactId), + 'content': serializer.toJson(content), + 'createdAt': serializer.toJson(createdAt), + }; + } + + MessageHistoriesData copyWith({ + int? id, + String? messageId, + Value contactId = const Value.absent(), + Value content = const Value.absent(), + int? createdAt, + }) => MessageHistoriesData( + id: id ?? this.id, + messageId: messageId ?? this.messageId, + contactId: contactId.present ? contactId.value : this.contactId, + content: content.present ? content.value : this.content, + createdAt: createdAt ?? this.createdAt, + ); + MessageHistoriesData copyWithCompanion(MessageHistoriesCompanion data) { + return MessageHistoriesData( + id: data.id.present ? data.id.value : this.id, + messageId: data.messageId.present ? data.messageId.value : this.messageId, + contactId: data.contactId.present ? data.contactId.value : this.contactId, + content: data.content.present ? data.content.value : this.content, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('MessageHistoriesData(') + ..write('id: $id, ') + ..write('messageId: $messageId, ') + ..write('contactId: $contactId, ') + ..write('content: $content, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, messageId, contactId, content, createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MessageHistoriesData && + other.id == this.id && + other.messageId == this.messageId && + other.contactId == this.contactId && + other.content == this.content && + other.createdAt == this.createdAt); +} + +class MessageHistoriesCompanion extends UpdateCompanion { + final Value id; + final Value messageId; + final Value contactId; + final Value content; + final Value createdAt; + const MessageHistoriesCompanion({ + this.id = const Value.absent(), + this.messageId = const Value.absent(), + this.contactId = const Value.absent(), + this.content = const Value.absent(), + this.createdAt = const Value.absent(), + }); + MessageHistoriesCompanion.insert({ + this.id = const Value.absent(), + required String messageId, + this.contactId = const Value.absent(), + this.content = const Value.absent(), + this.createdAt = const Value.absent(), + }) : messageId = Value(messageId); + static Insertable custom({ + Expression? id, + Expression? messageId, + Expression? contactId, + Expression? content, + Expression? createdAt, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (messageId != null) 'message_id': messageId, + if (contactId != null) 'contact_id': contactId, + if (content != null) 'content': content, + if (createdAt != null) 'created_at': createdAt, + }); + } + + MessageHistoriesCompanion copyWith({ + Value? id, + Value? messageId, + Value? contactId, + Value? content, + Value? createdAt, + }) { + return MessageHistoriesCompanion( + id: id ?? this.id, + messageId: messageId ?? this.messageId, + contactId: contactId ?? this.contactId, + content: content ?? this.content, + createdAt: createdAt ?? this.createdAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (messageId.present) { + map['message_id'] = Variable(messageId.value); + } + if (contactId.present) { + map['contact_id'] = Variable(contactId.value); + } + if (content.present) { + map['content'] = Variable(content.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MessageHistoriesCompanion(') + ..write('id: $id, ') + ..write('messageId: $messageId, ') + ..write('contactId: $contactId, ') + ..write('content: $content, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } +} + +class Reactions extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Reactions(this.attachedDatabase, [this._alias]); + late final GeneratedColumn messageId = GeneratedColumn( + 'message_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: + 'NOT NULL REFERENCES messages(message_id)ON DELETE CASCADE', + ); + late final GeneratedColumn emoji = GeneratedColumn( + 'emoji', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn senderId = GeneratedColumn( + 'sender_id', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL REFERENCES contacts(user_id)ON DELETE CASCADE', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT (CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER))', + defaultValue: const CustomExpression( + 'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)', + ), + ); + @override + List get $columns => [messageId, emoji, senderId, createdAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'reactions'; + @override + Set get $primaryKey => {messageId, senderId, emoji}; + @override + ReactionsData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ReactionsData( + messageId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}message_id'], + )!, + emoji: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}emoji'], + )!, + senderId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}sender_id'], + ), + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}created_at'], + )!, + ); + } + + @override + Reactions createAlias(String alias) { + return Reactions(attachedDatabase, alias); + } + + @override + List get customConstraints => const [ + 'PRIMARY KEY(message_id, sender_id, emoji)', + ]; + @override + bool get dontWriteConstraints => true; +} + +class ReactionsData extends DataClass implements Insertable { + final String messageId; + final String emoji; + final int? senderId; + final int createdAt; + const ReactionsData({ + required this.messageId, + required this.emoji, + this.senderId, + required this.createdAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['message_id'] = Variable(messageId); + map['emoji'] = Variable(emoji); + if (!nullToAbsent || senderId != null) { + map['sender_id'] = Variable(senderId); + } + map['created_at'] = Variable(createdAt); + return map; + } + + ReactionsCompanion toCompanion(bool nullToAbsent) { + return ReactionsCompanion( + messageId: Value(messageId), + emoji: Value(emoji), + senderId: senderId == null && nullToAbsent + ? const Value.absent() + : Value(senderId), + createdAt: Value(createdAt), + ); + } + + factory ReactionsData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ReactionsData( + messageId: serializer.fromJson(json['messageId']), + emoji: serializer.fromJson(json['emoji']), + senderId: serializer.fromJson(json['senderId']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'messageId': serializer.toJson(messageId), + 'emoji': serializer.toJson(emoji), + 'senderId': serializer.toJson(senderId), + 'createdAt': serializer.toJson(createdAt), + }; + } + + ReactionsData copyWith({ + String? messageId, + String? emoji, + Value senderId = const Value.absent(), + int? createdAt, + }) => ReactionsData( + messageId: messageId ?? this.messageId, + emoji: emoji ?? this.emoji, + senderId: senderId.present ? senderId.value : this.senderId, + createdAt: createdAt ?? this.createdAt, + ); + ReactionsData copyWithCompanion(ReactionsCompanion data) { + return ReactionsData( + messageId: data.messageId.present ? data.messageId.value : this.messageId, + emoji: data.emoji.present ? data.emoji.value : this.emoji, + senderId: data.senderId.present ? data.senderId.value : this.senderId, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('ReactionsData(') + ..write('messageId: $messageId, ') + ..write('emoji: $emoji, ') + ..write('senderId: $senderId, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(messageId, emoji, senderId, createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ReactionsData && + other.messageId == this.messageId && + other.emoji == this.emoji && + other.senderId == this.senderId && + other.createdAt == this.createdAt); +} + +class ReactionsCompanion extends UpdateCompanion { + final Value messageId; + final Value emoji; + final Value senderId; + final Value createdAt; + final Value rowid; + const ReactionsCompanion({ + this.messageId = const Value.absent(), + this.emoji = const Value.absent(), + this.senderId = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + ReactionsCompanion.insert({ + required String messageId, + required String emoji, + this.senderId = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : messageId = Value(messageId), + emoji = Value(emoji); + static Insertable custom({ + Expression? messageId, + Expression? emoji, + Expression? senderId, + Expression? createdAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (messageId != null) 'message_id': messageId, + if (emoji != null) 'emoji': emoji, + if (senderId != null) 'sender_id': senderId, + if (createdAt != null) 'created_at': createdAt, + if (rowid != null) 'rowid': rowid, + }); + } + + ReactionsCompanion copyWith({ + Value? messageId, + Value? emoji, + Value? senderId, + Value? createdAt, + Value? rowid, + }) { + return ReactionsCompanion( + messageId: messageId ?? this.messageId, + emoji: emoji ?? this.emoji, + senderId: senderId ?? this.senderId, + createdAt: createdAt ?? this.createdAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (messageId.present) { + map['message_id'] = Variable(messageId.value); + } + if (emoji.present) { + map['emoji'] = Variable(emoji.value); + } + if (senderId.present) { + map['sender_id'] = Variable(senderId.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ReactionsCompanion(') + ..write('messageId: $messageId, ') + ..write('emoji: $emoji, ') + ..write('senderId: $senderId, ') + ..write('createdAt: $createdAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class GroupMembers extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + GroupMembers(this.attachedDatabase, [this._alias]); + late final GeneratedColumn groupId = GeneratedColumn( + 'group_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: + 'NOT NULL REFERENCES "groups"(group_id)ON DELETE CASCADE', + ); + late final GeneratedColumn contactId = GeneratedColumn( + 'contact_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL REFERENCES contacts(user_id)', + ); + late final GeneratedColumn memberState = GeneratedColumn( + 'member_state', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn groupPublicKey = + GeneratedColumn( + 'group_public_key', + aliasedName, + true, + type: DriftSqlType.blob, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn lastChatOpened = GeneratedColumn( + 'last_chat_opened', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn lastTypeIndicator = GeneratedColumn( + 'last_type_indicator', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn lastMessage = GeneratedColumn( + 'last_message', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT (CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER))', + defaultValue: const CustomExpression( + 'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)', + ), + ); + @override + List get $columns => [ + groupId, + contactId, + memberState, + groupPublicKey, + lastChatOpened, + lastTypeIndicator, + lastMessage, + createdAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'group_members'; + @override + Set get $primaryKey => {groupId, contactId}; + @override + GroupMembersData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return GroupMembersData( + groupId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}group_id'], + )!, + contactId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}contact_id'], + )!, + memberState: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}member_state'], + ), + groupPublicKey: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}group_public_key'], + ), + lastChatOpened: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}last_chat_opened'], + ), + lastTypeIndicator: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}last_type_indicator'], + ), + lastMessage: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}last_message'], + ), + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}created_at'], + )!, + ); + } + + @override + GroupMembers createAlias(String alias) { + return GroupMembers(attachedDatabase, alias); + } + + @override + List get customConstraints => const [ + 'PRIMARY KEY(group_id, contact_id)', + ]; + @override + bool get dontWriteConstraints => true; +} + +class GroupMembersData extends DataClass + implements Insertable { + final String groupId; + final int contactId; + final String? memberState; + final i2.Uint8List? groupPublicKey; + final int? lastChatOpened; + final int? lastTypeIndicator; + final int? lastMessage; + final int createdAt; + const GroupMembersData({ + required this.groupId, + required this.contactId, + this.memberState, + this.groupPublicKey, + this.lastChatOpened, + this.lastTypeIndicator, + this.lastMessage, + required this.createdAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['group_id'] = Variable(groupId); + map['contact_id'] = Variable(contactId); + if (!nullToAbsent || memberState != null) { + map['member_state'] = Variable(memberState); + } + if (!nullToAbsent || groupPublicKey != null) { + map['group_public_key'] = Variable(groupPublicKey); + } + if (!nullToAbsent || lastChatOpened != null) { + map['last_chat_opened'] = Variable(lastChatOpened); + } + if (!nullToAbsent || lastTypeIndicator != null) { + map['last_type_indicator'] = Variable(lastTypeIndicator); + } + if (!nullToAbsent || lastMessage != null) { + map['last_message'] = Variable(lastMessage); + } + map['created_at'] = Variable(createdAt); + return map; + } + + GroupMembersCompanion toCompanion(bool nullToAbsent) { + return GroupMembersCompanion( + groupId: Value(groupId), + contactId: Value(contactId), + memberState: memberState == null && nullToAbsent + ? const Value.absent() + : Value(memberState), + groupPublicKey: groupPublicKey == null && nullToAbsent + ? const Value.absent() + : Value(groupPublicKey), + lastChatOpened: lastChatOpened == null && nullToAbsent + ? const Value.absent() + : Value(lastChatOpened), + lastTypeIndicator: lastTypeIndicator == null && nullToAbsent + ? const Value.absent() + : Value(lastTypeIndicator), + lastMessage: lastMessage == null && nullToAbsent + ? const Value.absent() + : Value(lastMessage), + createdAt: Value(createdAt), + ); + } + + factory GroupMembersData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return GroupMembersData( + groupId: serializer.fromJson(json['groupId']), + contactId: serializer.fromJson(json['contactId']), + memberState: serializer.fromJson(json['memberState']), + groupPublicKey: serializer.fromJson( + json['groupPublicKey'], + ), + lastChatOpened: serializer.fromJson(json['lastChatOpened']), + lastTypeIndicator: serializer.fromJson(json['lastTypeIndicator']), + lastMessage: serializer.fromJson(json['lastMessage']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'groupId': serializer.toJson(groupId), + 'contactId': serializer.toJson(contactId), + 'memberState': serializer.toJson(memberState), + 'groupPublicKey': serializer.toJson(groupPublicKey), + 'lastChatOpened': serializer.toJson(lastChatOpened), + 'lastTypeIndicator': serializer.toJson(lastTypeIndicator), + 'lastMessage': serializer.toJson(lastMessage), + 'createdAt': serializer.toJson(createdAt), + }; + } + + GroupMembersData copyWith({ + String? groupId, + int? contactId, + Value memberState = const Value.absent(), + Value groupPublicKey = const Value.absent(), + Value lastChatOpened = const Value.absent(), + Value lastTypeIndicator = const Value.absent(), + Value lastMessage = const Value.absent(), + int? createdAt, + }) => GroupMembersData( + groupId: groupId ?? this.groupId, + contactId: contactId ?? this.contactId, + memberState: memberState.present ? memberState.value : this.memberState, + groupPublicKey: groupPublicKey.present + ? groupPublicKey.value + : this.groupPublicKey, + lastChatOpened: lastChatOpened.present + ? lastChatOpened.value + : this.lastChatOpened, + lastTypeIndicator: lastTypeIndicator.present + ? lastTypeIndicator.value + : this.lastTypeIndicator, + lastMessage: lastMessage.present ? lastMessage.value : this.lastMessage, + createdAt: createdAt ?? this.createdAt, + ); + GroupMembersData copyWithCompanion(GroupMembersCompanion data) { + return GroupMembersData( + groupId: data.groupId.present ? data.groupId.value : this.groupId, + contactId: data.contactId.present ? data.contactId.value : this.contactId, + memberState: data.memberState.present + ? data.memberState.value + : this.memberState, + groupPublicKey: data.groupPublicKey.present + ? data.groupPublicKey.value + : this.groupPublicKey, + lastChatOpened: data.lastChatOpened.present + ? data.lastChatOpened.value + : this.lastChatOpened, + lastTypeIndicator: data.lastTypeIndicator.present + ? data.lastTypeIndicator.value + : this.lastTypeIndicator, + lastMessage: data.lastMessage.present + ? data.lastMessage.value + : this.lastMessage, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('GroupMembersData(') + ..write('groupId: $groupId, ') + ..write('contactId: $contactId, ') + ..write('memberState: $memberState, ') + ..write('groupPublicKey: $groupPublicKey, ') + ..write('lastChatOpened: $lastChatOpened, ') + ..write('lastTypeIndicator: $lastTypeIndicator, ') + ..write('lastMessage: $lastMessage, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + groupId, + contactId, + memberState, + $driftBlobEquality.hash(groupPublicKey), + lastChatOpened, + lastTypeIndicator, + lastMessage, + createdAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is GroupMembersData && + other.groupId == this.groupId && + other.contactId == this.contactId && + other.memberState == this.memberState && + $driftBlobEquality.equals( + other.groupPublicKey, + this.groupPublicKey, + ) && + other.lastChatOpened == this.lastChatOpened && + other.lastTypeIndicator == this.lastTypeIndicator && + other.lastMessage == this.lastMessage && + other.createdAt == this.createdAt); +} + +class GroupMembersCompanion extends UpdateCompanion { + final Value groupId; + final Value contactId; + final Value memberState; + final Value groupPublicKey; + final Value lastChatOpened; + final Value lastTypeIndicator; + final Value lastMessage; + final Value createdAt; + final Value rowid; + const GroupMembersCompanion({ + this.groupId = const Value.absent(), + this.contactId = const Value.absent(), + this.memberState = const Value.absent(), + this.groupPublicKey = const Value.absent(), + this.lastChatOpened = const Value.absent(), + this.lastTypeIndicator = const Value.absent(), + this.lastMessage = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + GroupMembersCompanion.insert({ + required String groupId, + required int contactId, + this.memberState = const Value.absent(), + this.groupPublicKey = const Value.absent(), + this.lastChatOpened = const Value.absent(), + this.lastTypeIndicator = const Value.absent(), + this.lastMessage = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : groupId = Value(groupId), + contactId = Value(contactId); + static Insertable custom({ + Expression? groupId, + Expression? contactId, + Expression? memberState, + Expression? groupPublicKey, + Expression? lastChatOpened, + Expression? lastTypeIndicator, + Expression? lastMessage, + Expression? createdAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (groupId != null) 'group_id': groupId, + if (contactId != null) 'contact_id': contactId, + if (memberState != null) 'member_state': memberState, + if (groupPublicKey != null) 'group_public_key': groupPublicKey, + if (lastChatOpened != null) 'last_chat_opened': lastChatOpened, + if (lastTypeIndicator != null) 'last_type_indicator': lastTypeIndicator, + if (lastMessage != null) 'last_message': lastMessage, + if (createdAt != null) 'created_at': createdAt, + if (rowid != null) 'rowid': rowid, + }); + } + + GroupMembersCompanion copyWith({ + Value? groupId, + Value? contactId, + Value? memberState, + Value? groupPublicKey, + Value? lastChatOpened, + Value? lastTypeIndicator, + Value? lastMessage, + Value? createdAt, + Value? rowid, + }) { + return GroupMembersCompanion( + groupId: groupId ?? this.groupId, + contactId: contactId ?? this.contactId, + memberState: memberState ?? this.memberState, + groupPublicKey: groupPublicKey ?? this.groupPublicKey, + lastChatOpened: lastChatOpened ?? this.lastChatOpened, + lastTypeIndicator: lastTypeIndicator ?? this.lastTypeIndicator, + lastMessage: lastMessage ?? this.lastMessage, + createdAt: createdAt ?? this.createdAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (groupId.present) { + map['group_id'] = Variable(groupId.value); + } + if (contactId.present) { + map['contact_id'] = Variable(contactId.value); + } + if (memberState.present) { + map['member_state'] = Variable(memberState.value); + } + if (groupPublicKey.present) { + map['group_public_key'] = Variable(groupPublicKey.value); + } + if (lastChatOpened.present) { + map['last_chat_opened'] = Variable(lastChatOpened.value); + } + if (lastTypeIndicator.present) { + map['last_type_indicator'] = Variable(lastTypeIndicator.value); + } + if (lastMessage.present) { + map['last_message'] = Variable(lastMessage.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('GroupMembersCompanion(') + ..write('groupId: $groupId, ') + ..write('contactId: $contactId, ') + ..write('memberState: $memberState, ') + ..write('groupPublicKey: $groupPublicKey, ') + ..write('lastChatOpened: $lastChatOpened, ') + ..write('lastTypeIndicator: $lastTypeIndicator, ') + ..write('lastMessage: $lastMessage, ') + ..write('createdAt: $createdAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class Receipts extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Receipts(this.attachedDatabase, [this._alias]); + late final GeneratedColumn receiptId = GeneratedColumn( + 'receipt_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn contactId = GeneratedColumn( + 'contact_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: + 'NOT NULL REFERENCES contacts(user_id)ON DELETE CASCADE', + ); + late final GeneratedColumn messageId = GeneratedColumn( + 'message_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL REFERENCES messages(message_id)ON DELETE CASCADE', + ); + late final GeneratedColumn message = + GeneratedColumn( + 'message', + aliasedName, + false, + type: DriftSqlType.blob, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn contactWillSendsReceipt = + GeneratedColumn( + 'contact_will_sends_receipt', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT 1 CHECK (contact_will_sends_receipt IN (0, 1))', + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn + willBeRetriedByMediaUpload = GeneratedColumn( + 'will_be_retried_by_media_upload', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT 0 CHECK (will_be_retried_by_media_upload IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn markForRetry = GeneratedColumn( + 'mark_for_retry', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn markForRetryAfterAccepted = + GeneratedColumn( + 'mark_for_retry_after_accepted', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn ackByServerAt = GeneratedColumn( + 'ack_by_server_at', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn retryCount = GeneratedColumn( + 'retry_count', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn lastRetry = GeneratedColumn( + 'last_retry', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT (CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER))', + defaultValue: const CustomExpression( + 'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)', + ), + ); + @override + List get $columns => [ + receiptId, + contactId, + messageId, + message, + contactWillSendsReceipt, + willBeRetriedByMediaUpload, + markForRetry, + markForRetryAfterAccepted, + ackByServerAt, + retryCount, + lastRetry, + createdAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'receipts'; + @override + Set get $primaryKey => {receiptId}; + @override + ReceiptsData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ReceiptsData( + receiptId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}receipt_id'], + )!, + contactId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}contact_id'], + )!, + messageId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}message_id'], + ), + message: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}message'], + )!, + contactWillSendsReceipt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}contact_will_sends_receipt'], + )!, + willBeRetriedByMediaUpload: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}will_be_retried_by_media_upload'], + )!, + markForRetry: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}mark_for_retry'], + ), + markForRetryAfterAccepted: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}mark_for_retry_after_accepted'], + ), + ackByServerAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}ack_by_server_at'], + ), + retryCount: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}retry_count'], + )!, + lastRetry: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}last_retry'], + ), + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}created_at'], + )!, + ); + } + + @override + Receipts createAlias(String alias) { + return Receipts(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(receipt_id)']; + @override + bool get dontWriteConstraints => true; +} + +class ReceiptsData extends DataClass implements Insertable { + final String receiptId; + final int contactId; + final String? messageId; + final i2.Uint8List message; + final int contactWillSendsReceipt; + final int willBeRetriedByMediaUpload; + final int? markForRetry; + final int? markForRetryAfterAccepted; + final int? ackByServerAt; + final int retryCount; + final int? lastRetry; + final int createdAt; + const ReceiptsData({ + required this.receiptId, + required this.contactId, + this.messageId, + required this.message, + required this.contactWillSendsReceipt, + required this.willBeRetriedByMediaUpload, + this.markForRetry, + this.markForRetryAfterAccepted, + this.ackByServerAt, + required this.retryCount, + this.lastRetry, + required this.createdAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['receipt_id'] = Variable(receiptId); + map['contact_id'] = Variable(contactId); + if (!nullToAbsent || messageId != null) { + map['message_id'] = Variable(messageId); + } + map['message'] = Variable(message); + map['contact_will_sends_receipt'] = Variable(contactWillSendsReceipt); + map['will_be_retried_by_media_upload'] = Variable( + willBeRetriedByMediaUpload, + ); + if (!nullToAbsent || markForRetry != null) { + map['mark_for_retry'] = Variable(markForRetry); + } + if (!nullToAbsent || markForRetryAfterAccepted != null) { + map['mark_for_retry_after_accepted'] = Variable( + markForRetryAfterAccepted, + ); + } + if (!nullToAbsent || ackByServerAt != null) { + map['ack_by_server_at'] = Variable(ackByServerAt); + } + map['retry_count'] = Variable(retryCount); + if (!nullToAbsent || lastRetry != null) { + map['last_retry'] = Variable(lastRetry); + } + map['created_at'] = Variable(createdAt); + return map; + } + + ReceiptsCompanion toCompanion(bool nullToAbsent) { + return ReceiptsCompanion( + receiptId: Value(receiptId), + contactId: Value(contactId), + messageId: messageId == null && nullToAbsent + ? const Value.absent() + : Value(messageId), + message: Value(message), + contactWillSendsReceipt: Value(contactWillSendsReceipt), + willBeRetriedByMediaUpload: Value(willBeRetriedByMediaUpload), + markForRetry: markForRetry == null && nullToAbsent + ? const Value.absent() + : Value(markForRetry), + markForRetryAfterAccepted: + markForRetryAfterAccepted == null && nullToAbsent + ? const Value.absent() + : Value(markForRetryAfterAccepted), + ackByServerAt: ackByServerAt == null && nullToAbsent + ? const Value.absent() + : Value(ackByServerAt), + retryCount: Value(retryCount), + lastRetry: lastRetry == null && nullToAbsent + ? const Value.absent() + : Value(lastRetry), + createdAt: Value(createdAt), + ); + } + + factory ReceiptsData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ReceiptsData( + receiptId: serializer.fromJson(json['receiptId']), + contactId: serializer.fromJson(json['contactId']), + messageId: serializer.fromJson(json['messageId']), + message: serializer.fromJson(json['message']), + contactWillSendsReceipt: serializer.fromJson( + json['contactWillSendsReceipt'], + ), + willBeRetriedByMediaUpload: serializer.fromJson( + json['willBeRetriedByMediaUpload'], + ), + markForRetry: serializer.fromJson(json['markForRetry']), + markForRetryAfterAccepted: serializer.fromJson( + json['markForRetryAfterAccepted'], + ), + ackByServerAt: serializer.fromJson(json['ackByServerAt']), + retryCount: serializer.fromJson(json['retryCount']), + lastRetry: serializer.fromJson(json['lastRetry']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'receiptId': serializer.toJson(receiptId), + 'contactId': serializer.toJson(contactId), + 'messageId': serializer.toJson(messageId), + 'message': serializer.toJson(message), + 'contactWillSendsReceipt': serializer.toJson( + contactWillSendsReceipt, + ), + 'willBeRetriedByMediaUpload': serializer.toJson( + willBeRetriedByMediaUpload, + ), + 'markForRetry': serializer.toJson(markForRetry), + 'markForRetryAfterAccepted': serializer.toJson( + markForRetryAfterAccepted, + ), + 'ackByServerAt': serializer.toJson(ackByServerAt), + 'retryCount': serializer.toJson(retryCount), + 'lastRetry': serializer.toJson(lastRetry), + 'createdAt': serializer.toJson(createdAt), + }; + } + + ReceiptsData copyWith({ + String? receiptId, + int? contactId, + Value messageId = const Value.absent(), + i2.Uint8List? message, + int? contactWillSendsReceipt, + int? willBeRetriedByMediaUpload, + Value markForRetry = const Value.absent(), + Value markForRetryAfterAccepted = const Value.absent(), + Value ackByServerAt = const Value.absent(), + int? retryCount, + Value lastRetry = const Value.absent(), + int? createdAt, + }) => ReceiptsData( + receiptId: receiptId ?? this.receiptId, + contactId: contactId ?? this.contactId, + messageId: messageId.present ? messageId.value : this.messageId, + message: message ?? this.message, + contactWillSendsReceipt: + contactWillSendsReceipt ?? this.contactWillSendsReceipt, + willBeRetriedByMediaUpload: + willBeRetriedByMediaUpload ?? this.willBeRetriedByMediaUpload, + markForRetry: markForRetry.present ? markForRetry.value : this.markForRetry, + markForRetryAfterAccepted: markForRetryAfterAccepted.present + ? markForRetryAfterAccepted.value + : this.markForRetryAfterAccepted, + ackByServerAt: ackByServerAt.present + ? ackByServerAt.value + : this.ackByServerAt, + retryCount: retryCount ?? this.retryCount, + lastRetry: lastRetry.present ? lastRetry.value : this.lastRetry, + createdAt: createdAt ?? this.createdAt, + ); + ReceiptsData copyWithCompanion(ReceiptsCompanion data) { + return ReceiptsData( + receiptId: data.receiptId.present ? data.receiptId.value : this.receiptId, + contactId: data.contactId.present ? data.contactId.value : this.contactId, + messageId: data.messageId.present ? data.messageId.value : this.messageId, + message: data.message.present ? data.message.value : this.message, + contactWillSendsReceipt: data.contactWillSendsReceipt.present + ? data.contactWillSendsReceipt.value + : this.contactWillSendsReceipt, + willBeRetriedByMediaUpload: data.willBeRetriedByMediaUpload.present + ? data.willBeRetriedByMediaUpload.value + : this.willBeRetriedByMediaUpload, + markForRetry: data.markForRetry.present + ? data.markForRetry.value + : this.markForRetry, + markForRetryAfterAccepted: data.markForRetryAfterAccepted.present + ? data.markForRetryAfterAccepted.value + : this.markForRetryAfterAccepted, + ackByServerAt: data.ackByServerAt.present + ? data.ackByServerAt.value + : this.ackByServerAt, + retryCount: data.retryCount.present + ? data.retryCount.value + : this.retryCount, + lastRetry: data.lastRetry.present ? data.lastRetry.value : this.lastRetry, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('ReceiptsData(') + ..write('receiptId: $receiptId, ') + ..write('contactId: $contactId, ') + ..write('messageId: $messageId, ') + ..write('message: $message, ') + ..write('contactWillSendsReceipt: $contactWillSendsReceipt, ') + ..write('willBeRetriedByMediaUpload: $willBeRetriedByMediaUpload, ') + ..write('markForRetry: $markForRetry, ') + ..write('markForRetryAfterAccepted: $markForRetryAfterAccepted, ') + ..write('ackByServerAt: $ackByServerAt, ') + ..write('retryCount: $retryCount, ') + ..write('lastRetry: $lastRetry, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + receiptId, + contactId, + messageId, + $driftBlobEquality.hash(message), + contactWillSendsReceipt, + willBeRetriedByMediaUpload, + markForRetry, + markForRetryAfterAccepted, + ackByServerAt, + retryCount, + lastRetry, + createdAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ReceiptsData && + other.receiptId == this.receiptId && + other.contactId == this.contactId && + other.messageId == this.messageId && + $driftBlobEquality.equals(other.message, this.message) && + other.contactWillSendsReceipt == this.contactWillSendsReceipt && + other.willBeRetriedByMediaUpload == this.willBeRetriedByMediaUpload && + other.markForRetry == this.markForRetry && + other.markForRetryAfterAccepted == this.markForRetryAfterAccepted && + other.ackByServerAt == this.ackByServerAt && + other.retryCount == this.retryCount && + other.lastRetry == this.lastRetry && + other.createdAt == this.createdAt); +} + +class ReceiptsCompanion extends UpdateCompanion { + final Value receiptId; + final Value contactId; + final Value messageId; + final Value message; + final Value contactWillSendsReceipt; + final Value willBeRetriedByMediaUpload; + final Value markForRetry; + final Value markForRetryAfterAccepted; + final Value ackByServerAt; + final Value retryCount; + final Value lastRetry; + final Value createdAt; + final Value rowid; + const ReceiptsCompanion({ + this.receiptId = const Value.absent(), + this.contactId = const Value.absent(), + this.messageId = const Value.absent(), + this.message = const Value.absent(), + this.contactWillSendsReceipt = const Value.absent(), + this.willBeRetriedByMediaUpload = const Value.absent(), + this.markForRetry = const Value.absent(), + this.markForRetryAfterAccepted = const Value.absent(), + this.ackByServerAt = const Value.absent(), + this.retryCount = const Value.absent(), + this.lastRetry = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + ReceiptsCompanion.insert({ + required String receiptId, + required int contactId, + this.messageId = const Value.absent(), + required i2.Uint8List message, + this.contactWillSendsReceipt = const Value.absent(), + this.willBeRetriedByMediaUpload = const Value.absent(), + this.markForRetry = const Value.absent(), + this.markForRetryAfterAccepted = const Value.absent(), + this.ackByServerAt = const Value.absent(), + this.retryCount = const Value.absent(), + this.lastRetry = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : receiptId = Value(receiptId), + contactId = Value(contactId), + message = Value(message); + static Insertable custom({ + Expression? receiptId, + Expression? contactId, + Expression? messageId, + Expression? message, + Expression? contactWillSendsReceipt, + Expression? willBeRetriedByMediaUpload, + Expression? markForRetry, + Expression? markForRetryAfterAccepted, + Expression? ackByServerAt, + Expression? retryCount, + Expression? lastRetry, + Expression? createdAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (receiptId != null) 'receipt_id': receiptId, + if (contactId != null) 'contact_id': contactId, + if (messageId != null) 'message_id': messageId, + if (message != null) 'message': message, + if (contactWillSendsReceipt != null) + 'contact_will_sends_receipt': contactWillSendsReceipt, + if (willBeRetriedByMediaUpload != null) + 'will_be_retried_by_media_upload': willBeRetriedByMediaUpload, + if (markForRetry != null) 'mark_for_retry': markForRetry, + if (markForRetryAfterAccepted != null) + 'mark_for_retry_after_accepted': markForRetryAfterAccepted, + if (ackByServerAt != null) 'ack_by_server_at': ackByServerAt, + if (retryCount != null) 'retry_count': retryCount, + if (lastRetry != null) 'last_retry': lastRetry, + if (createdAt != null) 'created_at': createdAt, + if (rowid != null) 'rowid': rowid, + }); + } + + ReceiptsCompanion copyWith({ + Value? receiptId, + Value? contactId, + Value? messageId, + Value? message, + Value? contactWillSendsReceipt, + Value? willBeRetriedByMediaUpload, + Value? markForRetry, + Value? markForRetryAfterAccepted, + Value? ackByServerAt, + Value? retryCount, + Value? lastRetry, + Value? createdAt, + Value? rowid, + }) { + return ReceiptsCompanion( + receiptId: receiptId ?? this.receiptId, + contactId: contactId ?? this.contactId, + messageId: messageId ?? this.messageId, + message: message ?? this.message, + contactWillSendsReceipt: + contactWillSendsReceipt ?? this.contactWillSendsReceipt, + willBeRetriedByMediaUpload: + willBeRetriedByMediaUpload ?? this.willBeRetriedByMediaUpload, + markForRetry: markForRetry ?? this.markForRetry, + markForRetryAfterAccepted: + markForRetryAfterAccepted ?? this.markForRetryAfterAccepted, + ackByServerAt: ackByServerAt ?? this.ackByServerAt, + retryCount: retryCount ?? this.retryCount, + lastRetry: lastRetry ?? this.lastRetry, + createdAt: createdAt ?? this.createdAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (receiptId.present) { + map['receipt_id'] = Variable(receiptId.value); + } + if (contactId.present) { + map['contact_id'] = Variable(contactId.value); + } + if (messageId.present) { + map['message_id'] = Variable(messageId.value); + } + if (message.present) { + map['message'] = Variable(message.value); + } + if (contactWillSendsReceipt.present) { + map['contact_will_sends_receipt'] = Variable( + contactWillSendsReceipt.value, + ); + } + if (willBeRetriedByMediaUpload.present) { + map['will_be_retried_by_media_upload'] = Variable( + willBeRetriedByMediaUpload.value, + ); + } + if (markForRetry.present) { + map['mark_for_retry'] = Variable(markForRetry.value); + } + if (markForRetryAfterAccepted.present) { + map['mark_for_retry_after_accepted'] = Variable( + markForRetryAfterAccepted.value, + ); + } + if (ackByServerAt.present) { + map['ack_by_server_at'] = Variable(ackByServerAt.value); + } + if (retryCount.present) { + map['retry_count'] = Variable(retryCount.value); + } + if (lastRetry.present) { + map['last_retry'] = Variable(lastRetry.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ReceiptsCompanion(') + ..write('receiptId: $receiptId, ') + ..write('contactId: $contactId, ') + ..write('messageId: $messageId, ') + ..write('message: $message, ') + ..write('contactWillSendsReceipt: $contactWillSendsReceipt, ') + ..write('willBeRetriedByMediaUpload: $willBeRetriedByMediaUpload, ') + ..write('markForRetry: $markForRetry, ') + ..write('markForRetryAfterAccepted: $markForRetryAfterAccepted, ') + ..write('ackByServerAt: $ackByServerAt, ') + ..write('retryCount: $retryCount, ') + ..write('lastRetry: $lastRetry, ') + ..write('createdAt: $createdAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class ReceivedReceipts extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + ReceivedReceipts(this.attachedDatabase, [this._alias]); + late final GeneratedColumn receiptId = GeneratedColumn( + 'receipt_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT (CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER))', + defaultValue: const CustomExpression( + 'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)', + ), + ); + @override + List get $columns => [receiptId, createdAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'received_receipts'; + @override + Set get $primaryKey => {receiptId}; + @override + ReceivedReceiptsData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ReceivedReceiptsData( + receiptId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}receipt_id'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}created_at'], + )!, + ); + } + + @override + ReceivedReceipts createAlias(String alias) { + return ReceivedReceipts(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(receipt_id)']; + @override + bool get dontWriteConstraints => true; +} + +class ReceivedReceiptsData extends DataClass + implements Insertable { + final String receiptId; + final int createdAt; + const ReceivedReceiptsData({ + required this.receiptId, + required this.createdAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['receipt_id'] = Variable(receiptId); + map['created_at'] = Variable(createdAt); + return map; + } + + ReceivedReceiptsCompanion toCompanion(bool nullToAbsent) { + return ReceivedReceiptsCompanion( + receiptId: Value(receiptId), + createdAt: Value(createdAt), + ); + } + + factory ReceivedReceiptsData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ReceivedReceiptsData( + receiptId: serializer.fromJson(json['receiptId']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'receiptId': serializer.toJson(receiptId), + 'createdAt': serializer.toJson(createdAt), + }; + } + + ReceivedReceiptsData copyWith({String? receiptId, int? createdAt}) => + ReceivedReceiptsData( + receiptId: receiptId ?? this.receiptId, + createdAt: createdAt ?? this.createdAt, + ); + ReceivedReceiptsData copyWithCompanion(ReceivedReceiptsCompanion data) { + return ReceivedReceiptsData( + receiptId: data.receiptId.present ? data.receiptId.value : this.receiptId, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('ReceivedReceiptsData(') + ..write('receiptId: $receiptId, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(receiptId, createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ReceivedReceiptsData && + other.receiptId == this.receiptId && + other.createdAt == this.createdAt); +} + +class ReceivedReceiptsCompanion extends UpdateCompanion { + final Value receiptId; + final Value createdAt; + final Value rowid; + const ReceivedReceiptsCompanion({ + this.receiptId = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + ReceivedReceiptsCompanion.insert({ + required String receiptId, + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : receiptId = Value(receiptId); + static Insertable custom({ + Expression? receiptId, + Expression? createdAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (receiptId != null) 'receipt_id': receiptId, + if (createdAt != null) 'created_at': createdAt, + if (rowid != null) 'rowid': rowid, + }); + } + + ReceivedReceiptsCompanion copyWith({ + Value? receiptId, + Value? createdAt, + Value? rowid, + }) { + return ReceivedReceiptsCompanion( + receiptId: receiptId ?? this.receiptId, + createdAt: createdAt ?? this.createdAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (receiptId.present) { + map['receipt_id'] = Variable(receiptId.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ReceivedReceiptsCompanion(') + ..write('receiptId: $receiptId, ') + ..write('createdAt: $createdAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class SignalIdentityKeyStores extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + SignalIdentityKeyStores(this.attachedDatabase, [this._alias]); + late final GeneratedColumn deviceId = GeneratedColumn( + 'device_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn identityKey = + GeneratedColumn( + 'identity_key', + aliasedName, + false, + type: DriftSqlType.blob, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT (CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER))', + defaultValue: const CustomExpression( + 'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)', + ), + ); + @override + List get $columns => [ + deviceId, + name, + identityKey, + createdAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'signal_identity_key_stores'; + @override + Set get $primaryKey => {deviceId, name}; + @override + SignalIdentityKeyStoresData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SignalIdentityKeyStoresData( + deviceId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}device_id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + identityKey: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}identity_key'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}created_at'], + )!, + ); + } + + @override + SignalIdentityKeyStores createAlias(String alias) { + return SignalIdentityKeyStores(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(device_id, name)']; + @override + bool get dontWriteConstraints => true; +} + +class SignalIdentityKeyStoresData extends DataClass + implements Insertable { + final int deviceId; + final String name; + final i2.Uint8List identityKey; + final int createdAt; + const SignalIdentityKeyStoresData({ + required this.deviceId, + required this.name, + required this.identityKey, + required this.createdAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['device_id'] = Variable(deviceId); + map['name'] = Variable(name); + map['identity_key'] = Variable(identityKey); + map['created_at'] = Variable(createdAt); + return map; + } + + SignalIdentityKeyStoresCompanion toCompanion(bool nullToAbsent) { + return SignalIdentityKeyStoresCompanion( + deviceId: Value(deviceId), + name: Value(name), + identityKey: Value(identityKey), + createdAt: Value(createdAt), + ); + } + + factory SignalIdentityKeyStoresData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SignalIdentityKeyStoresData( + deviceId: serializer.fromJson(json['deviceId']), + name: serializer.fromJson(json['name']), + identityKey: serializer.fromJson(json['identityKey']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'deviceId': serializer.toJson(deviceId), + 'name': serializer.toJson(name), + 'identityKey': serializer.toJson(identityKey), + 'createdAt': serializer.toJson(createdAt), + }; + } + + SignalIdentityKeyStoresData copyWith({ + int? deviceId, + String? name, + i2.Uint8List? identityKey, + int? createdAt, + }) => SignalIdentityKeyStoresData( + deviceId: deviceId ?? this.deviceId, + name: name ?? this.name, + identityKey: identityKey ?? this.identityKey, + createdAt: createdAt ?? this.createdAt, + ); + SignalIdentityKeyStoresData copyWithCompanion( + SignalIdentityKeyStoresCompanion data, + ) { + return SignalIdentityKeyStoresData( + deviceId: data.deviceId.present ? data.deviceId.value : this.deviceId, + name: data.name.present ? data.name.value : this.name, + identityKey: data.identityKey.present + ? data.identityKey.value + : this.identityKey, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('SignalIdentityKeyStoresData(') + ..write('deviceId: $deviceId, ') + ..write('name: $name, ') + ..write('identityKey: $identityKey, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + deviceId, + name, + $driftBlobEquality.hash(identityKey), + createdAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SignalIdentityKeyStoresData && + other.deviceId == this.deviceId && + other.name == this.name && + $driftBlobEquality.equals(other.identityKey, this.identityKey) && + other.createdAt == this.createdAt); +} + +class SignalIdentityKeyStoresCompanion + extends UpdateCompanion { + final Value deviceId; + final Value name; + final Value identityKey; + final Value createdAt; + final Value rowid; + const SignalIdentityKeyStoresCompanion({ + this.deviceId = const Value.absent(), + this.name = const Value.absent(), + this.identityKey = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + SignalIdentityKeyStoresCompanion.insert({ + required int deviceId, + required String name, + required i2.Uint8List identityKey, + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : deviceId = Value(deviceId), + name = Value(name), + identityKey = Value(identityKey); + static Insertable custom({ + Expression? deviceId, + Expression? name, + Expression? identityKey, + Expression? createdAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (deviceId != null) 'device_id': deviceId, + if (name != null) 'name': name, + if (identityKey != null) 'identity_key': identityKey, + if (createdAt != null) 'created_at': createdAt, + if (rowid != null) 'rowid': rowid, + }); + } + + SignalIdentityKeyStoresCompanion copyWith({ + Value? deviceId, + Value? name, + Value? identityKey, + Value? createdAt, + Value? rowid, + }) { + return SignalIdentityKeyStoresCompanion( + deviceId: deviceId ?? this.deviceId, + name: name ?? this.name, + identityKey: identityKey ?? this.identityKey, + createdAt: createdAt ?? this.createdAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (deviceId.present) { + map['device_id'] = Variable(deviceId.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (identityKey.present) { + map['identity_key'] = Variable(identityKey.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SignalIdentityKeyStoresCompanion(') + ..write('deviceId: $deviceId, ') + ..write('name: $name, ') + ..write('identityKey: $identityKey, ') + ..write('createdAt: $createdAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class SignalPreKeyStores extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + SignalPreKeyStores(this.attachedDatabase, [this._alias]); + late final GeneratedColumn preKeyId = GeneratedColumn( + 'pre_key_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn preKey = + GeneratedColumn( + 'pre_key', + aliasedName, + false, + type: DriftSqlType.blob, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT (CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER))', + defaultValue: const CustomExpression( + 'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)', + ), + ); + @override + List get $columns => [preKeyId, preKey, createdAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'signal_pre_key_stores'; + @override + Set get $primaryKey => {preKeyId}; + @override + SignalPreKeyStoresData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SignalPreKeyStoresData( + preKeyId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}pre_key_id'], + )!, + preKey: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}pre_key'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}created_at'], + )!, + ); + } + + @override + SignalPreKeyStores createAlias(String alias) { + return SignalPreKeyStores(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(pre_key_id)']; + @override + bool get dontWriteConstraints => true; +} + +class SignalPreKeyStoresData extends DataClass + implements Insertable { + final int preKeyId; + final i2.Uint8List preKey; + final int createdAt; + const SignalPreKeyStoresData({ + required this.preKeyId, + required this.preKey, + required this.createdAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['pre_key_id'] = Variable(preKeyId); + map['pre_key'] = Variable(preKey); + map['created_at'] = Variable(createdAt); + return map; + } + + SignalPreKeyStoresCompanion toCompanion(bool nullToAbsent) { + return SignalPreKeyStoresCompanion( + preKeyId: Value(preKeyId), + preKey: Value(preKey), + createdAt: Value(createdAt), + ); + } + + factory SignalPreKeyStoresData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SignalPreKeyStoresData( + preKeyId: serializer.fromJson(json['preKeyId']), + preKey: serializer.fromJson(json['preKey']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'preKeyId': serializer.toJson(preKeyId), + 'preKey': serializer.toJson(preKey), + 'createdAt': serializer.toJson(createdAt), + }; + } + + SignalPreKeyStoresData copyWith({ + int? preKeyId, + i2.Uint8List? preKey, + int? createdAt, + }) => SignalPreKeyStoresData( + preKeyId: preKeyId ?? this.preKeyId, + preKey: preKey ?? this.preKey, + createdAt: createdAt ?? this.createdAt, + ); + SignalPreKeyStoresData copyWithCompanion(SignalPreKeyStoresCompanion data) { + return SignalPreKeyStoresData( + preKeyId: data.preKeyId.present ? data.preKeyId.value : this.preKeyId, + preKey: data.preKey.present ? data.preKey.value : this.preKey, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('SignalPreKeyStoresData(') + ..write('preKeyId: $preKeyId, ') + ..write('preKey: $preKey, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(preKeyId, $driftBlobEquality.hash(preKey), createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SignalPreKeyStoresData && + other.preKeyId == this.preKeyId && + $driftBlobEquality.equals(other.preKey, this.preKey) && + other.createdAt == this.createdAt); +} + +class SignalPreKeyStoresCompanion + extends UpdateCompanion { + final Value preKeyId; + final Value preKey; + final Value createdAt; + const SignalPreKeyStoresCompanion({ + this.preKeyId = const Value.absent(), + this.preKey = const Value.absent(), + this.createdAt = const Value.absent(), + }); + SignalPreKeyStoresCompanion.insert({ + this.preKeyId = const Value.absent(), + required i2.Uint8List preKey, + this.createdAt = const Value.absent(), + }) : preKey = Value(preKey); + static Insertable custom({ + Expression? preKeyId, + Expression? preKey, + Expression? createdAt, + }) { + return RawValuesInsertable({ + if (preKeyId != null) 'pre_key_id': preKeyId, + if (preKey != null) 'pre_key': preKey, + if (createdAt != null) 'created_at': createdAt, + }); + } + + SignalPreKeyStoresCompanion copyWith({ + Value? preKeyId, + Value? preKey, + Value? createdAt, + }) { + return SignalPreKeyStoresCompanion( + preKeyId: preKeyId ?? this.preKeyId, + preKey: preKey ?? this.preKey, + createdAt: createdAt ?? this.createdAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (preKeyId.present) { + map['pre_key_id'] = Variable(preKeyId.value); + } + if (preKey.present) { + map['pre_key'] = Variable(preKey.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SignalPreKeyStoresCompanion(') + ..write('preKeyId: $preKeyId, ') + ..write('preKey: $preKey, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } +} + +class SignalSenderKeyStores extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + SignalSenderKeyStores(this.attachedDatabase, [this._alias]); + late final GeneratedColumn senderKeyName = GeneratedColumn( + 'sender_key_name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn senderKey = + GeneratedColumn( + 'sender_key', + aliasedName, + false, + type: DriftSqlType.blob, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + @override + List get $columns => [senderKeyName, senderKey]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'signal_sender_key_stores'; + @override + Set get $primaryKey => {senderKeyName}; + @override + SignalSenderKeyStoresData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SignalSenderKeyStoresData( + senderKeyName: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}sender_key_name'], + )!, + senderKey: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}sender_key'], + )!, + ); + } + + @override + SignalSenderKeyStores createAlias(String alias) { + return SignalSenderKeyStores(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(sender_key_name)']; + @override + bool get dontWriteConstraints => true; +} + +class SignalSenderKeyStoresData extends DataClass + implements Insertable { + final String senderKeyName; + final i2.Uint8List senderKey; + const SignalSenderKeyStoresData({ + required this.senderKeyName, + required this.senderKey, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['sender_key_name'] = Variable(senderKeyName); + map['sender_key'] = Variable(senderKey); + return map; + } + + SignalSenderKeyStoresCompanion toCompanion(bool nullToAbsent) { + return SignalSenderKeyStoresCompanion( + senderKeyName: Value(senderKeyName), + senderKey: Value(senderKey), + ); + } + + factory SignalSenderKeyStoresData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SignalSenderKeyStoresData( + senderKeyName: serializer.fromJson(json['senderKeyName']), + senderKey: serializer.fromJson(json['senderKey']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'senderKeyName': serializer.toJson(senderKeyName), + 'senderKey': serializer.toJson(senderKey), + }; + } + + SignalSenderKeyStoresData copyWith({ + String? senderKeyName, + i2.Uint8List? senderKey, + }) => SignalSenderKeyStoresData( + senderKeyName: senderKeyName ?? this.senderKeyName, + senderKey: senderKey ?? this.senderKey, + ); + SignalSenderKeyStoresData copyWithCompanion( + SignalSenderKeyStoresCompanion data, + ) { + return SignalSenderKeyStoresData( + senderKeyName: data.senderKeyName.present + ? data.senderKeyName.value + : this.senderKeyName, + senderKey: data.senderKey.present ? data.senderKey.value : this.senderKey, + ); + } + + @override + String toString() { + return (StringBuffer('SignalSenderKeyStoresData(') + ..write('senderKeyName: $senderKeyName, ') + ..write('senderKey: $senderKey') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(senderKeyName, $driftBlobEquality.hash(senderKey)); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SignalSenderKeyStoresData && + other.senderKeyName == this.senderKeyName && + $driftBlobEquality.equals(other.senderKey, this.senderKey)); +} + +class SignalSenderKeyStoresCompanion + extends UpdateCompanion { + final Value senderKeyName; + final Value senderKey; + final Value rowid; + const SignalSenderKeyStoresCompanion({ + this.senderKeyName = const Value.absent(), + this.senderKey = const Value.absent(), + this.rowid = const Value.absent(), + }); + SignalSenderKeyStoresCompanion.insert({ + required String senderKeyName, + required i2.Uint8List senderKey, + this.rowid = const Value.absent(), + }) : senderKeyName = Value(senderKeyName), + senderKey = Value(senderKey); + static Insertable custom({ + Expression? senderKeyName, + Expression? senderKey, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (senderKeyName != null) 'sender_key_name': senderKeyName, + if (senderKey != null) 'sender_key': senderKey, + if (rowid != null) 'rowid': rowid, + }); + } + + SignalSenderKeyStoresCompanion copyWith({ + Value? senderKeyName, + Value? senderKey, + Value? rowid, + }) { + return SignalSenderKeyStoresCompanion( + senderKeyName: senderKeyName ?? this.senderKeyName, + senderKey: senderKey ?? this.senderKey, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (senderKeyName.present) { + map['sender_key_name'] = Variable(senderKeyName.value); + } + if (senderKey.present) { + map['sender_key'] = Variable(senderKey.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SignalSenderKeyStoresCompanion(') + ..write('senderKeyName: $senderKeyName, ') + ..write('senderKey: $senderKey, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class SignalSessionStores extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + SignalSessionStores(this.attachedDatabase, [this._alias]); + late final GeneratedColumn deviceId = GeneratedColumn( + 'device_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn sessionRecord = + GeneratedColumn( + 'session_record', + aliasedName, + false, + type: DriftSqlType.blob, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT (CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER))', + defaultValue: const CustomExpression( + 'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)', + ), + ); + @override + List get $columns => [ + deviceId, + name, + sessionRecord, + createdAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'signal_session_stores'; + @override + Set get $primaryKey => {deviceId, name}; + @override + SignalSessionStoresData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SignalSessionStoresData( + deviceId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}device_id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + sessionRecord: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}session_record'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}created_at'], + )!, + ); + } + + @override + SignalSessionStores createAlias(String alias) { + return SignalSessionStores(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(device_id, name)']; + @override + bool get dontWriteConstraints => true; +} + +class SignalSessionStoresData extends DataClass + implements Insertable { + final int deviceId; + final String name; + final i2.Uint8List sessionRecord; + final int createdAt; + const SignalSessionStoresData({ + required this.deviceId, + required this.name, + required this.sessionRecord, + required this.createdAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['device_id'] = Variable(deviceId); + map['name'] = Variable(name); + map['session_record'] = Variable(sessionRecord); + map['created_at'] = Variable(createdAt); + return map; + } + + SignalSessionStoresCompanion toCompanion(bool nullToAbsent) { + return SignalSessionStoresCompanion( + deviceId: Value(deviceId), + name: Value(name), + sessionRecord: Value(sessionRecord), + createdAt: Value(createdAt), + ); + } + + factory SignalSessionStoresData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SignalSessionStoresData( + deviceId: serializer.fromJson(json['deviceId']), + name: serializer.fromJson(json['name']), + sessionRecord: serializer.fromJson(json['sessionRecord']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'deviceId': serializer.toJson(deviceId), + 'name': serializer.toJson(name), + 'sessionRecord': serializer.toJson(sessionRecord), + 'createdAt': serializer.toJson(createdAt), + }; + } + + SignalSessionStoresData copyWith({ + int? deviceId, + String? name, + i2.Uint8List? sessionRecord, + int? createdAt, + }) => SignalSessionStoresData( + deviceId: deviceId ?? this.deviceId, + name: name ?? this.name, + sessionRecord: sessionRecord ?? this.sessionRecord, + createdAt: createdAt ?? this.createdAt, + ); + SignalSessionStoresData copyWithCompanion(SignalSessionStoresCompanion data) { + return SignalSessionStoresData( + deviceId: data.deviceId.present ? data.deviceId.value : this.deviceId, + name: data.name.present ? data.name.value : this.name, + sessionRecord: data.sessionRecord.present + ? data.sessionRecord.value + : this.sessionRecord, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('SignalSessionStoresData(') + ..write('deviceId: $deviceId, ') + ..write('name: $name, ') + ..write('sessionRecord: $sessionRecord, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + deviceId, + name, + $driftBlobEquality.hash(sessionRecord), + createdAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SignalSessionStoresData && + other.deviceId == this.deviceId && + other.name == this.name && + $driftBlobEquality.equals(other.sessionRecord, this.sessionRecord) && + other.createdAt == this.createdAt); +} + +class SignalSessionStoresCompanion + extends UpdateCompanion { + final Value deviceId; + final Value name; + final Value sessionRecord; + final Value createdAt; + final Value rowid; + const SignalSessionStoresCompanion({ + this.deviceId = const Value.absent(), + this.name = const Value.absent(), + this.sessionRecord = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + SignalSessionStoresCompanion.insert({ + required int deviceId, + required String name, + required i2.Uint8List sessionRecord, + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : deviceId = Value(deviceId), + name = Value(name), + sessionRecord = Value(sessionRecord); + static Insertable custom({ + Expression? deviceId, + Expression? name, + Expression? sessionRecord, + Expression? createdAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (deviceId != null) 'device_id': deviceId, + if (name != null) 'name': name, + if (sessionRecord != null) 'session_record': sessionRecord, + if (createdAt != null) 'created_at': createdAt, + if (rowid != null) 'rowid': rowid, + }); + } + + SignalSessionStoresCompanion copyWith({ + Value? deviceId, + Value? name, + Value? sessionRecord, + Value? createdAt, + Value? rowid, + }) { + return SignalSessionStoresCompanion( + deviceId: deviceId ?? this.deviceId, + name: name ?? this.name, + sessionRecord: sessionRecord ?? this.sessionRecord, + createdAt: createdAt ?? this.createdAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (deviceId.present) { + map['device_id'] = Variable(deviceId.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (sessionRecord.present) { + map['session_record'] = Variable(sessionRecord.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SignalSessionStoresCompanion(') + ..write('deviceId: $deviceId, ') + ..write('name: $name, ') + ..write('sessionRecord: $sessionRecord, ') + ..write('createdAt: $createdAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class SignalSignedPreKeyStores extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + SignalSignedPreKeyStores(this.attachedDatabase, [this._alias]); + late final GeneratedColumn signedPreKeyId = GeneratedColumn( + 'signed_pre_key_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn signedPreKey = + GeneratedColumn( + 'signed_pre_key', + aliasedName, + false, + type: DriftSqlType.blob, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT (CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER))', + defaultValue: const CustomExpression( + 'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)', + ), + ); + @override + List get $columns => [ + signedPreKeyId, + signedPreKey, + createdAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'signal_signed_pre_key_stores'; + @override + Set get $primaryKey => {signedPreKeyId}; + @override + SignalSignedPreKeyStoresData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SignalSignedPreKeyStoresData( + signedPreKeyId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}signed_pre_key_id'], + )!, + signedPreKey: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}signed_pre_key'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}created_at'], + )!, + ); + } + + @override + SignalSignedPreKeyStores createAlias(String alias) { + return SignalSignedPreKeyStores(attachedDatabase, alias); + } + + @override + List get customConstraints => const [ + 'PRIMARY KEY(signed_pre_key_id)', + ]; + @override + bool get dontWriteConstraints => true; +} + +class SignalSignedPreKeyStoresData extends DataClass + implements Insertable { + final int signedPreKeyId; + final i2.Uint8List signedPreKey; + final int createdAt; + const SignalSignedPreKeyStoresData({ + required this.signedPreKeyId, + required this.signedPreKey, + required this.createdAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['signed_pre_key_id'] = Variable(signedPreKeyId); + map['signed_pre_key'] = Variable(signedPreKey); + map['created_at'] = Variable(createdAt); + return map; + } + + SignalSignedPreKeyStoresCompanion toCompanion(bool nullToAbsent) { + return SignalSignedPreKeyStoresCompanion( + signedPreKeyId: Value(signedPreKeyId), + signedPreKey: Value(signedPreKey), + createdAt: Value(createdAt), + ); + } + + factory SignalSignedPreKeyStoresData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SignalSignedPreKeyStoresData( + signedPreKeyId: serializer.fromJson(json['signedPreKeyId']), + signedPreKey: serializer.fromJson(json['signedPreKey']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'signedPreKeyId': serializer.toJson(signedPreKeyId), + 'signedPreKey': serializer.toJson(signedPreKey), + 'createdAt': serializer.toJson(createdAt), + }; + } + + SignalSignedPreKeyStoresData copyWith({ + int? signedPreKeyId, + i2.Uint8List? signedPreKey, + int? createdAt, + }) => SignalSignedPreKeyStoresData( + signedPreKeyId: signedPreKeyId ?? this.signedPreKeyId, + signedPreKey: signedPreKey ?? this.signedPreKey, + createdAt: createdAt ?? this.createdAt, + ); + SignalSignedPreKeyStoresData copyWithCompanion( + SignalSignedPreKeyStoresCompanion data, + ) { + return SignalSignedPreKeyStoresData( + signedPreKeyId: data.signedPreKeyId.present + ? data.signedPreKeyId.value + : this.signedPreKeyId, + signedPreKey: data.signedPreKey.present + ? data.signedPreKey.value + : this.signedPreKey, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('SignalSignedPreKeyStoresData(') + ..write('signedPreKeyId: $signedPreKeyId, ') + ..write('signedPreKey: $signedPreKey, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + signedPreKeyId, + $driftBlobEquality.hash(signedPreKey), + createdAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SignalSignedPreKeyStoresData && + other.signedPreKeyId == this.signedPreKeyId && + $driftBlobEquality.equals(other.signedPreKey, this.signedPreKey) && + other.createdAt == this.createdAt); +} + +class SignalSignedPreKeyStoresCompanion + extends UpdateCompanion { + final Value signedPreKeyId; + final Value signedPreKey; + final Value createdAt; + const SignalSignedPreKeyStoresCompanion({ + this.signedPreKeyId = const Value.absent(), + this.signedPreKey = const Value.absent(), + this.createdAt = const Value.absent(), + }); + SignalSignedPreKeyStoresCompanion.insert({ + this.signedPreKeyId = const Value.absent(), + required i2.Uint8List signedPreKey, + this.createdAt = const Value.absent(), + }) : signedPreKey = Value(signedPreKey); + static Insertable custom({ + Expression? signedPreKeyId, + Expression? signedPreKey, + Expression? createdAt, + }) { + return RawValuesInsertable({ + if (signedPreKeyId != null) 'signed_pre_key_id': signedPreKeyId, + if (signedPreKey != null) 'signed_pre_key': signedPreKey, + if (createdAt != null) 'created_at': createdAt, + }); + } + + SignalSignedPreKeyStoresCompanion copyWith({ + Value? signedPreKeyId, + Value? signedPreKey, + Value? createdAt, + }) { + return SignalSignedPreKeyStoresCompanion( + signedPreKeyId: signedPreKeyId ?? this.signedPreKeyId, + signedPreKey: signedPreKey ?? this.signedPreKey, + createdAt: createdAt ?? this.createdAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (signedPreKeyId.present) { + map['signed_pre_key_id'] = Variable(signedPreKeyId.value); + } + if (signedPreKey.present) { + map['signed_pre_key'] = Variable(signedPreKey.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SignalSignedPreKeyStoresCompanion(') + ..write('signedPreKeyId: $signedPreKeyId, ') + ..write('signedPreKey: $signedPreKey, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } +} + +class MessageActions extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + MessageActions(this.attachedDatabase, [this._alias]); + late final GeneratedColumn messageId = GeneratedColumn( + 'message_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: + 'NOT NULL REFERENCES messages(message_id)ON DELETE CASCADE', + ); + late final GeneratedColumn contactId = GeneratedColumn( + 'contact_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: + 'NOT NULL REFERENCES contacts(user_id)ON DELETE CASCADE', + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn actionAt = GeneratedColumn( + 'action_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT (CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER))', + defaultValue: const CustomExpression( + 'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)', + ), + ); + @override + List get $columns => [messageId, contactId, type, actionAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'message_actions'; + @override + Set get $primaryKey => {messageId, contactId, type}; + @override + MessageActionsData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MessageActionsData( + messageId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}message_id'], + )!, + contactId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}contact_id'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}type'], + )!, + actionAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}action_at'], + )!, + ); + } + + @override + MessageActions createAlias(String alias) { + return MessageActions(attachedDatabase, alias); + } + + @override + List get customConstraints => const [ + 'PRIMARY KEY(message_id, contact_id, type)', + ]; + @override + bool get dontWriteConstraints => true; +} + +class MessageActionsData extends DataClass + implements Insertable { + final String messageId; + final int contactId; + final String type; + final int actionAt; + const MessageActionsData({ + required this.messageId, + required this.contactId, + required this.type, + required this.actionAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['message_id'] = Variable(messageId); + map['contact_id'] = Variable(contactId); + map['type'] = Variable(type); + map['action_at'] = Variable(actionAt); + return map; + } + + MessageActionsCompanion toCompanion(bool nullToAbsent) { + return MessageActionsCompanion( + messageId: Value(messageId), + contactId: Value(contactId), + type: Value(type), + actionAt: Value(actionAt), + ); + } + + factory MessageActionsData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MessageActionsData( + messageId: serializer.fromJson(json['messageId']), + contactId: serializer.fromJson(json['contactId']), + type: serializer.fromJson(json['type']), + actionAt: serializer.fromJson(json['actionAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'messageId': serializer.toJson(messageId), + 'contactId': serializer.toJson(contactId), + 'type': serializer.toJson(type), + 'actionAt': serializer.toJson(actionAt), + }; + } + + MessageActionsData copyWith({ + String? messageId, + int? contactId, + String? type, + int? actionAt, + }) => MessageActionsData( + messageId: messageId ?? this.messageId, + contactId: contactId ?? this.contactId, + type: type ?? this.type, + actionAt: actionAt ?? this.actionAt, + ); + MessageActionsData copyWithCompanion(MessageActionsCompanion data) { + return MessageActionsData( + messageId: data.messageId.present ? data.messageId.value : this.messageId, + contactId: data.contactId.present ? data.contactId.value : this.contactId, + type: data.type.present ? data.type.value : this.type, + actionAt: data.actionAt.present ? data.actionAt.value : this.actionAt, + ); + } + + @override + String toString() { + return (StringBuffer('MessageActionsData(') + ..write('messageId: $messageId, ') + ..write('contactId: $contactId, ') + ..write('type: $type, ') + ..write('actionAt: $actionAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(messageId, contactId, type, actionAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MessageActionsData && + other.messageId == this.messageId && + other.contactId == this.contactId && + other.type == this.type && + other.actionAt == this.actionAt); +} + +class MessageActionsCompanion extends UpdateCompanion { + final Value messageId; + final Value contactId; + final Value type; + final Value actionAt; + final Value rowid; + const MessageActionsCompanion({ + this.messageId = const Value.absent(), + this.contactId = const Value.absent(), + this.type = const Value.absent(), + this.actionAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + MessageActionsCompanion.insert({ + required String messageId, + required int contactId, + required String type, + this.actionAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : messageId = Value(messageId), + contactId = Value(contactId), + type = Value(type); + static Insertable custom({ + Expression? messageId, + Expression? contactId, + Expression? type, + Expression? actionAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (messageId != null) 'message_id': messageId, + if (contactId != null) 'contact_id': contactId, + if (type != null) 'type': type, + if (actionAt != null) 'action_at': actionAt, + if (rowid != null) 'rowid': rowid, + }); + } + + MessageActionsCompanion copyWith({ + Value? messageId, + Value? contactId, + Value? type, + Value? actionAt, + Value? rowid, + }) { + return MessageActionsCompanion( + messageId: messageId ?? this.messageId, + contactId: contactId ?? this.contactId, + type: type ?? this.type, + actionAt: actionAt ?? this.actionAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (messageId.present) { + map['message_id'] = Variable(messageId.value); + } + if (contactId.present) { + map['contact_id'] = Variable(contactId.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (actionAt.present) { + map['action_at'] = Variable(actionAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MessageActionsCompanion(') + ..write('messageId: $messageId, ') + ..write('contactId: $contactId, ') + ..write('type: $type, ') + ..write('actionAt: $actionAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class GroupHistories extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + GroupHistories(this.attachedDatabase, [this._alias]); + late final GeneratedColumn groupHistoryId = GeneratedColumn( + 'group_history_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn groupId = GeneratedColumn( + 'group_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: + 'NOT NULL REFERENCES "groups"(group_id)ON DELETE CASCADE', + ); + late final GeneratedColumn contactId = GeneratedColumn( + 'contact_id', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL REFERENCES contacts(user_id)', + ); + late final GeneratedColumn affectedContactId = GeneratedColumn( + 'affected_contact_id', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn oldGroupName = GeneratedColumn( + 'old_group_name', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn newGroupName = GeneratedColumn( + 'new_group_name', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn newDeleteMessagesAfterMilliseconds = + GeneratedColumn( + 'new_delete_messages_after_milliseconds', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn actionAt = GeneratedColumn( + 'action_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT (CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER))', + defaultValue: const CustomExpression( + 'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)', + ), + ); + @override + List get $columns => [ + groupHistoryId, + groupId, + contactId, + affectedContactId, + oldGroupName, + newGroupName, + newDeleteMessagesAfterMilliseconds, + type, + actionAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'group_histories'; + @override + Set get $primaryKey => {groupHistoryId}; + @override + GroupHistoriesData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return GroupHistoriesData( + groupHistoryId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}group_history_id'], + )!, + groupId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}group_id'], + )!, + contactId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}contact_id'], + ), + affectedContactId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}affected_contact_id'], + ), + oldGroupName: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}old_group_name'], + ), + newGroupName: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}new_group_name'], + ), + newDeleteMessagesAfterMilliseconds: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}new_delete_messages_after_milliseconds'], + ), + type: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}type'], + )!, + actionAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}action_at'], + )!, + ); + } + + @override + GroupHistories createAlias(String alias) { + return GroupHistories(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(group_history_id)']; + @override + bool get dontWriteConstraints => true; +} + +class GroupHistoriesData extends DataClass + implements Insertable { + final String groupHistoryId; + final String groupId; + final int? contactId; + final int? affectedContactId; + final String? oldGroupName; + final String? newGroupName; + final int? newDeleteMessagesAfterMilliseconds; + final String type; + final int actionAt; + const GroupHistoriesData({ + required this.groupHistoryId, + required this.groupId, + this.contactId, + this.affectedContactId, + this.oldGroupName, + this.newGroupName, + this.newDeleteMessagesAfterMilliseconds, + required this.type, + required this.actionAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['group_history_id'] = Variable(groupHistoryId); + map['group_id'] = Variable(groupId); + if (!nullToAbsent || contactId != null) { + map['contact_id'] = Variable(contactId); + } + if (!nullToAbsent || affectedContactId != null) { + map['affected_contact_id'] = Variable(affectedContactId); + } + if (!nullToAbsent || oldGroupName != null) { + map['old_group_name'] = Variable(oldGroupName); + } + if (!nullToAbsent || newGroupName != null) { + map['new_group_name'] = Variable(newGroupName); + } + if (!nullToAbsent || newDeleteMessagesAfterMilliseconds != null) { + map['new_delete_messages_after_milliseconds'] = Variable( + newDeleteMessagesAfterMilliseconds, + ); + } + map['type'] = Variable(type); + map['action_at'] = Variable(actionAt); + return map; + } + + GroupHistoriesCompanion toCompanion(bool nullToAbsent) { + return GroupHistoriesCompanion( + groupHistoryId: Value(groupHistoryId), + groupId: Value(groupId), + contactId: contactId == null && nullToAbsent + ? const Value.absent() + : Value(contactId), + affectedContactId: affectedContactId == null && nullToAbsent + ? const Value.absent() + : Value(affectedContactId), + oldGroupName: oldGroupName == null && nullToAbsent + ? const Value.absent() + : Value(oldGroupName), + newGroupName: newGroupName == null && nullToAbsent + ? const Value.absent() + : Value(newGroupName), + newDeleteMessagesAfterMilliseconds: + newDeleteMessagesAfterMilliseconds == null && nullToAbsent + ? const Value.absent() + : Value(newDeleteMessagesAfterMilliseconds), + type: Value(type), + actionAt: Value(actionAt), + ); + } + + factory GroupHistoriesData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return GroupHistoriesData( + groupHistoryId: serializer.fromJson(json['groupHistoryId']), + groupId: serializer.fromJson(json['groupId']), + contactId: serializer.fromJson(json['contactId']), + affectedContactId: serializer.fromJson(json['affectedContactId']), + oldGroupName: serializer.fromJson(json['oldGroupName']), + newGroupName: serializer.fromJson(json['newGroupName']), + newDeleteMessagesAfterMilliseconds: serializer.fromJson( + json['newDeleteMessagesAfterMilliseconds'], + ), + type: serializer.fromJson(json['type']), + actionAt: serializer.fromJson(json['actionAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'groupHistoryId': serializer.toJson(groupHistoryId), + 'groupId': serializer.toJson(groupId), + 'contactId': serializer.toJson(contactId), + 'affectedContactId': serializer.toJson(affectedContactId), + 'oldGroupName': serializer.toJson(oldGroupName), + 'newGroupName': serializer.toJson(newGroupName), + 'newDeleteMessagesAfterMilliseconds': serializer.toJson( + newDeleteMessagesAfterMilliseconds, + ), + 'type': serializer.toJson(type), + 'actionAt': serializer.toJson(actionAt), + }; + } + + GroupHistoriesData copyWith({ + String? groupHistoryId, + String? groupId, + Value contactId = const Value.absent(), + Value affectedContactId = const Value.absent(), + Value oldGroupName = const Value.absent(), + Value newGroupName = const Value.absent(), + Value newDeleteMessagesAfterMilliseconds = const Value.absent(), + String? type, + int? actionAt, + }) => GroupHistoriesData( + groupHistoryId: groupHistoryId ?? this.groupHistoryId, + groupId: groupId ?? this.groupId, + contactId: contactId.present ? contactId.value : this.contactId, + affectedContactId: affectedContactId.present + ? affectedContactId.value + : this.affectedContactId, + oldGroupName: oldGroupName.present ? oldGroupName.value : this.oldGroupName, + newGroupName: newGroupName.present ? newGroupName.value : this.newGroupName, + newDeleteMessagesAfterMilliseconds: + newDeleteMessagesAfterMilliseconds.present + ? newDeleteMessagesAfterMilliseconds.value + : this.newDeleteMessagesAfterMilliseconds, + type: type ?? this.type, + actionAt: actionAt ?? this.actionAt, + ); + GroupHistoriesData copyWithCompanion(GroupHistoriesCompanion data) { + return GroupHistoriesData( + groupHistoryId: data.groupHistoryId.present + ? data.groupHistoryId.value + : this.groupHistoryId, + groupId: data.groupId.present ? data.groupId.value : this.groupId, + contactId: data.contactId.present ? data.contactId.value : this.contactId, + affectedContactId: data.affectedContactId.present + ? data.affectedContactId.value + : this.affectedContactId, + oldGroupName: data.oldGroupName.present + ? data.oldGroupName.value + : this.oldGroupName, + newGroupName: data.newGroupName.present + ? data.newGroupName.value + : this.newGroupName, + newDeleteMessagesAfterMilliseconds: + data.newDeleteMessagesAfterMilliseconds.present + ? data.newDeleteMessagesAfterMilliseconds.value + : this.newDeleteMessagesAfterMilliseconds, + type: data.type.present ? data.type.value : this.type, + actionAt: data.actionAt.present ? data.actionAt.value : this.actionAt, + ); + } + + @override + String toString() { + return (StringBuffer('GroupHistoriesData(') + ..write('groupHistoryId: $groupHistoryId, ') + ..write('groupId: $groupId, ') + ..write('contactId: $contactId, ') + ..write('affectedContactId: $affectedContactId, ') + ..write('oldGroupName: $oldGroupName, ') + ..write('newGroupName: $newGroupName, ') + ..write( + 'newDeleteMessagesAfterMilliseconds: $newDeleteMessagesAfterMilliseconds, ', + ) + ..write('type: $type, ') + ..write('actionAt: $actionAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + groupHistoryId, + groupId, + contactId, + affectedContactId, + oldGroupName, + newGroupName, + newDeleteMessagesAfterMilliseconds, + type, + actionAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is GroupHistoriesData && + other.groupHistoryId == this.groupHistoryId && + other.groupId == this.groupId && + other.contactId == this.contactId && + other.affectedContactId == this.affectedContactId && + other.oldGroupName == this.oldGroupName && + other.newGroupName == this.newGroupName && + other.newDeleteMessagesAfterMilliseconds == + this.newDeleteMessagesAfterMilliseconds && + other.type == this.type && + other.actionAt == this.actionAt); +} + +class GroupHistoriesCompanion extends UpdateCompanion { + final Value groupHistoryId; + final Value groupId; + final Value contactId; + final Value affectedContactId; + final Value oldGroupName; + final Value newGroupName; + final Value newDeleteMessagesAfterMilliseconds; + final Value type; + final Value actionAt; + final Value rowid; + const GroupHistoriesCompanion({ + this.groupHistoryId = const Value.absent(), + this.groupId = const Value.absent(), + this.contactId = const Value.absent(), + this.affectedContactId = const Value.absent(), + this.oldGroupName = const Value.absent(), + this.newGroupName = const Value.absent(), + this.newDeleteMessagesAfterMilliseconds = const Value.absent(), + this.type = const Value.absent(), + this.actionAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + GroupHistoriesCompanion.insert({ + required String groupHistoryId, + required String groupId, + this.contactId = const Value.absent(), + this.affectedContactId = const Value.absent(), + this.oldGroupName = const Value.absent(), + this.newGroupName = const Value.absent(), + this.newDeleteMessagesAfterMilliseconds = const Value.absent(), + required String type, + this.actionAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : groupHistoryId = Value(groupHistoryId), + groupId = Value(groupId), + type = Value(type); + static Insertable custom({ + Expression? groupHistoryId, + Expression? groupId, + Expression? contactId, + Expression? affectedContactId, + Expression? oldGroupName, + Expression? newGroupName, + Expression? newDeleteMessagesAfterMilliseconds, + Expression? type, + Expression? actionAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (groupHistoryId != null) 'group_history_id': groupHistoryId, + if (groupId != null) 'group_id': groupId, + if (contactId != null) 'contact_id': contactId, + if (affectedContactId != null) 'affected_contact_id': affectedContactId, + if (oldGroupName != null) 'old_group_name': oldGroupName, + if (newGroupName != null) 'new_group_name': newGroupName, + if (newDeleteMessagesAfterMilliseconds != null) + 'new_delete_messages_after_milliseconds': + newDeleteMessagesAfterMilliseconds, + if (type != null) 'type': type, + if (actionAt != null) 'action_at': actionAt, + if (rowid != null) 'rowid': rowid, + }); + } + + GroupHistoriesCompanion copyWith({ + Value? groupHistoryId, + Value? groupId, + Value? contactId, + Value? affectedContactId, + Value? oldGroupName, + Value? newGroupName, + Value? newDeleteMessagesAfterMilliseconds, + Value? type, + Value? actionAt, + Value? rowid, + }) { + return GroupHistoriesCompanion( + groupHistoryId: groupHistoryId ?? this.groupHistoryId, + groupId: groupId ?? this.groupId, + contactId: contactId ?? this.contactId, + affectedContactId: affectedContactId ?? this.affectedContactId, + oldGroupName: oldGroupName ?? this.oldGroupName, + newGroupName: newGroupName ?? this.newGroupName, + newDeleteMessagesAfterMilliseconds: + newDeleteMessagesAfterMilliseconds ?? + this.newDeleteMessagesAfterMilliseconds, + type: type ?? this.type, + actionAt: actionAt ?? this.actionAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (groupHistoryId.present) { + map['group_history_id'] = Variable(groupHistoryId.value); + } + if (groupId.present) { + map['group_id'] = Variable(groupId.value); + } + if (contactId.present) { + map['contact_id'] = Variable(contactId.value); + } + if (affectedContactId.present) { + map['affected_contact_id'] = Variable(affectedContactId.value); + } + if (oldGroupName.present) { + map['old_group_name'] = Variable(oldGroupName.value); + } + if (newGroupName.present) { + map['new_group_name'] = Variable(newGroupName.value); + } + if (newDeleteMessagesAfterMilliseconds.present) { + map['new_delete_messages_after_milliseconds'] = Variable( + newDeleteMessagesAfterMilliseconds.value, + ); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (actionAt.present) { + map['action_at'] = Variable(actionAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('GroupHistoriesCompanion(') + ..write('groupHistoryId: $groupHistoryId, ') + ..write('groupId: $groupId, ') + ..write('contactId: $contactId, ') + ..write('affectedContactId: $affectedContactId, ') + ..write('oldGroupName: $oldGroupName, ') + ..write('newGroupName: $newGroupName, ') + ..write( + 'newDeleteMessagesAfterMilliseconds: $newDeleteMessagesAfterMilliseconds, ', + ) + ..write('type: $type, ') + ..write('actionAt: $actionAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class KeyVerifications extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + KeyVerifications(this.attachedDatabase, [this._alias]); + late final GeneratedColumn verificationId = GeneratedColumn( + 'verification_id', + aliasedName, + false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL PRIMARY KEY AUTOINCREMENT', + ); + late final GeneratedColumn contactId = GeneratedColumn( + 'contact_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: + 'NOT NULL REFERENCES contacts(user_id)ON DELETE CASCADE', + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT (CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER))', + defaultValue: const CustomExpression( + 'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)', + ), + ); + @override + List get $columns => [ + verificationId, + contactId, + type, + createdAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'key_verifications'; + @override + Set get $primaryKey => {verificationId}; + @override + KeyVerificationsData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return KeyVerificationsData( + verificationId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}verification_id'], + )!, + contactId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}contact_id'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}type'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}created_at'], + )!, + ); + } + + @override + KeyVerifications createAlias(String alias) { + return KeyVerifications(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; +} + +class KeyVerificationsData extends DataClass + implements Insertable { + final int verificationId; + final int contactId; + final String type; + final int createdAt; + const KeyVerificationsData({ + required this.verificationId, + required this.contactId, + required this.type, + required this.createdAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['verification_id'] = Variable(verificationId); + map['contact_id'] = Variable(contactId); + map['type'] = Variable(type); + map['created_at'] = Variable(createdAt); + return map; + } + + KeyVerificationsCompanion toCompanion(bool nullToAbsent) { + return KeyVerificationsCompanion( + verificationId: Value(verificationId), + contactId: Value(contactId), + type: Value(type), + createdAt: Value(createdAt), + ); + } + + factory KeyVerificationsData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return KeyVerificationsData( + verificationId: serializer.fromJson(json['verificationId']), + contactId: serializer.fromJson(json['contactId']), + type: serializer.fromJson(json['type']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'verificationId': serializer.toJson(verificationId), + 'contactId': serializer.toJson(contactId), + 'type': serializer.toJson(type), + 'createdAt': serializer.toJson(createdAt), + }; + } + + KeyVerificationsData copyWith({ + int? verificationId, + int? contactId, + String? type, + int? createdAt, + }) => KeyVerificationsData( + verificationId: verificationId ?? this.verificationId, + contactId: contactId ?? this.contactId, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + ); + KeyVerificationsData copyWithCompanion(KeyVerificationsCompanion data) { + return KeyVerificationsData( + verificationId: data.verificationId.present + ? data.verificationId.value + : this.verificationId, + contactId: data.contactId.present ? data.contactId.value : this.contactId, + type: data.type.present ? data.type.value : this.type, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('KeyVerificationsData(') + ..write('verificationId: $verificationId, ') + ..write('contactId: $contactId, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(verificationId, contactId, type, createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is KeyVerificationsData && + other.verificationId == this.verificationId && + other.contactId == this.contactId && + other.type == this.type && + other.createdAt == this.createdAt); +} + +class KeyVerificationsCompanion extends UpdateCompanion { + final Value verificationId; + final Value contactId; + final Value type; + final Value createdAt; + const KeyVerificationsCompanion({ + this.verificationId = const Value.absent(), + this.contactId = const Value.absent(), + this.type = const Value.absent(), + this.createdAt = const Value.absent(), + }); + KeyVerificationsCompanion.insert({ + this.verificationId = const Value.absent(), + required int contactId, + required String type, + this.createdAt = const Value.absent(), + }) : contactId = Value(contactId), + type = Value(type); + static Insertable custom({ + Expression? verificationId, + Expression? contactId, + Expression? type, + Expression? createdAt, + }) { + return RawValuesInsertable({ + if (verificationId != null) 'verification_id': verificationId, + if (contactId != null) 'contact_id': contactId, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + }); + } + + KeyVerificationsCompanion copyWith({ + Value? verificationId, + Value? contactId, + Value? type, + Value? createdAt, + }) { + return KeyVerificationsCompanion( + verificationId: verificationId ?? this.verificationId, + contactId: contactId ?? this.contactId, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (verificationId.present) { + map['verification_id'] = Variable(verificationId.value); + } + if (contactId.present) { + map['contact_id'] = Variable(contactId.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('KeyVerificationsCompanion(') + ..write('verificationId: $verificationId, ') + ..write('contactId: $contactId, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } +} + +class VerificationTokens extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + VerificationTokens(this.attachedDatabase, [this._alias]); + late final GeneratedColumn tokenId = GeneratedColumn( + 'token_id', + aliasedName, + false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL PRIMARY KEY AUTOINCREMENT', + ); + late final GeneratedColumn token = + GeneratedColumn( + 'token', + aliasedName, + false, + type: DriftSqlType.blob, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT (CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER))', + defaultValue: const CustomExpression( + 'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)', + ), + ); + @override + List get $columns => [tokenId, token, createdAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'verification_tokens'; + @override + Set get $primaryKey => {tokenId}; + @override + VerificationTokensData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return VerificationTokensData( + tokenId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}token_id'], + )!, + token: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}token'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}created_at'], + )!, + ); + } + + @override + VerificationTokens createAlias(String alias) { + return VerificationTokens(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; +} + +class VerificationTokensData extends DataClass + implements Insertable { + final int tokenId; + final i2.Uint8List token; + final int createdAt; + const VerificationTokensData({ + required this.tokenId, + required this.token, + required this.createdAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['token_id'] = Variable(tokenId); + map['token'] = Variable(token); + map['created_at'] = Variable(createdAt); + return map; + } + + VerificationTokensCompanion toCompanion(bool nullToAbsent) { + return VerificationTokensCompanion( + tokenId: Value(tokenId), + token: Value(token), + createdAt: Value(createdAt), + ); + } + + factory VerificationTokensData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return VerificationTokensData( + tokenId: serializer.fromJson(json['tokenId']), + token: serializer.fromJson(json['token']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'tokenId': serializer.toJson(tokenId), + 'token': serializer.toJson(token), + 'createdAt': serializer.toJson(createdAt), + }; + } + + VerificationTokensData copyWith({ + int? tokenId, + i2.Uint8List? token, + int? createdAt, + }) => VerificationTokensData( + tokenId: tokenId ?? this.tokenId, + token: token ?? this.token, + createdAt: createdAt ?? this.createdAt, + ); + VerificationTokensData copyWithCompanion(VerificationTokensCompanion data) { + return VerificationTokensData( + tokenId: data.tokenId.present ? data.tokenId.value : this.tokenId, + token: data.token.present ? data.token.value : this.token, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('VerificationTokensData(') + ..write('tokenId: $tokenId, ') + ..write('token: $token, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(tokenId, $driftBlobEquality.hash(token), createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is VerificationTokensData && + other.tokenId == this.tokenId && + $driftBlobEquality.equals(other.token, this.token) && + other.createdAt == this.createdAt); +} + +class VerificationTokensCompanion + extends UpdateCompanion { + final Value tokenId; + final Value token; + final Value createdAt; + const VerificationTokensCompanion({ + this.tokenId = const Value.absent(), + this.token = const Value.absent(), + this.createdAt = const Value.absent(), + }); + VerificationTokensCompanion.insert({ + this.tokenId = const Value.absent(), + required i2.Uint8List token, + this.createdAt = const Value.absent(), + }) : token = Value(token); + static Insertable custom({ + Expression? tokenId, + Expression? token, + Expression? createdAt, + }) { + return RawValuesInsertable({ + if (tokenId != null) 'token_id': tokenId, + if (token != null) 'token': token, + if (createdAt != null) 'created_at': createdAt, + }); + } + + VerificationTokensCompanion copyWith({ + Value? tokenId, + Value? token, + Value? createdAt, + }) { + return VerificationTokensCompanion( + tokenId: tokenId ?? this.tokenId, + token: token ?? this.token, + createdAt: createdAt ?? this.createdAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (tokenId.present) { + map['token_id'] = Variable(tokenId.value); + } + if (token.present) { + map['token'] = Variable(token.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('VerificationTokensCompanion(') + ..write('tokenId: $tokenId, ') + ..write('token: $token, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } +} + +class UserDiscoveryAnnouncedUsers extends Table + with + TableInfo< + UserDiscoveryAnnouncedUsers, + UserDiscoveryAnnouncedUsersData + > { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserDiscoveryAnnouncedUsers(this.attachedDatabase, [this._alias]); + late final GeneratedColumn announcedUserId = GeneratedColumn( + 'announced_user_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn announcedPublicKey = + GeneratedColumn( + 'announced_public_key', + aliasedName, + false, + type: DriftSqlType.blob, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn publicId = GeneratedColumn( + 'public_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL UNIQUE', + ); + late final GeneratedColumn username = GeneratedColumn( + 'username', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn wasShownToTheUser = GeneratedColumn( + 'was_shown_to_the_user', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT 0 CHECK (was_shown_to_the_user IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn isHidden = GeneratedColumn( + 'is_hidden', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_hidden IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn wasAskedFriends = GeneratedColumn( + 'was_asked_friends', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT 0 CHECK (was_asked_friends IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [ + announcedUserId, + announcedPublicKey, + publicId, + username, + wasShownToTheUser, + isHidden, + wasAskedFriends, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_discovery_announced_users'; + @override + Set get $primaryKey => {announcedUserId}; + @override + UserDiscoveryAnnouncedUsersData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserDiscoveryAnnouncedUsersData( + announcedUserId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}announced_user_id'], + )!, + announcedPublicKey: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}announced_public_key'], + )!, + publicId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}public_id'], + )!, + username: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}username'], + ), + wasShownToTheUser: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}was_shown_to_the_user'], + )!, + isHidden: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}is_hidden'], + )!, + wasAskedFriends: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}was_asked_friends'], + )!, + ); + } + + @override + UserDiscoveryAnnouncedUsers createAlias(String alias) { + return UserDiscoveryAnnouncedUsers(attachedDatabase, alias); + } + + @override + List get customConstraints => const [ + 'PRIMARY KEY(announced_user_id)', + ]; + @override + bool get dontWriteConstraints => true; +} + +class UserDiscoveryAnnouncedUsersData extends DataClass + implements Insertable { + final int announcedUserId; + final i2.Uint8List announcedPublicKey; + final int publicId; + final String? username; + final int wasShownToTheUser; + final int isHidden; + final int wasAskedFriends; + const UserDiscoveryAnnouncedUsersData({ + required this.announcedUserId, + required this.announcedPublicKey, + required this.publicId, + this.username, + required this.wasShownToTheUser, + required this.isHidden, + required this.wasAskedFriends, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['announced_user_id'] = Variable(announcedUserId); + map['announced_public_key'] = Variable(announcedPublicKey); + map['public_id'] = Variable(publicId); + if (!nullToAbsent || username != null) { + map['username'] = Variable(username); + } + map['was_shown_to_the_user'] = Variable(wasShownToTheUser); + map['is_hidden'] = Variable(isHidden); + map['was_asked_friends'] = Variable(wasAskedFriends); + return map; + } + + UserDiscoveryAnnouncedUsersCompanion toCompanion(bool nullToAbsent) { + return UserDiscoveryAnnouncedUsersCompanion( + announcedUserId: Value(announcedUserId), + announcedPublicKey: Value(announcedPublicKey), + publicId: Value(publicId), + username: username == null && nullToAbsent + ? const Value.absent() + : Value(username), + wasShownToTheUser: Value(wasShownToTheUser), + isHidden: Value(isHidden), + wasAskedFriends: Value(wasAskedFriends), + ); + } + + factory UserDiscoveryAnnouncedUsersData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserDiscoveryAnnouncedUsersData( + announcedUserId: serializer.fromJson(json['announcedUserId']), + announcedPublicKey: serializer.fromJson( + json['announcedPublicKey'], + ), + publicId: serializer.fromJson(json['publicId']), + username: serializer.fromJson(json['username']), + wasShownToTheUser: serializer.fromJson(json['wasShownToTheUser']), + isHidden: serializer.fromJson(json['isHidden']), + wasAskedFriends: serializer.fromJson(json['wasAskedFriends']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'announcedUserId': serializer.toJson(announcedUserId), + 'announcedPublicKey': serializer.toJson(announcedPublicKey), + 'publicId': serializer.toJson(publicId), + 'username': serializer.toJson(username), + 'wasShownToTheUser': serializer.toJson(wasShownToTheUser), + 'isHidden': serializer.toJson(isHidden), + 'wasAskedFriends': serializer.toJson(wasAskedFriends), + }; + } + + UserDiscoveryAnnouncedUsersData copyWith({ + int? announcedUserId, + i2.Uint8List? announcedPublicKey, + int? publicId, + Value username = const Value.absent(), + int? wasShownToTheUser, + int? isHidden, + int? wasAskedFriends, + }) => UserDiscoveryAnnouncedUsersData( + announcedUserId: announcedUserId ?? this.announcedUserId, + announcedPublicKey: announcedPublicKey ?? this.announcedPublicKey, + publicId: publicId ?? this.publicId, + username: username.present ? username.value : this.username, + wasShownToTheUser: wasShownToTheUser ?? this.wasShownToTheUser, + isHidden: isHidden ?? this.isHidden, + wasAskedFriends: wasAskedFriends ?? this.wasAskedFriends, + ); + UserDiscoveryAnnouncedUsersData copyWithCompanion( + UserDiscoveryAnnouncedUsersCompanion data, + ) { + return UserDiscoveryAnnouncedUsersData( + announcedUserId: data.announcedUserId.present + ? data.announcedUserId.value + : this.announcedUserId, + announcedPublicKey: data.announcedPublicKey.present + ? data.announcedPublicKey.value + : this.announcedPublicKey, + publicId: data.publicId.present ? data.publicId.value : this.publicId, + username: data.username.present ? data.username.value : this.username, + wasShownToTheUser: data.wasShownToTheUser.present + ? data.wasShownToTheUser.value + : this.wasShownToTheUser, + isHidden: data.isHidden.present ? data.isHidden.value : this.isHidden, + wasAskedFriends: data.wasAskedFriends.present + ? data.wasAskedFriends.value + : this.wasAskedFriends, + ); + } + + @override + String toString() { + return (StringBuffer('UserDiscoveryAnnouncedUsersData(') + ..write('announcedUserId: $announcedUserId, ') + ..write('announcedPublicKey: $announcedPublicKey, ') + ..write('publicId: $publicId, ') + ..write('username: $username, ') + ..write('wasShownToTheUser: $wasShownToTheUser, ') + ..write('isHidden: $isHidden, ') + ..write('wasAskedFriends: $wasAskedFriends') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + announcedUserId, + $driftBlobEquality.hash(announcedPublicKey), + publicId, + username, + wasShownToTheUser, + isHidden, + wasAskedFriends, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserDiscoveryAnnouncedUsersData && + other.announcedUserId == this.announcedUserId && + $driftBlobEquality.equals( + other.announcedPublicKey, + this.announcedPublicKey, + ) && + other.publicId == this.publicId && + other.username == this.username && + other.wasShownToTheUser == this.wasShownToTheUser && + other.isHidden == this.isHidden && + other.wasAskedFriends == this.wasAskedFriends); +} + +class UserDiscoveryAnnouncedUsersCompanion + extends UpdateCompanion { + final Value announcedUserId; + final Value announcedPublicKey; + final Value publicId; + final Value username; + final Value wasShownToTheUser; + final Value isHidden; + final Value wasAskedFriends; + const UserDiscoveryAnnouncedUsersCompanion({ + this.announcedUserId = const Value.absent(), + this.announcedPublicKey = const Value.absent(), + this.publicId = const Value.absent(), + this.username = const Value.absent(), + this.wasShownToTheUser = const Value.absent(), + this.isHidden = const Value.absent(), + this.wasAskedFriends = const Value.absent(), + }); + UserDiscoveryAnnouncedUsersCompanion.insert({ + this.announcedUserId = const Value.absent(), + required i2.Uint8List announcedPublicKey, + required int publicId, + this.username = const Value.absent(), + this.wasShownToTheUser = const Value.absent(), + this.isHidden = const Value.absent(), + this.wasAskedFriends = const Value.absent(), + }) : announcedPublicKey = Value(announcedPublicKey), + publicId = Value(publicId); + static Insertable custom({ + Expression? announcedUserId, + Expression? announcedPublicKey, + Expression? publicId, + Expression? username, + Expression? wasShownToTheUser, + Expression? isHidden, + Expression? wasAskedFriends, + }) { + return RawValuesInsertable({ + if (announcedUserId != null) 'announced_user_id': announcedUserId, + if (announcedPublicKey != null) + 'announced_public_key': announcedPublicKey, + if (publicId != null) 'public_id': publicId, + if (username != null) 'username': username, + if (wasShownToTheUser != null) 'was_shown_to_the_user': wasShownToTheUser, + if (isHidden != null) 'is_hidden': isHidden, + if (wasAskedFriends != null) 'was_asked_friends': wasAskedFriends, + }); + } + + UserDiscoveryAnnouncedUsersCompanion copyWith({ + Value? announcedUserId, + Value? announcedPublicKey, + Value? publicId, + Value? username, + Value? wasShownToTheUser, + Value? isHidden, + Value? wasAskedFriends, + }) { + return UserDiscoveryAnnouncedUsersCompanion( + announcedUserId: announcedUserId ?? this.announcedUserId, + announcedPublicKey: announcedPublicKey ?? this.announcedPublicKey, + publicId: publicId ?? this.publicId, + username: username ?? this.username, + wasShownToTheUser: wasShownToTheUser ?? this.wasShownToTheUser, + isHidden: isHidden ?? this.isHidden, + wasAskedFriends: wasAskedFriends ?? this.wasAskedFriends, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (announcedUserId.present) { + map['announced_user_id'] = Variable(announcedUserId.value); + } + if (announcedPublicKey.present) { + map['announced_public_key'] = Variable( + announcedPublicKey.value, + ); + } + if (publicId.present) { + map['public_id'] = Variable(publicId.value); + } + if (username.present) { + map['username'] = Variable(username.value); + } + if (wasShownToTheUser.present) { + map['was_shown_to_the_user'] = Variable(wasShownToTheUser.value); + } + if (isHidden.present) { + map['is_hidden'] = Variable(isHidden.value); + } + if (wasAskedFriends.present) { + map['was_asked_friends'] = Variable(wasAskedFriends.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserDiscoveryAnnouncedUsersCompanion(') + ..write('announcedUserId: $announcedUserId, ') + ..write('announcedPublicKey: $announcedPublicKey, ') + ..write('publicId: $publicId, ') + ..write('username: $username, ') + ..write('wasShownToTheUser: $wasShownToTheUser, ') + ..write('isHidden: $isHidden, ') + ..write('wasAskedFriends: $wasAskedFriends') + ..write(')')) + .toString(); + } +} + +class UserDiscoveryUserRelations extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserDiscoveryUserRelations(this.attachedDatabase, [this._alias]); + late final GeneratedColumn announcedUserId = GeneratedColumn( + 'announced_user_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: + 'NOT NULL REFERENCES user_discovery_announced_users(announced_user_id)ON DELETE CASCADE', + ); + late final GeneratedColumn fromContactId = GeneratedColumn( + 'from_contact_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: + 'NOT NULL REFERENCES contacts(user_id)ON DELETE CASCADE', + ); + late final GeneratedColumn publicKeyVerifiedTimestamp = + GeneratedColumn( + 'public_key_verified_timestamp', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [ + announcedUserId, + fromContactId, + publicKeyVerifiedTimestamp, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_discovery_user_relations'; + @override + Set get $primaryKey => {announcedUserId, fromContactId}; + @override + UserDiscoveryUserRelationsData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserDiscoveryUserRelationsData( + announcedUserId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}announced_user_id'], + )!, + fromContactId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}from_contact_id'], + )!, + publicKeyVerifiedTimestamp: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}public_key_verified_timestamp'], + ), + ); + } + + @override + UserDiscoveryUserRelations createAlias(String alias) { + return UserDiscoveryUserRelations(attachedDatabase, alias); + } + + @override + List get customConstraints => const [ + 'PRIMARY KEY(announced_user_id, from_contact_id)', + ]; + @override + bool get dontWriteConstraints => true; +} + +class UserDiscoveryUserRelationsData extends DataClass + implements Insertable { + final int announcedUserId; + final int fromContactId; + final int? publicKeyVerifiedTimestamp; + const UserDiscoveryUserRelationsData({ + required this.announcedUserId, + required this.fromContactId, + this.publicKeyVerifiedTimestamp, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['announced_user_id'] = Variable(announcedUserId); + map['from_contact_id'] = Variable(fromContactId); + if (!nullToAbsent || publicKeyVerifiedTimestamp != null) { + map['public_key_verified_timestamp'] = Variable( + publicKeyVerifiedTimestamp, + ); + } + return map; + } + + UserDiscoveryUserRelationsCompanion toCompanion(bool nullToAbsent) { + return UserDiscoveryUserRelationsCompanion( + announcedUserId: Value(announcedUserId), + fromContactId: Value(fromContactId), + publicKeyVerifiedTimestamp: + publicKeyVerifiedTimestamp == null && nullToAbsent + ? const Value.absent() + : Value(publicKeyVerifiedTimestamp), + ); + } + + factory UserDiscoveryUserRelationsData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserDiscoveryUserRelationsData( + announcedUserId: serializer.fromJson(json['announcedUserId']), + fromContactId: serializer.fromJson(json['fromContactId']), + publicKeyVerifiedTimestamp: serializer.fromJson( + json['publicKeyVerifiedTimestamp'], + ), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'announcedUserId': serializer.toJson(announcedUserId), + 'fromContactId': serializer.toJson(fromContactId), + 'publicKeyVerifiedTimestamp': serializer.toJson( + publicKeyVerifiedTimestamp, + ), + }; + } + + UserDiscoveryUserRelationsData copyWith({ + int? announcedUserId, + int? fromContactId, + Value publicKeyVerifiedTimestamp = const Value.absent(), + }) => UserDiscoveryUserRelationsData( + announcedUserId: announcedUserId ?? this.announcedUserId, + fromContactId: fromContactId ?? this.fromContactId, + publicKeyVerifiedTimestamp: publicKeyVerifiedTimestamp.present + ? publicKeyVerifiedTimestamp.value + : this.publicKeyVerifiedTimestamp, + ); + UserDiscoveryUserRelationsData copyWithCompanion( + UserDiscoveryUserRelationsCompanion data, + ) { + return UserDiscoveryUserRelationsData( + announcedUserId: data.announcedUserId.present + ? data.announcedUserId.value + : this.announcedUserId, + fromContactId: data.fromContactId.present + ? data.fromContactId.value + : this.fromContactId, + publicKeyVerifiedTimestamp: data.publicKeyVerifiedTimestamp.present + ? data.publicKeyVerifiedTimestamp.value + : this.publicKeyVerifiedTimestamp, + ); + } + + @override + String toString() { + return (StringBuffer('UserDiscoveryUserRelationsData(') + ..write('announcedUserId: $announcedUserId, ') + ..write('fromContactId: $fromContactId, ') + ..write('publicKeyVerifiedTimestamp: $publicKeyVerifiedTimestamp') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(announcedUserId, fromContactId, publicKeyVerifiedTimestamp); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserDiscoveryUserRelationsData && + other.announcedUserId == this.announcedUserId && + other.fromContactId == this.fromContactId && + other.publicKeyVerifiedTimestamp == this.publicKeyVerifiedTimestamp); +} + +class UserDiscoveryUserRelationsCompanion + extends UpdateCompanion { + final Value announcedUserId; + final Value fromContactId; + final Value publicKeyVerifiedTimestamp; + final Value rowid; + const UserDiscoveryUserRelationsCompanion({ + this.announcedUserId = const Value.absent(), + this.fromContactId = const Value.absent(), + this.publicKeyVerifiedTimestamp = const Value.absent(), + this.rowid = const Value.absent(), + }); + UserDiscoveryUserRelationsCompanion.insert({ + required int announcedUserId, + required int fromContactId, + this.publicKeyVerifiedTimestamp = const Value.absent(), + this.rowid = const Value.absent(), + }) : announcedUserId = Value(announcedUserId), + fromContactId = Value(fromContactId); + static Insertable custom({ + Expression? announcedUserId, + Expression? fromContactId, + Expression? publicKeyVerifiedTimestamp, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (announcedUserId != null) 'announced_user_id': announcedUserId, + if (fromContactId != null) 'from_contact_id': fromContactId, + if (publicKeyVerifiedTimestamp != null) + 'public_key_verified_timestamp': publicKeyVerifiedTimestamp, + if (rowid != null) 'rowid': rowid, + }); + } + + UserDiscoveryUserRelationsCompanion copyWith({ + Value? announcedUserId, + Value? fromContactId, + Value? publicKeyVerifiedTimestamp, + Value? rowid, + }) { + return UserDiscoveryUserRelationsCompanion( + announcedUserId: announcedUserId ?? this.announcedUserId, + fromContactId: fromContactId ?? this.fromContactId, + publicKeyVerifiedTimestamp: + publicKeyVerifiedTimestamp ?? this.publicKeyVerifiedTimestamp, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (announcedUserId.present) { + map['announced_user_id'] = Variable(announcedUserId.value); + } + if (fromContactId.present) { + map['from_contact_id'] = Variable(fromContactId.value); + } + if (publicKeyVerifiedTimestamp.present) { + map['public_key_verified_timestamp'] = Variable( + publicKeyVerifiedTimestamp.value, + ); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserDiscoveryUserRelationsCompanion(') + ..write('announcedUserId: $announcedUserId, ') + ..write('fromContactId: $fromContactId, ') + ..write('publicKeyVerifiedTimestamp: $publicKeyVerifiedTimestamp, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class UserDiscoveryOtherPromotions extends Table + with + TableInfo< + UserDiscoveryOtherPromotions, + UserDiscoveryOtherPromotionsData + > { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserDiscoveryOtherPromotions(this.attachedDatabase, [this._alias]); + late final GeneratedColumn fromContactId = GeneratedColumn( + 'from_contact_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: + 'NOT NULL REFERENCES contacts(user_id)ON DELETE CASCADE', + ); + late final GeneratedColumn promotionId = GeneratedColumn( + 'promotion_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn publicId = GeneratedColumn( + 'public_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn threshold = GeneratedColumn( + 'threshold', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn announcementShare = + GeneratedColumn( + 'announcement_share', + aliasedName, + false, + type: DriftSqlType.blob, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn publicKeyVerifiedTimestamp = + GeneratedColumn( + 'public_key_verified_timestamp', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [ + fromContactId, + promotionId, + publicId, + threshold, + announcementShare, + publicKeyVerifiedTimestamp, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_discovery_other_promotions'; + @override + Set get $primaryKey => {fromContactId, publicId}; + @override + UserDiscoveryOtherPromotionsData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserDiscoveryOtherPromotionsData( + fromContactId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}from_contact_id'], + )!, + promotionId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}promotion_id'], + )!, + publicId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}public_id'], + )!, + threshold: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}threshold'], + )!, + announcementShare: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}announcement_share'], + )!, + publicKeyVerifiedTimestamp: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}public_key_verified_timestamp'], + ), + ); + } + + @override + UserDiscoveryOtherPromotions createAlias(String alias) { + return UserDiscoveryOtherPromotions(attachedDatabase, alias); + } + + @override + List get customConstraints => const [ + 'PRIMARY KEY(from_contact_id, public_id)', + ]; + @override + bool get dontWriteConstraints => true; +} + +class UserDiscoveryOtherPromotionsData extends DataClass + implements Insertable { + final int fromContactId; + final int promotionId; + final int publicId; + final int threshold; + final i2.Uint8List announcementShare; + final int? publicKeyVerifiedTimestamp; + const UserDiscoveryOtherPromotionsData({ + required this.fromContactId, + required this.promotionId, + required this.publicId, + required this.threshold, + required this.announcementShare, + this.publicKeyVerifiedTimestamp, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['from_contact_id'] = Variable(fromContactId); + map['promotion_id'] = Variable(promotionId); + map['public_id'] = Variable(publicId); + map['threshold'] = Variable(threshold); + map['announcement_share'] = Variable(announcementShare); + if (!nullToAbsent || publicKeyVerifiedTimestamp != null) { + map['public_key_verified_timestamp'] = Variable( + publicKeyVerifiedTimestamp, + ); + } + return map; + } + + UserDiscoveryOtherPromotionsCompanion toCompanion(bool nullToAbsent) { + return UserDiscoveryOtherPromotionsCompanion( + fromContactId: Value(fromContactId), + promotionId: Value(promotionId), + publicId: Value(publicId), + threshold: Value(threshold), + announcementShare: Value(announcementShare), + publicKeyVerifiedTimestamp: + publicKeyVerifiedTimestamp == null && nullToAbsent + ? const Value.absent() + : Value(publicKeyVerifiedTimestamp), + ); + } + + factory UserDiscoveryOtherPromotionsData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserDiscoveryOtherPromotionsData( + fromContactId: serializer.fromJson(json['fromContactId']), + promotionId: serializer.fromJson(json['promotionId']), + publicId: serializer.fromJson(json['publicId']), + threshold: serializer.fromJson(json['threshold']), + announcementShare: serializer.fromJson( + json['announcementShare'], + ), + publicKeyVerifiedTimestamp: serializer.fromJson( + json['publicKeyVerifiedTimestamp'], + ), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'fromContactId': serializer.toJson(fromContactId), + 'promotionId': serializer.toJson(promotionId), + 'publicId': serializer.toJson(publicId), + 'threshold': serializer.toJson(threshold), + 'announcementShare': serializer.toJson(announcementShare), + 'publicKeyVerifiedTimestamp': serializer.toJson( + publicKeyVerifiedTimestamp, + ), + }; + } + + UserDiscoveryOtherPromotionsData copyWith({ + int? fromContactId, + int? promotionId, + int? publicId, + int? threshold, + i2.Uint8List? announcementShare, + Value publicKeyVerifiedTimestamp = const Value.absent(), + }) => UserDiscoveryOtherPromotionsData( + fromContactId: fromContactId ?? this.fromContactId, + promotionId: promotionId ?? this.promotionId, + publicId: publicId ?? this.publicId, + threshold: threshold ?? this.threshold, + announcementShare: announcementShare ?? this.announcementShare, + publicKeyVerifiedTimestamp: publicKeyVerifiedTimestamp.present + ? publicKeyVerifiedTimestamp.value + : this.publicKeyVerifiedTimestamp, + ); + UserDiscoveryOtherPromotionsData copyWithCompanion( + UserDiscoveryOtherPromotionsCompanion data, + ) { + return UserDiscoveryOtherPromotionsData( + fromContactId: data.fromContactId.present + ? data.fromContactId.value + : this.fromContactId, + promotionId: data.promotionId.present + ? data.promotionId.value + : this.promotionId, + publicId: data.publicId.present ? data.publicId.value : this.publicId, + threshold: data.threshold.present ? data.threshold.value : this.threshold, + announcementShare: data.announcementShare.present + ? data.announcementShare.value + : this.announcementShare, + publicKeyVerifiedTimestamp: data.publicKeyVerifiedTimestamp.present + ? data.publicKeyVerifiedTimestamp.value + : this.publicKeyVerifiedTimestamp, + ); + } + + @override + String toString() { + return (StringBuffer('UserDiscoveryOtherPromotionsData(') + ..write('fromContactId: $fromContactId, ') + ..write('promotionId: $promotionId, ') + ..write('publicId: $publicId, ') + ..write('threshold: $threshold, ') + ..write('announcementShare: $announcementShare, ') + ..write('publicKeyVerifiedTimestamp: $publicKeyVerifiedTimestamp') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + fromContactId, + promotionId, + publicId, + threshold, + $driftBlobEquality.hash(announcementShare), + publicKeyVerifiedTimestamp, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserDiscoveryOtherPromotionsData && + other.fromContactId == this.fromContactId && + other.promotionId == this.promotionId && + other.publicId == this.publicId && + other.threshold == this.threshold && + $driftBlobEquality.equals( + other.announcementShare, + this.announcementShare, + ) && + other.publicKeyVerifiedTimestamp == this.publicKeyVerifiedTimestamp); +} + +class UserDiscoveryOtherPromotionsCompanion + extends UpdateCompanion { + final Value fromContactId; + final Value promotionId; + final Value publicId; + final Value threshold; + final Value announcementShare; + final Value publicKeyVerifiedTimestamp; + final Value rowid; + const UserDiscoveryOtherPromotionsCompanion({ + this.fromContactId = const Value.absent(), + this.promotionId = const Value.absent(), + this.publicId = const Value.absent(), + this.threshold = const Value.absent(), + this.announcementShare = const Value.absent(), + this.publicKeyVerifiedTimestamp = const Value.absent(), + this.rowid = const Value.absent(), + }); + UserDiscoveryOtherPromotionsCompanion.insert({ + required int fromContactId, + required int promotionId, + required int publicId, + required int threshold, + required i2.Uint8List announcementShare, + this.publicKeyVerifiedTimestamp = const Value.absent(), + this.rowid = const Value.absent(), + }) : fromContactId = Value(fromContactId), + promotionId = Value(promotionId), + publicId = Value(publicId), + threshold = Value(threshold), + announcementShare = Value(announcementShare); + static Insertable custom({ + Expression? fromContactId, + Expression? promotionId, + Expression? publicId, + Expression? threshold, + Expression? announcementShare, + Expression? publicKeyVerifiedTimestamp, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (fromContactId != null) 'from_contact_id': fromContactId, + if (promotionId != null) 'promotion_id': promotionId, + if (publicId != null) 'public_id': publicId, + if (threshold != null) 'threshold': threshold, + if (announcementShare != null) 'announcement_share': announcementShare, + if (publicKeyVerifiedTimestamp != null) + 'public_key_verified_timestamp': publicKeyVerifiedTimestamp, + if (rowid != null) 'rowid': rowid, + }); + } + + UserDiscoveryOtherPromotionsCompanion copyWith({ + Value? fromContactId, + Value? promotionId, + Value? publicId, + Value? threshold, + Value? announcementShare, + Value? publicKeyVerifiedTimestamp, + Value? rowid, + }) { + return UserDiscoveryOtherPromotionsCompanion( + fromContactId: fromContactId ?? this.fromContactId, + promotionId: promotionId ?? this.promotionId, + publicId: publicId ?? this.publicId, + threshold: threshold ?? this.threshold, + announcementShare: announcementShare ?? this.announcementShare, + publicKeyVerifiedTimestamp: + publicKeyVerifiedTimestamp ?? this.publicKeyVerifiedTimestamp, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (fromContactId.present) { + map['from_contact_id'] = Variable(fromContactId.value); + } + if (promotionId.present) { + map['promotion_id'] = Variable(promotionId.value); + } + if (publicId.present) { + map['public_id'] = Variable(publicId.value); + } + if (threshold.present) { + map['threshold'] = Variable(threshold.value); + } + if (announcementShare.present) { + map['announcement_share'] = Variable( + announcementShare.value, + ); + } + if (publicKeyVerifiedTimestamp.present) { + map['public_key_verified_timestamp'] = Variable( + publicKeyVerifiedTimestamp.value, + ); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserDiscoveryOtherPromotionsCompanion(') + ..write('fromContactId: $fromContactId, ') + ..write('promotionId: $promotionId, ') + ..write('publicId: $publicId, ') + ..write('threshold: $threshold, ') + ..write('announcementShare: $announcementShare, ') + ..write('publicKeyVerifiedTimestamp: $publicKeyVerifiedTimestamp, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class UserDiscoveryOwnPromotions extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserDiscoveryOwnPromotions(this.attachedDatabase, [this._alias]); + late final GeneratedColumn versionId = GeneratedColumn( + 'version_id', + aliasedName, + false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL PRIMARY KEY AUTOINCREMENT', + ); + late final GeneratedColumn contactId = GeneratedColumn( + 'contact_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: + 'NOT NULL REFERENCES contacts(user_id)ON DELETE CASCADE', + ); + late final GeneratedColumn promotion = + GeneratedColumn( + 'promotion', + aliasedName, + false, + type: DriftSqlType.blob, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + @override + List get $columns => [versionId, contactId, promotion]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_discovery_own_promotions'; + @override + Set get $primaryKey => {versionId}; + @override + UserDiscoveryOwnPromotionsData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserDiscoveryOwnPromotionsData( + versionId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}version_id'], + )!, + contactId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}contact_id'], + )!, + promotion: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}promotion'], + )!, + ); + } + + @override + UserDiscoveryOwnPromotions createAlias(String alias) { + return UserDiscoveryOwnPromotions(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; +} + +class UserDiscoveryOwnPromotionsData extends DataClass + implements Insertable { + final int versionId; + final int contactId; + final i2.Uint8List promotion; + const UserDiscoveryOwnPromotionsData({ + required this.versionId, + required this.contactId, + required this.promotion, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['version_id'] = Variable(versionId); + map['contact_id'] = Variable(contactId); + map['promotion'] = Variable(promotion); + return map; + } + + UserDiscoveryOwnPromotionsCompanion toCompanion(bool nullToAbsent) { + return UserDiscoveryOwnPromotionsCompanion( + versionId: Value(versionId), + contactId: Value(contactId), + promotion: Value(promotion), + ); + } + + factory UserDiscoveryOwnPromotionsData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserDiscoveryOwnPromotionsData( + versionId: serializer.fromJson(json['versionId']), + contactId: serializer.fromJson(json['contactId']), + promotion: serializer.fromJson(json['promotion']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'versionId': serializer.toJson(versionId), + 'contactId': serializer.toJson(contactId), + 'promotion': serializer.toJson(promotion), + }; + } + + UserDiscoveryOwnPromotionsData copyWith({ + int? versionId, + int? contactId, + i2.Uint8List? promotion, + }) => UserDiscoveryOwnPromotionsData( + versionId: versionId ?? this.versionId, + contactId: contactId ?? this.contactId, + promotion: promotion ?? this.promotion, + ); + UserDiscoveryOwnPromotionsData copyWithCompanion( + UserDiscoveryOwnPromotionsCompanion data, + ) { + return UserDiscoveryOwnPromotionsData( + versionId: data.versionId.present ? data.versionId.value : this.versionId, + contactId: data.contactId.present ? data.contactId.value : this.contactId, + promotion: data.promotion.present ? data.promotion.value : this.promotion, + ); + } + + @override + String toString() { + return (StringBuffer('UserDiscoveryOwnPromotionsData(') + ..write('versionId: $versionId, ') + ..write('contactId: $contactId, ') + ..write('promotion: $promotion') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(versionId, contactId, $driftBlobEquality.hash(promotion)); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserDiscoveryOwnPromotionsData && + other.versionId == this.versionId && + other.contactId == this.contactId && + $driftBlobEquality.equals(other.promotion, this.promotion)); +} + +class UserDiscoveryOwnPromotionsCompanion + extends UpdateCompanion { + final Value versionId; + final Value contactId; + final Value promotion; + const UserDiscoveryOwnPromotionsCompanion({ + this.versionId = const Value.absent(), + this.contactId = const Value.absent(), + this.promotion = const Value.absent(), + }); + UserDiscoveryOwnPromotionsCompanion.insert({ + this.versionId = const Value.absent(), + required int contactId, + required i2.Uint8List promotion, + }) : contactId = Value(contactId), + promotion = Value(promotion); + static Insertable custom({ + Expression? versionId, + Expression? contactId, + Expression? promotion, + }) { + return RawValuesInsertable({ + if (versionId != null) 'version_id': versionId, + if (contactId != null) 'contact_id': contactId, + if (promotion != null) 'promotion': promotion, + }); + } + + UserDiscoveryOwnPromotionsCompanion copyWith({ + Value? versionId, + Value? contactId, + Value? promotion, + }) { + return UserDiscoveryOwnPromotionsCompanion( + versionId: versionId ?? this.versionId, + contactId: contactId ?? this.contactId, + promotion: promotion ?? this.promotion, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (versionId.present) { + map['version_id'] = Variable(versionId.value); + } + if (contactId.present) { + map['contact_id'] = Variable(contactId.value); + } + if (promotion.present) { + map['promotion'] = Variable(promotion.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserDiscoveryOwnPromotionsCompanion(') + ..write('versionId: $versionId, ') + ..write('contactId: $contactId, ') + ..write('promotion: $promotion') + ..write(')')) + .toString(); + } +} + +class UserDiscoveryShares extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserDiscoveryShares(this.attachedDatabase, [this._alias]); + late final GeneratedColumn shareId = GeneratedColumn( + 'share_id', + aliasedName, + false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL PRIMARY KEY AUTOINCREMENT', + ); + late final GeneratedColumn share = + GeneratedColumn( + 'share', + aliasedName, + false, + type: DriftSqlType.blob, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn contactId = GeneratedColumn( + 'contact_id', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL REFERENCES contacts(user_id)ON DELETE CASCADE', + ); + @override + List get $columns => [shareId, share, contactId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_discovery_shares'; + @override + Set get $primaryKey => {shareId}; + @override + UserDiscoverySharesData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserDiscoverySharesData( + shareId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}share_id'], + )!, + share: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}share'], + )!, + contactId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}contact_id'], + ), + ); + } + + @override + UserDiscoveryShares createAlias(String alias) { + return UserDiscoveryShares(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; +} + +class UserDiscoverySharesData extends DataClass + implements Insertable { + final int shareId; + final i2.Uint8List share; + final int? contactId; + const UserDiscoverySharesData({ + required this.shareId, + required this.share, + this.contactId, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['share_id'] = Variable(shareId); + map['share'] = Variable(share); + if (!nullToAbsent || contactId != null) { + map['contact_id'] = Variable(contactId); + } + return map; + } + + UserDiscoverySharesCompanion toCompanion(bool nullToAbsent) { + return UserDiscoverySharesCompanion( + shareId: Value(shareId), + share: Value(share), + contactId: contactId == null && nullToAbsent + ? const Value.absent() + : Value(contactId), + ); + } + + factory UserDiscoverySharesData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserDiscoverySharesData( + shareId: serializer.fromJson(json['shareId']), + share: serializer.fromJson(json['share']), + contactId: serializer.fromJson(json['contactId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'shareId': serializer.toJson(shareId), + 'share': serializer.toJson(share), + 'contactId': serializer.toJson(contactId), + }; + } + + UserDiscoverySharesData copyWith({ + int? shareId, + i2.Uint8List? share, + Value contactId = const Value.absent(), + }) => UserDiscoverySharesData( + shareId: shareId ?? this.shareId, + share: share ?? this.share, + contactId: contactId.present ? contactId.value : this.contactId, + ); + UserDiscoverySharesData copyWithCompanion(UserDiscoverySharesCompanion data) { + return UserDiscoverySharesData( + shareId: data.shareId.present ? data.shareId.value : this.shareId, + share: data.share.present ? data.share.value : this.share, + contactId: data.contactId.present ? data.contactId.value : this.contactId, + ); + } + + @override + String toString() { + return (StringBuffer('UserDiscoverySharesData(') + ..write('shareId: $shareId, ') + ..write('share: $share, ') + ..write('contactId: $contactId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(shareId, $driftBlobEquality.hash(share), contactId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserDiscoverySharesData && + other.shareId == this.shareId && + $driftBlobEquality.equals(other.share, this.share) && + other.contactId == this.contactId); +} + +class UserDiscoverySharesCompanion + extends UpdateCompanion { + final Value shareId; + final Value share; + final Value contactId; + const UserDiscoverySharesCompanion({ + this.shareId = const Value.absent(), + this.share = const Value.absent(), + this.contactId = const Value.absent(), + }); + UserDiscoverySharesCompanion.insert({ + this.shareId = const Value.absent(), + required i2.Uint8List share, + this.contactId = const Value.absent(), + }) : share = Value(share); + static Insertable custom({ + Expression? shareId, + Expression? share, + Expression? contactId, + }) { + return RawValuesInsertable({ + if (shareId != null) 'share_id': shareId, + if (share != null) 'share': share, + if (contactId != null) 'contact_id': contactId, + }); + } + + UserDiscoverySharesCompanion copyWith({ + Value? shareId, + Value? share, + Value? contactId, + }) { + return UserDiscoverySharesCompanion( + shareId: shareId ?? this.shareId, + share: share ?? this.share, + contactId: contactId ?? this.contactId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (shareId.present) { + map['share_id'] = Variable(shareId.value); + } + if (share.present) { + map['share'] = Variable(share.value); + } + if (contactId.present) { + map['contact_id'] = Variable(contactId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserDiscoverySharesCompanion(') + ..write('shareId: $shareId, ') + ..write('share: $share, ') + ..write('contactId: $contactId') + ..write(')')) + .toString(); + } +} + +class Shortcuts extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Shortcuts(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL PRIMARY KEY AUTOINCREMENT', + ); + late final GeneratedColumn emoji = GeneratedColumn( + 'emoji', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL UNIQUE', + ); + late final GeneratedColumn usageCounter = GeneratedColumn( + 'usage_counter', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0', + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [id, emoji, usageCounter]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'shortcuts'; + @override + Set get $primaryKey => {id}; + @override + ShortcutsData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ShortcutsData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}id'], + )!, + emoji: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}emoji'], + )!, + usageCounter: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}usage_counter'], + )!, + ); + } + + @override + Shortcuts createAlias(String alias) { + return Shortcuts(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; +} + +class ShortcutsData extends DataClass implements Insertable { + final int id; + final String emoji; + final int usageCounter; + const ShortcutsData({ + required this.id, + required this.emoji, + required this.usageCounter, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['emoji'] = Variable(emoji); + map['usage_counter'] = Variable(usageCounter); + return map; + } + + ShortcutsCompanion toCompanion(bool nullToAbsent) { + return ShortcutsCompanion( + id: Value(id), + emoji: Value(emoji), + usageCounter: Value(usageCounter), + ); + } + + factory ShortcutsData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ShortcutsData( + id: serializer.fromJson(json['id']), + emoji: serializer.fromJson(json['emoji']), + usageCounter: serializer.fromJson(json['usageCounter']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'emoji': serializer.toJson(emoji), + 'usageCounter': serializer.toJson(usageCounter), + }; + } + + ShortcutsData copyWith({int? id, String? emoji, int? usageCounter}) => + ShortcutsData( + id: id ?? this.id, + emoji: emoji ?? this.emoji, + usageCounter: usageCounter ?? this.usageCounter, + ); + ShortcutsData copyWithCompanion(ShortcutsCompanion data) { + return ShortcutsData( + id: data.id.present ? data.id.value : this.id, + emoji: data.emoji.present ? data.emoji.value : this.emoji, + usageCounter: data.usageCounter.present + ? data.usageCounter.value + : this.usageCounter, + ); + } + + @override + String toString() { + return (StringBuffer('ShortcutsData(') + ..write('id: $id, ') + ..write('emoji: $emoji, ') + ..write('usageCounter: $usageCounter') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, emoji, usageCounter); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ShortcutsData && + other.id == this.id && + other.emoji == this.emoji && + other.usageCounter == this.usageCounter); +} + +class ShortcutsCompanion extends UpdateCompanion { + final Value id; + final Value emoji; + final Value usageCounter; + const ShortcutsCompanion({ + this.id = const Value.absent(), + this.emoji = const Value.absent(), + this.usageCounter = const Value.absent(), + }); + ShortcutsCompanion.insert({ + this.id = const Value.absent(), + required String emoji, + this.usageCounter = const Value.absent(), + }) : emoji = Value(emoji); + static Insertable custom({ + Expression? id, + Expression? emoji, + Expression? usageCounter, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (emoji != null) 'emoji': emoji, + if (usageCounter != null) 'usage_counter': usageCounter, + }); + } + + ShortcutsCompanion copyWith({ + Value? id, + Value? emoji, + Value? usageCounter, + }) { + return ShortcutsCompanion( + id: id ?? this.id, + emoji: emoji ?? this.emoji, + usageCounter: usageCounter ?? this.usageCounter, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (emoji.present) { + map['emoji'] = Variable(emoji.value); + } + if (usageCounter.present) { + map['usage_counter'] = Variable(usageCounter.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ShortcutsCompanion(') + ..write('id: $id, ') + ..write('emoji: $emoji, ') + ..write('usageCounter: $usageCounter') + ..write(')')) + .toString(); + } +} + +class ShortcutMembers extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + ShortcutMembers(this.attachedDatabase, [this._alias]); + late final GeneratedColumn shortcutId = GeneratedColumn( + 'shortcut_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL REFERENCES shortcuts(id)ON DELETE CASCADE', + ); + late final GeneratedColumn groupId = GeneratedColumn( + 'group_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: + 'NOT NULL REFERENCES "groups"(group_id)ON DELETE CASCADE', + ); + @override + List get $columns => [shortcutId, groupId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'shortcut_members'; + @override + Set get $primaryKey => {shortcutId, groupId}; + @override + ShortcutMembersData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ShortcutMembersData( + shortcutId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}shortcut_id'], + )!, + groupId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}group_id'], + )!, + ); + } + + @override + ShortcutMembers createAlias(String alias) { + return ShortcutMembers(attachedDatabase, alias); + } + + @override + List get customConstraints => const [ + 'PRIMARY KEY(shortcut_id, group_id)', + ]; + @override + bool get dontWriteConstraints => true; +} + +class ShortcutMembersData extends DataClass + implements Insertable { + final int shortcutId; + final String groupId; + const ShortcutMembersData({required this.shortcutId, required this.groupId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['shortcut_id'] = Variable(shortcutId); + map['group_id'] = Variable(groupId); + return map; + } + + ShortcutMembersCompanion toCompanion(bool nullToAbsent) { + return ShortcutMembersCompanion( + shortcutId: Value(shortcutId), + groupId: Value(groupId), + ); + } + + factory ShortcutMembersData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ShortcutMembersData( + shortcutId: serializer.fromJson(json['shortcutId']), + groupId: serializer.fromJson(json['groupId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'shortcutId': serializer.toJson(shortcutId), + 'groupId': serializer.toJson(groupId), + }; + } + + ShortcutMembersData copyWith({int? shortcutId, String? groupId}) => + ShortcutMembersData( + shortcutId: shortcutId ?? this.shortcutId, + groupId: groupId ?? this.groupId, + ); + ShortcutMembersData copyWithCompanion(ShortcutMembersCompanion data) { + return ShortcutMembersData( + shortcutId: data.shortcutId.present + ? data.shortcutId.value + : this.shortcutId, + groupId: data.groupId.present ? data.groupId.value : this.groupId, + ); + } + + @override + String toString() { + return (StringBuffer('ShortcutMembersData(') + ..write('shortcutId: $shortcutId, ') + ..write('groupId: $groupId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(shortcutId, groupId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ShortcutMembersData && + other.shortcutId == this.shortcutId && + other.groupId == this.groupId); +} + +class ShortcutMembersCompanion extends UpdateCompanion { + final Value shortcutId; + final Value groupId; + final Value rowid; + const ShortcutMembersCompanion({ + this.shortcutId = const Value.absent(), + this.groupId = const Value.absent(), + this.rowid = const Value.absent(), + }); + ShortcutMembersCompanion.insert({ + required int shortcutId, + required String groupId, + this.rowid = const Value.absent(), + }) : shortcutId = Value(shortcutId), + groupId = Value(groupId); + static Insertable custom({ + Expression? shortcutId, + Expression? groupId, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (shortcutId != null) 'shortcut_id': shortcutId, + if (groupId != null) 'group_id': groupId, + if (rowid != null) 'rowid': rowid, + }); + } + + ShortcutMembersCompanion copyWith({ + Value? shortcutId, + Value? groupId, + Value? rowid, + }) { + return ShortcutMembersCompanion( + shortcutId: shortcutId ?? this.shortcutId, + groupId: groupId ?? this.groupId, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (shortcutId.present) { + map['shortcut_id'] = Variable(shortcutId.value); + } + if (groupId.present) { + map['group_id'] = Variable(groupId.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ShortcutMembersCompanion(') + ..write('shortcutId: $shortcutId, ') + ..write('groupId: $groupId, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class DatabaseAtV17 extends GeneratedDatabase { + DatabaseAtV17(QueryExecutor e) : super(e); + late final Contacts contacts = Contacts(this); + late final Groups groups = Groups(this); + late final MediaFiles mediaFiles = MediaFiles(this); + late final Messages messages = Messages(this); + late final MessageHistories messageHistories = MessageHistories(this); + late final Reactions reactions = Reactions(this); + late final GroupMembers groupMembers = GroupMembers(this); + late final Receipts receipts = Receipts(this); + late final ReceivedReceipts receivedReceipts = ReceivedReceipts(this); + late final SignalIdentityKeyStores signalIdentityKeyStores = + SignalIdentityKeyStores(this); + late final SignalPreKeyStores signalPreKeyStores = SignalPreKeyStores(this); + late final SignalSenderKeyStores signalSenderKeyStores = + SignalSenderKeyStores(this); + late final SignalSessionStores signalSessionStores = SignalSessionStores( + this, + ); + late final SignalSignedPreKeyStores signalSignedPreKeyStores = + SignalSignedPreKeyStores(this); + late final MessageActions messageActions = MessageActions(this); + late final GroupHistories groupHistories = GroupHistories(this); + late final KeyVerifications keyVerifications = KeyVerifications(this); + late final VerificationTokens verificationTokens = VerificationTokens(this); + late final UserDiscoveryAnnouncedUsers userDiscoveryAnnouncedUsers = + UserDiscoveryAnnouncedUsers(this); + late final UserDiscoveryUserRelations userDiscoveryUserRelations = + UserDiscoveryUserRelations(this); + late final UserDiscoveryOtherPromotions userDiscoveryOtherPromotions = + UserDiscoveryOtherPromotions(this); + late final UserDiscoveryOwnPromotions userDiscoveryOwnPromotions = + UserDiscoveryOwnPromotions(this); + late final UserDiscoveryShares userDiscoveryShares = UserDiscoveryShares( + this, + ); + late final Shortcuts shortcuts = Shortcuts(this); + late final ShortcutMembers shortcutMembers = ShortcutMembers(this); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + contacts, + groups, + mediaFiles, + messages, + messageHistories, + reactions, + groupMembers, + receipts, + receivedReceipts, + signalIdentityKeyStores, + signalPreKeyStores, + signalSenderKeyStores, + signalSessionStores, + signalSignedPreKeyStores, + messageActions, + groupHistories, + keyVerifications, + verificationTokens, + userDiscoveryAnnouncedUsers, + userDiscoveryUserRelations, + userDiscoveryOtherPromotions, + userDiscoveryOwnPromotions, + userDiscoveryShares, + shortcuts, + shortcutMembers, + ]; + @override + StreamQueryUpdateRules get streamUpdateRules => const StreamQueryUpdateRules([ + WritePropagation( + on: TableUpdateQuery.onTableName( + 'groups', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('messages', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'media_files', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('messages', kind: UpdateKind.update)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'messages', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('message_histories', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'contacts', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('message_histories', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'messages', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('reactions', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'contacts', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('reactions', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'groups', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('group_members', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'contacts', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('receipts', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'messages', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('receipts', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'messages', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('message_actions', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'contacts', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('message_actions', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'groups', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('group_histories', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'contacts', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('key_verifications', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'user_discovery_announced_users', + limitUpdateKind: UpdateKind.delete, + ), + result: [ + TableUpdate('user_discovery_user_relations', kind: UpdateKind.delete), + ], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'contacts', + limitUpdateKind: UpdateKind.delete, + ), + result: [ + TableUpdate('user_discovery_user_relations', kind: UpdateKind.delete), + ], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'contacts', + limitUpdateKind: UpdateKind.delete, + ), + result: [ + TableUpdate('user_discovery_other_promotions', kind: UpdateKind.delete), + ], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'contacts', + limitUpdateKind: UpdateKind.delete, + ), + result: [ + TableUpdate('user_discovery_own_promotions', kind: UpdateKind.delete), + ], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'contacts', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('user_discovery_shares', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'shortcuts', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('shortcut_members', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'groups', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('shortcut_members', kind: UpdateKind.delete)], + ), + ]); + @override + int get schemaVersion => 17; +} diff --git a/test/features/flame_counter_test.dart b/test/features/flame_counter_test.dart index 4f597547..7e7a0d19 100644 --- a/test/features/flame_counter_test.dart +++ b/test/features/flame_counter_test.dart @@ -83,10 +83,11 @@ void main() { displayName: 'Test User', subscriptionPlan: 'Free', currentSetupPage: null, - )..appVersion = 62; + appVersion: 62, + ); }); - test('test flame counter', () async { + test('normal flame expiring', () async { final contactId = await getAndCreateUserId(); final contact = (await twonlyDB.contactsDao.getContactById(contactId))!; await twonlyDB.groupsDao.createNewDirectChat( @@ -181,6 +182,21 @@ void main() { counter: 0, isExpiring: false, ); + }); + + test('isRestore Possible', () async { + final contactId = await getAndCreateUserId(); + final contact = (await twonlyDB.contactsDao.getContactById(contactId))!; + await twonlyDB.groupsDao.createNewDirectChat( + contactId, + GroupsCompanion( + groupName: Value( + getContactDisplayName(contact), + ), + ), + ); + + final group = (await twonlyDB.groupsDao.getDirectChat(contactId))!; for (var i = 1; i <= 20; i++) { await withClock( @@ -225,6 +241,48 @@ void main() { ); }); + test('flame restoring', () async { + final contactId = await getAndCreateUserId(); + final contact = (await twonlyDB.contactsDao.getContactById(contactId))!; + await twonlyDB.groupsDao.createNewDirectChat( + contactId, + GroupsCompanion( + groupName: Value( + getContactDisplayName(contact), + ), + ), + ); + + final group = (await twonlyDB.groupsDao.getDirectChat(contactId))!; + + for (var i = 1; i <= 5; i++) { + await withClock( + Clock.fixed(DateTime(2026, 3, i, 1)), + () async { + await incFlameCounter(group.groupId, true, DateTime(2026, 3, i, 2)); + await incFlameCounter(group.groupId, false, DateTime(2026, 3, i, 3)); + }, + ); + } + + await expectFlame(DateTime(2026, 3, 5, 19), group.groupId, 5); + await expectFlame(DateTime(2026, 3, 8, 19), group.groupId, 0); + + await withClock( + Clock.fixed(DateTime(2026, 3, 9, 12)), + () async { + await restoreFlames(group.groupId); + }, + ); + + await expectFlameExpiring( + DateTime(2026, 3, 9, 13), + group.groupId, + counter: 5, + isExpiring: false, + ); + }); + tearDown(() async { await twonlyDB.close(); }); diff --git a/test/features/link_parser_test.dart b/test/features/link_parser_test.dart index b7797d6a..03e206cd 100644 --- a/test/features/link_parser_test.dart +++ b/test/features/link_parser_test.dart @@ -102,6 +102,7 @@ void main() { ), LinkParserTest( title: 'twonly Public Launch', + siteName: 'twonly', desc: 'After about a year of development, twonly is finally ready for its public launch.', url: 'https://twonly.eu/en/blog/2026-public-launch.html', diff --git a/test/mocks/platform_channels.dart b/test/mocks/platform_channels.dart new file mode 100644 index 00000000..a722b058 --- /dev/null +++ b/test/mocks/platform_channels.dart @@ -0,0 +1,77 @@ +// ignore_for_file: avoid_dynamic_calls + +import 'dart:async'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void setupPlatformChannelMocks() { + final secureStorageMock = {}; + Future mockHandler(MethodCall methodCall) async { + final userId = Zone.current[#userId] as int?; + final keyPrefix = userId != null ? '${userId}_' : ''; + if (methodCall.method == 'read') { + final key = methodCall.arguments['key'] as String; + return secureStorageMock[keyPrefix + key]; + } else if (methodCall.method == 'write') { + final key = methodCall.arguments['key'] as String; + final value = methodCall.arguments['value'] as String; + secureStorageMock[keyPrefix + key] = value; + return true; + } else if (methodCall.method == 'delete') { + final key = methodCall.arguments['key'] as String; + secureStorageMock.remove(keyPrefix + key); + return true; + } else if (methodCall.method == 'readAll') { + final result = {}; + secureStorageMock.forEach((k, v) { + if (k.startsWith(keyPrefix)) { + result[k.substring(keyPrefix.length)] = v; + } + }); + return result; + } else if (methodCall.method == 'deleteAll') { + if (userId != null) { + secureStorageMock.removeWhere((k, v) => k.startsWith(keyPrefix)); + } else { + secureStorageMock.clear(); + } + return true; + } + return null; + } + + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( + const MethodChannel( + 'plugins.it_crowd.double_tapp/flutter_secure_storage', + ), + mockHandler, + ); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( + const MethodChannel('plugins.it_nomads.com/flutter_secure_storage'), + mockHandler, + ); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( + const MethodChannel('dev.fluttercommunity.plus/connectivity'), + (call) async { + if (call.method == 'check') { + return ['wifi']; + } + return null; + }, + ); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( + const MethodChannel( + 'be.tramesch.workmanager/foreground_channel_workmanager', + ), + (call) async => true, + ); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( + const MethodChannel('com.bbflight.background_downloader'), + (call) async { + if (call.method == 'enqueue') { + return true; + } + return null; + }, + ); +} diff --git a/test/mocks/test_client.dart b/test/mocks/test_client.dart new file mode 100644 index 00000000..ea750335 --- /dev/null +++ b/test/mocks/test_client.dart @@ -0,0 +1,299 @@ +// ignore_for_file: avoid_dynamic_calls + +import 'dart:async'; +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:drift/drift.dart' hide isNotNull, isNull; +import 'package:fixnum/fixnum.dart'; +import 'package:twonly/src/constants/secure_storage.keys.dart'; +import 'package:twonly/src/database/tables/messages.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/json/userdata.model.dart'; +import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart' as pb; +import 'package:twonly/src/model/protobuf/client/generated/push_notification.pb.dart'; +import 'package:twonly/src/services/api.service.dart'; +import 'package:twonly/src/services/api/messages.api.dart'; +import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; +import 'package:twonly/src/services/signal/identity.signal.dart'; +import 'package:twonly/src/services/signal/session.signal.dart'; +import 'package:twonly/src/services/user.service.dart'; +import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/utils/pow.dart'; + +import 'user_environment.dart'; + +class RealHttpOverrides extends HttpOverrides { + @override + HttpClient createHttpClient(SecurityContext? context) { + return super.createHttpClient(context) + ..badCertificateCallback = (cert, host, port) { + return true; + }; + } +} + +class TestClient { + TestClient(this.localIdSeed) { + final timeStr = DateTime.now().millisecondsSinceEpoch.toString(); + username = 't_${timeStr.substring(timeStr.length - 6)}$localIdSeed'; + } + late final UserEnvironment env; + late final ApiService api; + final int localIdSeed; + late String username; + Group? defaultGroup; + int realUserId = 0; + + Future init() async { + env = await UserEnvironment.create(localIdSeed, username); + api = ApiService(); + + await run(() async { + await createIfNotExistsSignalIdentity(); + + Log.info('Connecting to API...'); + final connected = await api.connect(); + Log.info('Connected: $connected'); + if (!connected) throw Exception('Failed to connect to API'); + + Log.info('Requesting POW...'); + final powRes = await api.getProofOfWork(); + Log.info('POW result: $powRes'); + if (powRes.$1 == null) throw Exception('Failed to get POW'); + + final prefix = powRes.$1!.prefix; + final difficulty = powRes.$1!.difficulty.toInt(); + final proof = await calculatePoW(prefix, difficulty); + + final regRes = await api.register(username, '', proof); + if (regRes.isError) { + throw Exception('Registration failed: ${regRes.error}'); + } + + realUserId = regRes.value.userid.toInt() as int; + + final userData = UserData( + userId: realUserId, + username: username, + displayName: username, + subscriptionPlan: 'Free', + currentSetupPage: null, + appVersion: 100, + ); + await UserService.save(userData); + + await api.authenticate(); + await signalGetPreKeys(); + }); + } + + Future initContact(TestClient other) async { + await run(() async { + await env.db.contactsDao.insertContact( + ContactsCompanion.insert( + userId: Value(other.realUserId), + username: other.username, + accepted: const Value(true), + ), + ); + defaultGroup = await env.db.groupsDao.createNewDirectChat( + other.realUserId, + GroupsCompanion(groupName: Value(other.username)), + ); + + final dummyPushKeys = [ + PushUser() + ..userId = Int64(other.realUserId) + ..pushKeys.add( + PushKey() + ..key = Uint8List(32) + ..id = Int64(12345) + ..createdAtUnixTimestamp = Int64( + DateTime.now().millisecondsSinceEpoch, + ), + ), + PushUser() + ..userId = Int64(realUserId) + ..pushKeys.add( + PushKey() + ..key = Uint8List(32) + ..id = Int64(67890) + ..createdAtUnixTimestamp = Int64( + DateTime.now().millisecondsSinceEpoch, + ), + ), + ]; + await setPushKeys(SecureStorageKeys.sendingPushKeys, dummyPushKeys); + await setPushKeys(SecureStorageKeys.receivingPushKeys, dummyPushKeys); + + final userData = await api.getUserById(other.realUserId); + final sessionStarted = await processSignalUserData(userData!); + if (!sessionStarted) throw Exception('Failed to start session'); + }); + } + + Future run(Future Function() computation) { + return runInZone(env, api, computation); + } + + Future sendText(TestClient target, String text) async { + return run(() async { + final m = await env.db.messagesDao.insertMessage( + MessagesCompanion( + groupId: Value(defaultGroup!.groupId), + content: Value(text), + type: Value(MessageType.text.name), + ), + ); + await sendCipherText( + target.realUserId, + pb.EncryptedContent( + groupId: defaultGroup!.groupId, + textMessage: pb.EncryptedContent_TextMessage() + ..senderMessageId = m!.messageId + ..text = text, + ), + messageId: m.messageId, + ); + return m; + }); + } + + Future sendEncryptedContent( + TestClient target, + pb.EncryptedContent content, + ) async { + await run(() async { + await sendCipherText(target.realUserId, content); + }); + } + + Future sendGroupJoin( + TestClient target, + String groupId, + List groupPublicKey, + ) async { + final content = pb.EncryptedContent() + ..groupId = groupId + ..groupJoin = (pb.EncryptedContent_GroupJoin() + ..groupPublicKey = groupPublicKey); + await sendEncryptedContent(target, content); + } + + Future sendResendGroupPublicKey( + TestClient target, + String groupId, + ) async { + final content = pb.EncryptedContent() + ..groupId = groupId + ..resendGroupPublicKey = pb.EncryptedContent_ResendGroupPublicKey(); + await sendEncryptedContent(target, content); + } + + Future sendErrorMessages( + TestClient target, + pb.EncryptedContent_ErrorMessages_Type type, + String receiptId, + ) async { + final content = pb.EncryptedContent() + ..errorMessages = (pb.EncryptedContent_ErrorMessages() + ..type = type + ..relatedReceiptId = receiptId); + await sendEncryptedContent(target, content); + } + + Future sendUserDiscoveryRequest( + TestClient target, + List version, + ) async { + final content = pb.EncryptedContent() + ..userDiscoveryRequest = (pb.EncryptedContent_UserDiscoveryRequest() + ..currentVersion = version); + await sendEncryptedContent(target, content); + } + + Future sendUserDiscoveryUpdate( + TestClient target, + List> messages, + ) async { + final content = pb.EncryptedContent() + ..userDiscoveryUpdate = (pb.EncryptedContent_UserDiscoveryUpdate() + ..messages.addAll(messages)); + await sendEncryptedContent(target, content); + } + + Future sendKeyVerificationProof( + TestClient target, + List mac, + ) async { + final content = pb.EncryptedContent() + ..keyVerificationProof = (pb.EncryptedContent_KeyVerificationProof() + ..calculatedMac = mac); + await sendEncryptedContent(target, content); + } + + Future sendDeliveryReceipt(TestClient target, String receiptId) async { + final msg = pb.Message() + ..type = pb.Message_Type.SENDER_DELIVERY_RECEIPT + ..receiptId = receiptId; + await api.sendTextMessage(target.realUserId, msg.writeToBuffer(), null); + } + + Future sendReaction(TestClient target, String targetMessageId, String emoji, {bool remove = false}) async { + final content = pb.EncryptedContent() + ..groupId = defaultGroup!.groupId + ..reaction = (pb.EncryptedContent_Reaction() + ..targetMessageId = targetMessageId + ..emoji = emoji + ..remove = remove); + await sendEncryptedContent(target, content); + } + + Future sendMessageUpdate(TestClient target, String targetMessageId, pb.EncryptedContent_MessageUpdate_Type type, {String? text}) async { + final update = pb.EncryptedContent_MessageUpdate() + ..type = type + ..senderMessageId = targetMessageId + ..timestamp = Int64(DateTime.now().millisecondsSinceEpoch); + if (text != null) update.text = text; + final content = pb.EncryptedContent() + ..groupId = defaultGroup!.groupId + ..messageUpdate = update; + await sendEncryptedContent(target, content); + } + + Future sendMedia(TestClient target, String senderMessageId, pb.EncryptedContent_Media_Type type) async { + final content = pb.EncryptedContent() + ..groupId = defaultGroup!.groupId + ..media = (pb.EncryptedContent_Media() + ..senderMessageId = senderMessageId + ..type = type + ..requiresAuthentication = false + ..timestamp = Int64(DateTime.now().millisecondsSinceEpoch)); + await sendEncryptedContent(target, content); + } + + Future expectMessage(bool Function(Message) predicate) async { + for (var i = 0; i < 500; i++) { + final msg = await run(() async { + final msgs = await env.db.select(env.db.messages).get(); + return msgs.firstWhereOrNull(predicate); + }); + if (msg != null) return msg; + await Future.delayed(const Duration(milliseconds: 10)); + } + throw Exception('Message matching predicate not received'); + } + + Future expectReaction(String messageId, String emoji) async { + for (var i = 0; i < 500; i++) { + final reaction = await run(() async { + final reactions = await (env.db.select(env.db.reactions)..where((t) => t.messageId.equals(messageId))).get(); + return reactions.firstWhereOrNull((r) => r.emoji == emoji); + }); + if (reaction != null) return reaction; + await Future.delayed(const Duration(milliseconds: 10)); + } + throw Exception('Reaction $emoji not received on message $messageId'); + } +} diff --git a/test/mocks/user_environment.dart b/test/mocks/user_environment.dart new file mode 100644 index 00000000..264aa08b --- /dev/null +++ b/test/mocks/user_environment.dart @@ -0,0 +1,125 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:drift/drift.dart' hide isNotNull, isNull; +import 'package:drift/native.dart'; +import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/json/userdata.model.dart'; +import 'package:twonly/src/services/api.service.dart'; +import 'package:twonly/src/services/user.service.dart'; +import 'package:twonly/src/utils/keyvalue.dart'; + +base class ZoneIOOverrides extends IOOverrides { + @override + File createFile(String path) { + final userId = Zone.current[#userId] as int?; + if (userId != null) { + final newPath = _rewritePath(path, userId); + return super.createFile(newPath); + } + return super.createFile(path); + } + + @override + Directory createDirectory(String path) { + final userId = Zone.current[#userId] as int?; + if (userId != null) { + final newPath = _rewritePath(path, userId); + return super.createDirectory(newPath); + } + return super.createDirectory(path); + } + + String _rewritePath(String path, int userId) { + if (path.contains('/user_$userId') || path.contains('/$userId')) { + return path; + } + if (path.contains('/keyvalue/')) { + return path.replaceFirst('/keyvalue/', '/keyvalue/user_$userId/'); + } + return path; + } +} + +Future runInZone( + UserEnvironment env, + ApiService api, + Future Function() computation, +) { + return IOOverrides.runWithIOOverrides( + () => runZoned( + computation, + zoneValues: { + #twonlyDB: env.db, + #userService: env.userService, + #apiService: api, + #userId: env.userId, + }, + ), + ZoneIOOverrides(), + ); +} + +class UserEnvironment { + UserEnvironment({ + required this.userId, + required this.username, + required this.db, + required this.userService, + required this.identityKeyPair, + required this.registrationId, + }); + final int userId; + final String username; + final TwonlyDB db; + final UserService userService; + final IdentityKeyPair identityKeyPair; + final int registrationId; + + static Future create( + int userId, + String username, + ) async { + final db = TwonlyDB.forTesting( + DatabaseConnection( + NativeDatabase.memory(), + closeStreamsSynchronously: true, + ), + ); + + final us = UserService(); + // ignore: cascade_invocations + us.currentUser = UserData( + userId: userId, + username: username, + displayName: '$username Display', + subscriptionPlan: 'Free', + currentSetupPage: null, + appVersion: 100, + ); + + // ignore: cascade_invocations + us.isUserCreated = true; + + final identityKeyPair = generateIdentityKeyPair(); + final registrationId = generateRegistrationId(true); + + // Save to keyvalue store using zone so it is isolated per-user + await runZoned( + () => KeyValueStore.put('user', us.currentUser.toJson()), + zoneValues: { + #userId: userId, + }, + ); + + return UserEnvironment( + userId: userId, + username: username, + db: db, + userService: us, + identityKeyPair: identityKeyPair, + registrationId: registrationId, + ); + } +} diff --git a/test/mocks/workmanager.dart b/test/mocks/workmanager.dart new file mode 100644 index 00000000..cca02a14 --- /dev/null +++ b/test/mocks/workmanager.dart @@ -0,0 +1,29 @@ +import 'package:workmanager_platform_interface/workmanager_platform_interface.dart'; + +class MockWorkmanagerPlatform extends WorkmanagerPlatform { + @override + Future initialize( + Function callbackDispatcher, { + bool isInDebugMode = false, + }) async {} + + @override + Future registerOneOffTask( + String uniqueName, + String taskName, { + Map? inputData, + Duration? initialDelay, + Constraints? constraints, + ExistingWorkPolicy? existingWorkPolicy, + BackoffPolicy? backoffPolicy, + Duration? backoffPolicyDelay, + String? tag, + OutOfQuotaPolicy? outOfQuotaPolicy, + }) async {} + + @override + Future cancelByUniqueName(String uniqueName) async {} + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} diff --git a/test/services/api_and_c2c_test.dart b/test/services/api_and_c2c_test.dart new file mode 100644 index 00000000..30bd5dfc --- /dev/null +++ b/test/services/api_and_c2c_test.dart @@ -0,0 +1,242 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:drift/drift.dart' hide isNotNull, isNull; +import 'package:flutter/services.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:twonly/core/bridge.dart' as bridge; +import 'package:twonly/core/frb_generated.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/locator.dart'; +import 'package:twonly/src/callbacks/callbacks.dart'; +import 'package:twonly/src/database/tables/messages.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart' + as pb; +import 'package:twonly/src/services/api.service.dart'; +import 'package:twonly/src/services/user.service.dart'; +import 'package:workmanager/workmanager.dart'; + +import '../mocks/platform_channels.dart'; +import '../mocks/test_client.dart'; +import '../mocks/workmanager.dart'; + +void main() { + if (!Platform.isMacOS) { + return; + } + + TestWidgetsFlutterBinding.ensureInitialized(); + + late Directory tempDir; + + setUpAll(() async { + // Log.init(); + driftRuntimeOptions.dontWarnAboutMultipleDatabases = true; + WorkmanagerPlatform.instance = MockWorkmanagerPlatform(); + tempDir = Directory.systemTemp.createTempSync('twonly_messaging_test_'); + AppEnvironment.initTesting( + customCacheDir: tempDir.path, + customSupportDir: tempDir.path, + ); + + final dylibPath = + '${Directory.current.path}/rust/target/debug/librust_lib_twonly.dylib'; + if (File(dylibPath).existsSync()) { + await RustLib.init(externalLibrary: ExternalLibrary.open(dylibPath)); + } else { + await RustLib.init(); + } + await initFlutterCallbacksForRust(); + + await bridge.initializeTwonlyFlutter( + config: bridge.InitConfig( + databaseDir: tempDir.path, + dataDir: tempDir.path, + ), + ); + + if (locator.isRegistered()) await locator.unregister(); + if (locator.isRegistered()) { + await locator.unregister(); + } + if (locator.isRegistered()) { + await locator.unregister(); + } + + locator + ..registerFactory(() { + final db = Zone.current[#twonlyDB] as TwonlyDB?; + if (db != null) return db; + throw StateError('No TwonlyDB in active Zone.'); + }) + ..registerFactory(() { + final us = Zone.current[#userService] as UserService?; + if (us != null) return us; + throw StateError('No UserService in active Zone.'); + }) + ..registerFactory(() { + final api = Zone.current[#apiService] as ApiService?; + if (api != null) return api; + throw StateError('No ApiService in active Zone.'); + }); + }); + + tearDownAll(() async { + if (tempDir.existsSync()) { + try { + tempDir.deleteSync(recursive: true); + } catch (_) {} + } + }); + + group('C2C Messaging Protocol - Real API Stress Tests', () { + late TestClient clientA; + late TestClient clientB; + + setUp(() async { + setupPlatformChannelMocks(); + HttpOverrides.global = RealHttpOverrides(); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler( + const MethodChannel('dev.fluttercommunity.plus/package_info'), + (call) async { + return { + 'appName': 'twonly', + 'packageName': 'eu.twonly.app', + 'version': '1.0.0', + 'buildNumber': '100', + }; + }, + ); + await Workmanager().initialize(() {}); + + clientA = TestClient(1001); + clientB = TestClient(2002); + + await clientA.init(); + await clientB.init(); + + await clientA.initContact(clientB); + await clientB.initContact(clientA); + }); + + tearDown(() async { + await clientA.run(() async => clientA.api.close(null)); + await clientB.run(() async => clientB.api.close(null)); + await clientA.env.db.close(); + await clientB.env.db.close(); + }); + + test('C2C: Text Message Send & Receive', () async { + const text = 'Hello User B!'; + await clientA.sendText(clientB, text); + + final receivedMsg = await clientB.expectMessage( + (m) => m.content == text && m.senderId == clientA.realUserId, + ); + + expect(receivedMsg.content, text); + expect(receivedMsg.senderId, clientA.realUserId); + expect(receivedMsg.type, MessageType.text.name); + }); + + test('C2C: GroupJoin & ResendGroupPublicKey', () async { + // clientA creates a fake group ID + const groupId = 'test-group-id'; + + // clientA asks clientB for public key + await clientA.sendResendGroupPublicKey(clientB, groupId); + + // We expect clientB to process it without crashing. + // Wait for it to settle by sending a text message and awaiting it. + await clientA.sendText(clientB, 'sync1'); + await clientB.expectMessage((m) => m.content == 'sync1'); + + // clientA sends GroupJoin + await clientA.sendGroupJoin(clientB, groupId, [1, 2, 3]); + + await clientA.sendText(clientB, 'sync2'); + await clientB.expectMessage((m) => m.content == 'sync2'); + }); + + test('C2C: ErrorMessages', () async { + await clientA.sendErrorMessages( + clientB, + pb.EncryptedContent_ErrorMessages_Type.SESSION_OUT_OF_SYNC, + 'fake-receipt', + ); + await clientA.sendText(clientB, 'sync3'); + await clientB.expectMessage((m) => m.content == 'sync3'); + }); + + test('C2C: UserDiscoveryRequest & Update', () async { + await clientA.sendUserDiscoveryRequest(clientB, [1]); + await clientA.sendUserDiscoveryUpdate(clientB, [ + [1, 2], + ]); + await clientA.sendText(clientB, 'sync4'); + await clientB.expectMessage((m) => m.content == 'sync4'); + }); + + test('C2C: KeyVerificationProof', () async { + await clientA.sendKeyVerificationProof(clientB, [0, 0, 0]); + await clientA.sendText(clientB, 'sync5'); + await clientB.expectMessage((m) => m.content == 'sync5'); + }); + + test('C2C: SENDER_DELIVERY_RECEIPT', () async { + await clientA.sendDeliveryReceipt(clientB, 'fake-receipt-999'); + await clientA.sendText(clientB, 'sync6'); + await clientB.expectMessage((m) => m.content == 'sync6'); + }); + + test('C2C: Reaction Send & Receive', () async { + final msgA = await clientA.sendText(clientB, 'Message to react to'); + await clientB.expectMessage((m) => m.content == 'Message to react to'); + + await clientB.sendReaction(clientA, msgA.messageId, '👍'); + final receivedReaction = await clientA.expectReaction( + msgA.messageId, + '👍', + ); + expect(receivedReaction.emoji, '👍'); + }); + + test('C2C: MessageUpdate (Edit & Delete)', () async { + final msgA = await clientA.sendText(clientB, 'Message to edit'); + await clientB.expectMessage((m) => m.content == 'Message to edit'); + + await clientA.sendMessageUpdate( + clientB, + msgA.messageId, + pb.EncryptedContent_MessageUpdate_Type.EDIT_TEXT, + text: 'Edited text', + ); + + final editedMsg = await clientB.expectMessage( + (m) => m.content == 'Edited text' && m.messageId == msgA.messageId, + ); + expect(editedMsg.content, 'Edited text'); + + await clientA.sendMessageUpdate( + clientB, + msgA.messageId, + pb.EncryptedContent_MessageUpdate_Type.DELETE, + ); + await clientA.sendText(clientB, 'sync7'); + await clientB.expectMessage((m) => m.content == 'sync7'); + }); + + test('C2C: Media Message', () async { + await clientA.sendMedia( + clientB, + 'media-msg-id', + pb.EncryptedContent_Media_Type.IMAGE, + ); + await clientA.sendText(clientB, 'sync8'); + await clientB.expectMessage((m) => m.content == 'sync8'); + }); + }); +} diff --git a/test/services/backup_service_test.dart b/test/services/backup_service_test.dart index 7f3c428c..331ccf4a 100644 --- a/test/services/backup_service_test.dart +++ b/test/services/backup_service_test.dart @@ -81,7 +81,8 @@ void main() { displayName: 'Test User', subscriptionPlan: 'Free', currentSetupPage: null, - )..appVersion = 100; + appVersion: 100, + ); userService.isUserCreated = true; await UserService.save(userService.currentUser); initialUserData = (await KeyValueStore.get('user'))!; diff --git a/test/services/group_service_test.dart b/test/services/group_service_test.dart index 2d603ae4..29065336 100644 --- a/test/services/group_service_test.dart +++ b/test/services/group_service_test.dart @@ -49,7 +49,8 @@ void main() { displayName: 'Test User', subscriptionPlan: 'Free', currentSetupPage: null, - )..appVersion = 100; + appVersion: 100, + ); userService.isUserCreated = true; AppEnvironment.initTesting(); // Log.init();