From 41dc30b3c2e491dacf14660b50c41091305f18aa Mon Sep 17 00:00:00 2001 From: otsmr Date: Wed, 29 Apr 2026 00:22:42 +0200 Subject: [PATCH] finish setup --- lib/app.dart | 7 +- lib/core/bridge/wrapper/user_discovery.dart | 2 + lib/core/frb_generated.dart | 7 +- lib/globals.dart | 9 +- lib/main.dart | 10 +- lib/src/database/daos/contacts.dao.dart | 4 +- .../schemas/twonly_db/drift_schema_v12.json | 39 +- lib/src/database/tables/contacts.table.dart | 3 + lib/src/database/twonly.db.dart | 18 +- lib/src/database/twonly.db.g.dart | 87 + lib/src/database/twonly.db.steps.dart | 88 +- .../generated/app_localizations.dart | 740 ++----- .../generated/app_localizations_de.dart | 435 +--- .../generated/app_localizations_en.dart | 428 +--- .../generated/app_localizations_sv.dart | 1915 ----------------- lib/src/localization/translations | 2 +- lib/src/model/json/userdata.model.dart | 8 +- lib/src/model/json/userdata.model.g.dart | 12 +- .../api/client2client/user_discovery.c2c.dart | 5 +- lib/src/services/api/messages.api.dart | 2 +- lib/src/services/user_discovery.service.dart | 6 +- lib/src/services/user_study.service.dart | 4 +- .../components/verification_badge.comp.dart | 6 + .../camera_bottom_controls.dart | 2 +- .../main_camera_controller.dart | 4 + .../visual/views/chats/chat_list.view.dart | 114 +- .../friend_suggestions.comp.dart | 10 +- .../visual/views/contact/contact.view.dart | 4 +- .../visual/views/onboarding/setup.view.dart | 82 +- .../setup/add_new_contacts_setup.view.dart | 130 ++ .../onboarding/setup/backup_setup.view.dart | 60 +- .../setup/components/finish_setup.comp.dart | 161 ++ .../mock_contact_request_actions.comp.dart | 121 ++ .../setup/components/next_button.comp.dart | 60 + .../components/setup_switch_card.comp.dart | 68 + .../onboarding/setup/finish_setup.comp.dart | 15 - .../onboarding/setup/profile_setup.view.dart | 83 +- .../setup/user_discovery_setup.view.dart | 44 +- .../setup/verification_badge_setup.view.dart | 29 +- .../user_discovery_disabled.comp.dart | 110 +- .../components/user_discovery_setup.comp.dart | 535 +++++ .../user_discovery_settings.view.dart | 187 +- rust/src/bridge/wrapper/user_discovery.rs | 3 +- rust/src/database/mod.rs | 110 +- rust/src/frb_generated.rs | 5 +- rust/src/standalone/mod.rs | 16 +- .../protocols/src/user_discovery.rs | 57 +- .../protocols/src/user_discovery/tests.rs | 6 +- .../protocols/src/user_discovery/types.proto | 5 +- scripts/check_arb.py | 121 ++ .../drift/twonly_db/generated/schema_v12.dart | 110 +- 51 files changed, 2328 insertions(+), 3761 deletions(-) delete mode 100644 lib/src/localization/generated/app_localizations_sv.dart create mode 100644 lib/src/visual/views/onboarding/setup/add_new_contacts_setup.view.dart create mode 100644 lib/src/visual/views/onboarding/setup/components/finish_setup.comp.dart create mode 100644 lib/src/visual/views/onboarding/setup/components/mock_contact_request_actions.comp.dart create mode 100644 lib/src/visual/views/onboarding/setup/components/next_button.comp.dart create mode 100644 lib/src/visual/views/onboarding/setup/components/setup_switch_card.comp.dart delete mode 100644 lib/src/visual/views/onboarding/setup/finish_setup.comp.dart create mode 100644 lib/src/visual/views/settings/privacy/user_discovery/components/user_discovery_setup.comp.dart create mode 100644 scripts/check_arb.py diff --git a/lib/app.dart b/lib/app.dart index 080994d3..a438168f 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -169,10 +169,9 @@ class _AppMainWidgetState extends State { _isTwonlyLocked = false; }), ); - } else if (true || - !userService.currentUser.skipSetupPages && - userService.currentUser.currentSetupPage == - SetupPages.profile.name) { + } 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(() { // userService.currentUser has updated... diff --git a/lib/core/bridge/wrapper/user_discovery.dart b/lib/core/bridge/wrapper/user_discovery.dart index db1a53e4..dab9c080 100644 --- a/lib/core/bridge/wrapper/user_discovery.dart +++ b/lib/core/bridge/wrapper/user_discovery.dart @@ -36,11 +36,13 @@ class FlutterUserDiscovery { required int threshold, required PlatformInt64 userId, required List publicKey, + required bool sharePromotion, }) => RustLib.instance.api .crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryInitializeOrUpdate( threshold: threshold, userId: userId, publicKey: publicKey, + sharePromotion: sharePromotion, ); static Future shouldRequestNewMessages({ diff --git a/lib/core/frb_generated.dart b/lib/core/frb_generated.dart index 83f38520..28dac796 100644 --- a/lib/core/frb_generated.dart +++ b/lib/core/frb_generated.dart @@ -103,6 +103,7 @@ abstract class RustLibApi extends BaseApi { required int threshold, required PlatformInt64 userId, required List publicKey, + required bool sharePromotion, }); Future @@ -284,6 +285,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { required int threshold, required PlatformInt64 userId, required List publicKey, + required bool sharePromotion, }) { return handler.executeNormal( NormalTask( @@ -292,6 +294,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_u_8(threshold, serializer); sse_encode_i_64(userId, serializer); sse_encode_list_prim_u_8_loose(publicKey, serializer); + sse_encode_bool(sharePromotion, serializer); pdeCallFfi( generalizedFrbRustBinding, serializer, @@ -305,7 +308,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ), constMeta: kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryInitializeOrUpdateConstMeta, - argValues: [threshold, userId, publicKey], + argValues: [threshold, userId, publicKey, sharePromotion], apiImpl: this, ), ); @@ -315,7 +318,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { get kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryInitializeOrUpdateConstMeta => const TaskConstMeta( debugName: "flutter_user_discovery_initialize_or_update", - argNames: ["threshold", "userId", "publicKey"], + argNames: ["threshold", "userId", "publicKey", "sharePromotion"], ); @override diff --git a/lib/globals.dart b/lib/globals.dart index c77aec37..e5d728df 100644 --- a/lib/globals.dart +++ b/lib/globals.dart @@ -7,18 +7,18 @@ import 'package:path_provider/path_provider.dart'; class AppEnvironment { static late final String cacheDir; static late final String supportDir; - static late final List cameras; + + // will be loaded in the main_camera_controller.dart + static List cameras = []; static Future init() async { cacheDir = (await getApplicationCacheDirectory()).path; supportDir = (await getApplicationSupportDirectory()).path; - cameras = await availableCameras(); } static void initTesting() { cacheDir = '/tmp/twonly_cache'; supportDir = '/tmp/twonly_support'; - cameras = []; } } @@ -27,8 +27,7 @@ class AppState { static bool isInBackgroundTask = false; static bool allowErrorTrackingViaSentry = false; static bool gotMessageFromServer = false; - // initialized in runMigrations (main.dart) - static late int latestAppVersionId; + static int latestAppVersionId = 110; } class AppGlobalKeys { diff --git a/lib/main.dart b/lib/main.dart index c935cfc9..68114c60 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -151,10 +151,12 @@ Future runMigrations() async { await UserService.update((u) { u ..appVersion = 109 - ..skipSetupPages = true - ..currentSetupPage = SetupPages.userDiscovery.name; + ..skipSetupPages = true; + if (u.avatarSvg == null) { + u.currentSetupPage = SetupPages.profile.name; + } else { + u.currentSetupPage = SetupPages.userDiscovery.name; + } }); } - - AppState.latestAppVersionId = 110; } diff --git a/lib/src/database/daos/contacts.dao.dart b/lib/src/database/daos/contacts.dao.dart index fd66c5b6..1212df6f 100644 --- a/lib/src/database/daos/contacts.dao.dart +++ b/lib/src/database/daos/contacts.dao.dart @@ -140,7 +140,7 @@ class ContactsDao extends DatabaseAccessor with _$ContactsDaoMixin { t.userDiscoveryVersion.isNotNull() & t.userDiscoveryExcluded.equals(false) & t.mediaSendCounter.isBiggerOrEqualValue( - userService.currentUser.minimumRequiredImagesExchanged, + userService.currentUser.requiredSendImages, ), )) .watch(); @@ -152,7 +152,7 @@ class ContactsDao extends DatabaseAccessor with _$ContactsDaoMixin { t.userDiscoveryVersion.isNotNull() & t.userDiscoveryExcluded.equals(false) & t.mediaSendCounter.isBiggerOrEqualValue( - userService.currentUser.minimumRequiredImagesExchanged, + userService.currentUser.requiredSendImages, ), )) .get(); diff --git a/lib/src/database/schemas/twonly_db/drift_schema_v12.json b/lib/src/database/schemas/twonly_db/drift_schema_v12.json index 4aaa9cf4..0c1a96f0 100644 --- a/lib/src/database/schemas/twonly_db/drift_schema_v12.json +++ b/lib/src/database/schemas/twonly_db/drift_schema_v12.json @@ -193,6 +193,20 @@ "default_client_dart": null, "dsl_features": [] }, + { + "name": "user_discovery_manual_approved", + "getter_name": "userDiscoveryManualApproved", + "moor_type": "bool", + "nullable": false, + "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", @@ -2008,6 +2022,22 @@ "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", @@ -2061,10 +2091,7 @@ ], "is_virtual": false, "without_rowid": false, - "constraints": [], - "explicit_pk": [ - "contact_id" - ] + "constraints": [] } }, { @@ -2530,7 +2557,7 @@ "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)), \"media_send_counter\" INTEGER NOT NULL DEFAULT 0, \"media_received_counter\" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (\"user_id\"));" + "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 NOT 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\"));" } ] }, @@ -2665,7 +2692,7 @@ "sql": [ { "dialect": "sqlite", - "sql": "CREATE TABLE IF NOT EXISTS \"key_verifications\" (\"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)), PRIMARY KEY (\"contact_id\"));" + "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)));" } ] }, diff --git a/lib/src/database/tables/contacts.table.dart b/lib/src/database/tables/contacts.table.dart index 5dbf4770..e32b7f97 100644 --- a/lib/src/database/tables/contacts.table.dart +++ b/lib/src/database/tables/contacts.table.dart @@ -29,6 +29,9 @@ class Contacts extends Table { BoolColumn get userDiscoveryExcluded => boolean().withDefault(const Constant(false))(); + BoolColumn get userDiscoveryManualApproved => + boolean().withDefault(const Constant(false))(); + IntColumn get mediaSendCounter => integer().withDefault(const Constant(0))(); IntColumn get mediaReceivedCounter => integer().withDefault(const Constant(0))(); diff --git a/lib/src/database/twonly.db.dart b/lib/src/database/twonly.db.dart index fdd83193..a53703bb 100644 --- a/lib/src/database/twonly.db.dart +++ b/lib/src/database/twonly.db.dart @@ -175,22 +175,16 @@ class TwonlyDB extends _$TwonlyDB { await m.createTable(schema.userDiscoveryOtherPromotions); await m.createTable(schema.userDiscoveryShares); await m.createTable(schema.userDiscoveryUserRelations); - await m.addColumn( - schema.contacts, + final columns = [ schema.contacts.userDiscoveryVersion, - ); - await m.addColumn( - schema.contacts, schema.contacts.mediaReceivedCounter, - ); - await m.addColumn( - schema.contacts, schema.contacts.mediaSendCounter, - ); - await m.addColumn( - schema.contacts, schema.contacts.userDiscoveryExcluded, - ); + schema.contacts.userDiscoveryManualApproved, + ]; + for (final column in columns) { + await m.addColumn(schema.contacts, column); + } }, )(m, from, to); }, diff --git a/lib/src/database/twonly.db.g.dart b/lib/src/database/twonly.db.g.dart index 6036d188..1cc53547 100644 --- a/lib/src/database/twonly.db.g.dart +++ b/lib/src/database/twonly.db.g.dart @@ -200,6 +200,21 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> { ), defaultValue: const Constant(false), ); + static const VerificationMeta _userDiscoveryManualApprovedMeta = + const VerificationMeta('userDiscoveryManualApproved'); + @override + late final GeneratedColumn userDiscoveryManualApproved = + GeneratedColumn( + 'user_discovery_manual_approved', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("user_discovery_manual_approved" IN (0, 1))', + ), + defaultValue: const Constant(false), + ); static const VerificationMeta _mediaSendCounterMeta = const VerificationMeta( 'mediaSendCounter', ); @@ -240,6 +255,7 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> { createdAt, userDiscoveryVersion, userDiscoveryExcluded, + userDiscoveryManualApproved, mediaSendCounter, mediaReceivedCounter, ]; @@ -368,6 +384,15 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> { ), ); } + if (data.containsKey('user_discovery_manual_approved')) { + context.handle( + _userDiscoveryManualApprovedMeta, + userDiscoveryManualApproved.isAcceptableOrUnknown( + data['user_discovery_manual_approved']!, + _userDiscoveryManualApprovedMeta, + ), + ); + } if (data.containsKey('media_send_counter')) { context.handle( _mediaSendCounterMeta, @@ -455,6 +480,10 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> { DriftSqlType.bool, data['${effectivePrefix}user_discovery_excluded'], )!, + userDiscoveryManualApproved: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}user_discovery_manual_approved'], + )!, mediaSendCounter: attachedDatabase.typeMapping.read( DriftSqlType.int, data['${effectivePrefix}media_send_counter'], @@ -488,6 +517,7 @@ class Contact extends DataClass implements Insertable { final DateTime createdAt; final Uint8List? userDiscoveryVersion; final bool userDiscoveryExcluded; + final bool userDiscoveryManualApproved; final int mediaSendCounter; final int mediaReceivedCounter; const Contact({ @@ -506,6 +536,7 @@ class Contact extends DataClass implements Insertable { required this.createdAt, this.userDiscoveryVersion, required this.userDiscoveryExcluded, + required this.userDiscoveryManualApproved, required this.mediaSendCounter, required this.mediaReceivedCounter, }); @@ -535,6 +566,9 @@ class Contact extends DataClass implements Insertable { map['user_discovery_version'] = Variable(userDiscoveryVersion); } map['user_discovery_excluded'] = Variable(userDiscoveryExcluded); + map['user_discovery_manual_approved'] = Variable( + userDiscoveryManualApproved, + ); map['media_send_counter'] = Variable(mediaSendCounter); map['media_received_counter'] = Variable(mediaReceivedCounter); return map; @@ -565,6 +599,7 @@ class Contact extends DataClass implements Insertable { ? const Value.absent() : Value(userDiscoveryVersion), userDiscoveryExcluded: Value(userDiscoveryExcluded), + userDiscoveryManualApproved: Value(userDiscoveryManualApproved), mediaSendCounter: Value(mediaSendCounter), mediaReceivedCounter: Value(mediaReceivedCounter), ); @@ -599,6 +634,9 @@ class Contact extends DataClass implements Insertable { userDiscoveryExcluded: serializer.fromJson( json['userDiscoveryExcluded'], ), + userDiscoveryManualApproved: serializer.fromJson( + json['userDiscoveryManualApproved'], + ), mediaSendCounter: serializer.fromJson(json['mediaSendCounter']), mediaReceivedCounter: serializer.fromJson( json['mediaReceivedCounter'], @@ -626,6 +664,9 @@ class Contact extends DataClass implements Insertable { userDiscoveryVersion, ), 'userDiscoveryExcluded': serializer.toJson(userDiscoveryExcluded), + 'userDiscoveryManualApproved': serializer.toJson( + userDiscoveryManualApproved, + ), 'mediaSendCounter': serializer.toJson(mediaSendCounter), 'mediaReceivedCounter': serializer.toJson(mediaReceivedCounter), }; @@ -647,6 +688,7 @@ class Contact extends DataClass implements Insertable { DateTime? createdAt, Value userDiscoveryVersion = const Value.absent(), bool? userDiscoveryExcluded, + bool? userDiscoveryManualApproved, int? mediaSendCounter, int? mediaReceivedCounter, }) => Contact( @@ -669,6 +711,8 @@ class Contact extends DataClass implements Insertable { ? userDiscoveryVersion.value : this.userDiscoveryVersion, userDiscoveryExcluded: userDiscoveryExcluded ?? this.userDiscoveryExcluded, + userDiscoveryManualApproved: + userDiscoveryManualApproved ?? this.userDiscoveryManualApproved, mediaSendCounter: mediaSendCounter ?? this.mediaSendCounter, mediaReceivedCounter: mediaReceivedCounter ?? this.mediaReceivedCounter, ); @@ -703,6 +747,9 @@ class Contact extends DataClass implements Insertable { userDiscoveryExcluded: data.userDiscoveryExcluded.present ? data.userDiscoveryExcluded.value : this.userDiscoveryExcluded, + userDiscoveryManualApproved: data.userDiscoveryManualApproved.present + ? data.userDiscoveryManualApproved.value + : this.userDiscoveryManualApproved, mediaSendCounter: data.mediaSendCounter.present ? data.mediaSendCounter.value : this.mediaSendCounter, @@ -730,6 +777,7 @@ class Contact extends DataClass implements Insertable { ..write('createdAt: $createdAt, ') ..write('userDiscoveryVersion: $userDiscoveryVersion, ') ..write('userDiscoveryExcluded: $userDiscoveryExcluded, ') + ..write('userDiscoveryManualApproved: $userDiscoveryManualApproved, ') ..write('mediaSendCounter: $mediaSendCounter, ') ..write('mediaReceivedCounter: $mediaReceivedCounter') ..write(')')) @@ -753,6 +801,7 @@ class Contact extends DataClass implements Insertable { createdAt, $driftBlobEquality.hash(userDiscoveryVersion), userDiscoveryExcluded, + userDiscoveryManualApproved, mediaSendCounter, mediaReceivedCounter, ); @@ -781,6 +830,8 @@ class Contact extends DataClass implements Insertable { this.userDiscoveryVersion, ) && other.userDiscoveryExcluded == this.userDiscoveryExcluded && + other.userDiscoveryManualApproved == + this.userDiscoveryManualApproved && other.mediaSendCounter == this.mediaSendCounter && other.mediaReceivedCounter == this.mediaReceivedCounter); } @@ -801,6 +852,7 @@ class ContactsCompanion extends UpdateCompanion { final Value createdAt; final Value userDiscoveryVersion; final Value userDiscoveryExcluded; + final Value userDiscoveryManualApproved; final Value mediaSendCounter; final Value mediaReceivedCounter; const ContactsCompanion({ @@ -819,6 +871,7 @@ class ContactsCompanion extends UpdateCompanion { this.createdAt = const Value.absent(), this.userDiscoveryVersion = const Value.absent(), this.userDiscoveryExcluded = const Value.absent(), + this.userDiscoveryManualApproved = const Value.absent(), this.mediaSendCounter = const Value.absent(), this.mediaReceivedCounter = const Value.absent(), }); @@ -838,6 +891,7 @@ class ContactsCompanion extends UpdateCompanion { this.createdAt = const Value.absent(), this.userDiscoveryVersion = const Value.absent(), this.userDiscoveryExcluded = const Value.absent(), + this.userDiscoveryManualApproved = const Value.absent(), this.mediaSendCounter = const Value.absent(), this.mediaReceivedCounter = const Value.absent(), }) : username = Value(username); @@ -857,6 +911,7 @@ class ContactsCompanion extends UpdateCompanion { Expression? createdAt, Expression? userDiscoveryVersion, Expression? userDiscoveryExcluded, + Expression? userDiscoveryManualApproved, Expression? mediaSendCounter, Expression? mediaReceivedCounter, }) { @@ -880,6 +935,8 @@ class ContactsCompanion extends UpdateCompanion { 'user_discovery_version': userDiscoveryVersion, if (userDiscoveryExcluded != null) 'user_discovery_excluded': userDiscoveryExcluded, + if (userDiscoveryManualApproved != null) + 'user_discovery_manual_approved': userDiscoveryManualApproved, if (mediaSendCounter != null) 'media_send_counter': mediaSendCounter, if (mediaReceivedCounter != null) 'media_received_counter': mediaReceivedCounter, @@ -902,6 +959,7 @@ class ContactsCompanion extends UpdateCompanion { Value? createdAt, Value? userDiscoveryVersion, Value? userDiscoveryExcluded, + Value? userDiscoveryManualApproved, Value? mediaSendCounter, Value? mediaReceivedCounter, }) { @@ -922,6 +980,8 @@ class ContactsCompanion extends UpdateCompanion { userDiscoveryVersion: userDiscoveryVersion ?? this.userDiscoveryVersion, userDiscoveryExcluded: userDiscoveryExcluded ?? this.userDiscoveryExcluded, + userDiscoveryManualApproved: + userDiscoveryManualApproved ?? this.userDiscoveryManualApproved, mediaSendCounter: mediaSendCounter ?? this.mediaSendCounter, mediaReceivedCounter: mediaReceivedCounter ?? this.mediaReceivedCounter, ); @@ -981,6 +1041,11 @@ class ContactsCompanion extends UpdateCompanion { userDiscoveryExcluded.value, ); } + if (userDiscoveryManualApproved.present) { + map['user_discovery_manual_approved'] = Variable( + userDiscoveryManualApproved.value, + ); + } if (mediaSendCounter.present) { map['media_send_counter'] = Variable(mediaSendCounter.value); } @@ -1008,6 +1073,7 @@ class ContactsCompanion extends UpdateCompanion { ..write('createdAt: $createdAt, ') ..write('userDiscoveryVersion: $userDiscoveryVersion, ') ..write('userDiscoveryExcluded: $userDiscoveryExcluded, ') + ..write('userDiscoveryManualApproved: $userDiscoveryManualApproved, ') ..write('mediaSendCounter: $mediaSendCounter, ') ..write('mediaReceivedCounter: $mediaReceivedCounter') ..write(')')) @@ -11621,6 +11687,7 @@ typedef $$ContactsTableCreateCompanionBuilder = Value createdAt, Value userDiscoveryVersion, Value userDiscoveryExcluded, + Value userDiscoveryManualApproved, Value mediaSendCounter, Value mediaReceivedCounter, }); @@ -11641,6 +11708,7 @@ typedef $$ContactsTableUpdateCompanionBuilder = Value createdAt, Value userDiscoveryVersion, Value userDiscoveryExcluded, + Value userDiscoveryManualApproved, Value mediaSendCounter, Value mediaReceivedCounter, }); @@ -12022,6 +12090,11 @@ class $$ContactsTableFilterComposer builder: (column) => ColumnFilters(column), ); + ColumnFilters get userDiscoveryManualApproved => $composableBuilder( + column: $table.userDiscoveryManualApproved, + builder: (column) => ColumnFilters(column), + ); + ColumnFilters get mediaSendCounter => $composableBuilder( column: $table.mediaSendCounter, builder: (column) => ColumnFilters(column), @@ -12425,6 +12498,11 @@ class $$ContactsTableOrderingComposer builder: (column) => ColumnOrderings(column), ); + ColumnOrderings get userDiscoveryManualApproved => $composableBuilder( + column: $table.userDiscoveryManualApproved, + builder: (column) => ColumnOrderings(column), + ); + ColumnOrderings get mediaSendCounter => $composableBuilder( column: $table.mediaSendCounter, builder: (column) => ColumnOrderings(column), @@ -12504,6 +12582,11 @@ class $$ContactsTableAnnotationComposer builder: (column) => column, ); + GeneratedColumn get userDiscoveryManualApproved => $composableBuilder( + column: $table.userDiscoveryManualApproved, + builder: (column) => column, + ); + GeneratedColumn get mediaSendCounter => $composableBuilder( column: $table.mediaSendCounter, builder: (column) => column, @@ -12884,6 +12967,7 @@ class $$ContactsTableTableManager Value createdAt = const Value.absent(), Value userDiscoveryVersion = const Value.absent(), Value userDiscoveryExcluded = const Value.absent(), + Value userDiscoveryManualApproved = const Value.absent(), Value mediaSendCounter = const Value.absent(), Value mediaReceivedCounter = const Value.absent(), }) => ContactsCompanion( @@ -12902,6 +12986,7 @@ class $$ContactsTableTableManager createdAt: createdAt, userDiscoveryVersion: userDiscoveryVersion, userDiscoveryExcluded: userDiscoveryExcluded, + userDiscoveryManualApproved: userDiscoveryManualApproved, mediaSendCounter: mediaSendCounter, mediaReceivedCounter: mediaReceivedCounter, ), @@ -12922,6 +13007,7 @@ class $$ContactsTableTableManager Value createdAt = const Value.absent(), Value userDiscoveryVersion = const Value.absent(), Value userDiscoveryExcluded = const Value.absent(), + Value userDiscoveryManualApproved = const Value.absent(), Value mediaSendCounter = const Value.absent(), Value mediaReceivedCounter = const Value.absent(), }) => ContactsCompanion.insert( @@ -12940,6 +13026,7 @@ class $$ContactsTableTableManager createdAt: createdAt, userDiscoveryVersion: userDiscoveryVersion, userDiscoveryExcluded: userDiscoveryExcluded, + userDiscoveryManualApproved: userDiscoveryManualApproved, mediaSendCounter: mediaSendCounter, mediaReceivedCounter: mediaReceivedCounter, ), diff --git a/lib/src/database/twonly.db.steps.dart b/lib/src/database/twonly.db.steps.dart index b7b06c99..46df63b5 100644 --- a/lib/src/database/twonly.db.steps.dart +++ b/lib/src/database/twonly.db.steps.dart @@ -5875,6 +5875,7 @@ final class Schema12 extends i0.VersionedSchema { _column_212, _column_213, _column_214, + _column_215, ], attachedDatabase: database, ), @@ -6140,8 +6141,8 @@ final class Schema12 extends i0.VersionedSchema { entityName: 'key_verifications', withoutRowId: false, isStrict: false, - tableConstraints: ['PRIMARY KEY(contact_id)'], - columns: [_column_183, _column_144, _column_118], + tableConstraints: [], + columns: [_column_216, _column_183, _column_144, _column_118], attachedDatabase: database, ), alias: null, @@ -6152,7 +6153,7 @@ final class Schema12 extends i0.VersionedSchema { withoutRowId: false, isStrict: false, tableConstraints: [], - columns: [_column_215, _column_216, _column_118], + columns: [_column_217, _column_218, _column_118], attachedDatabase: database, ), alias: null, @@ -6164,12 +6165,12 @@ final class Schema12 extends i0.VersionedSchema { isStrict: false, tableConstraints: ['PRIMARY KEY(announced_user_id)'], columns: [ - _column_217, - _column_218, _column_219, _column_220, _column_221, _column_222, + _column_223, + _column_224, ], attachedDatabase: database, ), @@ -6181,7 +6182,7 @@ final class Schema12 extends i0.VersionedSchema { withoutRowId: false, isStrict: false, tableConstraints: ['PRIMARY KEY(announced_user_id, from_contact_id)'], - columns: [_column_223, _column_224, _column_225], + columns: [_column_225, _column_226, _column_227], attachedDatabase: database, ), alias: null, @@ -6193,12 +6194,12 @@ final class Schema12 extends i0.VersionedSchema { isStrict: false, tableConstraints: ['PRIMARY KEY(from_contact_id, public_id)'], columns: [ - _column_224, _column_226, - _column_227, _column_228, _column_229, - _column_225, + _column_230, + _column_231, + _column_227, ], attachedDatabase: database, ), @@ -6210,7 +6211,7 @@ final class Schema12 extends i0.VersionedSchema { withoutRowId: false, isStrict: false, tableConstraints: [], - columns: [_column_230, _column_183, _column_231], + columns: [_column_232, _column_183, _column_233], attachedDatabase: database, ), alias: null, @@ -6221,7 +6222,7 @@ final class Schema12 extends i0.VersionedSchema { withoutRowId: false, isStrict: false, tableConstraints: [], - columns: [_column_232, _column_233, _column_175], + columns: [_column_234, _column_235, _column_175], attachedDatabase: database, ), alias: null, @@ -6262,6 +6263,9 @@ class Shape39 extends i0.VersionedTable { as i1.GeneratedColumn; i1.GeneratedColumn get userDiscoveryExcluded => columnsByName['user_discovery_excluded']! as i1.GeneratedColumn; + i1.GeneratedColumn get userDiscoveryManualApproved => + columnsByName['user_discovery_manual_approved']! + as i1.GeneratedColumn; i1.GeneratedColumn get mediaSendCounter => columnsByName['media_send_counter']! as i1.GeneratedColumn; i1.GeneratedColumn get mediaReceivedCounter => @@ -6287,6 +6291,16 @@ i1.GeneratedColumn _column_212(String aliasedName) => defaultValue: const i1.CustomExpression('0'), ); i1.GeneratedColumn _column_213(String aliasedName) => + i1.GeneratedColumn( + 'user_discovery_manual_approved', + aliasedName, + false, + type: i1.DriftSqlType.int, + $customConstraints: + 'NOT NULL DEFAULT 0 CHECK (user_discovery_manual_approved IN (0, 1))', + defaultValue: const i1.CustomExpression('0'), + ); +i1.GeneratedColumn _column_214(String aliasedName) => i1.GeneratedColumn( 'media_send_counter', aliasedName, @@ -6295,7 +6309,7 @@ i1.GeneratedColumn _column_213(String aliasedName) => $customConstraints: 'NOT NULL DEFAULT 0', defaultValue: const i1.CustomExpression('0'), ); -i1.GeneratedColumn _column_214(String aliasedName) => +i1.GeneratedColumn _column_215(String aliasedName) => i1.GeneratedColumn( 'media_received_counter', aliasedName, @@ -6307,6 +6321,8 @@ i1.GeneratedColumn _column_214(String aliasedName) => class Shape40 extends i0.VersionedTable { Shape40({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get verificationId => + columnsByName['verification_id']! as i1.GeneratedColumn; i1.GeneratedColumn get contactId => columnsByName['contact_id']! as i1.GeneratedColumn; i1.GeneratedColumn get type => @@ -6315,6 +6331,16 @@ class Shape40 extends i0.VersionedTable { columnsByName['created_at']! as i1.GeneratedColumn; } +i1.GeneratedColumn _column_216(String aliasedName) => + i1.GeneratedColumn( + 'verification_id', + aliasedName, + false, + hasAutoIncrement: true, + type: i1.DriftSqlType.int, + $customConstraints: 'NOT NULL PRIMARY KEY AUTOINCREMENT', + ); + class Shape41 extends i0.VersionedTable { Shape41({required super.source, required super.alias}) : super.aliased(); i1.GeneratedColumn get tokenId => @@ -6325,7 +6351,7 @@ class Shape41 extends i0.VersionedTable { columnsByName['created_at']! as i1.GeneratedColumn; } -i1.GeneratedColumn _column_215(String aliasedName) => +i1.GeneratedColumn _column_217(String aliasedName) => i1.GeneratedColumn( 'token_id', aliasedName, @@ -6334,7 +6360,7 @@ i1.GeneratedColumn _column_215(String aliasedName) => type: i1.DriftSqlType.int, $customConstraints: 'NOT NULL PRIMARY KEY AUTOINCREMENT', ); -i1.GeneratedColumn _column_216(String aliasedName) => +i1.GeneratedColumn _column_218(String aliasedName) => i1.GeneratedColumn( 'token', aliasedName, @@ -6360,7 +6386,7 @@ class Shape42 extends i0.VersionedTable { columnsByName['is_hidden']! as i1.GeneratedColumn; } -i1.GeneratedColumn _column_217(String aliasedName) => +i1.GeneratedColumn _column_219(String aliasedName) => i1.GeneratedColumn( 'announced_user_id', aliasedName, @@ -6368,7 +6394,7 @@ i1.GeneratedColumn _column_217(String aliasedName) => type: i1.DriftSqlType.int, $customConstraints: 'NOT NULL', ); -i1.GeneratedColumn _column_218(String aliasedName) => +i1.GeneratedColumn _column_220(String aliasedName) => i1.GeneratedColumn( 'announced_public_key', aliasedName, @@ -6376,7 +6402,7 @@ i1.GeneratedColumn _column_218(String aliasedName) => type: i1.DriftSqlType.blob, $customConstraints: 'NOT NULL', ); -i1.GeneratedColumn _column_219(String aliasedName) => +i1.GeneratedColumn _column_221(String aliasedName) => i1.GeneratedColumn( 'public_id', aliasedName, @@ -6384,7 +6410,7 @@ i1.GeneratedColumn _column_219(String aliasedName) => type: i1.DriftSqlType.int, $customConstraints: 'NOT NULL UNIQUE', ); -i1.GeneratedColumn _column_220(String aliasedName) => +i1.GeneratedColumn _column_222(String aliasedName) => i1.GeneratedColumn( 'username', aliasedName, @@ -6392,7 +6418,7 @@ i1.GeneratedColumn _column_220(String aliasedName) => type: i1.DriftSqlType.string, $customConstraints: 'NULL', ); -i1.GeneratedColumn _column_221(String aliasedName) => +i1.GeneratedColumn _column_223(String aliasedName) => i1.GeneratedColumn( 'was_shown_to_the_user', aliasedName, @@ -6402,7 +6428,7 @@ i1.GeneratedColumn _column_221(String aliasedName) => 'NOT NULL DEFAULT 0 CHECK (was_shown_to_the_user IN (0, 1))', defaultValue: const i1.CustomExpression('0'), ); -i1.GeneratedColumn _column_222(String aliasedName) => +i1.GeneratedColumn _column_224(String aliasedName) => i1.GeneratedColumn( 'is_hidden', aliasedName, @@ -6423,7 +6449,7 @@ class Shape43 extends i0.VersionedTable { as i1.GeneratedColumn; } -i1.GeneratedColumn _column_223( +i1.GeneratedColumn _column_225( String aliasedName, ) => i1.GeneratedColumn( 'announced_user_id', @@ -6433,7 +6459,7 @@ i1.GeneratedColumn _column_223( $customConstraints: 'NOT NULL REFERENCES user_discovery_announced_users(announced_user_id)ON DELETE CASCADE', ); -i1.GeneratedColumn _column_224(String aliasedName) => +i1.GeneratedColumn _column_226(String aliasedName) => i1.GeneratedColumn( 'from_contact_id', aliasedName, @@ -6442,7 +6468,7 @@ i1.GeneratedColumn _column_224(String aliasedName) => $customConstraints: 'NOT NULL REFERENCES contacts(user_id)ON DELETE CASCADE', ); -i1.GeneratedColumn _column_225(String aliasedName) => +i1.GeneratedColumn _column_227(String aliasedName) => i1.GeneratedColumn( 'public_key_verified_timestamp', aliasedName, @@ -6468,7 +6494,7 @@ class Shape44 extends i0.VersionedTable { as i1.GeneratedColumn; } -i1.GeneratedColumn _column_226(String aliasedName) => +i1.GeneratedColumn _column_228(String aliasedName) => i1.GeneratedColumn( 'promotion_id', aliasedName, @@ -6476,7 +6502,7 @@ i1.GeneratedColumn _column_226(String aliasedName) => type: i1.DriftSqlType.int, $customConstraints: 'NOT NULL', ); -i1.GeneratedColumn _column_227(String aliasedName) => +i1.GeneratedColumn _column_229(String aliasedName) => i1.GeneratedColumn( 'public_id', aliasedName, @@ -6484,7 +6510,7 @@ i1.GeneratedColumn _column_227(String aliasedName) => type: i1.DriftSqlType.int, $customConstraints: 'NOT NULL', ); -i1.GeneratedColumn _column_228(String aliasedName) => +i1.GeneratedColumn _column_230(String aliasedName) => i1.GeneratedColumn( 'threshold', aliasedName, @@ -6492,7 +6518,7 @@ i1.GeneratedColumn _column_228(String aliasedName) => type: i1.DriftSqlType.int, $customConstraints: 'NOT NULL', ); -i1.GeneratedColumn _column_229(String aliasedName) => +i1.GeneratedColumn _column_231(String aliasedName) => i1.GeneratedColumn( 'announcement_share', aliasedName, @@ -6511,7 +6537,7 @@ class Shape45 extends i0.VersionedTable { columnsByName['promotion']! as i1.GeneratedColumn; } -i1.GeneratedColumn _column_230(String aliasedName) => +i1.GeneratedColumn _column_232(String aliasedName) => i1.GeneratedColumn( 'version_id', aliasedName, @@ -6520,7 +6546,7 @@ i1.GeneratedColumn _column_230(String aliasedName) => type: i1.DriftSqlType.int, $customConstraints: 'NOT NULL PRIMARY KEY AUTOINCREMENT', ); -i1.GeneratedColumn _column_231(String aliasedName) => +i1.GeneratedColumn _column_233(String aliasedName) => i1.GeneratedColumn( 'promotion', aliasedName, @@ -6539,7 +6565,7 @@ class Shape46 extends i0.VersionedTable { columnsByName['contact_id']! as i1.GeneratedColumn; } -i1.GeneratedColumn _column_232(String aliasedName) => +i1.GeneratedColumn _column_234(String aliasedName) => i1.GeneratedColumn( 'share_id', aliasedName, @@ -6548,7 +6574,7 @@ i1.GeneratedColumn _column_232(String aliasedName) => type: i1.DriftSqlType.int, $customConstraints: 'NOT NULL PRIMARY KEY AUTOINCREMENT', ); -i1.GeneratedColumn _column_233(String aliasedName) => +i1.GeneratedColumn _column_235(String aliasedName) => i1.GeneratedColumn( 'share', aliasedName, diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index 0b260764..510bacff 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -7,7 +7,6 @@ import 'package:intl/intl.dart' as intl; import 'app_localizations_de.dart'; import 'app_localizations_en.dart'; -import 'app_localizations_sv.dart'; // ignore_for_file: type=lint @@ -97,7 +96,6 @@ abstract class AppLocalizations { static const List supportedLocales = [ Locale('de'), Locale('en'), - Locale('sv'), ]; /// No description provided for @registerTitle. @@ -172,36 +170,12 @@ abstract class AppLocalizations { /// **'twonly is financed by donations and an optional subscription. Your data will never be sold.'** String get onboardingNotProductBody; - /// No description provided for @onboardingBuyOneGetTwoTitle. - /// - /// In en, this message translates to: - /// **'Buy one get two'** - String get onboardingBuyOneGetTwoTitle; - - /// No description provided for @onboardingBuyOneGetTwoBody. - /// - /// In en, this message translates to: - /// **'twonly always requires at least two people, which is why you receive a second free license for your twonly partner with your purchase.'** - String get onboardingBuyOneGetTwoBody; - /// No description provided for @onboardingGetStartedTitle. /// /// In en, this message translates to: /// **'Let\'s go!'** String get onboardingGetStartedTitle; - /// No description provided for @onboardingGetStartedBody. - /// - /// In en, this message translates to: - /// **'You can test twonly free of charge in preview mode. In this mode you can be found by others and receive pictures or videos but you cannot send any yourself.'** - String get onboardingGetStartedBody; - - /// No description provided for @onboardingTryForFree. - /// - /// In en, this message translates to: - /// **'Try for free'** - String get onboardingTryForFree; - /// No description provided for @registerUsernameSlogan. /// /// In en, this message translates to: @@ -232,24 +206,6 @@ abstract class AppLocalizations { /// **'Register now!'** String get registerSubmitButton; - /// No description provided for @registerTwonlyCodeText. - /// - /// In en, this message translates to: - /// **'Have you received a twonly code? Then redeem it either directly here or later!'** - String get registerTwonlyCodeText; - - /// No description provided for @registerTwonlyCodeLabel. - /// - /// In en, this message translates to: - /// **'twonly-Code'** - String get registerTwonlyCodeLabel; - - /// No description provided for @newMessageTitle. - /// - /// In en, this message translates to: - /// **'New message'** - String get newMessageTitle; - /// No description provided for @chatsTapToSend. /// /// In en, this message translates to: @@ -334,36 +290,12 @@ abstract class AppLocalizations { /// **'New Contact'** String get startNewChatNewContact; - /// No description provided for @startNewChatYourContacts. - /// - /// In en, this message translates to: - /// **'Your Contacts'** - String get startNewChatYourContacts; - /// No description provided for @shareImageAllUsers. /// /// In en, this message translates to: /// **'All contacts'** String get shareImageAllUsers; - /// No description provided for @shareImageAllTwonlyWarning. - /// - /// In en, this message translates to: - /// **'twonlies can only be send to verified contacts!'** - String get shareImageAllTwonlyWarning; - - /// No description provided for @shareImageUserNotVerified. - /// - /// In en, this message translates to: - /// **'User is not verified'** - String get shareImageUserNotVerified; - - /// No description provided for @shareImageUserNotVerifiedDesc. - /// - /// In en, this message translates to: - /// **'twonlies can only be sent to verified users. To verify a user, go to their profile and to verify security number.'** - String get shareImageUserNotVerifiedDesc; - /// No description provided for @shareImageShowArchived. /// /// In en, this message translates to: @@ -382,18 +314,6 @@ abstract class AppLocalizations { /// **'Add friends'** String get addFriendTitle; - /// No description provided for @searchUserNamePreview. - /// - /// In en, this message translates to: - /// **'To protect you and other twonly users from spam and abuse, it is not possible to search for other people in preview mode. Other users can find you and their requests will be displayed here!'** - String get searchUserNamePreview; - - /// No description provided for @selectSubscription. - /// - /// In en, this message translates to: - /// **'Select subscription'** - String get selectSubscription; - /// No description provided for @searchUserNamePending. /// /// In en, this message translates to: @@ -418,24 +338,12 @@ abstract class AppLocalizations { /// **'Open requests'** String get searchUsernameNewFollowerTitle; - /// No description provided for @searchUsernameQrCodeBtn. - /// - /// In en, this message translates to: - /// **'Scan QR code'** - String get searchUsernameQrCodeBtn; - /// No description provided for @chatListViewSearchUserNameBtn. /// /// In en, this message translates to: /// **'Add your first twonly contact!'** String get chatListViewSearchUserNameBtn; - /// No description provided for @chatListViewSendFirstTwonly. - /// - /// In en, this message translates to: - /// **'Send your first twonly!'** - String get chatListViewSendFirstTwonly; - /// No description provided for @chatListDetailInput. /// /// In en, this message translates to: @@ -454,12 +362,6 @@ abstract class AppLocalizations { /// **'User profile'** String get contextMenuUserProfile; - /// No description provided for @contextMenuVerifyUser. - /// - /// In en, this message translates to: - /// **'Verify'** - String get contextMenuVerifyUser; - /// No description provided for @contextMenuArchiveUser. /// /// In en, this message translates to: @@ -916,24 +818,12 @@ abstract class AppLocalizations { /// **'Delete account'** String get settingsAccountDeleteAccount; - /// No description provided for @settingsAccountDeleteAccountWithBallance. - /// - /// In en, this message translates to: - /// **'In the next step, you can select what you want to to with the remaining credit ({credit}).'** - String settingsAccountDeleteAccountWithBallance(Object credit); - /// No description provided for @settingsAccountDeleteAccountNoBallance. /// /// In en, this message translates to: /// **'Once you delete your account, there is no going back.'** String get settingsAccountDeleteAccountNoBallance; - /// No description provided for @settingsAccountDeleteAccountNoInternet. - /// - /// In en, this message translates to: - /// **'An Internet connection is required to delete your account.'** - String get settingsAccountDeleteAccountNoInternet; - /// No description provided for @settingsAccountDeleteModalTitle. /// /// In en, this message translates to: @@ -952,24 +842,6 @@ abstract class AppLocalizations { /// **'Verify contact'** String get contactVerifyNumberTitle; - /// No description provided for @contactVerifyNumberTapToScan. - /// - /// In en, this message translates to: - /// **'Tap to scan'** - String get contactVerifyNumberTapToScan; - - /// No description provided for @contactVerifyNumberMarkAsVerified. - /// - /// In en, this message translates to: - /// **'Mark as verified'** - String get contactVerifyNumberMarkAsVerified; - - /// No description provided for @contactVerifyNumberClearVerification. - /// - /// In en, this message translates to: - /// **'Clear verification'** - String get contactVerifyNumberClearVerification; - /// No description provided for @userVerifiedTitle. /// /// In en, this message translates to: @@ -1012,12 +884,6 @@ abstract class AppLocalizations { /// **'Migrated from old version.'** String get verificationTypeMigratedFromOldVersion; - /// No description provided for @contactVerifyNumberLongDesc. - /// - /// In en, this message translates to: - /// **'To verify the end-to-end encryption with {username}, compare the numbers with their device. The person can also scan your code with their device.'** - String contactVerifyNumberLongDesc(Object username); - /// No description provided for @contactViewMessage. /// /// In en, this message translates to: @@ -1036,18 +902,6 @@ abstract class AppLocalizations { /// **'New nickname'** String get contactNicknameNew; - /// No description provided for @deleteAllContactMessages. - /// - /// In en, this message translates to: - /// **'Delete all text-messages'** - String get deleteAllContactMessages; - - /// No description provided for @deleteAllContactMessagesBody. - /// - /// In en, this message translates to: - /// **'This will remove all messages, except stored media files, in your chat with {username}. This will NOT delete the messages stored at {username}\'s device!'** - String deleteAllContactMessagesBody(Object username); - /// No description provided for @contactBlock. /// /// In en, this message translates to: @@ -1102,6 +956,12 @@ abstract class AppLocalizations { /// **'Next'** String get next; + /// No description provided for @finishSetup. + /// + /// In en, this message translates to: + /// **'Complete setup'** + String get finishSetup; + /// No description provided for @submit. /// /// In en, this message translates to: @@ -1126,12 +986,6 @@ abstract class AppLocalizations { /// **'Enable'** String get enable; - /// No description provided for @understood. - /// - /// In en, this message translates to: - /// **'Understood'** - String get understood; - /// No description provided for @cancel. /// /// In en, this message translates to: @@ -1246,12 +1100,6 @@ abstract class AppLocalizations { /// **'Toggle the flash light'** String get toggleFlashLight; - /// No description provided for @toggleHighQuality. - /// - /// In en, this message translates to: - /// **'Toggle better resolution'** - String get toggleHighQuality; - /// No description provided for @userFound. /// /// In en, this message translates to: @@ -1264,30 +1112,6 @@ abstract class AppLocalizations { /// **'Do you want to create a follow request?'** String get userFoundBody; - /// No description provided for @searchUsernameNotFoundLong. - /// - /// In en, this message translates to: - /// **'\"{username}\" is not a twonly user. Please check the username and try again.'** - String searchUsernameNotFoundLong(Object username); - - /// No description provided for @errorUnknown. - /// - /// In en, this message translates to: - /// **'An unexpected error has occurred. Please try again later.'** - String get errorUnknown; - - /// No description provided for @errorBadRequest. - /// - /// In en, this message translates to: - /// **'The request could not be understood by the server due to malformed syntax. Please check your input and try again.'** - String get errorBadRequest; - - /// No description provided for @errorTooManyRequests. - /// - /// In en, this message translates to: - /// **'You have made too many requests in a short period. Please wait a moment before trying again.'** - String get errorTooManyRequests; - /// No description provided for @errorInternalError. /// /// In en, this message translates to: @@ -1306,48 +1130,12 @@ abstract class AppLocalizations { /// **'The username is already taken.'** String get errorUsernameAlreadyTaken; - /// No description provided for @errorSignatureNotValid. - /// - /// In en, this message translates to: - /// **'The provided signature is not valid. Please check your credentials and try again.'** - String get errorSignatureNotValid; - - /// No description provided for @errorUsernameNotFound. - /// - /// In en, this message translates to: - /// **'The username you entered does not exist. Please check the spelling or create a new account.'** - String get errorUsernameNotFound; - /// No description provided for @errorUsernameNotValid. /// /// In en, this message translates to: /// **'The username you provided does not meet the required criteria. Please choose a valid username.'** String get errorUsernameNotValid; - /// No description provided for @errorInvalidPublicKey. - /// - /// In en, this message translates to: - /// **'The public key you provided is invalid. Please check the key and try again.'** - String get errorInvalidPublicKey; - - /// No description provided for @errorSessionAlreadyAuthenticated. - /// - /// In en, this message translates to: - /// **'You are already logged in. Please log out if you want to log in with a different account.'** - String get errorSessionAlreadyAuthenticated; - - /// No description provided for @errorSessionNotAuthenticated. - /// - /// In en, this message translates to: - /// **'Your session is not authenticated. Please log in to continue.'** - String get errorSessionNotAuthenticated; - - /// No description provided for @errorOnlyOneSessionAllowed. - /// - /// In en, this message translates to: - /// **'Only one active session is allowed per user. Please log out from other devices to continue.'** - String get errorOnlyOneSessionAllowed; - /// No description provided for @errorNotEnoughCredit. /// /// In en, this message translates to: @@ -1486,30 +1274,6 @@ abstract class AppLocalizations { /// **'✓ Additional features (coming-soon)'** String get plusFeature2; - /// No description provided for @transactionHistory. - /// - /// In en, this message translates to: - /// **'Your transaction history'** - String get transactionHistory; - - /// No description provided for @manageSubscription. - /// - /// In en, this message translates to: - /// **'Manage subscription'** - String get manageSubscription; - - /// No description provided for @nextPayment. - /// - /// In en, this message translates to: - /// **'Next payment'** - String get nextPayment; - - /// No description provided for @currentBalance. - /// - /// In en, this message translates to: - /// **'Current balance'** - String get currentBalance; - /// No description provided for @manageAdditionalUsers. /// /// In en, this message translates to: @@ -1522,234 +1286,36 @@ abstract class AppLocalizations { /// **'Open'** String get open; - /// No description provided for @createOrRedeemVoucher. - /// - /// In en, this message translates to: - /// **'Buy or redeem voucher'** - String get createOrRedeemVoucher; - /// No description provided for @createVoucher. /// /// In en, this message translates to: /// **'Buy voucher'** String get createVoucher; - /// No description provided for @createVoucherDesc. - /// - /// In en, this message translates to: - /// **'Choose the value of the voucher. The value of the voucher will be deducted from your twonly balance.'** - String get createVoucherDesc; - /// No description provided for @redeemVoucher. /// /// In en, this message translates to: /// **'Redeem voucher'** String get redeemVoucher; - /// No description provided for @openVouchers. - /// - /// In en, this message translates to: - /// **'Open vouchers'** - String get openVouchers; - - /// No description provided for @voucherCreated. - /// - /// In en, this message translates to: - /// **'Voucher created'** - String get voucherCreated; - - /// No description provided for @voucherRedeemed. - /// - /// In en, this message translates to: - /// **'Voucher redeemed'** - String get voucherRedeemed; - - /// No description provided for @enterVoucherCode. - /// - /// In en, this message translates to: - /// **'Enter Voucher Code'** - String get enterVoucherCode; - - /// No description provided for @requestedVouchers. - /// - /// In en, this message translates to: - /// **'Requested vouchers'** - String get requestedVouchers; - - /// No description provided for @redeemedVouchers. - /// - /// In en, this message translates to: - /// **'Redeemed vouchers'** - String get redeemedVouchers; - /// No description provided for @buy. /// /// In en, this message translates to: /// **'Buy'** String get buy; - /// No description provided for @subscriptionRefund. - /// - /// In en, this message translates to: - /// **'When you upgrade, you will receive a refund of {refund} for your current subscription.'** - String subscriptionRefund(Object refund); - - /// No description provided for @transactionCash. - /// - /// In en, this message translates to: - /// **'Cash transaction'** - String get transactionCash; - - /// No description provided for @transactionPlanUpgrade. - /// - /// In en, this message translates to: - /// **'Plan upgrade'** - String get transactionPlanUpgrade; - - /// No description provided for @transactionRefund. - /// - /// In en, this message translates to: - /// **'Refund transaction'** - String get transactionRefund; - - /// No description provided for @transactionThanksForTesting. - /// - /// In en, this message translates to: - /// **'Thank you for testing'** - String get transactionThanksForTesting; - - /// No description provided for @transactionUnknown. - /// - /// In en, this message translates to: - /// **'Unknown transaction'** - String get transactionUnknown; - - /// No description provided for @transactionVoucherCreated. - /// - /// In en, this message translates to: - /// **'Voucher created'** - String get transactionVoucherCreated; - - /// No description provided for @transactionVoucherRedeemed. - /// - /// In en, this message translates to: - /// **'Voucher redeemed'** - String get transactionVoucherRedeemed; - - /// No description provided for @transactionAutoRenewal. - /// - /// In en, this message translates to: - /// **'Automatic renewal'** - String get transactionAutoRenewal; - - /// No description provided for @checkoutOptions. - /// - /// In en, this message translates to: - /// **'Options'** - String get checkoutOptions; - - /// No description provided for @refund. - /// - /// In en, this message translates to: - /// **'Refund'** - String get refund; - - /// No description provided for @checkoutPayYearly. - /// - /// In en, this message translates to: - /// **'Pay yearly'** - String get checkoutPayYearly; - - /// No description provided for @checkoutTotal. - /// - /// In en, this message translates to: - /// **'Total'** - String get checkoutTotal; - - /// No description provided for @selectPaymentMethod. - /// - /// In en, this message translates to: - /// **'Select Payment Method'** - String get selectPaymentMethod; - - /// No description provided for @twonlyCredit. - /// - /// In en, this message translates to: - /// **'twonly-Credit'** - String get twonlyCredit; - - /// No description provided for @notEnoughCredit. - /// - /// In en, this message translates to: - /// **'You do not have enough credit!'** - String get notEnoughCredit; - - /// No description provided for @chargeCredit. - /// - /// In en, this message translates to: - /// **'Charge credit'** - String get chargeCredit; - /// No description provided for @autoRenewal. /// /// In en, this message translates to: /// **'Auto renewal'** String get autoRenewal; - /// No description provided for @autoRenewalDesc. - /// - /// In en, this message translates to: - /// **'You can change this at any time.'** - String get autoRenewalDesc; - - /// No description provided for @autoRenewalLongDesc. - /// - /// In en, this message translates to: - /// **'When your subscription expires, you will automatically be downgraded to the Preview plan. If you activate the automatic renewal, please make sure that you have enough credit for the automatic renewal. We will notify you in good time before the automatic renewal.'** - String get autoRenewalLongDesc; - - /// No description provided for @planSuccessUpgraded. - /// - /// In en, this message translates to: - /// **'Successfully upgraded your plan.'** - String get planSuccessUpgraded; - - /// No description provided for @checkoutSubmit. - /// - /// In en, this message translates to: - /// **'Order with a fee.'** - String get checkoutSubmit; - /// No description provided for @additionalUsersList. /// /// In en, this message translates to: /// **'Your additional users'** String get additionalUsersList; - /// No description provided for @additionalUsersPlusTokens. - /// - /// In en, this message translates to: - /// **'twonly-codes for \"Plus\" user'** - String get additionalUsersPlusTokens; - - /// No description provided for @additionalUsersFreeTokens. - /// - /// In en, this message translates to: - /// **'twonly-codes for \"Free\" user'** - String get additionalUsersFreeTokens; - - /// No description provided for @planLimitReached. - /// - /// In en, this message translates to: - /// **'You have reached your plan limit for today. Upgrade your plan now to send the media file.'** - String get planLimitReached; - - /// No description provided for @planNotAllowed. - /// - /// In en, this message translates to: - /// **'You cannot send media files with your current tariff. Upgrade your plan now to send the media file.'** - String get planNotAllowed; - /// No description provided for @galleryDelete. /// /// In en, this message translates to: @@ -1774,72 +1340,6 @@ abstract class AppLocalizations { /// **'Successfully saved in the Gallery.'** String get galleryExportSuccess; - /// No description provided for @settingsResetTutorials. - /// - /// In en, this message translates to: - /// **'Show tutorials again'** - String get settingsResetTutorials; - - /// No description provided for @settingsResetTutorialsDesc. - /// - /// In en, this message translates to: - /// **'Click here to show already displayed tutorials again.'** - String get settingsResetTutorialsDesc; - - /// No description provided for @settingsResetTutorialsSuccess. - /// - /// In en, this message translates to: - /// **'Tutorials will be displayed again.'** - String get settingsResetTutorialsSuccess; - - /// No description provided for @tutorialChatListSearchUsersTitle. - /// - /// In en, this message translates to: - /// **'Find Friends and Manage Friend Requests'** - String get tutorialChatListSearchUsersTitle; - - /// No description provided for @tutorialChatListSearchUsersDesc. - /// - /// In en, this message translates to: - /// **'If you know your friends\' usernames, you can search for them here and send a friend request. You will also see all requests from other users that you can accept or block.'** - String get tutorialChatListSearchUsersDesc; - - /// No description provided for @tutorialChatListContextMenuTitle. - /// - /// In en, this message translates to: - /// **'Long press on the contact to open the context menu.'** - String get tutorialChatListContextMenuTitle; - - /// No description provided for @tutorialChatListContextMenuDesc. - /// - /// In en, this message translates to: - /// **'With the context menu, you can pin, archive, and perform various actions on your contacts. Simply long press the contact and then move your finger to the desired option or tap directly on it.'** - String get tutorialChatListContextMenuDesc; - - /// No description provided for @tutorialChatMessagesVerifyShieldTitle. - /// - /// In en, this message translates to: - /// **'Verify your contacts!'** - String get tutorialChatMessagesVerifyShieldTitle; - - /// No description provided for @tutorialChatMessagesVerifyShieldDesc. - /// - /// In en, this message translates to: - /// **'twonly uses the Signal protocol for secure end-to-end encryption. When you first contact someone, their public identity key is downloaded. To ensure that this key has not been tampered with by third parties, you should compare it with your friend when you meet in person. Once you have verified the user, you can also enable the twonly mode when sending images and videos.'** - String get tutorialChatMessagesVerifyShieldDesc; - - /// No description provided for @tutorialChatMessagesReopenMessageTitle. - /// - /// In en, this message translates to: - /// **'Reopen Images and Videos'** - String get tutorialChatMessagesReopenMessageTitle; - - /// No description provided for @tutorialChatMessagesReopenMessageDesc. - /// - /// In en, this message translates to: - /// **'If your friend has sent you a picture or video with infinite display time, you can open it again at any time until you restart the app. To do this, simply double-click on the message. Your friend will then receive a notification that you have viewed the picture again.'** - String get tutorialChatMessagesReopenMessageDesc; - /// No description provided for @memoriesEmpty. /// /// In en, this message translates to: @@ -1882,30 +1382,6 @@ abstract class AppLocalizations { /// **'Backup'** String get settingsBackup; - /// No description provided for @backupNoticeTitle. - /// - /// In en, this message translates to: - /// **'No backup configured'** - String get backupNoticeTitle; - - /// No description provided for @backupNoticeDesc. - /// - /// In en, this message translates to: - /// **'If you change or lose your device, no one can restore your account without a backup. Therefore, back up your data.'** - String get backupNoticeDesc; - - /// No description provided for @backupNoticeLater. - /// - /// In en, this message translates to: - /// **'Remind later'** - String get backupNoticeLater; - - /// No description provided for @backupNoticeOpenBackup. - /// - /// In en, this message translates to: - /// **'Create backup'** - String get backupNoticeOpenBackup; - /// No description provided for @backupPending. /// /// In en, this message translates to: @@ -1972,30 +1448,12 @@ abstract class AppLocalizations { /// **'Result'** String get backupLastBackupResult; - /// No description provided for @deleteBackupTitle. - /// - /// In en, this message translates to: - /// **'Are you sure?'** - String get deleteBackupTitle; - - /// No description provided for @deleteBackupBody. - /// - /// In en, this message translates to: - /// **'Without an backup, you can not restore your user account.'** - String get deleteBackupBody; - /// No description provided for @backupData. /// /// In en, this message translates to: /// **'Data-Backup'** String get backupData; - /// No description provided for @backupDataDesc. - /// - /// In en, this message translates to: - /// **'This backup contains besides of your twonly-Identity also all of your media files. This backup will is also encrypted but stored locally. You then have to ensure to manually copy it onto your laptop or device of your choice.'** - String get backupDataDesc; - /// No description provided for @backupInsecurePassword. /// /// In en, this message translates to: @@ -2170,12 +1628,6 @@ abstract class AppLocalizations { /// **'Retransmission requested'** String get retransmissionRequested; - /// No description provided for @testPaymentMethod. - /// - /// In en, this message translates to: - /// **'Thanks for the interest in a paid plan. Currently the paid plans are still deactivated. But they will be activated soon!'** - String get testPaymentMethod; - /// No description provided for @openChangeLog. /// /// In en, this message translates to: @@ -2884,6 +2336,24 @@ abstract class AppLocalizations { /// **'Skip for now'** String get skipForNow; + /// No description provided for @finishSetupCardTitle. + /// + /// In en, this message translates to: + /// **'Complete your profile'** + String get finishSetupCardTitle; + + /// No description provided for @finishSetupCardDesc. + /// + /// In en, this message translates to: + /// **'You are almost there! Finish setting up your account to get the most out of twonly.'** + String get finishSetupCardDesc; + + /// No description provided for @finishSetupCardAction. + /// + /// In en, this message translates to: + /// **'Resume Setup'** + String get finishSetupCardAction; + /// No description provided for @onboardingFinishLater. /// /// In en, this message translates to: @@ -2902,12 +2372,6 @@ abstract class AppLocalizations { /// **'Select an avatar and a display name that friends will see.'** String get onboardingProfileBody; - /// No description provided for @onboardingBackupTitle. - /// - /// In en, this message translates to: - /// **'Backup Setup'** - String get onboardingBackupTitle; - /// No description provided for @onboardingBackupBody. /// /// In en, this message translates to: @@ -2920,17 +2384,119 @@ abstract class AppLocalizations { /// **'Verification Badge'** String get onboardingVerificationBadgeTitle; - /// No description provided for @onboardingUserDiscoveryTitle. + /// No description provided for @onboardingUserDiscoveryShareFriends. /// /// In en, this message translates to: - /// **'User Discovery'** - String get onboardingUserDiscoveryTitle; + /// **'Share your friends'** + String get onboardingUserDiscoveryShareFriends; - /// No description provided for @onboardingResetSetup. + /// No description provided for @onboardingUserDiscoveryIncreaseTrust. /// /// In en, this message translates to: - /// **'Reset Setup'** - String get onboardingResetSetup; + /// **'Increase trust'** + String get onboardingUserDiscoveryIncreaseTrust; + + /// No description provided for @onboardingUserDiscoveryShareFriendsDesc. + /// + /// In en, this message translates to: + /// **'Share with your friends who you know and who you have verified. Friends can *only see mutual friends* from your friend list.'** + String get onboardingUserDiscoveryShareFriendsDesc; + + /// No description provided for @onboardingUserDiscoveryContactsVerifiedBadge. + /// + /// In en, this message translates to: + /// **'Contacts verified by your friends get a badge'** + String get onboardingUserDiscoveryContactsVerifiedBadge; + + /// No description provided for @onboardingUserDiscoveryWhoIsRequesting. + /// + /// In en, this message translates to: + /// **'Be informed about who is requesting'** + String get onboardingUserDiscoveryWhoIsRequesting; + + /// No description provided for @userDiscoverySettingsEnableAllContacts. + /// + /// In en, this message translates to: + /// **'Enabled for all contacts'** + String get userDiscoverySettingsEnableAllContacts; + + /// No description provided for @userDiscoverySettingsManualApproval. + /// + /// In en, this message translates to: + /// **'Manual approval'** + String get userDiscoverySettingsManualApproval; + + /// No description provided for @userDiscoverySettingsManualApprovalDesc. + /// + /// In en, this message translates to: + /// **'Before sharing someone, you will be asked every time someone reaches the number of send images.'** + String get userDiscoverySettingsManualApprovalDesc; + + /// No description provided for @onboardingUserDiscoveryLetFriendsFindYou. + /// + /// In en, this message translates to: + /// **'Let your friends find you'** + String get onboardingUserDiscoveryLetFriendsFindYou; + + /// No description provided for @onboardingUserDiscoveryLetFriendsFindYouDesc. + /// + /// In en, this message translates to: + /// **'To help your friends find you, *you can be suggested* to people with whom you have *mutual friends*.'** + String get onboardingUserDiscoveryLetFriendsFindYouDesc; + + /// No description provided for @onboardingUserDiscoveryBeRecommended. + /// + /// In en, this message translates to: + /// **'Be recommended to others'** + String get onboardingUserDiscoveryBeRecommended; + + /// No description provided for @onboardingUserDiscoveryWhatOthersSee. + /// + /// In en, this message translates to: + /// **'What others will see'** + String get onboardingUserDiscoveryWhatOthersSee; + + /// No description provided for @onboardingUserDiscoveryWhatYouSee. + /// + /// In en, this message translates to: + /// **'If requested, that\'s what you will see'** + String get onboardingUserDiscoveryWhatYouSee; + + /// No description provided for @onboardingAddContactsTitle. + /// + /// In en, this message translates to: + /// **'Add new contacts'** + String get onboardingAddContactsTitle; + + /// No description provided for @onboardingAddContactsAcceptDesc. + /// + /// In en, this message translates to: + /// **'In twonly, every contact must first be accepted before you can communicate.'** + String get onboardingAddContactsAcceptDesc; + + /// No description provided for @onboardingAddContactsMethodHeading. + /// + /// In en, this message translates to: + /// **'Add contacts'** + String get onboardingAddContactsMethodHeading; + + /// No description provided for @onboardingAddContactsMethodScan. + /// + /// In en, this message translates to: + /// **'Scan the contact\'s QR code.'** + String get onboardingAddContactsMethodScan; + + /// No description provided for @onboardingAddContactsMethodSearch. + /// + /// In en, this message translates to: + /// **'Search for the username.'** + String get onboardingAddContactsMethodSearch; + + /// No description provided for @onboardingAddContactsMethodShare. + /// + /// In en, this message translates to: + /// **'Share a contact in chats.'** + String get onboardingAddContactsMethodShare; /// No description provided for @linkFromUsername. /// @@ -3241,7 +2807,7 @@ abstract class AppLocalizations { /// No description provided for @userDiscoverySettingsMinImages. /// /// In en, this message translates to: - /// **'Choose the minimum number of images you must have exchanged with a person before you securely share your friends with them.'** + /// **'Choose the minimum number of images you must have send to a person before you securely share your friends with them.'** String get userDiscoverySettingsMinImages; /// No description provided for @userDiscoverySettingsMutualFriends. @@ -3259,7 +2825,7 @@ abstract class AppLocalizations { /// No description provided for @userDiscoveryEnabledDisableWarning. /// /// In en, this message translates to: - /// **'If you disable the \"Find friends\" feature, you will no longer see suggestions. You will also stop sharing your friends with new contacts.'** + /// **'If you disable the \"Share your friends\" feature, you will no longer see suggestions. You will also stop sharing your friends with new contacts.'** String get userDiscoveryEnabledDisableWarning; /// No description provided for @userDiscoveryEnabledChangeSettings. @@ -3271,37 +2837,31 @@ abstract class AppLocalizations { /// No description provided for @userDiscoveryEnabledFaq. /// /// In en, this message translates to: - /// **'In our FAQ we explain how the \"Find friends\" feature works.'** + /// **'In our FAQ we explain how the \"Share your friends\" feature works.'** String get userDiscoveryEnabledFaq; /// No description provided for @userDiscoveryDisabledIntro. /// /// In en, this message translates to: - /// **'twonly doesn\'t use phone numbers, so we suggest friends based on mutual contacts instead – securely and privately.'** + /// **'twonly does *not* collect your phone number or needs access to your contacts. Instead, twonly can *find your friends through mutual friends*.'** String get userDiscoveryDisabledIntro; - /// No description provided for @userDiscoveryDisabledInvisible. - /// - /// In en, this message translates to: - /// **'Your friend list is *completely invisible to strangers*. Only your friends can see parts of it – and only those people with whom they have *mutual friends* themselves.'** - String get userDiscoveryDisabledInvisible; - /// No description provided for @userDiscoveryDisabledDecide. /// /// In en, this message translates to: - /// **'Decide for yourself who can see your friends. You can change your mind at any time or hide specific people.'** + /// **'Decide for yourself who can see your friends. You can change your mind at *any time* or *hide specific people*.'** String get userDiscoveryDisabledDecide; /// No description provided for @userDiscoverySettingsTitle. /// /// In en, this message translates to: - /// **'Find friends'** + /// **'Share your friends'** String get userDiscoverySettingsTitle; /// No description provided for @userDiscoverySettingsMinImagesTitle. /// /// In en, this message translates to: - /// **'Number of shared images'** + /// **'Number of images send'** String get userDiscoverySettingsMinImagesTitle; /// No description provided for @userDiscoverySettingsMutualFriendsTitle. @@ -3316,18 +2876,6 @@ abstract class AppLocalizations { /// **'You are in control'** String get userDiscoveryDisabledYouHaveControl; - /// No description provided for @userDiscoveryDisabledEnableWithDefault. - /// - /// In en, this message translates to: - /// **'Enable with default settings'** - String get userDiscoveryDisabledEnableWithDefault; - - /// No description provided for @userDiscoveryDisabledCustomizeSettings. - /// - /// In en, this message translates to: - /// **'Customize settings'** - String get userDiscoveryDisabledCustomizeSettings; - /// No description provided for @userDiscoveryDisabledLearnMore. /// /// In en, this message translates to: @@ -3352,6 +2900,12 @@ abstract class AppLocalizations { /// **'You only share friends who have also activated this feature and who have reached the threshold you set.'** String get userDiscoveryEnabledFriendsSharedDesc; + /// No description provided for @userDiscoverySettingsCurrentlyDisabled. + /// + /// In en, this message translates to: + /// **'The feature \"Share your friends\" is currently disabled.'** + String get userDiscoverySettingsCurrentlyDisabled; + /// No description provided for @userDiscoveryEnabledNoFriendsShared. /// /// In en, this message translates to: @@ -3430,7 +2984,7 @@ class _AppLocalizationsDelegate @override bool isSupported(Locale locale) => - ['de', 'en', 'sv'].contains(locale.languageCode); + ['de', 'en'].contains(locale.languageCode); @override bool shouldReload(_AppLocalizationsDelegate old) => false; @@ -3443,8 +2997,6 @@ AppLocalizations lookupAppLocalizations(Locale locale) { return AppLocalizationsDe(); case 'en': return AppLocalizationsEn(); - case 'sv': - return AppLocalizationsSv(); } throw FlutterError( diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index c5dfa2e6..c7a4938a 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -51,23 +51,9 @@ class AppLocalizationsDe extends AppLocalizations { String get onboardingNotProductBody => 'twonly wird durch Spenden und ein optionales Abonnement finanziert. Deine Daten werden niemals verkauft.'; - @override - String get onboardingBuyOneGetTwoTitle => 'Kaufe eins, bekomme zwei'; - - @override - String get onboardingBuyOneGetTwoBody => - 'twonly benötigt immer mindestens zwei Personen, daher erhältst du beim Kauf eine zweite kostenlose Lizenz für deinen twonly-Partner.'; - @override String get onboardingGetStartedTitle => 'Auf geht\'s'; - @override - String get onboardingGetStartedBody => - 'Du kannst twonly kostenlos im Preview-Modus testen. In diesem Modus kannst du von anderen gefunden werden und Bilder oder Videos empfangen, aber du kannst selbst keine senden.'; - - @override - String get onboardingTryForFree => 'Jetzt registrieren'; - @override String get registerUsernameSlogan => 'Bitte wähle einen Benutzernamen, damit dich andere finden können!'; @@ -86,16 +72,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get registerSubmitButton => 'Jetzt registrieren!'; - @override - String get registerTwonlyCodeText => - 'Hast du einen twonly-Code erhalten? Dann löse ihn entweder direkt hier oder später ein!'; - - @override - String get registerTwonlyCodeLabel => 'twonly-Code'; - - @override - String get newMessageTitle => 'Neue Nachricht'; - @override String get chatsTapToSend => 'Klicke, um dein erstes Bild zu teilen.'; @@ -138,23 +114,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get startNewChatNewContact => 'Neuer Kontakt'; - @override - String get startNewChatYourContacts => 'Deine Kontakte'; - @override String get shareImageAllUsers => 'Alle Kontakte'; - @override - String get shareImageAllTwonlyWarning => - 'twonlies können nur an verifizierte Kontakte gesendet werden!'; - - @override - String get shareImageUserNotVerified => 'Benutzer ist nicht verifiziert'; - - @override - String get shareImageUserNotVerifiedDesc => - 'twonlies können nur an verifizierte Nutzer gesendet werden. Um einen Nutzer zu verifizieren, gehe auf deren Profil und auf „Sicherheitsnummer verifizieren“.'; - @override String get shareImageShowArchived => 'Archivierte Benutzer anzeigen'; @@ -164,13 +126,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get addFriendTitle => 'Freunde hinzufügen'; - @override - String get searchUserNamePreview => - 'Um dich und andere twonly Benutzer vor Spam und Missbrauch zu schützen, ist es nicht möglich, im Preview-Modus nach anderen Personen zu suchen. Andere Benutzer können dich finden und deren Anfragen werden dann hier angezeigt!'; - - @override - String get selectSubscription => 'Abo auswählen'; - @override String get searchUserNamePending => 'Anfrage ausstehend'; @@ -185,16 +140,10 @@ class AppLocalizationsDe extends AppLocalizations { @override String get searchUsernameNewFollowerTitle => 'Offene Anfragen'; - @override - String get searchUsernameQrCodeBtn => 'QR-Code scannen'; - @override String get chatListViewSearchUserNameBtn => 'Füge deinen ersten twonly-Kontakt hinzu!'; - @override - String get chatListViewSendFirstTwonly => 'Sende dein erstes twonly!'; - @override String get chatListDetailInput => 'Nachricht eingeben'; @@ -204,9 +153,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get contextMenuUserProfile => 'Userprofil'; - @override - String get contextMenuVerifyUser => 'Verifizieren'; - @override String get contextMenuArchiveUser => 'Archivieren'; @@ -450,19 +396,10 @@ class AppLocalizationsDe extends AppLocalizations { @override String get settingsAccountDeleteAccount => 'Konto löschen'; - @override - String settingsAccountDeleteAccountWithBallance(Object credit) { - return 'Im nächsten Schritt kannst du auswählen, was du mit dem Restguthaben ($credit) machen willst.'; - } - @override String get settingsAccountDeleteAccountNoBallance => 'Wenn du dein Konto gelöscht hast, gibt es keinen Weg zurück.'; - @override - String get settingsAccountDeleteAccountNoInternet => - 'Zum Löschen deines Accounts ist eine Internetverbindung erforderlich.'; - @override String get settingsAccountDeleteModalTitle => 'Bist du sicher?'; @@ -473,15 +410,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get contactVerifyNumberTitle => 'Benutzer verifizieren'; - @override - String get contactVerifyNumberTapToScan => 'Zum Scannen tippen'; - - @override - String get contactVerifyNumberMarkAsVerified => 'Als verifiziert markieren'; - - @override - String get contactVerifyNumberClearVerification => 'Verifizierung aufheben'; - @override String get userVerifiedTitle => 'Benutzer verifiziert'; @@ -508,11 +436,6 @@ class AppLocalizationsDe extends AppLocalizations { String get verificationTypeMigratedFromOldVersion => 'Von alter Version migriert'; - @override - String contactVerifyNumberLongDesc(Object username) { - return 'Um die Ende-zu-Ende-Verschlüsselung mit $username zu verifizieren, vergleiche die Zahlen mit deren Gerät. Die Person kann auch deinen Code mit deren Gerät scannen.'; - } - @override String get contactViewMessage => 'Nachricht'; @@ -522,14 +445,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get contactNicknameNew => 'Neuer Spitzname'; - @override - String get deleteAllContactMessages => 'Textnachrichten löschen'; - - @override - String deleteAllContactMessagesBody(Object username) { - return 'Dadurch werden alle Nachrichten, ausgenommen gespeicherte Mediendateien, in deinem Chat mit $username gelöscht. Dies löscht NICHT die auf dem Gerät von $username gespeicherten Nachrichten!'; - } - @override String get contactBlock => 'Blockieren'; @@ -563,6 +478,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get next => 'Weiter'; + @override + String get finishSetup => 'Setup abschließen'; + @override String get submit => 'Abschicken'; @@ -575,9 +493,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get enable => 'Aktivieren'; - @override - String get understood => 'Verstanden'; - @override String get cancel => 'Abbrechen'; @@ -636,9 +551,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get toggleFlashLight => 'Taschenlampe umschalten'; - @override - String get toggleHighQuality => 'Bessere Auflösung umschalten'; - @override String userFound(Object username) { return '$username gefunden'; @@ -647,23 +559,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get userFoundBody => 'Möchtest du eine Folgeanfrage stellen?'; - @override - String searchUsernameNotFoundLong(Object username) { - return '\"$username\" ist kein twonly-Benutzer. Bitte überprüfe den Benutzernamen und versuche es erneut.'; - } - - @override - String get errorUnknown => - 'Ein unerwarteter Fehler ist aufgetreten. Bitte versuche es später erneut.'; - - @override - String get errorBadRequest => - 'Die Anfrage konnte vom Server aufgrund einer fehlerhaften Syntax nicht verstanden werden. Bitte überprüfe deine Eingabe und versuche es erneut.'; - - @override - String get errorTooManyRequests => - 'Du hast in kurzer Zeit zu viele Anfragen gestellt. Bitte warte einen Moment, bevor du es erneut versuchst.'; - @override String get errorInternalError => 'Der Server ist derzeit nicht verfügbar. Bitte versuche es später erneut.'; @@ -676,34 +571,10 @@ class AppLocalizationsDe extends AppLocalizations { String get errorUsernameAlreadyTaken => 'Der Benutzername ist bereits vergeben.'; - @override - String get errorSignatureNotValid => - 'Die bereitgestellte Signatur ist nicht gültig. Bitte überprüfe deine Anmeldeinformationen und versuche es erneut.'; - - @override - String get errorUsernameNotFound => - 'Der eingegebene Benutzername existiert nicht. Bitte überprüfe die Schreibweise oder erstelle ein neues Konto.'; - @override String get errorUsernameNotValid => 'Der von dir angegebene Benutzername entspricht nicht den erforderlichen Kriterien. Bitte wähle einen gültigen Benutzernamen.'; - @override - String get errorInvalidPublicKey => - 'Der von dir angegebene öffentliche Schlüssel ist ungültig. Bitte überprüfe den Schlüssel und versuche es erneut.'; - - @override - String get errorSessionAlreadyAuthenticated => - 'Du bist bereits angemeldet. Bitte melde dich ab, wenn du dich mit einem anderen Konto anmelden möchtest.'; - - @override - String get errorSessionNotAuthenticated => - 'Deine Sitzung ist nicht authentifiziert. Bitte melde dich an, um fortzufahren.'; - - @override - String get errorOnlyOneSessionAllowed => - 'Es ist nur eine aktive Sitzung pro Benutzer erlaubt. Bitte melde dich von anderen Geräten ab, um fortzufahren.'; - @override String get errorNotEnoughCredit => 'Du hast nicht genügend twonly-Guthaben.'; @@ -781,144 +652,27 @@ class AppLocalizationsDe extends AppLocalizations { @override String get plusFeature2 => '✓ Zusatzfunktionen (coming-soon)'; - @override - String get transactionHistory => 'Transaktionshistorie'; - - @override - String get manageSubscription => 'Abonnement verwalten'; - - @override - String get nextPayment => 'Nächste Zahlung'; - - @override - String get currentBalance => 'Dein Guthaben'; - @override String get manageAdditionalUsers => 'Zusätzliche Benutzer verwalten'; @override String get open => 'Offene'; - @override - String get createOrRedeemVoucher => 'Gutschein erstellen oder einlösen'; - @override String get createVoucher => 'Gutschein kaufen'; - @override - String get createVoucherDesc => - 'Wähle den Wert des Gutscheins. Der Wert des Gutschein wird von deinem twonly-Guthaben abgezogen.'; - @override String get redeemVoucher => 'Gutschein einlösen'; - @override - String get openVouchers => 'Offene Gutscheine'; - - @override - String get voucherCreated => 'Gutschein wurde erstellt'; - - @override - String get voucherRedeemed => 'Gutschein eingelöst'; - - @override - String get enterVoucherCode => 'Gutschein Code eingeben'; - - @override - String get requestedVouchers => 'Beantragte Gutscheine'; - - @override - String get redeemedVouchers => 'Eingelöste Gutscheine'; - @override String get buy => 'Kaufen'; - @override - String subscriptionRefund(Object refund) { - return 'Wenn du ein Upgrade durchführst, erhältst du eine Rückerstattung von $refund für dein aktuelles Abonnement.'; - } - - @override - String get transactionCash => 'Bargeldtransaktion'; - - @override - String get transactionPlanUpgrade => 'Planupgrade'; - - @override - String get transactionRefund => 'Rückerstattung'; - - @override - String get transactionThanksForTesting => 'Danke fürs Testen'; - - @override - String get transactionUnknown => 'Unbekannte Transaktion'; - - @override - String get transactionVoucherCreated => 'Gutschein erstellt'; - - @override - String get transactionVoucherRedeemed => 'Gutschein eingelöst'; - - @override - String get transactionAutoRenewal => 'Automatische Verlängerung'; - - @override - String get checkoutOptions => 'Optionen'; - - @override - String get refund => 'Rückerstattung'; - - @override - String get checkoutPayYearly => 'Jährlich bezahlen'; - - @override - String get checkoutTotal => 'Gesamt'; - - @override - String get selectPaymentMethod => 'Zahlungsmethode auswählen'; - - @override - String get twonlyCredit => 'twonly-Guthaben'; - - @override - String get notEnoughCredit => 'Du hast nicht genügend Guthaben!'; - - @override - String get chargeCredit => 'Guthaben aufladen'; - @override String get autoRenewal => 'Automatische Verlängerung'; - @override - String get autoRenewalDesc => 'Du kannst dies jederzeit ändern.'; - - @override - String get autoRenewalLongDesc => - 'Wenn dein Abonnement ausläuft, wirst du automatisch auf den Preview-Plan zurückgestuft. Wenn du die automatische Verlängerung aktivierst, vergewissere dich bitte, dass du über genügend Guthaben für die automatische Erneuerung verfügst. Wir werden dich rechtzeitig vor der automatischen Erneuerung benachrichtigen.'; - - @override - String get planSuccessUpgraded => 'Dein Plan wurde erfolgreich aktualisiert.'; - - @override - String get checkoutSubmit => 'Kostenpflichtig bestellen'; - @override String get additionalUsersList => 'Deine zusätzlichen Benutzer'; - @override - String get additionalUsersPlusTokens => 'twonly-Codes für \"Plus\"-Benutzer'; - - @override - String get additionalUsersFreeTokens => 'twonly-Codes für \"Free\"-Benutzer'; - - @override - String get planLimitReached => - 'Du hast dein Planlimit für heute erreicht. Aktualisiere deinen Plan jetzt, um die Mediendatei zu senden.'; - - @override - String get planNotAllowed => - 'In deinem aktuellen Plan kannst du keine Mediendateien versenden. Aktualisiere deinen Plan jetzt, um die Mediendatei zu senden.'; - @override String get galleryDelete => 'Datei löschen'; @@ -931,49 +685,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get galleryExportSuccess => 'Erfolgreich in der Gallery gespeichert.'; - @override - String get settingsResetTutorials => 'Tutorials erneut anzeigen'; - - @override - String get settingsResetTutorialsDesc => - 'Klicke hier, um bereits angezeigte Tutorials erneut anzuzeigen.'; - - @override - String get settingsResetTutorialsSuccess => - 'Tutorials werden erneut angezeigt.'; - - @override - String get tutorialChatListSearchUsersTitle => - 'Freunde finden und Freundschaftsanfragen verwalten'; - - @override - String get tutorialChatListSearchUsersDesc => - 'Wenn du die Benutzernamen deiner Freunde kennst, kannst du sie hier suchen und eine Freundschaftsanfrage senden. Außerdem siehst du hier alle Anfragen von anderen Nutzern, die du annehmen oder blockieren kannst.'; - - @override - String get tutorialChatListContextMenuTitle => - 'Klicke lange auf den Kontakt, um das Kontextmenü zu öffnen.'; - - @override - String get tutorialChatListContextMenuDesc => - 'Mit dem Kontextmenü kannst du deine Kontakte anheften, archivieren und verschiedene Aktionen durchführen. Halte dazu einfach den Kontakt lange gedrückt und bewege dann deinen Finger auf die gewünschte Option oder tippe direkt darauf.'; - - @override - String get tutorialChatMessagesVerifyShieldTitle => - 'Verifiziere deine Kontakte!'; - - @override - String get tutorialChatMessagesVerifyShieldDesc => - 'twonly nutzt das Signal-Protokoll für eine sichere Ende-zu-Ende Verschlüsselung. Bei der ersten Kontaktaufnahme wird dafür der öffentliche Identitätsschlüssel von deinem Kontakt heruntergeladen. Um sicherzustellen, dass dieser Schlüssel nicht von Dritten ausgetauscht wurde, solltest du ihn mit deinem Freund vergleichen, wenn ihr euch persönlich trefft. Sobald du den Benutzer verifiziert hast, kannst du auch beim verschicken von Bildern und Videos den twonly-Modus aktivieren.'; - - @override - String get tutorialChatMessagesReopenMessageTitle => - 'Bilder und Videos erneut öffnen'; - - @override - String get tutorialChatMessagesReopenMessageDesc => - 'Wenn dein Freund dir ein Bild oder Video mit unendlicher Anzeigezeit gesendet hat, kannst du es bis zum Neustart der App jederzeit erneut öffnen. Um dies zu tun, musst du einfach doppelt auf die Nachricht klicken. Dein Freund erhält dann eine Benachrichtigung, dass du das Bild erneut angesehen hast.'; - @override String get memoriesEmpty => 'Sobald du Bilder oder Videos speicherst, landen sie hier in deinen Erinnerungen.'; @@ -996,19 +707,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get settingsBackup => 'Backup'; - @override - String get backupNoticeTitle => 'Kein Backup konfiguriert'; - - @override - String get backupNoticeDesc => - 'Wenn du dein Gerät wechselst oder verlierst, kann ohne Backup niemand dein Account wiederherstellen. Sichere deshalb deine Daten.'; - - @override - String get backupNoticeLater => 'Später erinnern'; - - @override - String get backupNoticeOpenBackup => 'Backup erstellen'; - @override String get backupPending => 'Ausstehend'; @@ -1044,20 +742,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get backupLastBackupResult => 'Ergebnis'; - @override - String get deleteBackupTitle => 'Bist du sicher?'; - - @override - String get deleteBackupBody => - 'Ohne ein Backup kannst du dein Benutzerkonto nicht wiederherstellen.'; - @override String get backupData => 'Daten-Backup'; - @override - String get backupDataDesc => - 'Das Daten-Backup enthält neben deiner twonly-Identität auch alle deine Mediendateien. Dieses Backup ist ebenfalls verschlüsselt, wird jedoch lokal gespeichert. Du musst es dann manuell auf deinen Laptop oder ein Gerät deiner Wahl kopieren.'; - @override String get backupInsecurePassword => 'Unsicheres Passwort'; @@ -1154,10 +841,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get retransmissionRequested => 'Wird erneut versucht.'; - @override - String get testPaymentMethod => - 'Vielen Dank für dein Interesse an einem kostenpflichtigen Tarif. Die kostenpflichtigen Pläne sind derzeit noch deaktiviert. Sie werden aber bald aktiviert!'; - @override String get openChangeLog => 'Changelog automatisch öffnen'; @@ -1591,6 +1274,16 @@ class AppLocalizationsDe extends AppLocalizations { @override String get skipForNow => 'Vorerst überspringen'; + @override + String get finishSetupCardTitle => 'Profil vervollständigen'; + + @override + String get finishSetupCardDesc => + 'Du hast es fast geschafft! Schließe die Einrichtung deines Kontos ab, um twonly optimal zu nutzen.'; + + @override + String get finishSetupCardAction => 'Setup fortsetzen'; + @override String get onboardingFinishLater => 'Später abschließen'; @@ -1601,21 +1294,82 @@ class AppLocalizationsDe extends AppLocalizations { String get onboardingProfileBody => 'Wähle einen Avatar und einen Anzeigenamen, den deine Freunde sehen werden.'; - @override - String get onboardingBackupTitle => 'Backup einrichten'; - @override String get onboardingBackupBody => 'Sichere deine twonly-Identität, da dies die einzige Möglichkeit ist, dein Konto wiederherzustellen, wenn du die App deinstallierst oder dein Handy verlierst.'; @override - String get onboardingVerificationBadgeTitle => 'Verifizierungs-Abzeichen'; + String get onboardingVerificationBadgeTitle => 'Verifizierungs-Haken'; @override - String get onboardingUserDiscoveryTitle => 'Freunde finden'; + String get onboardingUserDiscoveryShareFriends => 'Freunde teilen'; @override - String get onboardingResetSetup => 'Setup zurücksetzen'; + String get onboardingUserDiscoveryIncreaseTrust => 'Erhöhe Vertrauen'; + + @override + String get onboardingUserDiscoveryShareFriendsDesc => + 'Teile deinen Freunden mit, wen du kennst und wen du verifiziert hast. Freunde können von deiner Freundesliste *nur gemeinsame Freunde sehen*.'; + + @override + String get onboardingUserDiscoveryContactsVerifiedBadge => + 'Von Freunden verifizierte Kontakte erhalten einen Haken'; + + @override + String get onboardingUserDiscoveryWhoIsRequesting => + 'Erfahre, wer dich anfragt'; + + @override + String get userDiscoverySettingsEnableAllContacts => + 'Für alle Kontakte aktivieren'; + + @override + String get userDiscoverySettingsManualApproval => 'Manuelle Zustimmung'; + + @override + String get userDiscoverySettingsManualApprovalDesc => + 'Bevor du jemanden teilst, wirst du jedes Mal gefragt, sobald jemand die Anzahl der gesendeten Bilder erreicht hat.'; + + @override + String get onboardingUserDiscoveryLetFriendsFindYou => + 'Lass dich von deinen Freunden finden'; + + @override + String get onboardingUserDiscoveryLetFriendsFindYouDesc => + 'Damit deine Freunde dich finden können, kannst du Personen vorgeschlagen werden, die gemeinsame Freunde mit dir haben.'; + + @override + String get onboardingUserDiscoveryBeRecommended => + 'Anderen vorgeschlagen werden'; + + @override + String get onboardingUserDiscoveryWhatOthersSee => 'Was andere sehen werden'; + + @override + String get onboardingUserDiscoveryWhatYouSee => + 'Wenn du angefragt wirst, wirst du das sehen'; + + @override + String get onboardingAddContactsTitle => 'Neue Kontakte hinzufügen'; + + @override + String get onboardingAddContactsAcceptDesc => + 'In twonly muss jeder Kontakt zuerst akzeptiert werden, bevor ihr kommunizieren könnt.'; + + @override + String get onboardingAddContactsMethodHeading => 'Kontakte hinzufügen'; + + @override + String get onboardingAddContactsMethodScan => + 'Den QR-Code des Kontaktes scannen.'; + + @override + String get onboardingAddContactsMethodSearch => + 'Nach dem Benutzernamen suchen.'; + + @override + String get onboardingAddContactsMethodShare => + 'Einen Kontakt in den Chats teilen.'; @override String linkFromUsername(Object username) { @@ -1816,7 +1570,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get userDiscoverySettingsMinImages => - 'Wähle die Mindestanzahl an Bildern, die du mit einer Person ausgetauscht haben musst, bevor du ihr deine Freunde sicher teilst.'; + 'Wähle die Mindestanzahl an Bildern, die du an eine Person gesendet haben musst, bevor du ihr deine Freunde sicher teilst.'; @override String get userDiscoverySettingsMutualFriends => @@ -1827,33 +1581,29 @@ class AppLocalizationsDe extends AppLocalizations { @override String get userDiscoveryEnabledDisableWarning => - 'Wenn du das Feature „Freunde finden“ deaktivierst, werden dir keine Vorschläge mehr angezeigt. Du teilst neuen Kontakten dann auch nicht mehr deine Freunde.'; + 'Wenn du das Feature „Freunde teilen“ deaktivierst, werden dir keine Vorschläge mehr angezeigt. Du teilst neuen Kontakten dann auch nicht mehr deine Freunde.'; @override String get userDiscoveryEnabledChangeSettings => 'Einstellungen ändern'; @override String get userDiscoveryEnabledFaq => - 'In unserem FAQ erklären wir dir wie das Feature \"Freunde finden\" funktioniert.'; + 'In unserem FAQ erklären wir dir wie das Feature \"Freunde teilen\" funktioniert.'; @override String get userDiscoveryDisabledIntro => - 'twonly verzichten auf Telefonnummern, daher schlagen wir dir Freunde stattdessen über gemeinsame Kontakte vor – sicher und privat.'; - - @override - String get userDiscoveryDisabledInvisible => - 'Deine Freundesliste ist für *Fremde komplett unsichtbar*. Nur deine Freunde können Teile davon sehen – und zwar nur die Personen, mit denen sie selbst *gemeinsame Freunde* haben.'; + 'twonly kann *ohne Telefonnummer* oder den Zugriff auf dein Adressbuch verwendet werden. Stattdessen kannst du *deine Freunde über gemeinsame Freunde* finden.'; @override String get userDiscoveryDisabledDecide => - 'Entscheide selbst, wer deine Freunde sehen darf. Du kannst deine Meinung jederzeit ändern oder bestimmte Personen verstecken.'; + 'Entscheide selbst, wer deine Freunde sehen darf. Du kannst deine Meinung *jederzeit ändern* oder *bestimmte Personen verstecken*.'; @override - String get userDiscoverySettingsTitle => 'Freunde finden'; + String get userDiscoverySettingsTitle => 'Freunde teilen'; @override String get userDiscoverySettingsMinImagesTitle => - 'Anzahl an geteilten Bildern'; + 'Anzahl an gesendeten Bildern'; @override String get userDiscoverySettingsMutualFriendsTitle => @@ -1862,13 +1612,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get userDiscoveryDisabledYouHaveControl => 'Du hast die Kontrolle'; - @override - String get userDiscoveryDisabledEnableWithDefault => - 'Mit Standardeinstellungen aktivieren'; - - @override - String get userDiscoveryDisabledCustomizeSettings => 'Einstellungen anpassen'; - @override String get userDiscoveryDisabledLearnMore => 'Mehr erfahren'; @@ -1882,6 +1625,10 @@ class AppLocalizationsDe extends AppLocalizations { String get userDiscoveryEnabledFriendsSharedDesc => 'Du teilst nur Freunde, die diese Funktion ebenfalls aktiviert haben und die den von dir festgelegten Schwellenwert erreicht haben.'; + @override + String get userDiscoverySettingsCurrentlyDisabled => + 'Das Feature \"Freunde teilen\" ist derzeit deaktiviert.'; + @override String get userDiscoveryEnabledNoFriendsShared => 'Bisher teilst du noch niemanden.'; diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 4ad9d711..1d44059e 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -50,23 +50,9 @@ class AppLocalizationsEn extends AppLocalizations { String get onboardingNotProductBody => 'twonly is financed by donations and an optional subscription. Your data will never be sold.'; - @override - String get onboardingBuyOneGetTwoTitle => 'Buy one get two'; - - @override - String get onboardingBuyOneGetTwoBody => - 'twonly always requires at least two people, which is why you receive a second free license for your twonly partner with your purchase.'; - @override String get onboardingGetStartedTitle => 'Let\'s go!'; - @override - String get onboardingGetStartedBody => - 'You can test twonly free of charge in preview mode. In this mode you can be found by others and receive pictures or videos but you cannot send any yourself.'; - - @override - String get onboardingTryForFree => 'Try for free'; - @override String get registerUsernameSlogan => 'Please select a username so others can find you!'; @@ -85,16 +71,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get registerSubmitButton => 'Register now!'; - @override - String get registerTwonlyCodeText => - 'Have you received a twonly code? Then redeem it either directly here or later!'; - - @override - String get registerTwonlyCodeLabel => 'twonly-Code'; - - @override - String get newMessageTitle => 'New message'; - @override String get chatsTapToSend => 'Click to send your first image'; @@ -137,23 +113,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get startNewChatNewContact => 'New Contact'; - @override - String get startNewChatYourContacts => 'Your Contacts'; - @override String get shareImageAllUsers => 'All contacts'; - @override - String get shareImageAllTwonlyWarning => - 'twonlies can only be send to verified contacts!'; - - @override - String get shareImageUserNotVerified => 'User is not verified'; - - @override - String get shareImageUserNotVerifiedDesc => - 'twonlies can only be sent to verified users. To verify a user, go to their profile and to verify security number.'; - @override String get shareImageShowArchived => 'Show archived users'; @@ -163,13 +125,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get addFriendTitle => 'Add friends'; - @override - String get searchUserNamePreview => - 'To protect you and other twonly users from spam and abuse, it is not possible to search for other people in preview mode. Other users can find you and their requests will be displayed here!'; - - @override - String get selectSubscription => 'Select subscription'; - @override String get searchUserNamePending => 'Request pending'; @@ -184,15 +139,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get searchUsernameNewFollowerTitle => 'Open requests'; - @override - String get searchUsernameQrCodeBtn => 'Scan QR code'; - @override String get chatListViewSearchUserNameBtn => 'Add your first twonly contact!'; - @override - String get chatListViewSendFirstTwonly => 'Send your first twonly!'; - @override String get chatListDetailInput => 'Type a message'; @@ -202,9 +151,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get contextMenuUserProfile => 'User profile'; - @override - String get contextMenuVerifyUser => 'Verify'; - @override String get contextMenuArchiveUser => 'Archive'; @@ -445,19 +391,10 @@ class AppLocalizationsEn extends AppLocalizations { @override String get settingsAccountDeleteAccount => 'Delete account'; - @override - String settingsAccountDeleteAccountWithBallance(Object credit) { - return 'In the next step, you can select what you want to to with the remaining credit ($credit).'; - } - @override String get settingsAccountDeleteAccountNoBallance => 'Once you delete your account, there is no going back.'; - @override - String get settingsAccountDeleteAccountNoInternet => - 'An Internet connection is required to delete your account.'; - @override String get settingsAccountDeleteModalTitle => 'Are you sure?'; @@ -468,15 +405,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get contactVerifyNumberTitle => 'Verify contact'; - @override - String get contactVerifyNumberTapToScan => 'Tap to scan'; - - @override - String get contactVerifyNumberMarkAsVerified => 'Mark as verified'; - - @override - String get contactVerifyNumberClearVerification => 'Clear verification'; - @override String get userVerifiedTitle => 'User verified'; @@ -503,11 +431,6 @@ class AppLocalizationsEn extends AppLocalizations { String get verificationTypeMigratedFromOldVersion => 'Migrated from old version.'; - @override - String contactVerifyNumberLongDesc(Object username) { - return 'To verify the end-to-end encryption with $username, compare the numbers with their device. The person can also scan your code with their device.'; - } - @override String get contactViewMessage => 'Message'; @@ -517,14 +440,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get contactNicknameNew => 'New nickname'; - @override - String get deleteAllContactMessages => 'Delete all text-messages'; - - @override - String deleteAllContactMessagesBody(Object username) { - return 'This will remove all messages, except stored media files, in your chat with $username. This will NOT delete the messages stored at $username\'s device!'; - } - @override String get contactBlock => 'Block'; @@ -558,6 +473,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get next => 'Next'; + @override + String get finishSetup => 'Complete setup'; + @override String get submit => 'Submit'; @@ -570,9 +488,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get enable => 'Enable'; - @override - String get understood => 'Understood'; - @override String get cancel => 'Cancel'; @@ -631,9 +546,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get toggleFlashLight => 'Toggle the flash light'; - @override - String get toggleHighQuality => 'Toggle better resolution'; - @override String userFound(Object username) { return '$username found'; @@ -642,23 +554,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get userFoundBody => 'Do you want to create a follow request?'; - @override - String searchUsernameNotFoundLong(Object username) { - return '\"$username\" is not a twonly user. Please check the username and try again.'; - } - - @override - String get errorUnknown => - 'An unexpected error has occurred. Please try again later.'; - - @override - String get errorBadRequest => - 'The request could not be understood by the server due to malformed syntax. Please check your input and try again.'; - - @override - String get errorTooManyRequests => - 'You have made too many requests in a short period. Please wait a moment before trying again.'; - @override String get errorInternalError => 'The server is currently not available. Please try again later.'; @@ -670,34 +565,10 @@ class AppLocalizationsEn extends AppLocalizations { @override String get errorUsernameAlreadyTaken => 'The username is already taken.'; - @override - String get errorSignatureNotValid => - 'The provided signature is not valid. Please check your credentials and try again.'; - - @override - String get errorUsernameNotFound => - 'The username you entered does not exist. Please check the spelling or create a new account.'; - @override String get errorUsernameNotValid => 'The username you provided does not meet the required criteria. Please choose a valid username.'; - @override - String get errorInvalidPublicKey => - 'The public key you provided is invalid. Please check the key and try again.'; - - @override - String get errorSessionAlreadyAuthenticated => - 'You are already logged in. Please log out if you want to log in with a different account.'; - - @override - String get errorSessionNotAuthenticated => - 'Your session is not authenticated. Please log in to continue.'; - - @override - String get errorOnlyOneSessionAllowed => - 'Only one active session is allowed per user. Please log out from other devices to continue.'; - @override String get errorNotEnoughCredit => 'You do not have enough twonly-credit.'; @@ -775,144 +646,27 @@ class AppLocalizationsEn extends AppLocalizations { @override String get plusFeature2 => '✓ Additional features (coming-soon)'; - @override - String get transactionHistory => 'Your transaction history'; - - @override - String get manageSubscription => 'Manage subscription'; - - @override - String get nextPayment => 'Next payment'; - - @override - String get currentBalance => 'Current balance'; - @override String get manageAdditionalUsers => 'Manage additional users'; @override String get open => 'Open'; - @override - String get createOrRedeemVoucher => 'Buy or redeem voucher'; - @override String get createVoucher => 'Buy voucher'; - @override - String get createVoucherDesc => - 'Choose the value of the voucher. The value of the voucher will be deducted from your twonly balance.'; - @override String get redeemVoucher => 'Redeem voucher'; - @override - String get openVouchers => 'Open vouchers'; - - @override - String get voucherCreated => 'Voucher created'; - - @override - String get voucherRedeemed => 'Voucher redeemed'; - - @override - String get enterVoucherCode => 'Enter Voucher Code'; - - @override - String get requestedVouchers => 'Requested vouchers'; - - @override - String get redeemedVouchers => 'Redeemed vouchers'; - @override String get buy => 'Buy'; - @override - String subscriptionRefund(Object refund) { - return 'When you upgrade, you will receive a refund of $refund for your current subscription.'; - } - - @override - String get transactionCash => 'Cash transaction'; - - @override - String get transactionPlanUpgrade => 'Plan upgrade'; - - @override - String get transactionRefund => 'Refund transaction'; - - @override - String get transactionThanksForTesting => 'Thank you for testing'; - - @override - String get transactionUnknown => 'Unknown transaction'; - - @override - String get transactionVoucherCreated => 'Voucher created'; - - @override - String get transactionVoucherRedeemed => 'Voucher redeemed'; - - @override - String get transactionAutoRenewal => 'Automatic renewal'; - - @override - String get checkoutOptions => 'Options'; - - @override - String get refund => 'Refund'; - - @override - String get checkoutPayYearly => 'Pay yearly'; - - @override - String get checkoutTotal => 'Total'; - - @override - String get selectPaymentMethod => 'Select Payment Method'; - - @override - String get twonlyCredit => 'twonly-Credit'; - - @override - String get notEnoughCredit => 'You do not have enough credit!'; - - @override - String get chargeCredit => 'Charge credit'; - @override String get autoRenewal => 'Auto renewal'; - @override - String get autoRenewalDesc => 'You can change this at any time.'; - - @override - String get autoRenewalLongDesc => - 'When your subscription expires, you will automatically be downgraded to the Preview plan. If you activate the automatic renewal, please make sure that you have enough credit for the automatic renewal. We will notify you in good time before the automatic renewal.'; - - @override - String get planSuccessUpgraded => 'Successfully upgraded your plan.'; - - @override - String get checkoutSubmit => 'Order with a fee.'; - @override String get additionalUsersList => 'Your additional users'; - @override - String get additionalUsersPlusTokens => 'twonly-codes for \"Plus\" user'; - - @override - String get additionalUsersFreeTokens => 'twonly-codes for \"Free\" user'; - - @override - String get planLimitReached => - 'You have reached your plan limit for today. Upgrade your plan now to send the media file.'; - - @override - String get planNotAllowed => - 'You cannot send media files with your current tariff. Upgrade your plan now to send the media file.'; - @override String get galleryDelete => 'Delete file'; @@ -925,48 +679,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get galleryExportSuccess => 'Successfully saved in the Gallery.'; - @override - String get settingsResetTutorials => 'Show tutorials again'; - - @override - String get settingsResetTutorialsDesc => - 'Click here to show already displayed tutorials again.'; - - @override - String get settingsResetTutorialsSuccess => - 'Tutorials will be displayed again.'; - - @override - String get tutorialChatListSearchUsersTitle => - 'Find Friends and Manage Friend Requests'; - - @override - String get tutorialChatListSearchUsersDesc => - 'If you know your friends\' usernames, you can search for them here and send a friend request. You will also see all requests from other users that you can accept or block.'; - - @override - String get tutorialChatListContextMenuTitle => - 'Long press on the contact to open the context menu.'; - - @override - String get tutorialChatListContextMenuDesc => - 'With the context menu, you can pin, archive, and perform various actions on your contacts. Simply long press the contact and then move your finger to the desired option or tap directly on it.'; - - @override - String get tutorialChatMessagesVerifyShieldTitle => 'Verify your contacts!'; - - @override - String get tutorialChatMessagesVerifyShieldDesc => - 'twonly uses the Signal protocol for secure end-to-end encryption. When you first contact someone, their public identity key is downloaded. To ensure that this key has not been tampered with by third parties, you should compare it with your friend when you meet in person. Once you have verified the user, you can also enable the twonly mode when sending images and videos.'; - - @override - String get tutorialChatMessagesReopenMessageTitle => - 'Reopen Images and Videos'; - - @override - String get tutorialChatMessagesReopenMessageDesc => - 'If your friend has sent you a picture or video with infinite display time, you can open it again at any time until you restart the app. To do this, simply double-click on the message. Your friend will then receive a notification that you have viewed the picture again.'; - @override String get memoriesEmpty => 'As soon as you save pictures or videos, they end up here in your memories.'; @@ -989,19 +701,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get settingsBackup => 'Backup'; - @override - String get backupNoticeTitle => 'No backup configured'; - - @override - String get backupNoticeDesc => - 'If you change or lose your device, no one can restore your account without a backup. Therefore, back up your data.'; - - @override - String get backupNoticeLater => 'Remind later'; - - @override - String get backupNoticeOpenBackup => 'Create backup'; - @override String get backupPending => 'Pending'; @@ -1037,20 +736,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get backupLastBackupResult => 'Result'; - @override - String get deleteBackupTitle => 'Are you sure?'; - - @override - String get deleteBackupBody => - 'Without an backup, you can not restore your user account.'; - @override String get backupData => 'Data-Backup'; - @override - String get backupDataDesc => - 'This backup contains besides of your twonly-Identity also all of your media files. This backup will is also encrypted but stored locally. You then have to ensure to manually copy it onto your laptop or device of your choice.'; - @override String get backupInsecurePassword => 'Insecure password'; @@ -1147,10 +835,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get retransmissionRequested => 'Retransmission requested'; - @override - String get testPaymentMethod => - 'Thanks for the interest in a paid plan. Currently the paid plans are still deactivated. But they will be activated soon!'; - @override String get openChangeLog => 'Open changelog automatically'; @@ -1581,6 +1265,16 @@ class AppLocalizationsEn extends AppLocalizations { @override String get skipForNow => 'Skip for now'; + @override + String get finishSetupCardTitle => 'Complete your profile'; + + @override + String get finishSetupCardDesc => + 'You are almost there! Finish setting up your account to get the most out of twonly.'; + + @override + String get finishSetupCardAction => 'Resume Setup'; + @override String get onboardingFinishLater => 'Finish later'; @@ -1591,9 +1285,6 @@ class AppLocalizationsEn extends AppLocalizations { String get onboardingProfileBody => 'Select an avatar and a display name that friends will see.'; - @override - String get onboardingBackupTitle => 'Backup Setup'; - @override String get onboardingBackupBody => 'Back up your twonly identity, as this is the only way to restore your account if you uninstall the app or lose your phone.'; @@ -1602,10 +1293,70 @@ class AppLocalizationsEn extends AppLocalizations { String get onboardingVerificationBadgeTitle => 'Verification Badge'; @override - String get onboardingUserDiscoveryTitle => 'User Discovery'; + String get onboardingUserDiscoveryShareFriends => 'Share your friends'; @override - String get onboardingResetSetup => 'Reset Setup'; + String get onboardingUserDiscoveryIncreaseTrust => 'Increase trust'; + + @override + String get onboardingUserDiscoveryShareFriendsDesc => + 'Share with your friends who you know and who you have verified. Friends can *only see mutual friends* from your friend list.'; + + @override + String get onboardingUserDiscoveryContactsVerifiedBadge => + 'Contacts verified by your friends get a badge'; + + @override + String get onboardingUserDiscoveryWhoIsRequesting => + 'Be informed about who is requesting'; + + @override + String get userDiscoverySettingsEnableAllContacts => + 'Enabled for all contacts'; + + @override + String get userDiscoverySettingsManualApproval => 'Manual approval'; + + @override + String get userDiscoverySettingsManualApprovalDesc => + 'Before sharing someone, you will be asked every time someone reaches the number of send images.'; + + @override + String get onboardingUserDiscoveryLetFriendsFindYou => + 'Let your friends find you'; + + @override + String get onboardingUserDiscoveryLetFriendsFindYouDesc => + 'To help your friends find you, *you can be suggested* to people with whom you have *mutual friends*.'; + + @override + String get onboardingUserDiscoveryBeRecommended => 'Be recommended to others'; + + @override + String get onboardingUserDiscoveryWhatOthersSee => 'What others will see'; + + @override + String get onboardingUserDiscoveryWhatYouSee => + 'If requested, that\'s what you will see'; + + @override + String get onboardingAddContactsTitle => 'Add new contacts'; + + @override + String get onboardingAddContactsAcceptDesc => + 'In twonly, every contact must first be accepted before you can communicate.'; + + @override + String get onboardingAddContactsMethodHeading => 'Add contacts'; + + @override + String get onboardingAddContactsMethodScan => 'Scan the contact\'s QR code.'; + + @override + String get onboardingAddContactsMethodSearch => 'Search for the username.'; + + @override + String get onboardingAddContactsMethodShare => 'Share a contact in chats.'; @override String linkFromUsername(Object username) { @@ -1804,7 +1555,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String get userDiscoverySettingsMinImages => - 'Choose the minimum number of images you must have exchanged with a person before you securely share your friends with them.'; + 'Choose the minimum number of images you must have send to a person before you securely share your friends with them.'; @override String get userDiscoverySettingsMutualFriends => @@ -1815,32 +1566,28 @@ class AppLocalizationsEn extends AppLocalizations { @override String get userDiscoveryEnabledDisableWarning => - 'If you disable the \"Find friends\" feature, you will no longer see suggestions. You will also stop sharing your friends with new contacts.'; + 'If you disable the \"Share your friends\" feature, you will no longer see suggestions. You will also stop sharing your friends with new contacts.'; @override String get userDiscoveryEnabledChangeSettings => 'Change settings'; @override String get userDiscoveryEnabledFaq => - 'In our FAQ we explain how the \"Find friends\" feature works.'; + 'In our FAQ we explain how the \"Share your friends\" feature works.'; @override String get userDiscoveryDisabledIntro => - 'twonly doesn\'t use phone numbers, so we suggest friends based on mutual contacts instead – securely and privately.'; - - @override - String get userDiscoveryDisabledInvisible => - 'Your friend list is *completely invisible to strangers*. Only your friends can see parts of it – and only those people with whom they have *mutual friends* themselves.'; + 'twonly does *not* collect your phone number or needs access to your contacts. Instead, twonly can *find your friends through mutual friends*.'; @override String get userDiscoveryDisabledDecide => - 'Decide for yourself who can see your friends. You can change your mind at any time or hide specific people.'; + 'Decide for yourself who can see your friends. You can change your mind at *any time* or *hide specific people*.'; @override - String get userDiscoverySettingsTitle => 'Find friends'; + String get userDiscoverySettingsTitle => 'Share your friends'; @override - String get userDiscoverySettingsMinImagesTitle => 'Number of shared images'; + String get userDiscoverySettingsMinImagesTitle => 'Number of images send'; @override String get userDiscoverySettingsMutualFriendsTitle => @@ -1849,13 +1596,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get userDiscoveryDisabledYouHaveControl => 'You are in control'; - @override - String get userDiscoveryDisabledEnableWithDefault => - 'Enable with default settings'; - - @override - String get userDiscoveryDisabledCustomizeSettings => 'Customize settings'; - @override String get userDiscoveryDisabledLearnMore => 'Learn more'; @@ -1869,6 +1609,10 @@ class AppLocalizationsEn extends AppLocalizations { String get userDiscoveryEnabledFriendsSharedDesc => 'You only share friends who have also activated this feature and who have reached the threshold you set.'; + @override + String get userDiscoverySettingsCurrentlyDisabled => + 'The feature \"Share your friends\" is currently disabled.'; + @override String get userDiscoveryEnabledNoFriendsShared => 'You are not sharing anyone yet.'; diff --git a/lib/src/localization/generated/app_localizations_sv.dart b/lib/src/localization/generated/app_localizations_sv.dart deleted file mode 100644 index df6b315b..00000000 --- a/lib/src/localization/generated/app_localizations_sv.dart +++ /dev/null @@ -1,1915 +0,0 @@ -// ignore: unused_import -import 'package:intl/intl.dart' as intl; -import 'app_localizations.dart'; - -// ignore_for_file: type=lint - -/// The translations for Swedish (`sv`). -class AppLocalizationsSv extends AppLocalizations { - AppLocalizationsSv([String locale = 'sv']) : super(locale); - - @override - String get registerTitle => 'Welcome to twonly!'; - - @override - String get registerSlogan => - 'twonly, a privacy friendly way to connect with friends through secure, spontaneous image sharing'; - - @override - String get onboardingWelcomeTitle => 'Welcome to twonly!'; - - @override - String get onboardingWelcomeBody => - 'Experience a private and secure way to stay in touch with friends by sharing instant pictures.'; - - @override - String get onboardingE2eTitle => 'Carefree sharing'; - - @override - String get onboardingE2eBody => - 'With end-to-end encryption, enjoy the peace of mind that only you and your friends can see the moments you share.'; - - @override - String get onboardingFocusTitle => 'Focus on sharing moments'; - - @override - String get onboardingFocusBody => - 'Say goodbye to addictive features! twonly was created for sharing moments, free from useless distractions or ads.'; - - @override - String get onboardingSendTwonliesTitle => 'Send twonlies'; - - @override - String get onboardingSendTwonliesBody => - 'Share moments securely with your partner. twonly ensures that only your partner can open it, keeping your moments with your partner a two(o)nly thing!'; - - @override - String get onboardingNotProductTitle => 'You are not the product!'; - - @override - String get onboardingNotProductBody => - 'twonly is financed by donations and an optional subscription. Your data will never be sold.'; - - @override - String get onboardingBuyOneGetTwoTitle => 'Buy one get two'; - - @override - String get onboardingBuyOneGetTwoBody => - 'twonly always requires at least two people, which is why you receive a second free license for your twonly partner with your purchase.'; - - @override - String get onboardingGetStartedTitle => 'Let\'s go!'; - - @override - String get onboardingGetStartedBody => - 'You can test twonly free of charge in preview mode. In this mode you can be found by others and receive pictures or videos but you cannot send any yourself.'; - - @override - String get onboardingTryForFree => 'Try for free'; - - @override - String get registerUsernameSlogan => - 'Please select a username so others can find you!'; - - @override - String get registerUsernameDecoration => 'Username'; - - @override - String get registerUsernameLimits => - 'Your username must be at least 3 characters long.'; - - @override - String get registerProofOfWorkFailed => - 'There was an issue with the captcha test. Please try again.'; - - @override - String get registerSubmitButton => 'Register now!'; - - @override - String get registerTwonlyCodeText => - 'Have you received a twonly code? Then redeem it either directly here or later!'; - - @override - String get registerTwonlyCodeLabel => 'twonly-Code'; - - @override - String get newMessageTitle => 'New message'; - - @override - String get chatsTapToSend => 'Click to send your first image'; - - @override - String get cameraPreviewSendTo => 'Send to'; - - @override - String get shareImageTitle => 'Share with'; - - @override - String get shareImageBestFriends => 'Best friends'; - - @override - String get shareImagePinnedContacts => 'Pinnded'; - - @override - String get shareImagedEditorSendImage => 'Send'; - - @override - String get shareImagedEditorShareWith => 'Share with'; - - @override - String get shareImagedEditorSaveImage => 'Save'; - - @override - String get shareImagedEditorSavedImage => 'Saved'; - - @override - String get shareImageSearchAllContacts => 'Search all contacts'; - - @override - String get startNewChatSearchHint => 'Name, username or groupname'; - - @override - String get shareImagedSelectAll => 'Select all'; - - @override - String get startNewChatTitle => 'Select Contact'; - - @override - String get startNewChatNewContact => 'New Contact'; - - @override - String get startNewChatYourContacts => 'Your Contacts'; - - @override - String get shareImageAllUsers => 'All contacts'; - - @override - String get shareImageAllTwonlyWarning => - 'twonlies can only be send to verified contacts!'; - - @override - String get shareImageUserNotVerified => 'User is not verified'; - - @override - String get shareImageUserNotVerifiedDesc => - 'twonlies can only be sent to verified users. To verify a user, go to their profile and to verify security number.'; - - @override - String get shareImageShowArchived => 'Show archived users'; - - @override - String get searchUsernameInput => 'Username'; - - @override - String get addFriendTitle => 'Add friends'; - - @override - String get searchUserNamePreview => - 'To protect you and other twonly users from spam and abuse, it is not possible to search for other people in preview mode. Other users can find you and their requests will be displayed here!'; - - @override - String get selectSubscription => 'Select subscription'; - - @override - String get searchUserNamePending => 'Request pending'; - - @override - String get searchUsernameNotFound => 'Username not found'; - - @override - String searchUsernameNotFoundBody(Object username) { - return 'There is no user with the username \"$username\" registered'; - } - - @override - String get searchUsernameNewFollowerTitle => 'Open requests'; - - @override - String get searchUsernameQrCodeBtn => 'Scan QR code'; - - @override - String get chatListViewSearchUserNameBtn => 'Add your first twonly contact!'; - - @override - String get chatListViewSendFirstTwonly => 'Send your first twonly!'; - - @override - String get chatListDetailInput => 'Type a message'; - - @override - String get userDeletedAccount => 'The user has deleted their account.'; - - @override - String get contextMenuUserProfile => 'User profile'; - - @override - String get contextMenuVerifyUser => 'Verify'; - - @override - String get contextMenuArchiveUser => 'Archive'; - - @override - String get contextMenuUndoArchiveUser => 'Undo archiving'; - - @override - String get contextMenuOpenChat => 'Open chat'; - - @override - String get contextMenuPin => 'Pin'; - - @override - String get contextMenuUnpin => 'Unpin'; - - @override - String get contextMenuViewAgain => 'View again'; - - @override - String get mediaViewerAuthReason => 'Please authenticate to see this twonly!'; - - @override - String get mediaViewerTwonlyTapToOpen => 'Tap to open your twonly!'; - - @override - String get messageSendState_Received => 'Received'; - - @override - String get messageSendState_Opened => 'Opened'; - - @override - String get messageSendState_Send => 'Sent'; - - @override - String get messageSendState_Sending => 'Sending'; - - @override - String get messageSendState_TapToLoad => 'Tap to load'; - - @override - String get messageSendState_Loading => 'Downloading'; - - @override - String get messageStoredInGallery => 'Stored in gallery'; - - @override - String get messageReopened => 'Re-opened'; - - @override - String get imageEditorDrawOk => 'Take drawing'; - - @override - String get settingsTitle => 'Settings'; - - @override - String get settingsChats => 'Chats'; - - @override - String get settingsPreSelectedReactions => 'Preselected reaction emojis'; - - @override - String get settingsPreSelectedReactionsError => - 'A maximum of 12 reactions can be selected.'; - - @override - String get settingsProfile => 'Profile'; - - @override - String get settingsStorageData => 'Data and storage'; - - @override - String get settingsStorageDataStoreInGTitle => 'Store in Gallery'; - - @override - String get settingsStorageDataStoreInGSubtitle => - 'Store saved images additional in the systems gallery.'; - - @override - String get settingsStorageDataMediaAutoDownload => 'Media auto-download'; - - @override - String get settingsStorageDataAutoDownMobile => 'When using mobile data'; - - @override - String get settingsStorageDataAutoDownWifi => 'When using WI-FI'; - - @override - String get settingsProfileCustomizeAvatar => 'Customize your avatar'; - - @override - String get settingsProfileEditDisplayName => 'Displayname'; - - @override - String get settingsProfileEditDisplayNameNew => 'New Displayname'; - - @override - String get settingsAccount => 'Account'; - - @override - String get settingsSubscription => 'Subscription'; - - @override - String get settingsAppearance => 'Appearance'; - - @override - String get settingsPrivacy => 'Privacy & Security'; - - @override - String get settingsPrivacyBlockUsers => 'Block users'; - - @override - String get settingsPrivacyBlockUsersDesc => - 'Blocked users will not be able to communicate with you. You can unblock a blocked user at any time.'; - - @override - String settingsPrivacyBlockUsersCount(Object len) { - return '$len contact(s)'; - } - - @override - String get settingsNotification => 'Notification'; - - @override - String get settingsNotifyTroubleshooting => 'Troubleshooting'; - - @override - String get settingsNotifyTroubleshootingDesc => - 'Click here if you have problems receiving push notifications.'; - - @override - String get settingsNotifyTroubleshootingNoProblem => 'No problem detected'; - - @override - String get settingsNotifyTroubleshootingNoProblemDesc => - 'Press OK to receive a test notification. If you do not receive the test notification, please click on the new menu item that appears after you click “OK”.'; - - @override - String get settingsNotifyResetTitle => 'Didn\'t receive a test notification?'; - - @override - String get settingsNotifyResetTitleSubtitle => - 'If you haven\'t received any test notifications, click here to reset your notification tokens.'; - - @override - String get settingsNotifyResetTitleReset => - 'Your notification tokens have been reset.'; - - @override - String get settingsNotifyResetTitleResetDesc => - 'If the problem persists, please send us your debug log via Settings > Help so we can investigate the issue.'; - - @override - String get settingsHelp => 'Help'; - - @override - String get settingsHelpDiagnostics => 'Diagnostic protocol'; - - @override - String get settingsHelpFAQ => 'FAQ'; - - @override - String get feedbackTooltip => 'Give Feedback to improve twonly.'; - - @override - String get settingsHelpContactUs => 'Contact us'; - - @override - String get settingsHelpVersion => 'Version'; - - @override - String get settingsHelpLicenses => 'Licenses (Source-Code)'; - - @override - String get settingsHelpCredits => 'Licenses (Images)'; - - @override - String get settingsHelpImprint => 'Imprint & Privacy Policy'; - - @override - String get contactUsFaq => 'Have you read our FAQ yet?'; - - @override - String get contactUsEmojis => 'How do you feel? (optional)'; - - @override - String get contactUsSelectOption => 'Please select an option'; - - @override - String get contactUsReason => 'Tell us why you\'re reaching out'; - - @override - String get contactUsMessage => - 'If you want to receive an answer, please add your e-mail address so we can contact you.'; - - @override - String get contactUsYourMessage => 'Your message'; - - @override - String get contactUsMessageTitle => 'Tell us what\'s going on'; - - @override - String get contactUsReasonNotWorking => 'Something\'s not working'; - - @override - String get contactUsReasonFeatureRequest => 'Feature request'; - - @override - String get contactUsReasonQuestion => 'Question'; - - @override - String get contactUsReasonFeedback => 'Feedback'; - - @override - String get contactUsReasonOther => 'Other'; - - @override - String get contactUsIncludeLog => 'Include debug log'; - - @override - String get contactUsWhatsThat => 'What\'s that?'; - - @override - String get contactUsLastWarning => - 'This are the information\'s which will be send to us. Please verify them and then press submit.'; - - @override - String get contactUsSuccess => 'Feedback submitted successfully!'; - - @override - String get contactUsShortcut => 'Hide Feedback Icon'; - - @override - String get settingsHelpTerms => 'Terms of Service'; - - @override - String get settingsAppearanceTheme => 'Theme'; - - @override - String get settingsAccountDeleteAccount => 'Delete account'; - - @override - String settingsAccountDeleteAccountWithBallance(Object credit) { - return 'In the next step, you can select what you want to to with the remaining credit ($credit).'; - } - - @override - String get settingsAccountDeleteAccountNoBallance => - 'Once you delete your account, there is no going back.'; - - @override - String get settingsAccountDeleteAccountNoInternet => - 'An Internet connection is required to delete your account.'; - - @override - String get settingsAccountDeleteModalTitle => 'Are you sure?'; - - @override - String get settingsAccountDeleteModalBody => - 'Your account will be deleted. There is no change to restore it.'; - - @override - String get contactVerifyNumberTitle => 'Verify contact'; - - @override - String get contactVerifyNumberTapToScan => 'Tap to scan'; - - @override - String get contactVerifyNumberMarkAsVerified => 'Mark as verified'; - - @override - String get contactVerifyNumberClearVerification => 'Clear verification'; - - @override - String get userVerifiedTitle => 'User verified'; - - @override - String contactVerifiedBy(Object username) { - return 'Verified by $username'; - } - - @override - String get verificationTypeQrScanned => 'You scanned their QR code.'; - - @override - String get verificationTypeSecretQrToken => - 'The other person scanned your QR code.'; - - @override - String get verificationTypeLink => 'Verified via link.'; - - @override - String get verificationTypeContactSharedByVerified => - 'Contact received from a verified contact.'; - - @override - String get verificationTypeMigratedFromOldVersion => - 'Migrated from old version.'; - - @override - String contactVerifyNumberLongDesc(Object username) { - return 'To verify the end-to-end encryption with $username, compare the numbers with their device. The person can also scan your code with their device.'; - } - - @override - String get contactViewMessage => 'Message'; - - @override - String get contactNickname => 'Nickname'; - - @override - String get contactNicknameNew => 'New nickname'; - - @override - String get deleteAllContactMessages => 'Delete all text-messages'; - - @override - String deleteAllContactMessagesBody(Object username) { - return 'This will remove all messages, except stored media files, in your chat with $username. This will NOT delete the messages stored at $username\'s device!'; - } - - @override - String get contactBlock => 'Block'; - - @override - String contactBlockTitle(Object username) { - return 'Block $username'; - } - - @override - String get contactBlockBody => - 'A blocked user will no longer be able to send you messages and their profile will be hidden from view. To unblock a user, simply navigate to Settings > Privacy > Blocked Users.'; - - @override - String get contactRemove => 'Remove user'; - - @override - String contactRemoveTitle(Object username) { - return 'Remove $username'; - } - - @override - String get contactRemoveBody => - 'Permanently remove the user. If the user tries to send you a new message, you will have to accept the user again first.'; - - @override - String get undo => 'Undo'; - - @override - String get redo => 'Redo'; - - @override - String get next => 'Next'; - - @override - String get submit => 'Submit'; - - @override - String get close => 'Close'; - - @override - String get disable => 'Disable'; - - @override - String get enable => 'Enable'; - - @override - String get understood => 'Understood'; - - @override - String get cancel => 'Cancel'; - - @override - String get now => 'Now'; - - @override - String get you => 'You'; - - @override - String get minutesShort => 'min.'; - - @override - String get image => 'Image'; - - @override - String get video => 'Video'; - - @override - String get react => 'React'; - - @override - String get reply => 'Reply'; - - @override - String get copy => 'Copy'; - - @override - String get edit => 'Edit'; - - @override - String get delete => 'Delete'; - - @override - String get info => 'Info'; - - @override - String get ok => 'Ok'; - - @override - String get switchFrontAndBackCamera => - 'Switch between front and back camera.'; - - @override - String get addTextItem => 'Text'; - - @override - String get protectAsARealTwonly => 'Send as real twonly!'; - - @override - String get addDrawing => 'Drawing'; - - @override - String get addEmoji => 'Emoji'; - - @override - String get toggleFlashLight => 'Toggle the flash light'; - - @override - String get toggleHighQuality => 'Toggle better resolution'; - - @override - String userFound(Object username) { - return '$username found'; - } - - @override - String get userFoundBody => 'Do you want to create a follow request?'; - - @override - String searchUsernameNotFoundLong(Object username) { - return '\"$username\" is not a twonly user. Please check the username and try again.'; - } - - @override - String get errorUnknown => - 'An unexpected error has occurred. Please try again later.'; - - @override - String get errorBadRequest => - 'The request could not be understood by the server due to malformed syntax. Please check your input and try again.'; - - @override - String get errorTooManyRequests => - 'You have made too many requests in a short period. Please wait a moment before trying again.'; - - @override - String get errorInternalError => - 'The server is currently not available. Please try again later.'; - - @override - String get errorInvalidInvitationCode => - 'The invitation code you provided is invalid. Please check the code and try again.'; - - @override - String get errorUsernameAlreadyTaken => 'The username is already taken.'; - - @override - String get errorSignatureNotValid => - 'The provided signature is not valid. Please check your credentials and try again.'; - - @override - String get errorUsernameNotFound => - 'The username you entered does not exist. Please check the spelling or create a new account.'; - - @override - String get errorUsernameNotValid => - 'The username you provided does not meet the required criteria. Please choose a valid username.'; - - @override - String get errorInvalidPublicKey => - 'The public key you provided is invalid. Please check the key and try again.'; - - @override - String get errorSessionAlreadyAuthenticated => - 'You are already logged in. Please log out if you want to log in with a different account.'; - - @override - String get errorSessionNotAuthenticated => - 'Your session is not authenticated. Please log in to continue.'; - - @override - String get errorOnlyOneSessionAllowed => - 'Only one active session is allowed per user. Please log out from other devices to continue.'; - - @override - String get errorNotEnoughCredit => 'You do not have enough twonly-credit.'; - - @override - String get errorVoucherInvalid => - 'The voucher code you entered is not valid.'; - - @override - String get errorPlanLimitReached => - 'You have reached your plans limit. Please upgrade your plan.'; - - @override - String get errorPlanNotAllowed => - 'This feature is not available in your current plan.'; - - @override - String get errorPlanUpgradeNotYearly => - 'The plan upgrade must be paid for annually, as the current plan is also billed annually.'; - - @override - String get upgradeToPaidPlan => 'Upgrade to a paid plan.'; - - @override - String upgradeToPaidPlanButton(Object planId, Object sufix) { - return 'Upgrade to $planId$sufix'; - } - - @override - String partOfPaidPlanOf(Object username) { - return 'You are part of the paid plan of $username!'; - } - - @override - String get year => 'year'; - - @override - String get yearly => 'Yearly'; - - @override - String get month => 'month'; - - @override - String get monthly => 'Monthly'; - - @override - String get proFeature1 => '✓ Unlimited media file uploads'; - - @override - String get proFeature2 => '✓ 1 additional Plus user'; - - @override - String get proFeature3 => '✓ Restore flames'; - - @override - String get proFeature4 => '✓ Support twonly'; - - @override - String get familyFeature1 => '✓ Unlimited media file uploads'; - - @override - String get familyFeature2 => '✓ 4 additional Plus user'; - - @override - String get familyFeature3 => '✓ Restore flames'; - - @override - String get familyFeature4 => '✓ Support twonly'; - - @override - String get freeFeature1 => '✓ 10 Media file uploads per day'; - - @override - String get plusFeature1 => '✓ Unlimited media file uploads'; - - @override - String get plusFeature2 => '✓ Additional features (coming-soon)'; - - @override - String get transactionHistory => 'Your transaction history'; - - @override - String get manageSubscription => 'Manage subscription'; - - @override - String get nextPayment => 'Next payment'; - - @override - String get currentBalance => 'Current balance'; - - @override - String get manageAdditionalUsers => 'Manage additional users'; - - @override - String get open => 'Open'; - - @override - String get createOrRedeemVoucher => 'Buy or redeem voucher'; - - @override - String get createVoucher => 'Buy voucher'; - - @override - String get createVoucherDesc => - 'Choose the value of the voucher. The value of the voucher will be deducted from your twonly balance.'; - - @override - String get redeemVoucher => 'Redeem voucher'; - - @override - String get openVouchers => 'Open vouchers'; - - @override - String get voucherCreated => 'Voucher created'; - - @override - String get voucherRedeemed => 'Voucher redeemed'; - - @override - String get enterVoucherCode => 'Enter Voucher Code'; - - @override - String get requestedVouchers => 'Requested vouchers'; - - @override - String get redeemedVouchers => 'Redeemed vouchers'; - - @override - String get buy => 'Buy'; - - @override - String subscriptionRefund(Object refund) { - return 'When you upgrade, you will receive a refund of $refund for your current subscription.'; - } - - @override - String get transactionCash => 'Cash transaction'; - - @override - String get transactionPlanUpgrade => 'Plan upgrade'; - - @override - String get transactionRefund => 'Refund transaction'; - - @override - String get transactionThanksForTesting => 'Thank you for testing'; - - @override - String get transactionUnknown => 'Unknown transaction'; - - @override - String get transactionVoucherCreated => 'Voucher created'; - - @override - String get transactionVoucherRedeemed => 'Voucher redeemed'; - - @override - String get transactionAutoRenewal => 'Automatic renewal'; - - @override - String get checkoutOptions => 'Options'; - - @override - String get refund => 'Refund'; - - @override - String get checkoutPayYearly => 'Pay yearly'; - - @override - String get checkoutTotal => 'Total'; - - @override - String get selectPaymentMethod => 'Select Payment Method'; - - @override - String get twonlyCredit => 'twonly-Credit'; - - @override - String get notEnoughCredit => 'You do not have enough credit!'; - - @override - String get chargeCredit => 'Charge credit'; - - @override - String get autoRenewal => 'Auto renewal'; - - @override - String get autoRenewalDesc => 'You can change this at any time.'; - - @override - String get autoRenewalLongDesc => - 'When your subscription expires, you will automatically be downgraded to the Preview plan. If you activate the automatic renewal, please make sure that you have enough credit for the automatic renewal. We will notify you in good time before the automatic renewal.'; - - @override - String get planSuccessUpgraded => 'Successfully upgraded your plan.'; - - @override - String get checkoutSubmit => 'Order with a fee.'; - - @override - String get additionalUsersList => 'Your additional users'; - - @override - String get additionalUsersPlusTokens => 'twonly-codes for \"Plus\" user'; - - @override - String get additionalUsersFreeTokens => 'twonly-codes for \"Free\" user'; - - @override - String get planLimitReached => - 'You have reached your plan limit for today. Upgrade your plan now to send the media file.'; - - @override - String get planNotAllowed => - 'You cannot send media files with your current tariff. Upgrade your plan now to send the media file.'; - - @override - String get galleryDelete => 'Delete file'; - - @override - String get galleryDetails => 'Show details'; - - @override - String get galleryExport => 'Export to gallery'; - - @override - String get galleryExportSuccess => 'Successfully saved in the Gallery.'; - - @override - String get settingsResetTutorials => 'Show tutorials again'; - - @override - String get settingsResetTutorialsDesc => - 'Click here to show already displayed tutorials again.'; - - @override - String get settingsResetTutorialsSuccess => - 'Tutorials will be displayed again.'; - - @override - String get tutorialChatListSearchUsersTitle => - 'Find Friends and Manage Friend Requests'; - - @override - String get tutorialChatListSearchUsersDesc => - 'If you know your friends\' usernames, you can search for them here and send a friend request. You will also see all requests from other users that you can accept or block.'; - - @override - String get tutorialChatListContextMenuTitle => - 'Long press on the contact to open the context menu.'; - - @override - String get tutorialChatListContextMenuDesc => - 'With the context menu, you can pin, archive, and perform various actions on your contacts. Simply long press the contact and then move your finger to the desired option or tap directly on it.'; - - @override - String get tutorialChatMessagesVerifyShieldTitle => 'Verify your contacts!'; - - @override - String get tutorialChatMessagesVerifyShieldDesc => - 'twonly uses the Signal protocol for secure end-to-end encryption. When you first contact someone, their public identity key is downloaded. To ensure that this key has not been tampered with by third parties, you should compare it with your friend when you meet in person. Once you have verified the user, you can also enable the twonly mode when sending images and videos.'; - - @override - String get tutorialChatMessagesReopenMessageTitle => - 'Reopen Images and Videos'; - - @override - String get tutorialChatMessagesReopenMessageDesc => - 'If your friend has sent you a picture or video with infinite display time, you can open it again at any time until you restart the app. To do this, simply double-click on the message. Your friend will then receive a notification that you have viewed the picture again.'; - - @override - String get memoriesEmpty => - 'As soon as you save pictures or videos, they end up here in your memories.'; - - @override - String get deleteTitle => 'Are you sure?'; - - @override - String get deleteOkBtnForAll => 'Delete for all'; - - @override - String get deleteOkBtnForMe => 'Delete for me'; - - @override - String get deleteImageTitle => 'Are you sure?'; - - @override - String get deleteImageBody => 'The image will be irrevocably deleted.'; - - @override - String get settingsBackup => 'Backup'; - - @override - String get backupNoticeTitle => 'No backup configured'; - - @override - String get backupNoticeDesc => - 'If you change or lose your device, no one can restore your account without a backup. Therefore, back up your data.'; - - @override - String get backupNoticeLater => 'Remind later'; - - @override - String get backupNoticeOpenBackup => 'Create backup'; - - @override - String get backupPending => 'Pending'; - - @override - String get backupFailed => 'Failed'; - - @override - String get backupSuccess => 'Success'; - - @override - String get backupTwonlySafeDesc => - 'Back up your twonly identity, as this is the only way to restore your account if you uninstall the app or lose your phone.'; - - @override - String get backupNoPasswordRecovery => - 'Due to twonly\'s security system, there is (currently) no password recovery function. Therefore, you must remember your password or, better yet, write it down.'; - - @override - String get backupServer => 'Server'; - - @override - String get backupMaxBackupSize => 'max. backup size'; - - @override - String get backupStorageRetention => 'Storage retention'; - - @override - String get backupLastBackupDate => 'Last backup'; - - @override - String get backupLastBackupSize => 'Backup size'; - - @override - String get backupLastBackupResult => 'Result'; - - @override - String get deleteBackupTitle => 'Are you sure?'; - - @override - String get deleteBackupBody => - 'Without an backup, you can not restore your user account.'; - - @override - String get backupData => 'Data-Backup'; - - @override - String get backupDataDesc => - 'This backup contains besides of your twonly-Identity also all of your media files. This backup will is also encrypted but stored locally. You then have to ensure to manually copy it onto your laptop or device of your choice.'; - - @override - String get backupInsecurePassword => 'Insecure password'; - - @override - String get backupInsecurePasswordDesc => - 'The chosen password is very insecure and can therefore easily be guessed by attackers. Please choose a secure password.'; - - @override - String get backupInsecurePasswordOk => 'Continue anyway'; - - @override - String get backupInsecurePasswordCancel => 'Try again'; - - @override - String get backupTwonlySafeLongDesc => - 'twonly does not have any central user accounts. A key pair is created during installation, which consists of a public and a private key. The private key is only stored on your device to protect it from unauthorized access. The public key is uploaded to the server and linked to your chosen username so that others can find you.\n\ntwonly Backup regularly creates an encrypted, anonymous backup of your private key together with your contacts and settings. Your username and chosen password are enough to restore this data on another device.'; - - @override - String get backupSelectStrongPassword => - 'Choose a secure password. This is required if you want to restore your twonly Backup.'; - - @override - String get password => 'Password'; - - @override - String get passwordRepeated => 'Repeat password'; - - @override - String get passwordRepeatedNotEqual => 'Passwords do not match.'; - - @override - String get backupPasswordRequirement => - 'Password must be at least 8 characters long.'; - - @override - String get backupExpertSettings => 'Expert settings'; - - @override - String get backupEnableBackup => 'Activate automatic backup'; - - @override - String get backupOwnServerDesc => - 'Save your twonly Backup at twonly or on any server of your choice.'; - - @override - String get backupUseOwnServer => 'Use server'; - - @override - String get backupResetServer => 'Use standard server'; - - @override - String get backupTwonlySaveNow => 'Save now'; - - @override - String get backupChangePassword => 'Change password'; - - @override - String get twonlySafeRecoverTitle => 'Recovery'; - - @override - String get twonlySafeRecoverDesc => - 'If you have created a backup with twonly Backup, you can restore it here.'; - - @override - String get twonlySafeRecoverBtn => 'Restore backup'; - - @override - String get inviteFriends => 'Invite your friends'; - - @override - String get inviteFriendsShareBtn => 'Share'; - - @override - String inviteFriendsShareText(Object url) { - return 'Let\'s switch to twonly: $url'; - } - - @override - String get appOutdated => 'Your version of twonly is out of date.'; - - @override - String get appOutdatedBtn => 'Update Now'; - - @override - String get doubleClickToReopen => 'Double-click\nto open again'; - - @override - String get uploadLimitReached => - 'The upload limit has\nbeen reached. Upgrade to Pro\nor wait until tomorrow.'; - - @override - String get fileLimitReached => 'Maximum file size\nexceeded'; - - @override - String get retransmissionRequested => 'Retransmission requested'; - - @override - String get testPaymentMethod => - 'Thanks for the interest in a paid plan. Currently the paid plans are still deactivated. But they will be activated soon!'; - - @override - String get openChangeLog => 'Open changelog automatically'; - - @override - String reportUserTitle(Object username) { - return 'Report $username'; - } - - @override - String get reportUserReason => 'Reporting reason'; - - @override - String get reportUser => 'Report user'; - - @override - String get newDeviceRegistered => - 'You have logged in on another device. You have therefore been logged out here.'; - - @override - String get tabToRemoveEmoji => 'Tab to remove'; - - @override - String get quotedMessageWasDeleted => 'The quoted message has been deleted.'; - - @override - String get messageWasDeleted => 'Message has been deleted.'; - - @override - String get messageWasDeletedShort => 'Deleted'; - - @override - String get sent => 'Delivered'; - - @override - String get sentTo => 'Delivered to'; - - @override - String get received => 'Received'; - - @override - String get opened => 'Opened'; - - @override - String get waitingForInternet => 'Waiting for internet'; - - @override - String get editHistory => 'Edit history'; - - @override - String get archivedChats => 'Archived chats'; - - @override - String get durationShortSecond => 'Sec.'; - - @override - String get durationShortMinute => 'Min.'; - - @override - String get durationShortHour => 'Hrs.'; - - @override - String durationShortDays(num count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count Days', - one: '1 Day', - ); - return '$_temp0'; - } - - @override - String get contacts => 'Contacts'; - - @override - String get groups => 'Groups'; - - @override - String get newGroup => 'New group'; - - @override - String get selectMembers => 'Select members'; - - @override - String get selectGroupName => 'Select group name'; - - @override - String get groupNameInput => 'Group name'; - - @override - String get groupMembers => 'Members'; - - @override - String get addMember => 'Add member'; - - @override - String get createGroup => 'Create group'; - - @override - String get leaveGroup => 'Leave group'; - - @override - String get createContactRequest => 'Create contact request'; - - @override - String get contactRequestSend => 'Contact request send'; - - @override - String get makeAdmin => 'Make admin'; - - @override - String get removeAdmin => 'Remove as admin'; - - @override - String get removeFromGroup => 'Remove from group'; - - @override - String get admin => 'Admin'; - - @override - String revokeAdminRightsTitle(Object username) { - return 'Revoke $username\'s admin rights?'; - } - - @override - String get revokeAdminRightsOkBtn => 'Remove as admin'; - - @override - String makeAdminRightsTitle(Object username) { - return 'Make $username an admin?'; - } - - @override - String makeAdminRightsBody(Object username) { - return '$username will be able to edit this group and its members.'; - } - - @override - String get makeAdminRightsOkBtn => 'Make admin'; - - @override - String get updateGroup => 'Update group'; - - @override - String get alreadyInGroup => 'Already in Group'; - - @override - String removeContactFromGroupTitle(Object username) { - return 'Remove $username from this group?'; - } - - @override - String youChangedGroupName(Object newGroupName) { - return 'You have changed the group name to \"$newGroupName\".'; - } - - @override - String makerChangedGroupName(Object maker, Object newGroupName) { - return '$maker has changed the group name to \"$newGroupName\".'; - } - - @override - String get youCreatedGroup => 'You have created the group.'; - - @override - String makerCreatedGroup(Object maker) { - return '$maker has created the group.'; - } - - @override - String youRemovedMember(Object affected) { - return 'You have removed $affected from the group.'; - } - - @override - String makerRemovedMember(Object affected, Object maker) { - return '$maker has removed $affected from the group.'; - } - - @override - String youAddedMember(Object affected) { - return 'You have added $affected to the group.'; - } - - @override - String makerAddedMember(Object affected, Object maker) { - return '$maker has added $affected to the group.'; - } - - @override - String youMadeAdmin(Object affected) { - return 'You made $affected an admin.'; - } - - @override - String makerMadeAdmin(Object affected, Object maker) { - return '$maker made $affected an admin.'; - } - - @override - String youRevokedAdminRights(Object affectedR) { - return 'You revoked $affectedR\'s admin rights.'; - } - - @override - String makerRevokedAdminRights(Object affectedR, Object maker) { - return '$maker revoked $affectedR\'s admin rights.'; - } - - @override - String get youLeftGroup => 'You have left the group.'; - - @override - String makerLeftGroup(Object maker) { - return '$maker has left the group.'; - } - - @override - String get groupActionYou => 'you'; - - @override - String get groupActionYour => 'your'; - - @override - String get notificationFillerIn => 'in'; - - @override - String notificationText(Object inGroup) { - return 'sent a message$inGroup.'; - } - - @override - String notificationTwonly(Object inGroup) { - return 'sent a twonly$inGroup.'; - } - - @override - String notificationVideo(Object inGroup) { - return 'sent a video$inGroup.'; - } - - @override - String notificationImage(Object inGroup) { - return 'sent an image$inGroup.'; - } - - @override - String notificationAudio(Object inGroup) { - return 'sent a voice message$inGroup.'; - } - - @override - String notificationAddedToGroup(Object groupname) { - return 'has added you to \"$groupname\"'; - } - - @override - String get notificationContactRequest => 'wants to connect with you.'; - - @override - String get notificationContactRequestUnknownUser => - 'have received a new contact request.'; - - @override - String get notificationAcceptRequest => 'is now connected with you.'; - - @override - String get notificationStoredMediaFile => 'has stored your image.'; - - @override - String get notificationReaction => 'has reacted to your image.'; - - @override - String get notificationReopenedMedia => 'has reopened your image.'; - - @override - String notificationReactionToVideo(Object reaction) { - return 'has reacted with $reaction to your video.'; - } - - @override - String notificationReactionToText(Object reaction) { - return 'has reacted with $reaction to your message.'; - } - - @override - String notificationReactionToImage(Object reaction) { - return 'has reacted with $reaction to your image.'; - } - - @override - String notificationReactionToAudio(Object reaction) { - return 'has reacted with $reaction to your audio message.'; - } - - @override - String notificationResponse(Object inGroup) { - return 'has responded$inGroup.'; - } - - @override - String get notificationTitleUnknown => 'You have a new message.'; - - @override - String get notificationBodyUnknown => 'Open twonly to learn more.'; - - @override - String get notificationCategoryMessageTitle => 'Messages'; - - @override - String get notificationCategoryMessageDesc => 'Messages from other users.'; - - @override - String get groupContextMenuDeleteGroup => - 'This will permanently delete all messages in this chat.'; - - @override - String get groupYouAreNowLongerAMember => - 'You are no longer part of this group.'; - - @override - String get groupNetworkIssue => 'Network issue. Try again later.'; - - @override - String get leaveGroupSelectOtherAdminTitle => 'Select another admin'; - - @override - String get leaveGroupSelectOtherAdminBody => - 'To leave the group, you must first select a new administrator.'; - - @override - String get leaveGroupSureTitle => 'Leave group'; - - @override - String get leaveGroupSureBody => 'Do you really want to leave the group?'; - - @override - String get leaveGroupSureOkBtn => 'Leave group'; - - @override - String changeDisplayMaxTime(Object time, Object username) { - return 'Chats will now be deleted after $time ($username).'; - } - - @override - String youChangedDisplayMaxTime(Object time) { - return 'Chats will now be deleted after $time.'; - } - - @override - String get userGotReported => 'User has been reported.'; - - @override - String get deleteChatAfter => 'Delete chat after...'; - - @override - String get deleteChatAfterAnHour => 'one hour.'; - - @override - String get deleteChatAfterADay => 'one day.'; - - @override - String get deleteChatAfterAWeek => 'one week.'; - - @override - String get deleteChatAfterAMonth => 'one month.'; - - @override - String get deleteChatAfterAYear => 'one year.'; - - @override - String get yourTwonlyScore => 'Your twonly-Score'; - - @override - String get registrationClosed => - 'Due to the current high volume of registrations, we have temporarily disabled registration to ensure that the service remains reliable. Please try again in a few days.'; - - @override - String get dialogAskDeleteMediaFilePopTitle => - 'Are you sure you want to delete your masterpiece?'; - - @override - String get dialogAskDeleteMediaFilePopDelete => 'Delete'; - - @override - String get allowErrorTracking => 'Share errors and crashes with us'; - - @override - String get allowErrorTrackingSubtitle => - 'If twonly crashes or errors occur, these are automatically reported to our self-hosted Glitchtip instance. Personal data such as messages or images are never uploaded.'; - - @override - String get avatarSaveChanges => 'Would you like to save the changes?'; - - @override - String get avatarSaveChangesStore => 'Save'; - - @override - String get avatarSaveChangesDiscard => 'Discard'; - - @override - String get inProcess => 'In process'; - - @override - String get draftMessage => 'Draft'; - - @override - String get exportMemories => 'Export memories (Beta)'; - - @override - String get importMemories => 'Import memories (Beta)'; - - @override - String get voiceMessageSlideToCancel => 'Slide to cancel'; - - @override - String get voiceMessageCancel => 'Cancel'; - - @override - String get shareYourProfile => 'Share your profile'; - - @override - String get scanOtherProfile => 'Scan other profile'; - - @override - String get openYourOwnQRcode => 'Open your own QR code'; - - @override - String get skipForNow => 'Skip for now'; - - @override - String get onboardingFinishLater => 'Finish later'; - - @override - String get onboardingProfileTitle => 'Choose your look'; - - @override - String get onboardingProfileBody => - 'Select an avatar and a display name that friends will see.'; - - @override - String get onboardingBackupTitle => 'Backup Setup'; - - @override - String get onboardingBackupBody => - 'Back up your twonly identity, as this is the only way to restore your account if you uninstall the app or lose your phone.'; - - @override - String get onboardingVerificationBadgeTitle => 'Verification Badge'; - - @override - String get onboardingUserDiscoveryTitle => 'User Discovery'; - - @override - String get onboardingResetSetup => 'Reset Setup'; - - @override - String linkFromUsername(Object username) { - return 'Is the link from $username?'; - } - - @override - String get linkFromUsernameLong => - 'If you received the link from your friend, you can mark the user as verified, as the public key in the link matches the public key already stored for that user?'; - - @override - String get gotLinkFromFriend => 'Yes, I got the link from my friend!'; - - @override - String couldNotVerifyUsername(Object username) { - return 'Could not verify $username'; - } - - @override - String get linkPubkeyDoesNotMatch => - 'The public key in the link does not match the public key stored for this contact. Try to meet your friend in person and scan the QR code directly!'; - - @override - String get startWithCameraOpen => 'Start with camera open'; - - @override - String get showImagePreviewWhenSending => - 'Display image preview when selecting recipients'; - - @override - String verifiedPublicKey(Object username) { - return 'The public key of $username has been verified and is valid.'; - } - - @override - String get memoriesAYearAgo => 'One year ago'; - - @override - String memoriesXYearsAgo(Object years) { - return '$years years ago'; - } - - @override - String migrationOfMemories(Object open) { - return 'Migration of media files: $open still to be processed.'; - } - - @override - String get autoStoreAllSendUnlimitedMediaFiles => 'Save all sent media'; - - @override - String get autoStoreAllSendUnlimitedMediaFilesSubtitle => - 'If you enable this option, all images you send will be saved as long as they were sent with an infinite countdown and not in twonly mode.'; - - @override - String get termsOfService => 'Terms of service'; - - @override - String get privacyPolicy => 'Privacy policy'; - - @override - String additionalUserAddError(Object username) { - return '$username could not be added, please try again later.'; - } - - @override - String additionalUserAddErrorNotInFreePlan(Object username) { - return '$username is already on a paid plan and therefore could not be added.'; - } - - @override - String additionalUserAddButton(Object limit, Object used) { - return 'Add additional user ($used/$limit)'; - } - - @override - String get additionalUserRemoveTitle => 'Remove this additional user'; - - @override - String get additionalUserRemoveDesc => - 'After removal, the additional user will automatically be downgraded to the free plan, and you can add another person.'; - - @override - String get additionalUserSelectTitle => 'Select additional users'; - - @override - String additionalUserSelectButton(Object limit, Object used) { - return 'Select users ($used/$limit)'; - } - - @override - String get storeAsDefault => 'Store as default'; - - @override - String get deleteUserErrorMessage => - 'You can only delete the contact once the direct chat has been deleted and the contact is no longer a member of a group.'; - - @override - String groupSizeLimitError(Object size) { - return 'Currently, group size is limited to $size people!'; - } - - @override - String get authRequestReopenImage => - 'You must authenticate to reopen the image.'; - - @override - String get shareContactsMenu => 'Contact'; - - @override - String get shareContactsTitle => 'Select contacts'; - - @override - String get shareContactsSubmit => 'Share now'; - - @override - String get updateTwonlyMessage => - 'To see this message, you need to update twonly.'; - - @override - String get verificationBadgeNote => - 'You can verify your friends by scanning their public QR code. Click to learn more.'; - - @override - String get verificationBadgeTitle => 'Verification'; - - @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.'; - - @override - String get verificationBadgeGreenDesc => - 'A contact you have *personally* verified.'; - - @override - String get verificationBadgeYellowDesc => - 'A contact who has been verified by at least one of *your contacts*.'; - - @override - String get verificationBadgeRedDesc => - 'A contact whose identity has *not* yet been verified.'; - - @override - String chatEntryFlameRestored(Object count) { - return '$count flames restored'; - } - - @override - String requestedUserToastText(Object username) { - return '$username was successfully requested.'; - } - - @override - String get profileYourQrCode => 'Your QR code'; - - @override - String get settingsScreenLock => 'Screen lock'; - - @override - String get settingsScreenLockSubtitle => - 'To open twonly, you\'ll need to use your smartphone\'s unlock feature.'; - - @override - String get settingsScreenLockAuthMessageEnable => - 'Use the screen lock from twonly.'; - - @override - String get settingsScreenLockAuthMessageDisable => - 'Disable the screen lock from twonly.'; - - @override - String get unlockTwonly => 'Unlock twonly'; - - @override - String get unlockTwonlyTryAgain => 'Try again'; - - @override - String get unlockTwonlyDesc => - 'Use your phone\'s unlock settings to unlock twonly'; - - @override - String get settingsTypingIndication => 'Typing Indicators'; - - @override - String get settingsTypingIndicationSubtitle => - 'When the typing indicator is turned off, you can\'t see when others are typing a message.'; - - @override - String get scanQrOrShow => 'Scan / Show QR'; - - @override - String get contactActionBlock => 'Block'; - - @override - String get contactActionAccept => 'Accept'; - - @override - String get userDiscoverySettingsMinImages => - 'Choose the minimum number of images you must have exchanged with a person before you securely share your friends with them.'; - - @override - String get userDiscoverySettingsMutualFriends => - 'Choose how many mutual friends a person must have for you to be suggested to them.'; - - @override - String get userDiscoverySettingsApply => 'Apply changes'; - - @override - String get userDiscoveryEnabledDisableWarning => - 'If you disable the \"Find friends\" feature, you will no longer see suggestions. You will also stop sharing your friends with new contacts.'; - - @override - String get userDiscoveryEnabledChangeSettings => 'Change settings'; - - @override - String get userDiscoveryEnabledFaq => - 'In our FAQ we explain how the \"Find friends\" feature works.'; - - @override - String get userDiscoveryDisabledIntro => - 'twonly doesn\'t use phone numbers, so we suggest friends based on mutual contacts instead – securely and privately.'; - - @override - String get userDiscoveryDisabledInvisible => - 'Your friend list is *completely invisible to strangers*. Only your friends can see parts of it – and only those people with whom they have *mutual friends* themselves.'; - - @override - String get userDiscoveryDisabledDecide => - 'Decide for yourself who can see your friends. You can change your mind at any time or hide specific people.'; - - @override - String get userDiscoverySettingsTitle => 'Find friends'; - - @override - String get userDiscoverySettingsMinImagesTitle => 'Number of shared images'; - - @override - String get userDiscoverySettingsMutualFriendsTitle => - 'Number of mutual friends'; - - @override - String get userDiscoveryDisabledYouHaveControl => 'You are in control'; - - @override - String get userDiscoveryDisabledEnableWithDefault => - 'Enable with default settings'; - - @override - String get userDiscoveryDisabledCustomizeSettings => 'Customize settings'; - - @override - String get userDiscoveryDisabledLearnMore => 'Learn more'; - - @override - String get userDiscoveryEnabledDialogTitle => 'Really disable?'; - - @override - String get userDiscoveryEnabledFriendsShared => 'Friends you share'; - - @override - String get userDiscoveryEnabledFriendsSharedDesc => - 'You only share friends who have also activated this feature and who have reached the threshold you set.'; - - @override - String get userDiscoveryEnabledNoFriendsShared => - 'You are not sharing anyone yet.'; - - @override - String get userDiscoveryActionDisable => 'Disable'; - - @override - String get friendSuggestionsTitle => 'Friend suggestions'; - - @override - String get andWord => 'and'; - - @override - String friendSuggestionsFriendsWith(Object friends) { - return 'Friends with $friends.'; - } - - @override - String friendSuggestionsGroupMemberIn(Object groups) { - return ' Group member in $groups.'; - } - - @override - String get friendSuggestionsRequest => 'Request'; - - @override - String contactUserDiscoveryImagesLeft(Object imagesLeft, Object username) { - return '$imagesLeft more images are needed until your friends are shared with $username.'; - } - - @override - String userDiscoveryEnabledVersion(Object version) { - return 'Version: $version'; - } - - @override - String userDiscoveryEnabledYourVersion(Object version) { - return 'Your version: $version'; - } - - @override - String get userDiscoveryEnabledStopSharing => 'Stop sharing'; -} diff --git a/lib/src/localization/translations b/lib/src/localization/translations index 82c248ae..8583ecca 160000 --- a/lib/src/localization/translations +++ b/lib/src/localization/translations @@ -1 +1 @@ -Subproject commit 82c248ae18ffda0bf161fbb69ddb3fb30cfc1531 +Subproject commit 8583eccaeba07cd32f11a91767f4d2767518f1be diff --git a/lib/src/model/json/userdata.model.dart b/lib/src/model/json/userdata.model.dart index dc3e7cea..2a1a48a2 100644 --- a/lib/src/model/json/userdata.model.dart +++ b/lib/src/model/json/userdata.model.dart @@ -98,11 +98,17 @@ class UserData { bool isUserDiscoveryEnabled = false; @JsonKey(defaultValue: 4) - int minimumRequiredImagesExchanged = 4; + int requiredSendImages = 4; @JsonKey(defaultValue: 2) int userDiscoveryThreshold = 2; + @JsonKey(defaultValue: false) + bool userDiscoveryRequiresManualApproval = false; + + @JsonKey(defaultValue: true) + bool userDiscoverySharePromotion = true; + // -- Custom DATA -- @JsonKey(defaultValue: 100_000) diff --git a/lib/src/model/json/userdata.model.g.dart b/lib/src/model/json/userdata.model.g.dart index b81f23e3..8b590656 100644 --- a/lib/src/model/json/userdata.model.g.dart +++ b/lib/src/model/json/userdata.model.g.dart @@ -64,10 +64,13 @@ UserData _$UserDataFromJson(Map json) => ..screenLockEnabled = json['screenLockEnabled'] as bool? ?? false ..isUserDiscoveryEnabled = json['isUserDiscoveryEnabled'] as bool? ?? false - ..minimumRequiredImagesExchanged = - (json['minimumRequiredImagesExchanged'] as num?)?.toInt() ?? 4 + ..requiredSendImages = (json['requiredSendImages'] as num?)?.toInt() ?? 4 ..userDiscoveryThreshold = (json['userDiscoveryThreshold'] as num?)?.toInt() ?? 2 + ..userDiscoveryRequiresManualApproval = + json['userDiscoveryRequiresManualApproval'] as bool? ?? false + ..userDiscoverySharePromotion = + json['userDiscoverySharePromotion'] as bool? ?? true ..currentPreKeyIndexStart = (json['currentPreKeyIndexStart'] as num?)?.toInt() ?? 100000 ..currentSignedPreKeyIndexStart = @@ -132,8 +135,11 @@ Map _$UserDataToJson(UserData instance) => { 'allowErrorTrackingViaSentry': instance.allowErrorTrackingViaSentry, 'screenLockEnabled': instance.screenLockEnabled, 'isUserDiscoveryEnabled': instance.isUserDiscoveryEnabled, - 'minimumRequiredImagesExchanged': instance.minimumRequiredImagesExchanged, + 'requiredSendImages': instance.requiredSendImages, 'userDiscoveryThreshold': instance.userDiscoveryThreshold, + 'userDiscoveryRequiresManualApproval': + instance.userDiscoveryRequiresManualApproval, + 'userDiscoverySharePromotion': instance.userDiscoverySharePromotion, 'currentPreKeyIndexStart': instance.currentPreKeyIndexStart, 'currentSignedPreKeyIndexStart': instance.currentSignedPreKeyIndexStart, 'lastChangeLogHash': instance.lastChangeLogHash, diff --git a/lib/src/services/api/client2client/user_discovery.c2c.dart b/lib/src/services/api/client2client/user_discovery.c2c.dart index ba48c29e..0a081db8 100644 --- a/lib/src/services/api/client2client/user_discovery.c2c.dart +++ b/lib/src/services/api/client2client/user_discovery.c2c.dart @@ -48,11 +48,10 @@ Future handleUserDiscoveryRequest( final contact = await twonlyDB.contactsDao.getContactById(fromUserId); if (contact == null) return; - if (contact.mediaSendCounter < - userService.currentUser.minimumRequiredImagesExchanged || + if (contact.mediaSendCounter < userService.currentUser.requiredSendImages || contact.userDiscoveryExcluded) { Log.warn( - 'Got a request to update user discovery, but mediaSendCounter (${contact.mediaSendCounter}) < ${userService.currentUser.minimumRequiredImagesExchanged} or user is excluded ${contact.userDiscoveryExcluded}', + 'Got a request to update user discovery, but mediaSendCounter (${contact.mediaSendCounter}) < ${userService.currentUser.requiredSendImages} or user is excluded ${contact.userDiscoveryExcluded}', ); return; } diff --git a/lib/src/services/api/messages.api.dart b/lib/src/services/api/messages.api.dart index 7cce2ffe..9e33cfcd 100644 --- a/lib/src/services/api/messages.api.dart +++ b/lib/src/services/api/messages.api.dart @@ -354,7 +354,7 @@ Future<(Uint8List, Uint8List?)?> sendCipherText( final contact = await twonlyDB.contactsDao.getContactById(contactId); if (contact != null && contact.mediaSendCounter >= - userService.currentUser.minimumRequiredImagesExchanged && + userService.currentUser.requiredSendImages && !contact.userDiscoveryExcluded) { final version = await UserDiscoveryService.getCurrentVersion(); if (version != null) { diff --git a/lib/src/services/user_discovery.service.dart b/lib/src/services/user_discovery.service.dart index 00d992ce..297671fd 100644 --- a/lib/src/services/user_discovery.service.dart +++ b/lib/src/services/user_discovery.service.dart @@ -48,18 +48,20 @@ class UserDiscoveryService { static Future initializeOrUpdate({ required int threshold, - required int minimumRequiredImagesExchanged, + required bool sharePromotion, }) async { try { await FlutterUserDiscovery.initializeOrUpdate( threshold: threshold, userId: userService.currentUser.userId, publicKey: await getUserPublicKey(), + sharePromotion: sharePromotion, ); await UserService.update( (u) => u ..isUserDiscoveryEnabled = true - ..minimumRequiredImagesExchanged = minimumRequiredImagesExchanged, + ..userDiscoverySharePromotion = sharePromotion + ..userDiscoveryThreshold = threshold, ); } catch (e) { Log.error(e); diff --git a/lib/src/services/user_study.service.dart b/lib/src/services/user_study.service.dart index 0b4e3c61..b1192098 100644 --- a/lib/src/services/user_study.service.dart +++ b/lib/src/services/user_study.service.dart @@ -65,8 +65,8 @@ Future handleUserStudyUpload() async { 'total_contacts': contacts.length, 'user_discovery_enabled': userService.currentUser.isUserDiscoveryEnabled, - 'user_discovery_minimum_images': - userService.currentUser.minimumRequiredImagesExchanged, + 'user_discovery_required_send_images': + userService.currentUser.requiredSendImages, 'user_discovery_threshold': userService.currentUser.userDiscoveryThreshold, diff --git a/lib/src/visual/components/verification_badge.comp.dart b/lib/src/visual/components/verification_badge.comp.dart index ad8c26a0..8a7bc422 100644 --- a/lib/src/visual/components/verification_badge.comp.dart +++ b/lib/src/visual/components/verification_badge.comp.dart @@ -17,6 +17,7 @@ class VerificationBadgeComp extends StatefulWidget { super.key, this.size = 15, this.showOnlyIfVerified = false, + this.isVerifiedByTransferredTrust, this.clickable = true, }); final Group? group; @@ -25,6 +26,7 @@ class VerificationBadgeComp extends StatefulWidget { final bool showOnlyIfVerified; final bool clickable; + final bool? isVerifiedByTransferredTrust; @override State createState() => _VerificationBadgeCompState(); @@ -76,6 +78,10 @@ class _VerificationBadgeCompState extends State { _isVerifiedByTransferredTrust = update.isNotEmpty; }); }); + } else if (widget.isVerifiedByTransferredTrust != null) { + setState(() { + _isVerifiedByTransferredTrust = widget.isVerifiedByTransferredTrust!; + }); } } diff --git a/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_components/camera_bottom_controls.dart b/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_components/camera_bottom_controls.dart index 7434bbeb..412a2f56 100644 --- a/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_components/camera_bottom_controls.dart +++ b/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_components/camera_bottom_controls.dart @@ -39,7 +39,7 @@ class CameraBottomControls extends StatelessWidget { alignment: Alignment.bottomCenter, child: Column( children: [ - if (mc.cameraController!.value.isInitialized && + if (mc.cameraController?.value.isInitialized == true && mc.selectedCameraDetails.isZoomAble && !isVideoRecording) SizedBox( diff --git a/lib/src/visual/views/camera/camera_preview_components/main_camera_controller.dart b/lib/src/visual/views/camera/camera_preview_components/main_camera_controller.dart index 705d1de5..3fc516d8 100644 --- a/lib/src/visual/views/camera/camera_preview_components/main_camera_controller.dart +++ b/lib/src/visual/views/camera/camera_preview_components/main_camera_controller.dart @@ -104,6 +104,10 @@ class MainCameraController { Future selectCamera(int sCameraId, bool init) async { initCameraStarted = true; + if (AppEnvironment.cameras.isEmpty) { + AppEnvironment.cameras = await availableCameras(); + } + var cameraId = sCameraId; if (cameraId >= AppEnvironment.cameras.length) { Log.warn( diff --git a/lib/src/visual/views/chats/chat_list.view.dart b/lib/src/visual/views/chats/chat_list.view.dart index 40e8a866..64aba01d 100644 --- a/lib/src/visual/views/chats/chat_list.view.dart +++ b/lib/src/visual/views/chats/chat_list.view.dart @@ -20,6 +20,7 @@ import 'package:twonly/src/visual/components/notification_badge.comp.dart'; import 'package:twonly/src/visual/themes/light.dart'; import 'package:twonly/src/visual/views/chats/chat_list_components/feedback_btn.comp.dart'; import 'package:twonly/src/visual/views/chats/chat_list_components/group_list_item.comp.dart'; +import 'package:twonly/src/visual/views/onboarding/setup/components/finish_setup.comp.dart'; class ChatListView extends StatefulWidget { const ChatListView({super.key}); @@ -211,71 +212,80 @@ class _ChatListViewState extends State { await apiService.connect(); await Future.delayed(const Duration(seconds: 1)); }, - child: - (_groupsNotPinned.isEmpty && + child: Column( + children: [ + const FinishSetupComp(), + if (_groupsNotPinned.isEmpty && _groupsPinned.isEmpty && _groupsArchived.isEmpty) - ? Center( - child: Padding( - padding: const EdgeInsets.all(10), - child: FilledButton.icon( - icon: const Icon(Icons.person_add), - onPressed: () => context.push(Routes.chatsAddNewUser), - label: Text( - context.lang.chatListViewSearchUserNameBtn, + Expanded( + child: Center( + child: Padding( + padding: const EdgeInsets.all(10), + child: FilledButton.icon( + icon: const Icon(Icons.person_add), + onPressed: () => context.push(Routes.chatsAddNewUser), + label: Text( + context.lang.chatListViewSearchUserNameBtn, + ), ), ), ), ) - : ListView.builder( - itemCount: - _groupsPinned.length + - (_groupsPinned.isNotEmpty ? 1 : 0) + - _groupsNotPinned.length + - (_groupsArchived.isNotEmpty ? 1 : 0), - itemBuilder: (context, index) { - if (index >= + else + Expanded( + child: ListView.builder( + itemCount: + _groupsPinned.length + + (_groupsPinned.isNotEmpty ? 1 : 0) + _groupsNotPinned.length + - _groupsPinned.length + - (_groupsPinned.isNotEmpty ? 1 : 0)) { - if (_groupsArchived.isEmpty) return Container(); - return ListTile( - title: Text( - '${context.lang.archivedChats} (${_groupsArchived.length})', - textAlign: TextAlign.center, - style: const TextStyle(fontSize: 13), - ), - onTap: () => context.push(Routes.chatsArchived), + (_groupsArchived.isNotEmpty ? 1 : 0), + itemBuilder: (context, index) { + if (index >= + _groupsNotPinned.length + + _groupsPinned.length + + (_groupsPinned.isNotEmpty ? 1 : 0)) { + if (_groupsArchived.isEmpty) return Container(); + return ListTile( + title: Text( + '${context.lang.archivedChats} (${_groupsArchived.length})', + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 13), + ), + onTap: () => context.push(Routes.chatsArchived), + ); + } + // Check if the index is for the pinned users + if (index < _groupsPinned.length) { + final group = _groupsPinned[index]; + return GroupListItemComp( + key: ValueKey(group.groupId), + group: group, + ); + } + + // If there are pinned users, account for the Divider + var adjustedIndex = index - _groupsPinned.length; + if (_groupsPinned.isNotEmpty && adjustedIndex == 0) { + return const Divider(); + } + + // Adjust the index for the contacts list + adjustedIndex -= (_groupsPinned.isNotEmpty ? 1 : 0); + + // Get the contacts that are not pinned + final group = _groupsNotPinned.elementAt( + adjustedIndex, ); - } - // Check if the index is for the pinned users - if (index < _groupsPinned.length) { - final group = _groupsPinned[index]; return GroupListItemComp( key: ValueKey(group.groupId), group: group, ); - } - - // If there are pinned users, account for the Divider - var adjustedIndex = index - _groupsPinned.length; - if (_groupsPinned.isNotEmpty && adjustedIndex == 0) { - return const Divider(); - } - - // Adjust the index for the contacts list - adjustedIndex -= (_groupsPinned.isNotEmpty ? 1 : 0); - - // Get the contacts that are not pinned - final group = _groupsNotPinned.elementAt( - adjustedIndex, - ); - return GroupListItemComp( - key: ValueKey(group.groupId), - group: group, - ); - }, + }, + ), ), + ], + ), ), floatingActionButton: Padding( padding: const EdgeInsets.only(bottom: 30), diff --git a/lib/src/visual/views/contact/add_new_contact_components/friend_suggestions.comp.dart b/lib/src/visual/views/contact/add_new_contact_components/friend_suggestions.comp.dart index 85342ba4..16055b37 100644 --- a/lib/src/visual/views/contact/add_new_contact_components/friend_suggestions.comp.dart +++ b/lib/src/visual/views/contact/add_new_contact_components/friend_suggestions.comp.dart @@ -17,7 +17,15 @@ List buildFriendsListText( BuildContext context, List<(Contact, DateTime?)> friends, ) { - final names = friends.map((f) => '*${getContactDisplayName(f.$1)}*').toList(); + final names = friends.map((f) => getContactDisplayName(f.$1)).toList(); + return buildFriendsListTextString(context, names); +} + +List buildFriendsListTextString( + BuildContext context, + List friends, +) { + final names = friends.map((f) => '*$f*').toList(); return formattedText( context, diff --git a/lib/src/visual/views/contact/contact.view.dart b/lib/src/visual/views/contact/contact.view.dart index 6fe9e83b..1a2353c7 100644 --- a/lib/src/visual/views/contact/contact.view.dart +++ b/lib/src/visual/views/contact/contact.view.dart @@ -312,10 +312,10 @@ class _ContactViewState extends State { subtitle: !contact.userDiscoveryExcluded && contact.mediaSendCounter < - userService.currentUser.minimumRequiredImagesExchanged + userService.currentUser.requiredSendImages ? Text( context.lang.contactUserDiscoveryImagesLeft( - userService.currentUser.minimumRequiredImagesExchanged - + userService.currentUser.requiredSendImages - contact.mediaSendCounter, getContactDisplayName(contact), ), diff --git a/lib/src/visual/views/onboarding/setup.view.dart b/lib/src/visual/views/onboarding/setup.view.dart index 3ac2b44d..47306df0 100644 --- a/lib/src/visual/views/onboarding/setup.view.dart +++ b/lib/src/visual/views/onboarding/setup.view.dart @@ -1,7 +1,10 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/views/onboarding/setup/add_new_contacts_setup.view.dart'; import 'package:twonly/src/visual/views/onboarding/setup/backup_setup.view.dart'; import 'package:twonly/src/visual/views/onboarding/setup/profile_setup.view.dart'; import 'package:twonly/src/visual/views/onboarding/setup/user_discovery_setup.view.dart'; @@ -10,16 +13,26 @@ import 'package:twonly/src/visual/views/onboarding/setup/verification_badge_setu enum SetupPages { profile, backup, + addNewContact, verificationBadge, userDiscovery, } extension SetupPagesExtension on SetupPages { + static SetupPages fromStr(String? name) { + return SetupPages.values.firstWhere( + (e) => e.name == name, + orElse: () => SetupPages.profile, + ); + } + int get pageNumber => index + 1; int get totalPages => SetupPages.values.length; int get progressPercentage => (pageNumber / totalPages * 100).round(); String get progressText => '$pageNumber / $totalPages'; + bool get isLast => index == SetupPages.values.length - 1; + SetupPages? next() { final nextIndex = index + 1; if (nextIndex < SetupPages.values.length) { @@ -39,6 +52,26 @@ class SetupView extends StatefulWidget { } class _SetupViewState extends State { + StreamSubscription? _userUpdateStream; + + @override + void initState() { + super.initState(); + if (widget.onUpdate != null) { + _userUpdateStream = userService.onUserUpdated.listen((u) { + if (userService.currentUser.currentSetupPage == null) { + widget.onUpdate?.call(); + } + }); + } + } + + @override + void dispose() { + super.dispose(); + _userUpdateStream?.cancel(); + } + @override Widget build(BuildContext context) { return StreamBuilder( @@ -46,11 +79,8 @@ class _SetupViewState extends State { builder: (context, snapshot) { final user = userService.currentUser; final currentPageString = user.currentSetupPage; - - final currentPage = SetupPages.values.firstWhere( - (e) => e.name == currentPageString, - orElse: () => SetupPages.profile, - ); + if (currentPageString == null) return const SizedBox.shrink(); + final currentPage = SetupPagesExtension.fromStr(currentPageString); return Scaffold( appBar: AppBar( @@ -82,32 +112,32 @@ class _SetupViewState extends State { toolbarHeight: 48, ), body: ListView( - padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 12), + key: ValueKey(currentPage.name), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 30), children: [ _buildPage(currentPage), - SizedBox( - height: 50, - child: Center( - child: TextButton( - onPressed: () async { - await UserService.update((u) { - u - ..skipSetupPages = false - ..currentSetupPage = SetupPages.profile.name; - }); - //await UserService.update((u) => u.skipSetupPages = true); - widget.onUpdate?.call(); - }, - child: Text( - context.lang.onboardingFinishLater, - style: TextStyle( - color: context.color.primary, - fontWeight: FontWeight.bold, + if (!currentPage.isLast) + SizedBox( + height: 50, + child: Center( + child: TextButton( + onPressed: () async { + await UserService.update( + (u) => u.skipSetupPages = true, + ); + widget.onUpdate?.call(); + }, + child: Text( + context.lang.onboardingFinishLater, + style: TextStyle( + color: context.color.primary, + fontWeight: FontWeight.bold, + ), ), ), ), ), - ), + const SizedBox(height: 60), ], ), ); @@ -121,6 +151,8 @@ class _SetupViewState extends State { return const ProfileSetupPage(); case SetupPages.backup: return const BackupSetupPage(); + case SetupPages.addNewContact: + return const AddNewContactsPage(); case SetupPages.verificationBadge: return const VerificationBadgeSetupPage(); case SetupPages.userDiscovery: diff --git a/lib/src/visual/views/onboarding/setup/add_new_contacts_setup.view.dart b/lib/src/visual/views/onboarding/setup/add_new_contacts_setup.view.dart new file mode 100644 index 00000000..f50687d4 --- /dev/null +++ b/lib/src/visual/views/onboarding/setup/add_new_contacts_setup.view.dart @@ -0,0 +1,130 @@ +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/components/avatar_icon.comp.dart'; +import 'package:twonly/src/visual/views/onboarding/setup/components/next_button.comp.dart'; +import 'components/mock_contact_request_actions.comp.dart'; + +class AddNewContactsPage extends StatelessWidget { + const AddNewContactsPage({super.key}); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + context.lang.onboardingAddContactsTitle, + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + context.lang.onboardingAddContactsAcceptDesc, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: context.color.onSurfaceVariant, + ), + ), + const SizedBox(height: 32), + + Card( + elevation: 0, + color: context.color.surfaceContainerLow, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: const Padding( + padding: EdgeInsets.all(12), + child: Row( + children: [ + AvatarIcon(fontSize: 16), + SizedBox(width: 12), + Expanded( + child: Text( + 'max', + style: TextStyle(fontWeight: FontWeight.bold), + ), + ), + MockContactRequestActionsComp(), + ], + ), + ), + ), + + const SizedBox(height: 32), + + Text( + context.lang.onboardingAddContactsMethodHeading, + style: const TextStyle(fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ), + const SizedBox(height: 20), + + Padding( + padding: const EdgeInsets.symmetric(horizontal: 40), + + child: Column( + children: [ + // List of ways to add contacts + _buildMethodItem( + context, + icon: FontAwesomeIcons.qrcode, + text: context.lang.onboardingAddContactsMethodScan, + ), + _buildMethodItem( + context, + icon: FontAwesomeIcons.magnifyingGlass, + text: context.lang.onboardingAddContactsMethodSearch, + ), + _buildMethodItem( + context, + icon: FontAwesomeIcons.shareNodes, + text: context.lang.onboardingAddContactsMethodShare, + ), + ], + ), + ), + + const SizedBox(height: 60), + const NextButtonComp(), + ], + ); + } + + Widget _buildMethodItem( + BuildContext context, { + required IconData icon, + required String text, + }) { + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: context.color.primary.withValues(alpha: 0.1), + shape: BoxShape.circle, + ), + child: FaIcon( + icon, + size: 20, + color: context.color.primary, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Text( + text, + style: const TextStyle(fontSize: 14), + ), + ), + ], + ), + ); + } +} diff --git a/lib/src/visual/views/onboarding/setup/backup_setup.view.dart b/lib/src/visual/views/onboarding/setup/backup_setup.view.dart index c2dbc6f6..1ad0ab41 100644 --- a/lib/src/visual/views/onboarding/setup/backup_setup.view.dart +++ b/lib/src/visual/views/onboarding/setup/backup_setup.view.dart @@ -2,12 +2,14 @@ import 'package:flutter/foundation.dart'; 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/services/backup/common.backup.dart'; import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/components/alert.dialog.dart'; import 'package:twonly/src/visual/views/onboarding/setup.view.dart'; +import 'package:twonly/src/visual/views/onboarding/setup/components/next_button.comp.dart'; import 'package:twonly/src/visual/views/settings/backup/components/backup_setup.comp.dart'; class BackupSetupPage extends StatefulWidget { @@ -22,13 +24,26 @@ class _BackupSetupPageState extends State { final TextEditingController passwordCtrl = TextEditingController(); final TextEditingController repeatedPasswordCtrl = TextEditingController(); - Future onPressedEnableTwonlySafe() async { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (userService.currentUser.twonlySafeBackup != null) { + // twonly safe is already configured... + UserService.update((user) { + user.currentSetupPage = SetupPages.backup.next()?.name; + }); + } + }); + } + + Future onPressedEnableTwonlySafe() async { setState(() { isLoading = true; }); if (!await isSecurePassword(passwordCtrl.text)) { - if (!mounted) return; + if (!mounted) return true; final ignore = await showAlertDialog( context, context.lang.backupInsecurePassword, @@ -36,12 +51,12 @@ class _BackupSetupPageState extends State { customCancel: context.lang.backupInsecurePasswordOk, customOk: context.lang.backupInsecurePasswordCancel, ); - if (!mounted) return; + if (!mounted) return true; if (ignore) { setState(() { isLoading = false; }); - return; + return true; } } @@ -52,10 +67,11 @@ class _BackupSetupPageState extends State { user.currentSetupPage = SetupPages.backup.next()?.name; }); - if (!mounted) return; + if (!mounted) return true; setState(() { isLoading = false; }); + return false; } @override @@ -77,7 +93,6 @@ class _BackupSetupPageState extends State { return Column( mainAxisSize: MainAxisSize.min, children: [ - const SizedBox(height: 20), Text( 'twonly Backup', style: Theme.of(context).textTheme.headlineSmall?.copyWith( @@ -137,34 +152,11 @@ class _BackupSetupPageState extends State { child: Text(context.lang.backupExpertSettings), ), ), - const SizedBox(height: 10), - ElevatedButton( - onPressed: canSubmit ? onPressedEnableTwonlySafe : null, - style: ElevatedButton.styleFrom( - minimumSize: const Size(double.infinity, 56), - backgroundColor: context.color.primary, - foregroundColor: context.color.onPrimary, - elevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - ), - child: isLoading - ? const SizedBox( - height: 24, - width: 24, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation(Colors.white), - ), - ) - : Text( - context.lang.next, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), + const SizedBox(height: 40), + NextButtonComp( + isLoading: isLoading, + canSubmit: canSubmit, + onPressed: onPressedEnableTwonlySafe, ), ], ); diff --git a/lib/src/visual/views/onboarding/setup/components/finish_setup.comp.dart b/lib/src/visual/views/onboarding/setup/components/finish_setup.comp.dart new file mode 100644 index 00000000..a57a00d8 --- /dev/null +++ b/lib/src/visual/views/onboarding/setup/components/finish_setup.comp.dart @@ -0,0 +1,161 @@ +import 'package:flutter/material.dart'; +import 'package:twonly/locator.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/views/onboarding/setup.view.dart'; + +class FinishSetupComp extends StatefulWidget { + const FinishSetupComp({super.key}); + + @override + State createState() => _FinishSetupCompState(); +} + +class _FinishSetupCompState extends State { + Future onTap() async { + await context.navPush( + SetupView( + onUpdate: () { + Navigator.pop(context); + }, + ), + ); + } + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: userService.onUserUpdated, + builder: (context, snapshot) { + final user = userService.currentUser; + + if (user.currentSetupPage == null) { + return const SizedBox.shrink(); + } + + final currentPage = SetupPagesExtension.fromStr(user.currentSetupPage); + final progress = currentPage.progressPercentage / 100; + + return Container( + margin: const EdgeInsets.all(16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(24), + gradient: LinearGradient( + colors: [ + context.color.primaryContainer.withValues(alpha: 0.2), + context.color.primaryContainer.withValues(alpha: 0.1), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + border: Border.all( + color: context.color.primary.withValues(alpha: 0.15), + width: 1.5, + ), + boxShadow: [ + BoxShadow( + color: context.color.shadow.withValues(alpha: 0.05), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(24), + clipBehavior: Clip.antiAlias, + child: InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + SizedBox( + width: 68, + height: 68, + child: Stack( + fit: StackFit.expand, + children: [ + CircularProgressIndicator( + value: progress, + strokeWidth: 6, + backgroundColor: context.color.onSurface.withValues( + alpha: 0.12, + ), + color: context.color.primary, + strokeCap: StrokeCap.round, + ), + Center( + child: Text( + '${currentPage.progressPercentage}%', + style: TextStyle( + fontWeight: FontWeight.w800, + fontSize: 15, + color: context.color.primary, + ), + ), + ), + ], + ), + ), + const SizedBox(width: 20), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.lang.finishSetupCardTitle, + style: TextStyle( + fontWeight: FontWeight.w900, + fontSize: 17, + color: context.color.onSurface, + letterSpacing: -0.2, + ), + ), + const SizedBox(height: 6), + Text( + context.lang.finishSetupCardDesc, + style: TextStyle( + fontSize: 13, + color: context.color.onSurfaceVariant, + height: 1.3, + ), + ), + const SizedBox(height: 14), + FilledButton.icon( + onPressed: onTap, + icon: const Icon( + Icons.arrow_forward_rounded, + size: 18, + ), + label: Text( + context.lang.finishSetupCardAction, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + style: FilledButton.styleFrom( + backgroundColor: context.color.primary, + foregroundColor: context.color.onPrimary, + minimumSize: const Size(0, 40), + padding: const EdgeInsets.symmetric( + horizontal: 16, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 0, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/src/visual/views/onboarding/setup/components/mock_contact_request_actions.comp.dart b/lib/src/visual/views/onboarding/setup/components/mock_contact_request_actions.comp.dart new file mode 100644 index 00000000..5599da39 --- /dev/null +++ b/lib/src/visual/views/onboarding/setup/components/mock_contact_request_actions.comp.dart @@ -0,0 +1,121 @@ +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/themes/light.dart'; + +class MockContactRequestActionsComp extends StatelessWidget { + const MockContactRequestActionsComp({super.key}); + + @override + Widget build(BuildContext context) { + return SizedBox( + // width: 125, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(width: 4), + SizedBox( + height: 20, + // width: 45, + child: FilledButton( + style: FilledButton.styleFrom( + padding: const EdgeInsets.only(right: 2, left: 4), + backgroundColor: context.color.surfaceContainerHigh, + foregroundColor: context.color.onSurface, + ), + onPressed: () {}, + child: Row( + children: [ + const Icon( + Icons.person_off_rounded, + color: Color.fromARGB(164, 244, 67, 54), + size: 12, + ), + Text( + context.lang.contactActionBlock, + style: const TextStyle(fontSize: 8), + ), + ], + ), + ), + ), + const SizedBox(width: 6), + SizedBox( + height: 20, + // width: 50, + child: FilledButton( + style: FilledButton.styleFrom( + padding: const EdgeInsets.only(right: 2, left: 4), + backgroundColor: context.color.surfaceContainerHigh, + foregroundColor: context.color.onSurface, + ), + onPressed: () {}, + child: Row( + children: [ + const Icon(Icons.check, color: Colors.green, size: 12), + Text( + context.lang.contactActionAccept, + style: const TextStyle(fontSize: 8), + ), + ], + ), + ), + ), + IconButton( + style: IconButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 2), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + constraints: const BoxConstraints(), + icon: const Icon(Icons.close, size: 12), + onPressed: () {}, + ), + ], + ), + ); + } +} + +class MockContactSuggestedActionsComp extends StatelessWidget { + const MockContactSuggestedActionsComp({super.key}); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(width: 4), + SizedBox( + height: 20, + child: FilledButton( + style: FilledButton.styleFrom( + padding: const EdgeInsets.only(right: 8, left: 4), + ).merge(secondaryGreyButtonStyle(context)), + onPressed: () {}, + child: Row( + children: [ + const Padding( + padding: EdgeInsets.symmetric(horizontal: 6), + child: FaIcon(FontAwesomeIcons.userPlus, size: 10), + ), + Text( + context.lang.friendSuggestionsRequest, + style: const TextStyle(fontSize: 8), + ), + ], + ), + ), + ), + IconButton( + style: IconButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 2), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + constraints: const BoxConstraints(), + icon: const Icon(Icons.close, size: 12), + onPressed: () {}, + ), + ], + ); + } +} diff --git a/lib/src/visual/views/onboarding/setup/components/next_button.comp.dart b/lib/src/visual/views/onboarding/setup/components/next_button.comp.dart new file mode 100644 index 00000000..40f22be4 --- /dev/null +++ b/lib/src/visual/views/onboarding/setup/components/next_button.comp.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:twonly/locator.dart'; +import 'package:twonly/src/services/user.service.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/views/onboarding/setup.view.dart'; + +class NextButtonComp extends StatelessWidget { + const NextButtonComp({ + this.onPressed, + this.canSubmit = true, + this.isLoading = false, + super.key, + }); + + final Future Function()? onPressed; + final bool isLoading; + final bool canSubmit; + + @override + Widget build(BuildContext context) { + final currentPage = SetupPagesExtension.fromStr( + userService.currentUser.currentSetupPage, + ); + return ElevatedButton( + onPressed: canSubmit + ? () async { + if (onPressed != null) { + final error = await onPressed?.call(); + if (error == true) return; + } + await UserService.update((user) { + user.currentSetupPage = currentPage.next()?.name; + }); + } + : null, + style: ElevatedButton.styleFrom( + minimumSize: const Size(double.infinity, 56), + backgroundColor: context.color.primary, + foregroundColor: context.color.onPrimary, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + ), + child: isLoading + ? const SizedBox( + height: 24, + width: 24, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ) + : Text( + currentPage.isLast ? context.lang.finishSetup : context.lang.next, + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + ); + } +} diff --git a/lib/src/visual/views/onboarding/setup/components/setup_switch_card.comp.dart b/lib/src/visual/views/onboarding/setup/components/setup_switch_card.comp.dart new file mode 100644 index 00000000..168a9a98 --- /dev/null +++ b/lib/src/visual/views/onboarding/setup/components/setup_switch_card.comp.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:twonly/src/utils/misc.dart'; + +class SetupSwitchCard extends StatelessWidget { + const SetupSwitchCard({ + required this.value, + required this.onChanged, + required this.title, + required this.expandedChild, + this.openIfNot = false, + super.key, + }); + + final bool value; + final bool openIfNot; + final ValueChanged onChanged; + final String title; + final Widget expandedChild; + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: context.color.surfaceContainerLow, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SwitchListTile( + value: value, + onChanged: onChanged, + title: Text( + title, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + tileColor: context.color.surfaceContainerLow, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 16), + ), + AnimatedSize( + duration: const Duration(milliseconds: 150), + curve: Curves.bounceInOut, + alignment: Alignment.topCenter, + child: SizedBox( + width: double.infinity, + child: (value && !openIfNot) || (!value && openIfNot) + ? Container( + decoration: BoxDecoration( + color: context.color.surfaceContainerLow, + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.only(top: 8), + child: expandedChild, + ), + ) + : const SizedBox(height: 0), + ), + ), + ], + ), + ); + } +} diff --git a/lib/src/visual/views/onboarding/setup/finish_setup.comp.dart b/lib/src/visual/views/onboarding/setup/finish_setup.comp.dart deleted file mode 100644 index deff6591..00000000 --- a/lib/src/visual/views/onboarding/setup/finish_setup.comp.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:flutter/material.dart'; - -class FinishSetupComp extends StatefulWidget { - const FinishSetupComp({super.key}); - - @override - State createState() => _FinishSetupCompState(); -} - -class _FinishSetupCompState extends State { - @override - Widget build(BuildContext context) { - return const Placeholder(); - } -} diff --git a/lib/src/visual/views/onboarding/setup/profile_setup.view.dart b/lib/src/visual/views/onboarding/setup/profile_setup.view.dart index 344a34a0..c352055e 100644 --- a/lib/src/visual/views/onboarding/setup/profile_setup.view.dart +++ b/lib/src/visual/views/onboarding/setup/profile_setup.view.dart @@ -6,8 +6,8 @@ import 'package:twonly/locator.dart'; import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/views/onboarding/setup/components/next_button.comp.dart'; import 'package:vector_graphics/vector_graphics.dart'; -import '../setup.view.dart'; class ProfileSetupPage extends StatefulWidget { const ProfileSetupPage({super.key}); @@ -24,9 +24,7 @@ class _ProfileSetupPageState extends State { @override void initState() { super.initState(); - _displayNameController = TextEditingController( - text: userService.currentUser.displayName, - ); + _displayNameController = TextEditingController(); } @override @@ -55,32 +53,39 @@ class _ProfileSetupPageState extends State { ), ), const SizedBox(height: 40), - Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all( - color: context.color.primary.withValues(alpha: 0.2), - width: 4, - ), - ), - child: userService.currentUser.avatarSvg == null - ? ClipRRect( - borderRadius: BorderRadius.circular(80), - child: Container( - width: 160, - height: 160, - color: context.color.surfaceContainer, - child: const SvgPicture( - AssetBytesLoader('assets/images/default_avatar.svg.vec'), - ), - ), - ) - : AvatarMakerAvatar( - backgroundColor: context.color.surfaceContainer, - radius: 80, - controller: _avatarMakerController, + StreamBuilder( + stream: userService.onUserUpdated, + builder: (context, asyncSnapshot) { + return Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: context.color.primary.withValues(alpha: 0.2), + width: 4, ), + ), + child: userService.currentUser.avatarSvg == null + ? ClipRRect( + borderRadius: BorderRadius.circular(80), + child: Container( + width: 160, + height: 160, + color: context.color.surfaceContainer, + child: const SvgPicture( + AssetBytesLoader( + 'assets/images/default_avatar.svg.vec', + ), + ), + ), + ) + : AvatarMakerAvatar( + backgroundColor: context.color.surfaceContainer, + radius: 80, + controller: _avatarMakerController, + ), + ); + }, ), const SizedBox(height: 16), TextButton.icon( @@ -108,31 +113,15 @@ class _ProfileSetupPageState extends State { ), ), const SizedBox(height: 40), - ElevatedButton( + NextButtonComp( onPressed: () async { await UserService.update((user) { if (_displayNameController.text.isNotEmpty) { user.displayName = _displayNameController.text; } - user.currentSetupPage = SetupPages.profile.next()?.name; }); + return false; }, - style: ElevatedButton.styleFrom( - minimumSize: const Size(double.infinity, 56), - backgroundColor: context.color.primary, - foregroundColor: context.color.onPrimary, - elevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - ), - child: Text( - context.lang.next, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), ), ], ); diff --git a/lib/src/visual/views/onboarding/setup/user_discovery_setup.view.dart b/lib/src/visual/views/onboarding/setup/user_discovery_setup.view.dart index 54533cf5..5cf9423e 100644 --- a/lib/src/visual/views/onboarding/setup/user_discovery_setup.view.dart +++ b/lib/src/visual/views/onboarding/setup/user_discovery_setup.view.dart @@ -1,11 +1,35 @@ import 'package:flutter/material.dart'; +import 'package:twonly/locator.dart'; import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/utils/misc.dart'; -import '../setup.view.dart'; +import 'package:twonly/src/visual/views/onboarding/setup.view.dart'; +import 'package:twonly/src/visual/views/onboarding/setup/components/next_button.comp.dart'; +import 'package:twonly/src/visual/views/settings/privacy/user_discovery/components/user_discovery_setup.comp.dart'; -class UserDiscoverySetupPage extends StatelessWidget { +class UserDiscoverySetupPage extends StatefulWidget { const UserDiscoverySetupPage({super.key}); + @override + State createState() => _UserDiscoverySetupPageState(); +} + +class _UserDiscoverySetupPageState extends State { + late UserDiscoverySetupState state; + + @override + void initState() { + super.initState(); + state = UserDiscoverySetupState(setState: setState); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (userService.currentUser.isUserDiscoveryEnabled) { + // feature is already configured... + UserService.update((user) { + user.currentSetupPage = SetupPages.userDiscovery.next()?.name; + }); + } + }); + } + @override Widget build(BuildContext context) { return Center( @@ -13,22 +37,16 @@ class UserDiscoverySetupPage extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - context.lang.onboardingUserDiscoveryTitle, + context.lang.onboardingUserDiscoveryShareFriends, style: Theme.of(context).textTheme.headlineSmall, ), const SizedBox(height: 32), - ElevatedButton( + UserDiscoverySetupComp(state: state), + const SizedBox(height: 60), + NextButtonComp( onPressed: () async { - await UserService.update((user) { - user.currentSetupPage = SetupPages.profile.name; - }); + return !(await state.initializeOrUpdate()); }, - style: ElevatedButton.styleFrom( - minimumSize: const Size(200, 50), - backgroundColor: context.color.primary, - foregroundColor: context.color.onPrimary, - ), - child: Text(context.lang.onboardingResetSetup), ), ], ), diff --git a/lib/src/visual/views/onboarding/setup/verification_badge_setup.view.dart b/lib/src/visual/views/onboarding/setup/verification_badge_setup.view.dart index 4a9eaac5..555dfb4d 100644 --- a/lib/src/visual/views/onboarding/setup/verification_badge_setup.view.dart +++ b/lib/src/visual/views/onboarding/setup/verification_badge_setup.view.dart @@ -1,8 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/components/verification_badge_info.comp.dart'; -import 'package:twonly/src/visual/views/onboarding/setup.view.dart'; +import 'package:twonly/src/visual/views/onboarding/setup/components/next_button.comp.dart'; class VerificationBadgeSetupPage extends StatelessWidget { const VerificationBadgeSetupPage({super.key}); @@ -20,30 +19,8 @@ class VerificationBadgeSetupPage extends StatelessWidget { ), const SizedBox(height: 30), const VerificationBadgeInfo(), - const SizedBox(height: 40), - ElevatedButton( - onPressed: () async { - await UserService.update((user) { - user.currentSetupPage = SetupPages.verificationBadge.next()?.name; - }); - }, - style: ElevatedButton.styleFrom( - minimumSize: const Size(double.infinity, 56), - backgroundColor: context.color.primary, - foregroundColor: context.color.onPrimary, - elevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - ), - child: Text( - context.lang.understood, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - ), + const SizedBox(height: 60), + const NextButtonComp(), ], ); } diff --git a/lib/src/visual/views/settings/privacy/user_discovery/components/user_discovery_disabled.comp.dart b/lib/src/visual/views/settings/privacy/user_discovery/components/user_discovery_disabled.comp.dart index dd1c991f..ea261664 100644 --- a/lib/src/visual/views/settings/privacy/user_discovery/components/user_discovery_disabled.comp.dart +++ b/lib/src/visual/views/settings/privacy/user_discovery/components/user_discovery_disabled.comp.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:twonly/src/services/user_discovery.service.dart'; import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/visual/themes/light.dart'; +import 'package:twonly/src/visual/views/settings/privacy/user_discovery/components/user_discovery_setup.comp.dart'; class UserDiscoveryDisabledComp extends StatefulWidget { const UserDiscoveryDisabledComp({super.key}); @@ -12,11 +11,12 @@ class UserDiscoveryDisabledComp extends StatefulWidget { } class _UserDiscoveryDisabledCompState extends State { - Future initializeUserDiscoveryWithDefaultSettings() async { - await UserDiscoveryService.initializeOrUpdate( - threshold: 2, - minimumRequiredImagesExchanged: 4, - ); + late UserDiscoverySetupState state; + + @override + void initState() { + super.initState(); + state = UserDiscoverySetupState(setState: setState); } @override @@ -25,67 +25,51 @@ class _UserDiscoveryDisabledCompState extends State { padding: const EdgeInsets.symmetric(horizontal: 40), child: ListView( children: [ - const SizedBox(height: 45), - Text( - context.lang.userDiscoveryDisabledIntro, - textAlign: TextAlign.center, - ), - const SizedBox(height: 30), - RichText( - text: TextSpan( - children: formattedText( - context, - context.lang.userDiscoveryDisabledInvisible, - ), + const SizedBox(height: 60), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: context.color.surfaceContainerHigh, + borderRadius: BorderRadius.circular(16), ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 35), - Text( - context.lang.userDiscoveryDisabledYouHaveControl, - style: const TextStyle(fontSize: 17), - textAlign: TextAlign.center, - ), - const SizedBox(height: 20), - Text( - context.lang.userDiscoveryDisabledDecide, - textAlign: TextAlign.center, - ), - - const SizedBox(height: 50), - - FilledButton( - onPressed: initializeUserDiscoveryWithDefaultSettings, - style: primaryColorButtonStyle.merge( - FilledButton.styleFrom( - padding: const EdgeInsets.symmetric( - horizontal: 32, - vertical: 24, + child: Row( + children: [ + Icon( + Icons.info_rounded, + color: context.color.onSurfaceVariant, ), + const SizedBox(width: 16), + Expanded( + child: Text( + context.lang.userDiscoverySettingsCurrentlyDisabled, + style: TextStyle( + color: context.color.onSurfaceVariant, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ), + const SizedBox(height: 60), + UserDiscoverySetupComp(state: state), + const SizedBox(height: 60), + ElevatedButton( + onPressed: () { + state.initializeOrUpdate(); + }, + style: ElevatedButton.styleFrom( + minimumSize: const Size(double.infinity, 56), + backgroundColor: context.color.primary, + foregroundColor: context.color.onPrimary, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), ), ), - child: Text(context.lang.userDiscoveryDisabledEnableWithDefault), - ), - - const SizedBox(height: 20), - - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: FilledButton( - onPressed: () {}, - style: secondaryGreyButtonStyle(context), - child: Text(context.lang.userDiscoveryDisabledCustomizeSettings), - ), - ), - const SizedBox(height: 15), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: FilledButton( - onPressed: () {}, - style: secondaryGreyButtonStyle(context), - child: Text(context.lang.userDiscoveryDisabledLearnMore), - ), + child: Text(context.lang.userDiscoverySettingsApply), ), + const SizedBox(height: 60), ], ), ); diff --git a/lib/src/visual/views/settings/privacy/user_discovery/components/user_discovery_setup.comp.dart b/lib/src/visual/views/settings/privacy/user_discovery/components/user_discovery_setup.comp.dart new file mode 100644 index 00000000..5484031c --- /dev/null +++ b/lib/src/visual/views/settings/privacy/user_discovery/components/user_discovery_setup.comp.dart @@ -0,0 +1,535 @@ +import 'package:flutter/material.dart'; +import 'package:twonly/locator.dart'; +import 'package:twonly/src/services/user.service.dart'; +import 'package:twonly/src/services/user_discovery.service.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/components/avatar_icon.comp.dart'; +import 'package:twonly/src/visual/components/verification_badge.comp.dart'; +import 'package:twonly/src/visual/views/contact/add_new_contact_components/friend_suggestions.comp.dart'; +import 'package:twonly/src/visual/views/onboarding/setup/components/mock_contact_request_actions.comp.dart'; +import 'package:twonly/src/visual/views/onboarding/setup/components/setup_switch_card.comp.dart'; + +const exampleUsers = [ + 'james', + 'john', + 'robert', + 'michael', + 'william', + 'david', + 'mary', + 'patricia', + 'jennifer', + 'linda', +]; + +class UserDiscoverySetupState { + UserDiscoverySetupState({ + required this.setState, + this.isUserDiscoveryEnabled = true, + this.sharePromotion = true, + this.isShareAllContacts = false, + this.isManualApprovalEnabled = false, + this.threshold = 2, + this.requiredSendImages = 4, + }); + + bool wasChanged = false; + + bool isUserDiscoveryEnabled; + int threshold; + bool sharePromotion; + + bool isShareAllContacts; + bool isManualApprovalEnabled; + int requiredSendImages; + + void Function(void Function()) setState; + + void update(void Function() update) { + update(); + setState(() { + wasChanged = true; + }); + } + + Future initializeOrUpdate() async { + if (isShareAllContacts) { + requiredSendImages = 0; + isManualApprovalEnabled = false; + } + + if (isUserDiscoveryEnabled) { + await UserDiscoveryService.initializeOrUpdate( + threshold: threshold, + sharePromotion: sharePromotion, + ); + } + + await UserService.update((u) { + u + ..isUserDiscoveryEnabled = isUserDiscoveryEnabled + ..requiredSendImages = requiredSendImages + ..userDiscoveryRequiresManualApproval = isManualApprovalEnabled; + }); + + return true; + } +} + +class UserDiscoverySetupComp extends StatelessWidget { + const UserDiscoverySetupComp({ + required this.state, + super.key, + }); + + final UserDiscoverySetupState state; + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + RichText( + text: TextSpan( + children: formattedText( + context, + context.lang.userDiscoveryDisabledIntro, + ), + ), + textAlign: TextAlign.center, + ), + + const SizedBox(height: 80), + + Text( + context.lang.onboardingUserDiscoveryIncreaseTrust, + style: const TextStyle( + fontSize: 17, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + + const SizedBox(height: 8), + + RichText( + text: TextSpan( + children: formattedText( + context, + context.lang.onboardingUserDiscoveryShareFriendsDesc, + ), + ), + textAlign: TextAlign.center, + ), + + const SizedBox(height: 24), + + SetupSwitchCard( + value: state.isUserDiscoveryEnabled, + onChanged: (val) => state.update(() { + state.isUserDiscoveryEnabled = val; + if (!val) { + state.sharePromotion = false; + } + }), + title: context.lang.onboardingUserDiscoveryShareFriends, + expandedChild: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + context.lang.onboardingUserDiscoveryContactsVerifiedBadge, + style: TextStyle( + color: context.color.onSurfaceVariant, + fontSize: 12, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + Center( + child: Container( + width: 100, + height: 40, + decoration: BoxDecoration( + border: Border.all(color: Colors.grey, width: 0.5), + borderRadius: BorderRadius.circular(12), + ), + child: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + AvatarIcon(fontSize: 12), + SizedBox(width: 5), + Text( + 'jane', + style: TextStyle( + fontWeight: FontWeight.bold, + ), + ), + SizedBox(width: 5), + VerificationBadgeComp( + isVerifiedByTransferredTrust: true, + size: 14, + clickable: false, + ), + ], + ), + ), + ), + const SizedBox(height: 24), + Text( + context.lang.onboardingUserDiscoveryWhoIsRequesting, + style: TextStyle( + color: context.color.onSurfaceVariant, + fontSize: 12, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 3, + ), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey, width: 0.5), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const AvatarIcon(fontSize: 14), + const SizedBox(width: 5), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'jane', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 13, + ), + ), + RichText( + text: TextSpan( + children: buildFriendsListTextString( + context, + [ + 'mary', + 'james', + ], + ), + style: const TextStyle(fontSize: 10), + ), + ), + ], + ), + ), + const MockContactRequestActionsComp(), + ], + ), + ), + ), + const SizedBox(height: 16), + ], + ), + ), + + const SizedBox(height: 80), + + Text( + context.lang.userDiscoveryDisabledYouHaveControl, + style: const TextStyle( + fontSize: 17, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + + RichText( + text: TextSpan( + children: formattedText( + context, + context.lang.userDiscoveryDisabledDecide, + ), + ), + textAlign: TextAlign.center, + ), + + const SizedBox(height: 24), + + if (state.isUserDiscoveryEnabled) + Container( + decoration: BoxDecoration( + color: context.color.surfaceContainerLow, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SwitchListTile( + value: state.isShareAllContacts, + onChanged: (val) => state.update(() { + state.isShareAllContacts = val; + }), + title: Text( + context.lang.userDiscoverySettingsEnableAllContacts, + style: const TextStyle(fontSize: 13), + ), + tileColor: context.color.surfaceContainerLow, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 16), + ), + if (!state.isShareAllContacts) + ListTile( + title: Text( + context.lang.userDiscoverySettingsMinImagesTitle, + style: const TextStyle(fontSize: 13), + ), + trailing: DropdownButtonHideUnderline( + child: DropdownButton( + value: state.requiredSendImages, + items: List.generate( + 9, + (index) { + final value = index + 2; + return DropdownMenuItem( + value: value, + child: Text('$value'), + ); + }, + ), + onChanged: (newValue) { + if (newValue != null) { + state.update( + () => state.requiredSendImages = newValue, + ); + } + }, + ), + ), + ), + if (!state.isShareAllContacts) + SwitchListTile( + value: state.isManualApprovalEnabled, + onChanged: (val) => state.update( + () => state.isManualApprovalEnabled = val, + ), + title: Text( + context.lang.userDiscoverySettingsManualApproval, + style: const TextStyle(fontSize: 13), + ), + subtitle: Text( + context.lang.userDiscoverySettingsManualApprovalDesc, + style: const TextStyle(fontSize: 10), + ), + tileColor: context.color.surfaceContainerLow, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + ), + ), + ], + ), + ), + const SizedBox(height: 80), + + Text( + context.lang.onboardingUserDiscoveryLetFriendsFindYou, + style: const TextStyle( + fontSize: 17, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + + RichText( + text: TextSpan( + children: formattedText( + context, + context.lang.onboardingUserDiscoveryLetFriendsFindYouDesc, + ), + ), + textAlign: TextAlign.center, + ), + + const SizedBox(height: 24), + + SetupSwitchCard( + value: state.sharePromotion, + onChanged: (val) => state.update(() { + if (val) { + state.isUserDiscoveryEnabled = true; + } + state.sharePromotion = val; + }), + title: context.lang.onboardingUserDiscoveryBeRecommended, + expandedChild: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + Expanded( + child: Text( + context.lang.userDiscoverySettingsMutualFriends, + style: const TextStyle(fontSize: 12), + ), + ), + const SizedBox(width: 6), + DropdownButtonHideUnderline( + child: DropdownButton( + value: state.threshold, + items: List.generate( + 9, + (index) { + final value = index + 2; + return DropdownMenuItem( + value: value, + child: Text('$value'), + ); + }, + ), + onChanged: (newValue) { + if (newValue != null) { + state.update(() { + state.threshold = newValue; + }); + } + }, + ), + ), + ], + ), + const SizedBox(height: 16), + Text( + context.lang.onboardingUserDiscoveryWhatOthersSee, + style: TextStyle( + color: context.color.onSurfaceVariant, + fontSize: 12, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 3, + ), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey, width: 0.5), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const AvatarIcon(fontSize: 14), + const SizedBox(width: 5), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + userService.currentUser.username, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + RichText( + text: TextSpan( + children: buildFriendsListTextString( + context, + exampleUsers.sublist( + 0, + state.threshold, + ), + ), + style: const TextStyle(fontSize: 11), + ), + ), + ], + ), + ), + const MockContactSuggestedActionsComp(), + ], + ), + ), + ), + const SizedBox(height: 16), + Text( + context.lang.onboardingUserDiscoveryWhatYouSee, + style: TextStyle( + color: context.color.onSurfaceVariant, + fontSize: 12, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 3, + ), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey, width: 0.5), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const AvatarIcon(fontSize: 14), + const SizedBox(width: 5), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'jane', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 13, + ), + ), + RichText( + text: TextSpan( + children: buildFriendsListTextString( + context, + exampleUsers.sublist( + 0, + state.threshold, + ), + ), + style: const TextStyle(fontSize: 10), + ), + ), + ], + ), + ), + const MockContactRequestActionsComp(), + ], + ), + ), + ), + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/src/visual/views/settings/privacy/user_discovery/user_discovery_settings.view.dart b/lib/src/visual/views/settings/privacy/user_discovery/user_discovery_settings.view.dart index 62906fd5..6ebc456a 100644 --- a/lib/src/visual/views/settings/privacy/user_discovery/user_discovery_settings.view.dart +++ b/lib/src/visual/views/settings/privacy/user_discovery/user_discovery_settings.view.dart @@ -1,12 +1,9 @@ import 'dart:async'; - -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:twonly/locator.dart'; -import 'package:twonly/src/services/user.service.dart'; -import 'package:twonly/src/services/user_discovery.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/themes/light.dart'; +import 'package:twonly/src/visual/views/settings/privacy/user_discovery/components/user_discovery_setup.comp.dart'; class UserDiscoverySettingsView extends StatefulWidget { const UserDiscoverySettingsView({super.key}); @@ -17,123 +14,103 @@ class UserDiscoverySettingsView extends StatefulWidget { } class _UserDiscoverySettingsViewState extends State { - int _minimumRequiredImagesExchanged = 0; - int _userDiscoveryThreshold = 0; + late UserDiscoverySetupState state; @override void initState() { super.initState(); - _minimumRequiredImagesExchanged = - userService.currentUser.minimumRequiredImagesExchanged; - _userDiscoveryThreshold = userService.currentUser.userDiscoveryThreshold; + final u = userService.currentUser; + state = UserDiscoverySetupState( + setState: setState, + requiredSendImages: u.requiredSendImages, + isUserDiscoveryEnabled: u.isUserDiscoveryEnabled, + sharePromotion: u.userDiscoverySharePromotion, + isShareAllContacts: + u.requiredSendImages == 0 && !u.userDiscoveryRequiresManualApproval, + isManualApprovalEnabled: u.userDiscoveryRequiresManualApproval, + threshold: u.userDiscoveryThreshold, + ); } Future _saveChanges() async { - final requiresNewInitialization = - userService.currentUser.userDiscoveryThreshold != - _userDiscoveryThreshold; + await state.initializeOrUpdate(); + if (!mounted) return; + Navigator.pop(context, true); + } - await UserService.update((u) { - u - ..minimumRequiredImagesExchanged = _minimumRequiredImagesExchanged - ..userDiscoveryThreshold = _userDiscoveryThreshold; - }); - - if (requiresNewInitialization) { - await UserDiscoveryService.initializeOrUpdate( - threshold: userService.currentUser.userDiscoveryThreshold, - minimumRequiredImagesExchanged: - userService.currentUser.minimumRequiredImagesExchanged, - ); - } - if (mounted) Navigator.pop(context); + Future _showBackDialog() { + return showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text( + context.lang.avatarSaveChanges, + ), + actions: [ + FilledButton( + child: Text(context.lang.avatarSaveChangesStore), + onPressed: () async { + await _saveChanges(); + }, + ), + TextButton( + child: Text(context.lang.avatarSaveChangesDiscard), + onPressed: () { + Navigator.pop(context, true); + }, + ), + ], + ); + }, + ); } @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(context.lang.userDiscoverySettingsTitle), - ), - body: Padding( - padding: const EdgeInsets.only(top: 10), - child: ListView( - children: [ - ListTile( - title: Text(context.lang.userDiscoverySettingsMinImagesTitle), - subtitle: Text( - context.lang.userDiscoverySettingsMinImages, - ), - trailing: SizedBox( - width: 60, - child: CupertinoPicker( - magnification: 1.22, - squeeze: 1.2, - useMagnifier: true, - itemExtent: 32, - scrollController: FixedExtentScrollController( - initialItem: _minimumRequiredImagesExchanged, - ), - onSelectedItemChanged: (selectedItem) { - _minimumRequiredImagesExchanged = selectedItem; - setState(() {}); - }, - children: List.generate( - 9, - (index) => Center(child: Text('$index')), - ), - ), - ), - ), - ListTile( - title: Text(context.lang.userDiscoverySettingsMutualFriendsTitle), - subtitle: Text( - context.lang.userDiscoverySettingsMutualFriends, - ), - trailing: SizedBox( - width: 60, - child: CupertinoPicker( - magnification: 1.22, - squeeze: 1.2, - useMagnifier: true, - itemExtent: 32, - scrollController: FixedExtentScrollController( - initialItem: _userDiscoveryThreshold - 2, - ), - onSelectedItemChanged: (selectedItem) { - _userDiscoveryThreshold = selectedItem + 2; - setState(() {}); - }, - children: List.generate( - 9, - (index) => Center(child: Text('${index + 2}')), - ), - ), - ), - ), - const SizedBox( - height: 30, - ), - if (_minimumRequiredImagesExchanged != - userService.currentUser.minimumRequiredImagesExchanged || - _userDiscoveryThreshold != - userService.currentUser.userDiscoveryThreshold) - Padding( - padding: const EdgeInsets.all(17), - child: FilledButton( - onPressed: _saveChanges, - style: primaryColorButtonStyle.merge( - FilledButton.styleFrom( - padding: const EdgeInsets.symmetric( - horizontal: 32, - vertical: 24, + return PopScope( + canPop: false, + onPopInvokedWithResult: (didPop, result) async { + if (didPop) return; + if (state.wasChanged) { + // there where changes + final shouldPop = await _showBackDialog() ?? false; + if (context.mounted && shouldPop) { + Navigator.pop(context); + } + } else { + Navigator.pop(context); + } + }, + child: Scaffold( + appBar: AppBar( + title: Text(context.lang.userDiscoverySettingsTitle), + ), + body: Padding( + padding: const EdgeInsets.only(top: 10, left: 24, right: 24), + child: ListView( + children: [ + const SizedBox(height: 30), + UserDiscoverySetupComp(state: state), + const SizedBox(height: 30), + if (state.wasChanged) + Padding( + padding: const EdgeInsets.all(17), + child: FilledButton( + onPressed: _saveChanges, + style: primaryColorButtonStyle.merge( + FilledButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 32, + vertical: 24, + ), ), ), + child: Text(context.lang.userDiscoverySettingsApply), ), - child: Text(context.lang.userDiscoverySettingsApply), ), - ), - ], + const SizedBox(height: 30), + ], + ), ), ), ); diff --git a/rust/src/bridge/wrapper/user_discovery.rs b/rust/src/bridge/wrapper/user_discovery.rs index 4ce07990..175184dd 100644 --- a/rust/src/bridge/wrapper/user_discovery.rs +++ b/rust/src/bridge/wrapper/user_discovery.rs @@ -8,12 +8,13 @@ impl FlutterUserDiscovery { threshold: u8, user_id: i64, public_key: Vec, + share_promotion: bool, ) -> Result<()> { Ok(get_twonly_flutter()? .user_discovery .get() .await - .initialize_or_update(threshold, user_id, public_key) + .initialize_or_update(threshold, user_id, public_key, share_promotion) .await?) } diff --git a/rust/src/database/mod.rs b/rust/src/database/mod.rs index 4d9f4b71..ee29f7e9 100644 --- a/rust/src/database/mod.rs +++ b/rust/src/database/mod.rs @@ -1,65 +1,65 @@ -use crate::bridge::error::{Result, TwonlyError}; -use sqlx::migrate::MigrateDatabase; -use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions}; -use sqlx::{ConnectOptions, Sqlite, SqlitePool}; -use std::time::Duration; +// use crate::bridge::error::{Result, TwonlyError}; +// use sqlx::migrate::MigrateDatabase; +// use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions}; +// use sqlx::{ConnectOptions, Sqlite, SqlitePool}; +// use std::time::Duration; -pub(crate) struct Database { - pub(crate) pool: SqlitePool, -} +// pub(crate) struct Database { +// pub(crate) pool: SqlitePool, +// } -impl Database { - pub(crate) async fn new(db_path: &String, read_only: bool) -> Result { - let db_url = format!("sqlite://{}", db_path); +// impl Database { +// pub(crate) async fn new(db_path: &String, read_only: bool) -> Result { +// let db_url = format!("sqlite://{}", db_path); - match Sqlite::database_exists(&db_url).await { - Ok(true) => { - tracing::debug!("database exists"); - } - Ok(false) => { - tracing::error!("could not open the sqlite3 database"); - return Err(TwonlyError::DatabaseNotFound); - } - Err(e) => { - tracing::error!( - "Could not check if database exists: {:?}, attempting to create", - e - ); - return Err(TwonlyError::DatabaseNotFound); - } - } +// match Sqlite::database_exists(&db_url).await { +// Ok(true) => { +// tracing::debug!("database exists"); +// } +// Ok(false) => { +// tracing::error!("could not open the sqlite3 database"); +// return Err(TwonlyError::DatabaseNotFound); +// } +// Err(e) => { +// tracing::error!( +// "Could not check if database exists: {:?}, attempting to create", +// e +// ); +// return Err(TwonlyError::DatabaseNotFound); +// } +// } - tracing::debug!("Creating database connection pool"); +// tracing::debug!("Creating database connection pool"); - let log_statements_level = if std::env::var("SQLX_LOG_STATEMENTS").is_ok() { - tracing::log::LevelFilter::Info - } else { - tracing::log::LevelFilter::Off - }; +// let log_statements_level = if std::env::var("SQLX_LOG_STATEMENTS").is_ok() { +// tracing::log::LevelFilter::Info +// } else { +// tracing::log::LevelFilter::Off +// }; - let connect_options = format!("{db_url}?mode=rwc") - .parse::()? - .log_statements(log_statements_level) - .read_only(read_only) - .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal) - .foreign_keys(true) - .busy_timeout(Duration::from_millis(5000)) - .pragma("recursive_triggers", "ON") - .log_slow_statements(tracing::log::LevelFilter::Warn, Duration::from_millis(500)); +// let connect_options = format!("{db_url}?mode=rwc") +// .parse::()? +// .log_statements(log_statements_level) +// .read_only(read_only) +// .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal) +// .foreign_keys(true) +// .busy_timeout(Duration::from_millis(5000)) +// .pragma("recursive_triggers", "ON") +// .log_slow_statements(tracing::log::LevelFilter::Warn, Duration::from_millis(500)); - let pool = SqlitePoolOptions::new() - .acquire_timeout(Duration::from_secs(5)) - .max_connections(10) - .connect_with(connect_options) - .await?; +// let pool = SqlitePoolOptions::new() +// .acquire_timeout(Duration::from_secs(5)) +// .max_connections(10) +// .connect_with(connect_options) +// .await?; - let row: (String, String) = sqlx::query_as("SELECT sqlite_version(), sqlite_source_id()") - .fetch_one(&pool) - .await?; +// let row: (String, String) = sqlx::query_as("SELECT sqlite_version(), sqlite_source_id()") +// .fetch_one(&pool) +// .await?; - tracing::info!("Rust SQLite Version: {}", row.0); - tracing::info!("Rust SQLite Source ID: {}", row.1); +// tracing::info!("Rust SQLite Version: {}", row.0); +// tracing::info!("Rust SQLite Source ID: {}", row.1); - Ok(Self { pool: pool }) - } -} +// Ok(Self { pool: pool }) +// } +// } diff --git a/rust/src/frb_generated.rs b/rust/src/frb_generated.rs index 104bd87c..5e0cb33a 100644 --- a/rust/src/frb_generated.rs +++ b/rust/src/frb_generated.rs @@ -105,9 +105,10 @@ fn wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_initiali let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); let api_threshold = ::sse_decode(&mut deserializer); let api_user_id = ::sse_decode(&mut deserializer); -let api_public_key = >::sse_decode(&mut deserializer);deserializer.end(); move |context| async move { +let api_public_key = >::sse_decode(&mut deserializer); +let api_share_promotion = ::sse_decode(&mut deserializer);deserializer.end(); move |context| async move { transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>((move || async move { - let output_ok = crate::bridge::wrapper::user_discovery::FlutterUserDiscovery::initialize_or_update(api_threshold, api_user_id, api_public_key).await?; Ok(output_ok) + let output_ok = crate::bridge::wrapper::user_discovery::FlutterUserDiscovery::initialize_or_update(api_threshold, api_user_id, api_public_key, api_share_promotion).await?; Ok(output_ok) })().await) } }) } diff --git a/rust/src/standalone/mod.rs b/rust/src/standalone/mod.rs index 09822bc7..c5ece6b4 100644 --- a/rust/src/standalone/mod.rs +++ b/rust/src/standalone/mod.rs @@ -1,9 +1,9 @@ -use crate::{bridge::TwonlyConfig, database::Database}; -use std::sync::Arc; +// use crate::{bridge::TwonlyConfig, database::Database}; +// use std::sync::Arc; -pub(crate) struct TwonlyStandalone { - #[allow(dead_code)] - pub(crate) config: TwonlyConfig, - /// Because Rust is called from a different process it is safe to write to the twonly_db. - pub(crate) twonly_db: Arc, -} +// pub(crate) struct TwonlyStandalone { +// #[allow(dead_code)] +// pub(crate) config: TwonlyConfig, +// /// Because Rust is called from a different process it is safe to write to the twonly_db. +// pub(crate) twonly_db: Arc, +// } diff --git a/rust_dependencies/protocols/src/user_discovery.rs b/rust_dependencies/protocols/src/user_discovery.rs index c5c7ff37..47170c3b 100644 --- a/rust_dependencies/protocols/src/user_discovery.rs +++ b/rust_dependencies/protocols/src/user_discovery.rs @@ -40,6 +40,8 @@ struct UserDiscoveryConfig { verification_shares: Vec>, // The users' id: user_id: UserID, + // If others user should promote the promotion to other users + share_promotion: bool, } /// @@ -87,6 +89,7 @@ impl UserDiscovery, + share_promotion: bool, ) -> Result<()> { let config_lock = self.config_lock.lock().await; let mut config = match self.store.get_config().await { @@ -123,6 +126,7 @@ impl UserDiscovery UserDiscovery UserDiscovery( ) -> UserDiscovery { let ud = UserDiscovery::new(store.to_owned(), TestingUtils::default()).unwrap(); - ud.initialize_or_update(threshold, user_id as UserID, vec![user_id as u8; 32]) + ud.initialize_or_update(threshold, user_id as UserID, vec![user_id as u8; 32], true) .await .unwrap(); diff --git a/rust_dependencies/protocols/src/user_discovery/types.proto b/rust_dependencies/protocols/src/user_discovery/types.proto index 7df0cc63..5a81514b 100644 --- a/rust_dependencies/protocols/src/user_discovery/types.proto +++ b/rust_dependencies/protocols/src/user_discovery/types.proto @@ -17,8 +17,9 @@ message UserDiscoveryMessage { message UserDiscoveryAnnouncement { int64 public_id = 1; uint32 threshold = 2; - bytes announcement_share = 4; - repeated bytes verification_shares = 6; + bytes announcement_share = 3; + bool share_promotion = 4; + repeated bytes verification_shares = 5; } message UserDiscoveryPromotion { diff --git a/scripts/check_arb.py b/scripts/check_arb.py new file mode 100644 index 00000000..a9ecc289 --- /dev/null +++ b/scripts/check_arb.py @@ -0,0 +1,121 @@ +import os +import json +import re +import sys + +ARB_DIR = 'lib/src/localization/translations' +LIB_DIR = 'lib' + +def get_arb_data(filepath): + try: + with open(filepath, 'r', encoding='utf-8') as f: + return json.load(f) + except Exception as e: + print(f"Error reading {filepath}: {e}") + return {} + +def save_arb_data(filepath, data): + with open(filepath, 'w', encoding='utf-8') as f: + json.dump(data, f, indent=2, ensure_ascii=False) + f.write('\n') + +def get_keys(data): + return {k for k in data.keys() if k and not k.startswith('@')} + +def main(): + fix_mode = '--fix' in sys.argv + + en_file = os.path.join(ARB_DIR, 'en.arb') + if not os.path.exists(en_file): + print(f"Error: {en_file} not found. Please run this script from the project root.") + return + + en_data = get_arb_data(en_file) + en_keys = get_keys(en_data) + print(f"Found {len(en_keys)} keys in en.arb") + + # 1. Check for unused keys in Dart files + print("\n--- Checking for unused keys in Dart files ---") + all_dart_text = "" + for root, _, files in os.walk(LIB_DIR): + for file in files: + if file.endswith('.dart'): + filepath = os.path.join(root, file) + if "generated" in str(filepath): + continue + try: + with open(filepath, 'r', encoding='utf-8') as f: + all_dart_text += f.read() + "\n" + except Exception as e: + print(f"Could not read {filepath}: {e}") + + unused_keys = set() + for key in sorted(en_keys): + # Using word boundary to avoid partial matches + pattern = r'\b' + re.escape(key) + r'\b' + if not re.search(pattern, all_dart_text): + unused_keys.add(key) + + if unused_keys: + print(f"Found {len(unused_keys)} keys in en.arb that do not appear to be used in the Dart code:") + for k in sorted(unused_keys): + print(f" - {k}") + + if fix_mode: + print("\n--fix is enabled. Deleting unused keys from all .arb files...") + arb_files = [f for f in os.listdir(ARB_DIR) if f.endswith('.arb')] + for arb_file in arb_files: + filepath = os.path.join(ARB_DIR, arb_file) + data = get_arb_data(filepath) + deleted_count = 0 + for k in unused_keys: + if k in data: + del data[k] + # Also delete associated metadata if it exists + meta_k = f"@{k}" + if meta_k in data: + del data[meta_k] + deleted_count += 1 + + if deleted_count > 0: + save_arb_data(filepath, data) + print(f"Deleted {deleted_count} unused keys from {arb_file}") + + # Remove deleted keys from our set so the comparison below is accurate + en_keys = en_keys - unused_keys + else: + print("\nRun with --fix to automatically delete these unused keys.") + else: + print("All keys in en.arb seem to be used in the Dart files.") + + # 2. Compare other .arb files with en.arb + print("\n--- Comparing other .arb files with en.arb ---") + arb_files = [f for f in os.listdir(ARB_DIR) if f.endswith('.arb')] + for arb_file in arb_files: + if arb_file == 'en.arb': + continue + + filepath = os.path.join(ARB_DIR, arb_file) + data = get_arb_data(filepath) + other_keys = get_keys(data) + + extra_keys = other_keys - en_keys + missing_keys = en_keys - other_keys + + print(f"\n[{arb_file} vs en.arb]") + if extra_keys: + print(f"Keys in {arb_file} but NOT in en.arb ({len(extra_keys)}):") + for k in sorted(extra_keys): + print(f" + {k}") + else: + print(f"No extra keys found in {arb_file} (all keys are in en.arb).") + + if missing_keys: + print(f"Keys in en.arb but NOT in {arb_file} ({len(missing_keys)}):") + for k in sorted(missing_keys): + print(f" - {k}") + else: + print(f"No missing keys in {arb_file} (it has all en.arb keys).") + +if __name__ == "__main__": + main() diff --git a/test/drift/twonly_db/generated/schema_v12.dart b/test/drift/twonly_db/generated/schema_v12.dart index 901d0c22..9a646d65 100644 --- a/test/drift/twonly_db/generated/schema_v12.dart +++ b/test/drift/twonly_db/generated/schema_v12.dart @@ -145,6 +145,17 @@ class Contacts extends Table with TableInfo { 'NOT NULL DEFAULT 0 CHECK (user_discovery_excluded IN (0, 1))', defaultValue: const CustomExpression('0'), ); + late final GeneratedColumn + userDiscoveryManualApproved = GeneratedColumn( + 'user_discovery_manual_approved', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT 0 CHECK (user_discovery_manual_approved IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); late final GeneratedColumn mediaSendCounter = GeneratedColumn( 'media_send_counter', aliasedName, @@ -180,6 +191,7 @@ class Contacts extends Table with TableInfo { createdAt, userDiscoveryVersion, userDiscoveryExcluded, + userDiscoveryManualApproved, mediaSendCounter, mediaReceivedCounter, ]; @@ -254,6 +266,10 @@ class Contacts extends Table with TableInfo { DriftSqlType.int, data['${effectivePrefix}user_discovery_excluded'], )!, + userDiscoveryManualApproved: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}user_discovery_manual_approved'], + )!, mediaSendCounter: attachedDatabase.typeMapping.read( DriftSqlType.int, data['${effectivePrefix}media_send_counter'], @@ -292,6 +308,7 @@ class ContactsData extends DataClass implements Insertable { final int createdAt; final i2.Uint8List? userDiscoveryVersion; final int userDiscoveryExcluded; + final int userDiscoveryManualApproved; final int mediaSendCounter; final int mediaReceivedCounter; const ContactsData({ @@ -310,6 +327,7 @@ class ContactsData extends DataClass implements Insertable { required this.createdAt, this.userDiscoveryVersion, required this.userDiscoveryExcluded, + required this.userDiscoveryManualApproved, required this.mediaSendCounter, required this.mediaReceivedCounter, }); @@ -343,6 +361,9 @@ class ContactsData extends DataClass implements Insertable { ); } map['user_discovery_excluded'] = Variable(userDiscoveryExcluded); + map['user_discovery_manual_approved'] = Variable( + userDiscoveryManualApproved, + ); map['media_send_counter'] = Variable(mediaSendCounter); map['media_received_counter'] = Variable(mediaReceivedCounter); return map; @@ -373,6 +394,7 @@ class ContactsData extends DataClass implements Insertable { ? const Value.absent() : Value(userDiscoveryVersion), userDiscoveryExcluded: Value(userDiscoveryExcluded), + userDiscoveryManualApproved: Value(userDiscoveryManualApproved), mediaSendCounter: Value(mediaSendCounter), mediaReceivedCounter: Value(mediaReceivedCounter), ); @@ -407,6 +429,9 @@ class ContactsData extends DataClass implements Insertable { userDiscoveryExcluded: serializer.fromJson( json['userDiscoveryExcluded'], ), + userDiscoveryManualApproved: serializer.fromJson( + json['userDiscoveryManualApproved'], + ), mediaSendCounter: serializer.fromJson(json['mediaSendCounter']), mediaReceivedCounter: serializer.fromJson( json['mediaReceivedCounter'], @@ -436,6 +461,9 @@ class ContactsData extends DataClass implements Insertable { userDiscoveryVersion, ), 'userDiscoveryExcluded': serializer.toJson(userDiscoveryExcluded), + 'userDiscoveryManualApproved': serializer.toJson( + userDiscoveryManualApproved, + ), 'mediaSendCounter': serializer.toJson(mediaSendCounter), 'mediaReceivedCounter': serializer.toJson(mediaReceivedCounter), }; @@ -457,6 +485,7 @@ class ContactsData extends DataClass implements Insertable { int? createdAt, Value userDiscoveryVersion = const Value.absent(), int? userDiscoveryExcluded, + int? userDiscoveryManualApproved, int? mediaSendCounter, int? mediaReceivedCounter, }) => ContactsData( @@ -479,6 +508,8 @@ class ContactsData extends DataClass implements Insertable { ? userDiscoveryVersion.value : this.userDiscoveryVersion, userDiscoveryExcluded: userDiscoveryExcluded ?? this.userDiscoveryExcluded, + userDiscoveryManualApproved: + userDiscoveryManualApproved ?? this.userDiscoveryManualApproved, mediaSendCounter: mediaSendCounter ?? this.mediaSendCounter, mediaReceivedCounter: mediaReceivedCounter ?? this.mediaReceivedCounter, ); @@ -513,6 +544,9 @@ class ContactsData extends DataClass implements Insertable { userDiscoveryExcluded: data.userDiscoveryExcluded.present ? data.userDiscoveryExcluded.value : this.userDiscoveryExcluded, + userDiscoveryManualApproved: data.userDiscoveryManualApproved.present + ? data.userDiscoveryManualApproved.value + : this.userDiscoveryManualApproved, mediaSendCounter: data.mediaSendCounter.present ? data.mediaSendCounter.value : this.mediaSendCounter, @@ -540,6 +574,7 @@ class ContactsData extends DataClass implements Insertable { ..write('createdAt: $createdAt, ') ..write('userDiscoveryVersion: $userDiscoveryVersion, ') ..write('userDiscoveryExcluded: $userDiscoveryExcluded, ') + ..write('userDiscoveryManualApproved: $userDiscoveryManualApproved, ') ..write('mediaSendCounter: $mediaSendCounter, ') ..write('mediaReceivedCounter: $mediaReceivedCounter') ..write(')')) @@ -563,6 +598,7 @@ class ContactsData extends DataClass implements Insertable { createdAt, $driftBlobEquality.hash(userDiscoveryVersion), userDiscoveryExcluded, + userDiscoveryManualApproved, mediaSendCounter, mediaReceivedCounter, ); @@ -591,6 +627,8 @@ class ContactsData extends DataClass implements Insertable { this.userDiscoveryVersion, ) && other.userDiscoveryExcluded == this.userDiscoveryExcluded && + other.userDiscoveryManualApproved == + this.userDiscoveryManualApproved && other.mediaSendCounter == this.mediaSendCounter && other.mediaReceivedCounter == this.mediaReceivedCounter); } @@ -611,6 +649,7 @@ class ContactsCompanion extends UpdateCompanion { final Value createdAt; final Value userDiscoveryVersion; final Value userDiscoveryExcluded; + final Value userDiscoveryManualApproved; final Value mediaSendCounter; final Value mediaReceivedCounter; const ContactsCompanion({ @@ -629,6 +668,7 @@ class ContactsCompanion extends UpdateCompanion { this.createdAt = const Value.absent(), this.userDiscoveryVersion = const Value.absent(), this.userDiscoveryExcluded = const Value.absent(), + this.userDiscoveryManualApproved = const Value.absent(), this.mediaSendCounter = const Value.absent(), this.mediaReceivedCounter = const Value.absent(), }); @@ -648,6 +688,7 @@ class ContactsCompanion extends UpdateCompanion { this.createdAt = const Value.absent(), this.userDiscoveryVersion = const Value.absent(), this.userDiscoveryExcluded = const Value.absent(), + this.userDiscoveryManualApproved = const Value.absent(), this.mediaSendCounter = const Value.absent(), this.mediaReceivedCounter = const Value.absent(), }) : username = Value(username); @@ -667,6 +708,7 @@ class ContactsCompanion extends UpdateCompanion { Expression? createdAt, Expression? userDiscoveryVersion, Expression? userDiscoveryExcluded, + Expression? userDiscoveryManualApproved, Expression? mediaSendCounter, Expression? mediaReceivedCounter, }) { @@ -690,6 +732,8 @@ class ContactsCompanion extends UpdateCompanion { 'user_discovery_version': userDiscoveryVersion, if (userDiscoveryExcluded != null) 'user_discovery_excluded': userDiscoveryExcluded, + if (userDiscoveryManualApproved != null) + 'user_discovery_manual_approved': userDiscoveryManualApproved, if (mediaSendCounter != null) 'media_send_counter': mediaSendCounter, if (mediaReceivedCounter != null) 'media_received_counter': mediaReceivedCounter, @@ -712,6 +756,7 @@ class ContactsCompanion extends UpdateCompanion { Value? createdAt, Value? userDiscoveryVersion, Value? userDiscoveryExcluded, + Value? userDiscoveryManualApproved, Value? mediaSendCounter, Value? mediaReceivedCounter, }) { @@ -732,6 +777,8 @@ class ContactsCompanion extends UpdateCompanion { userDiscoveryVersion: userDiscoveryVersion ?? this.userDiscoveryVersion, userDiscoveryExcluded: userDiscoveryExcluded ?? this.userDiscoveryExcluded, + userDiscoveryManualApproved: + userDiscoveryManualApproved ?? this.userDiscoveryManualApproved, mediaSendCounter: mediaSendCounter ?? this.mediaSendCounter, mediaReceivedCounter: mediaReceivedCounter ?? this.mediaReceivedCounter, ); @@ -791,6 +838,11 @@ class ContactsCompanion extends UpdateCompanion { userDiscoveryExcluded.value, ); } + if (userDiscoveryManualApproved.present) { + map['user_discovery_manual_approved'] = Variable( + userDiscoveryManualApproved.value, + ); + } if (mediaSendCounter.present) { map['media_send_counter'] = Variable(mediaSendCounter.value); } @@ -818,6 +870,7 @@ class ContactsCompanion extends UpdateCompanion { ..write('createdAt: $createdAt, ') ..write('userDiscoveryVersion: $userDiscoveryVersion, ') ..write('userDiscoveryExcluded: $userDiscoveryExcluded, ') + ..write('userDiscoveryManualApproved: $userDiscoveryManualApproved, ') ..write('mediaSendCounter: $mediaSendCounter, ') ..write('mediaReceivedCounter: $mediaReceivedCounter') ..write(')')) @@ -7462,12 +7515,21 @@ class KeyVerifications extends Table final GeneratedDatabase attachedDatabase; final String? _alias; KeyVerifications(this.attachedDatabase, [this._alias]); + late final GeneratedColumn verificationId = GeneratedColumn( + 'verification_id', + aliasedName, + false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL PRIMARY KEY AUTOINCREMENT', + ); late final GeneratedColumn contactId = GeneratedColumn( 'contact_id', aliasedName, false, type: DriftSqlType.int, - requiredDuringInsert: false, + requiredDuringInsert: true, $customConstraints: 'NOT NULL REFERENCES contacts(user_id)ON DELETE CASCADE', ); @@ -7492,18 +7554,27 @@ class KeyVerifications extends Table ), ); @override - List get $columns => [contactId, type, createdAt]; + List get $columns => [ + verificationId, + contactId, + type, + createdAt, + ]; @override String get aliasedName => _alias ?? actualTableName; @override String get actualTableName => $name; static const String $name = 'key_verifications'; @override - Set get $primaryKey => {contactId}; + Set get $primaryKey => {verificationId}; @override KeyVerificationsData map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return KeyVerificationsData( + verificationId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}verification_id'], + )!, contactId: attachedDatabase.typeMapping.read( DriftSqlType.int, data['${effectivePrefix}contact_id'], @@ -7524,18 +7595,18 @@ class KeyVerifications extends Table return KeyVerifications(attachedDatabase, alias); } - @override - List get customConstraints => const ['PRIMARY KEY(contact_id)']; @override bool get dontWriteConstraints => true; } class KeyVerificationsData extends DataClass implements Insertable { + final int verificationId; final int contactId; final String type; final int createdAt; const KeyVerificationsData({ + required this.verificationId, required this.contactId, required this.type, required this.createdAt, @@ -7543,6 +7614,7 @@ class KeyVerificationsData extends DataClass @override Map toColumns(bool nullToAbsent) { final map = {}; + map['verification_id'] = Variable(verificationId); map['contact_id'] = Variable(contactId); map['type'] = Variable(type); map['created_at'] = Variable(createdAt); @@ -7551,6 +7623,7 @@ class KeyVerificationsData extends DataClass KeyVerificationsCompanion toCompanion(bool nullToAbsent) { return KeyVerificationsCompanion( + verificationId: Value(verificationId), contactId: Value(contactId), type: Value(type), createdAt: Value(createdAt), @@ -7563,6 +7636,7 @@ class KeyVerificationsData extends DataClass }) { serializer ??= driftRuntimeOptions.defaultSerializer; return KeyVerificationsData( + verificationId: serializer.fromJson(json['verificationId']), contactId: serializer.fromJson(json['contactId']), type: serializer.fromJson(json['type']), createdAt: serializer.fromJson(json['createdAt']), @@ -7572,6 +7646,7 @@ class KeyVerificationsData extends DataClass Map toJson({ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; return { + 'verificationId': serializer.toJson(verificationId), 'contactId': serializer.toJson(contactId), 'type': serializer.toJson(type), 'createdAt': serializer.toJson(createdAt), @@ -7579,16 +7654,21 @@ class KeyVerificationsData extends DataClass } KeyVerificationsData copyWith({ + int? verificationId, int? contactId, String? type, int? createdAt, }) => KeyVerificationsData( + verificationId: verificationId ?? this.verificationId, contactId: contactId ?? this.contactId, type: type ?? this.type, createdAt: createdAt ?? this.createdAt, ); KeyVerificationsData copyWithCompanion(KeyVerificationsCompanion data) { return KeyVerificationsData( + verificationId: data.verificationId.present + ? data.verificationId.value + : this.verificationId, contactId: data.contactId.present ? data.contactId.value : this.contactId, type: data.type.present ? data.type.value : this.type, createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, @@ -7598,6 +7678,7 @@ class KeyVerificationsData extends DataClass @override String toString() { return (StringBuffer('KeyVerificationsData(') + ..write('verificationId: $verificationId, ') ..write('contactId: $contactId, ') ..write('type: $type, ') ..write('createdAt: $createdAt') @@ -7606,36 +7687,43 @@ class KeyVerificationsData extends DataClass } @override - int get hashCode => Object.hash(contactId, type, createdAt); + int get hashCode => Object.hash(verificationId, contactId, type, createdAt); @override bool operator ==(Object other) => identical(this, other) || (other is KeyVerificationsData && + other.verificationId == this.verificationId && other.contactId == this.contactId && other.type == this.type && other.createdAt == this.createdAt); } class KeyVerificationsCompanion extends UpdateCompanion { + final Value verificationId; final Value contactId; final Value type; final Value createdAt; const KeyVerificationsCompanion({ + this.verificationId = const Value.absent(), this.contactId = const Value.absent(), this.type = const Value.absent(), this.createdAt = const Value.absent(), }); KeyVerificationsCompanion.insert({ - this.contactId = const Value.absent(), + this.verificationId = const Value.absent(), + required int contactId, required String type, this.createdAt = const Value.absent(), - }) : type = Value(type); + }) : contactId = Value(contactId), + type = Value(type); static Insertable custom({ + Expression? verificationId, Expression? contactId, Expression? type, Expression? createdAt, }) { return RawValuesInsertable({ + if (verificationId != null) 'verification_id': verificationId, if (contactId != null) 'contact_id': contactId, if (type != null) 'type': type, if (createdAt != null) 'created_at': createdAt, @@ -7643,11 +7731,13 @@ class KeyVerificationsCompanion extends UpdateCompanion { } KeyVerificationsCompanion copyWith({ + Value? verificationId, Value? contactId, Value? type, Value? createdAt, }) { return KeyVerificationsCompanion( + verificationId: verificationId ?? this.verificationId, contactId: contactId ?? this.contactId, type: type ?? this.type, createdAt: createdAt ?? this.createdAt, @@ -7657,6 +7747,9 @@ class KeyVerificationsCompanion extends UpdateCompanion { @override Map toColumns(bool nullToAbsent) { final map = {}; + if (verificationId.present) { + map['verification_id'] = Variable(verificationId.value); + } if (contactId.present) { map['contact_id'] = Variable(contactId.value); } @@ -7672,6 +7765,7 @@ class KeyVerificationsCompanion extends UpdateCompanion { @override String toString() { return (StringBuffer('KeyVerificationsCompanion(') + ..write('verificationId: $verificationId, ') ..write('contactId: $contactId, ') ..write('type: $type, ') ..write('createdAt: $createdAt')