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