finish setup

This commit is contained in:
otsmr 2026-04-29 00:22:42 +02:00
parent c47c91c1ba
commit 41dc30b3c2
51 changed files with 2328 additions and 3761 deletions

View file

@ -169,10 +169,9 @@ class _AppMainWidgetState extends State<AppMainWidget> {
_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...

View file

@ -36,11 +36,13 @@ class FlutterUserDiscovery {
required int threshold,
required PlatformInt64 userId,
required List<int> publicKey,
required bool sharePromotion,
}) => RustLib.instance.api
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryInitializeOrUpdate(
threshold: threshold,
userId: userId,
publicKey: publicKey,
sharePromotion: sharePromotion,
);
static Future<Uint8List?> shouldRequestNewMessages({

View file

@ -103,6 +103,7 @@ abstract class RustLibApi extends BaseApi {
required int threshold,
required PlatformInt64 userId,
required List<int> publicKey,
required bool sharePromotion,
});
Future<Uint8List?>
@ -284,6 +285,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
required int threshold,
required PlatformInt64 userId,
required List<int> 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

View file

@ -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<CameraDescription> cameras;
// will be loaded in the main_camera_controller.dart
static List<CameraDescription> cameras = [];
static Future<void> 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 {

View file

@ -151,10 +151,12 @@ Future<void> 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;
}

View file

@ -140,7 +140,7 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> 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<TwonlyDB> with _$ContactsDaoMixin {
t.userDiscoveryVersion.isNotNull() &
t.userDiscoveryExcluded.equals(false) &
t.mediaSendCounter.isBiggerOrEqualValue(
userService.currentUser.minimumRequiredImagesExchanged,
userService.currentUser.requiredSendImages,
),
))
.get();

View file

@ -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)));"
}
]
},

View file

@ -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))();

View file

@ -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);
},

View file

@ -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<bool> userDiscoveryManualApproved =
GeneratedColumn<bool>(
'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<Contact> {
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<Contact> {
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<Contact> {
map['user_discovery_version'] = Variable<Uint8List>(userDiscoveryVersion);
}
map['user_discovery_excluded'] = Variable<bool>(userDiscoveryExcluded);
map['user_discovery_manual_approved'] = Variable<bool>(
userDiscoveryManualApproved,
);
map['media_send_counter'] = Variable<int>(mediaSendCounter);
map['media_received_counter'] = Variable<int>(mediaReceivedCounter);
return map;
@ -565,6 +599,7 @@ class Contact extends DataClass implements Insertable<Contact> {
? 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<Contact> {
userDiscoveryExcluded: serializer.fromJson<bool>(
json['userDiscoveryExcluded'],
),
userDiscoveryManualApproved: serializer.fromJson<bool>(
json['userDiscoveryManualApproved'],
),
mediaSendCounter: serializer.fromJson<int>(json['mediaSendCounter']),
mediaReceivedCounter: serializer.fromJson<int>(
json['mediaReceivedCounter'],
@ -626,6 +664,9 @@ class Contact extends DataClass implements Insertable<Contact> {
userDiscoveryVersion,
),
'userDiscoveryExcluded': serializer.toJson<bool>(userDiscoveryExcluded),
'userDiscoveryManualApproved': serializer.toJson<bool>(
userDiscoveryManualApproved,
),
'mediaSendCounter': serializer.toJson<int>(mediaSendCounter),
'mediaReceivedCounter': serializer.toJson<int>(mediaReceivedCounter),
};
@ -647,6 +688,7 @@ class Contact extends DataClass implements Insertable<Contact> {
DateTime? createdAt,
Value<Uint8List?> userDiscoveryVersion = const Value.absent(),
bool? userDiscoveryExcluded,
bool? userDiscoveryManualApproved,
int? mediaSendCounter,
int? mediaReceivedCounter,
}) => Contact(
@ -669,6 +711,8 @@ class Contact extends DataClass implements Insertable<Contact> {
? 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<Contact> {
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<Contact> {
..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<Contact> {
createdAt,
$driftBlobEquality.hash(userDiscoveryVersion),
userDiscoveryExcluded,
userDiscoveryManualApproved,
mediaSendCounter,
mediaReceivedCounter,
);
@ -781,6 +830,8 @@ class Contact extends DataClass implements Insertable<Contact> {
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<Contact> {
final Value<DateTime> createdAt;
final Value<Uint8List?> userDiscoveryVersion;
final Value<bool> userDiscoveryExcluded;
final Value<bool> userDiscoveryManualApproved;
final Value<int> mediaSendCounter;
final Value<int> mediaReceivedCounter;
const ContactsCompanion({
@ -819,6 +871,7 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
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<Contact> {
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<Contact> {
Expression<DateTime>? createdAt,
Expression<Uint8List>? userDiscoveryVersion,
Expression<bool>? userDiscoveryExcluded,
Expression<bool>? userDiscoveryManualApproved,
Expression<int>? mediaSendCounter,
Expression<int>? mediaReceivedCounter,
}) {
@ -880,6 +935,8 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
'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<Contact> {
Value<DateTime>? createdAt,
Value<Uint8List?>? userDiscoveryVersion,
Value<bool>? userDiscoveryExcluded,
Value<bool>? userDiscoveryManualApproved,
Value<int>? mediaSendCounter,
Value<int>? mediaReceivedCounter,
}) {
@ -922,6 +980,8 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
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<Contact> {
userDiscoveryExcluded.value,
);
}
if (userDiscoveryManualApproved.present) {
map['user_discovery_manual_approved'] = Variable<bool>(
userDiscoveryManualApproved.value,
);
}
if (mediaSendCounter.present) {
map['media_send_counter'] = Variable<int>(mediaSendCounter.value);
}
@ -1008,6 +1073,7 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
..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<DateTime> createdAt,
Value<Uint8List?> userDiscoveryVersion,
Value<bool> userDiscoveryExcluded,
Value<bool> userDiscoveryManualApproved,
Value<int> mediaSendCounter,
Value<int> mediaReceivedCounter,
});
@ -11641,6 +11708,7 @@ typedef $$ContactsTableUpdateCompanionBuilder =
Value<DateTime> createdAt,
Value<Uint8List?> userDiscoveryVersion,
Value<bool> userDiscoveryExcluded,
Value<bool> userDiscoveryManualApproved,
Value<int> mediaSendCounter,
Value<int> mediaReceivedCounter,
});
@ -12022,6 +12090,11 @@ class $$ContactsTableFilterComposer
builder: (column) => ColumnFilters(column),
);
ColumnFilters<bool> get userDiscoveryManualApproved => $composableBuilder(
column: $table.userDiscoveryManualApproved,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<int> get mediaSendCounter => $composableBuilder(
column: $table.mediaSendCounter,
builder: (column) => ColumnFilters(column),
@ -12425,6 +12498,11 @@ class $$ContactsTableOrderingComposer
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<bool> get userDiscoveryManualApproved => $composableBuilder(
column: $table.userDiscoveryManualApproved,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<int> get mediaSendCounter => $composableBuilder(
column: $table.mediaSendCounter,
builder: (column) => ColumnOrderings(column),
@ -12504,6 +12582,11 @@ class $$ContactsTableAnnotationComposer
builder: (column) => column,
);
GeneratedColumn<bool> get userDiscoveryManualApproved => $composableBuilder(
column: $table.userDiscoveryManualApproved,
builder: (column) => column,
);
GeneratedColumn<int> get mediaSendCounter => $composableBuilder(
column: $table.mediaSendCounter,
builder: (column) => column,
@ -12884,6 +12967,7 @@ class $$ContactsTableTableManager
Value<DateTime> createdAt = const Value.absent(),
Value<Uint8List?> userDiscoveryVersion = const Value.absent(),
Value<bool> userDiscoveryExcluded = const Value.absent(),
Value<bool> userDiscoveryManualApproved = const Value.absent(),
Value<int> mediaSendCounter = const Value.absent(),
Value<int> 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<DateTime> createdAt = const Value.absent(),
Value<Uint8List?> userDiscoveryVersion = const Value.absent(),
Value<bool> userDiscoveryExcluded = const Value.absent(),
Value<bool> userDiscoveryManualApproved = const Value.absent(),
Value<int> mediaSendCounter = const Value.absent(),
Value<int> mediaReceivedCounter = const Value.absent(),
}) => ContactsCompanion.insert(
@ -12940,6 +13026,7 @@ class $$ContactsTableTableManager
createdAt: createdAt,
userDiscoveryVersion: userDiscoveryVersion,
userDiscoveryExcluded: userDiscoveryExcluded,
userDiscoveryManualApproved: userDiscoveryManualApproved,
mediaSendCounter: mediaSendCounter,
mediaReceivedCounter: mediaReceivedCounter,
),

View file

@ -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<i2.Uint8List>;
i1.GeneratedColumn<int> get userDiscoveryExcluded =>
columnsByName['user_discovery_excluded']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get userDiscoveryManualApproved =>
columnsByName['user_discovery_manual_approved']!
as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get mediaSendCounter =>
columnsByName['media_send_counter']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get mediaReceivedCounter =>
@ -6287,6 +6291,16 @@ i1.GeneratedColumn<int> _column_212(String aliasedName) =>
defaultValue: const i1.CustomExpression('0'),
);
i1.GeneratedColumn<int> _column_213(String aliasedName) =>
i1.GeneratedColumn<int>(
'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<int> _column_214(String aliasedName) =>
i1.GeneratedColumn<int>(
'media_send_counter',
aliasedName,
@ -6295,7 +6309,7 @@ i1.GeneratedColumn<int> _column_213(String aliasedName) =>
$customConstraints: 'NOT NULL DEFAULT 0',
defaultValue: const i1.CustomExpression('0'),
);
i1.GeneratedColumn<int> _column_214(String aliasedName) =>
i1.GeneratedColumn<int> _column_215(String aliasedName) =>
i1.GeneratedColumn<int>(
'media_received_counter',
aliasedName,
@ -6307,6 +6321,8 @@ i1.GeneratedColumn<int> _column_214(String aliasedName) =>
class Shape40 extends i0.VersionedTable {
Shape40({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<int> get verificationId =>
columnsByName['verification_id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get contactId =>
columnsByName['contact_id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get type =>
@ -6315,6 +6331,16 @@ class Shape40 extends i0.VersionedTable {
columnsByName['created_at']! as i1.GeneratedColumn<int>;
}
i1.GeneratedColumn<int> _column_216(String aliasedName) =>
i1.GeneratedColumn<int>(
'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<int> get tokenId =>
@ -6325,7 +6351,7 @@ class Shape41 extends i0.VersionedTable {
columnsByName['created_at']! as i1.GeneratedColumn<int>;
}
i1.GeneratedColumn<int> _column_215(String aliasedName) =>
i1.GeneratedColumn<int> _column_217(String aliasedName) =>
i1.GeneratedColumn<int>(
'token_id',
aliasedName,
@ -6334,7 +6360,7 @@ i1.GeneratedColumn<int> _column_215(String aliasedName) =>
type: i1.DriftSqlType.int,
$customConstraints: 'NOT NULL PRIMARY KEY AUTOINCREMENT',
);
i1.GeneratedColumn<i2.Uint8List> _column_216(String aliasedName) =>
i1.GeneratedColumn<i2.Uint8List> _column_218(String aliasedName) =>
i1.GeneratedColumn<i2.Uint8List>(
'token',
aliasedName,
@ -6360,7 +6386,7 @@ class Shape42 extends i0.VersionedTable {
columnsByName['is_hidden']! as i1.GeneratedColumn<int>;
}
i1.GeneratedColumn<int> _column_217(String aliasedName) =>
i1.GeneratedColumn<int> _column_219(String aliasedName) =>
i1.GeneratedColumn<int>(
'announced_user_id',
aliasedName,
@ -6368,7 +6394,7 @@ i1.GeneratedColumn<int> _column_217(String aliasedName) =>
type: i1.DriftSqlType.int,
$customConstraints: 'NOT NULL',
);
i1.GeneratedColumn<i2.Uint8List> _column_218(String aliasedName) =>
i1.GeneratedColumn<i2.Uint8List> _column_220(String aliasedName) =>
i1.GeneratedColumn<i2.Uint8List>(
'announced_public_key',
aliasedName,
@ -6376,7 +6402,7 @@ i1.GeneratedColumn<i2.Uint8List> _column_218(String aliasedName) =>
type: i1.DriftSqlType.blob,
$customConstraints: 'NOT NULL',
);
i1.GeneratedColumn<int> _column_219(String aliasedName) =>
i1.GeneratedColumn<int> _column_221(String aliasedName) =>
i1.GeneratedColumn<int>(
'public_id',
aliasedName,
@ -6384,7 +6410,7 @@ i1.GeneratedColumn<int> _column_219(String aliasedName) =>
type: i1.DriftSqlType.int,
$customConstraints: 'NOT NULL UNIQUE',
);
i1.GeneratedColumn<String> _column_220(String aliasedName) =>
i1.GeneratedColumn<String> _column_222(String aliasedName) =>
i1.GeneratedColumn<String>(
'username',
aliasedName,
@ -6392,7 +6418,7 @@ i1.GeneratedColumn<String> _column_220(String aliasedName) =>
type: i1.DriftSqlType.string,
$customConstraints: 'NULL',
);
i1.GeneratedColumn<int> _column_221(String aliasedName) =>
i1.GeneratedColumn<int> _column_223(String aliasedName) =>
i1.GeneratedColumn<int>(
'was_shown_to_the_user',
aliasedName,
@ -6402,7 +6428,7 @@ i1.GeneratedColumn<int> _column_221(String aliasedName) =>
'NOT NULL DEFAULT 0 CHECK (was_shown_to_the_user IN (0, 1))',
defaultValue: const i1.CustomExpression('0'),
);
i1.GeneratedColumn<int> _column_222(String aliasedName) =>
i1.GeneratedColumn<int> _column_224(String aliasedName) =>
i1.GeneratedColumn<int>(
'is_hidden',
aliasedName,
@ -6423,7 +6449,7 @@ class Shape43 extends i0.VersionedTable {
as i1.GeneratedColumn<int>;
}
i1.GeneratedColumn<int> _column_223(
i1.GeneratedColumn<int> _column_225(
String aliasedName,
) => i1.GeneratedColumn<int>(
'announced_user_id',
@ -6433,7 +6459,7 @@ i1.GeneratedColumn<int> _column_223(
$customConstraints:
'NOT NULL REFERENCES user_discovery_announced_users(announced_user_id)ON DELETE CASCADE',
);
i1.GeneratedColumn<int> _column_224(String aliasedName) =>
i1.GeneratedColumn<int> _column_226(String aliasedName) =>
i1.GeneratedColumn<int>(
'from_contact_id',
aliasedName,
@ -6442,7 +6468,7 @@ i1.GeneratedColumn<int> _column_224(String aliasedName) =>
$customConstraints:
'NOT NULL REFERENCES contacts(user_id)ON DELETE CASCADE',
);
i1.GeneratedColumn<int> _column_225(String aliasedName) =>
i1.GeneratedColumn<int> _column_227(String aliasedName) =>
i1.GeneratedColumn<int>(
'public_key_verified_timestamp',
aliasedName,
@ -6468,7 +6494,7 @@ class Shape44 extends i0.VersionedTable {
as i1.GeneratedColumn<int>;
}
i1.GeneratedColumn<int> _column_226(String aliasedName) =>
i1.GeneratedColumn<int> _column_228(String aliasedName) =>
i1.GeneratedColumn<int>(
'promotion_id',
aliasedName,
@ -6476,7 +6502,7 @@ i1.GeneratedColumn<int> _column_226(String aliasedName) =>
type: i1.DriftSqlType.int,
$customConstraints: 'NOT NULL',
);
i1.GeneratedColumn<int> _column_227(String aliasedName) =>
i1.GeneratedColumn<int> _column_229(String aliasedName) =>
i1.GeneratedColumn<int>(
'public_id',
aliasedName,
@ -6484,7 +6510,7 @@ i1.GeneratedColumn<int> _column_227(String aliasedName) =>
type: i1.DriftSqlType.int,
$customConstraints: 'NOT NULL',
);
i1.GeneratedColumn<int> _column_228(String aliasedName) =>
i1.GeneratedColumn<int> _column_230(String aliasedName) =>
i1.GeneratedColumn<int>(
'threshold',
aliasedName,
@ -6492,7 +6518,7 @@ i1.GeneratedColumn<int> _column_228(String aliasedName) =>
type: i1.DriftSqlType.int,
$customConstraints: 'NOT NULL',
);
i1.GeneratedColumn<i2.Uint8List> _column_229(String aliasedName) =>
i1.GeneratedColumn<i2.Uint8List> _column_231(String aliasedName) =>
i1.GeneratedColumn<i2.Uint8List>(
'announcement_share',
aliasedName,
@ -6511,7 +6537,7 @@ class Shape45 extends i0.VersionedTable {
columnsByName['promotion']! as i1.GeneratedColumn<i2.Uint8List>;
}
i1.GeneratedColumn<int> _column_230(String aliasedName) =>
i1.GeneratedColumn<int> _column_232(String aliasedName) =>
i1.GeneratedColumn<int>(
'version_id',
aliasedName,
@ -6520,7 +6546,7 @@ i1.GeneratedColumn<int> _column_230(String aliasedName) =>
type: i1.DriftSqlType.int,
$customConstraints: 'NOT NULL PRIMARY KEY AUTOINCREMENT',
);
i1.GeneratedColumn<i2.Uint8List> _column_231(String aliasedName) =>
i1.GeneratedColumn<i2.Uint8List> _column_233(String aliasedName) =>
i1.GeneratedColumn<i2.Uint8List>(
'promotion',
aliasedName,
@ -6539,7 +6565,7 @@ class Shape46 extends i0.VersionedTable {
columnsByName['contact_id']! as i1.GeneratedColumn<int>;
}
i1.GeneratedColumn<int> _column_232(String aliasedName) =>
i1.GeneratedColumn<int> _column_234(String aliasedName) =>
i1.GeneratedColumn<int>(
'share_id',
aliasedName,
@ -6548,7 +6574,7 @@ i1.GeneratedColumn<int> _column_232(String aliasedName) =>
type: i1.DriftSqlType.int,
$customConstraints: 'NOT NULL PRIMARY KEY AUTOINCREMENT',
);
i1.GeneratedColumn<i2.Uint8List> _column_233(String aliasedName) =>
i1.GeneratedColumn<i2.Uint8List> _column_235(String aliasedName) =>
i1.GeneratedColumn<i2.Uint8List>(
'share',
aliasedName,

File diff suppressed because it is too large Load diff

View file

@ -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.';

View file

@ -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.';

File diff suppressed because it is too large Load diff

@ -1 +1 @@
Subproject commit 82c248ae18ffda0bf161fbb69ddb3fb30cfc1531
Subproject commit 8583eccaeba07cd32f11a91767f4d2767518f1be

View file

@ -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)

View file

@ -64,10 +64,13 @@ UserData _$UserDataFromJson(Map<String, dynamic> 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<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'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,

View file

@ -48,11 +48,10 @@ Future<void> 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;
}

View file

@ -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) {

View file

@ -48,18 +48,20 @@ class UserDiscoveryService {
static Future<void> 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);

View file

@ -65,8 +65,8 @@ Future<void> 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,

View file

@ -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<VerificationBadgeComp> createState() => _VerificationBadgeCompState();
@ -76,6 +78,10 @@ class _VerificationBadgeCompState extends State<VerificationBadgeComp> {
_isVerifiedByTransferredTrust = update.isNotEmpty;
});
});
} else if (widget.isVerifiedByTransferredTrust != null) {
setState(() {
_isVerifiedByTransferredTrust = widget.isVerifiedByTransferredTrust!;
});
}
}

View file

@ -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(

View file

@ -104,6 +104,10 @@ class MainCameraController {
Future<void> 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(

View file

@ -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<ChatListView> {
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),

View file

@ -17,7 +17,15 @@ List<TextSpan> 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<TextSpan> buildFriendsListTextString(
BuildContext context,
List<String> friends,
) {
final names = friends.map((f) => '*$f*').toList();
return formattedText(
context,

View file

@ -312,10 +312,10 @@ class _ContactViewState extends State<ContactView> {
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),
),

View file

@ -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<SetupView> {
StreamSubscription<void>? _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<void>(
@ -46,11 +79,8 @@ class _SetupViewState extends State<SetupView> {
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<SetupView> {
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<SetupView> {
return const ProfileSetupPage();
case SetupPages.backup:
return const BackupSetupPage();
case SetupPages.addNewContact:
return const AddNewContactsPage();
case SetupPages.verificationBadge:
return const VerificationBadgeSetupPage();
case SetupPages.userDiscovery:

View file

@ -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),
),
),
],
),
);
}
}

View file

@ -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<BackupSetupPage> {
final TextEditingController passwordCtrl = TextEditingController();
final TextEditingController repeatedPasswordCtrl = TextEditingController();
Future<void> 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<bool> 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<BackupSetupPage> {
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<BackupSetupPage> {
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<BackupSetupPage> {
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<BackupSetupPage> {
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<Color>(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,
),
],
);

View file

@ -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<FinishSetupComp> createState() => _FinishSetupCompState();
}
class _FinishSetupCompState extends State<FinishSetupComp> {
Future<void> onTap() async {
await context.navPush(
SetupView(
onUpdate: () {
Navigator.pop(context);
},
),
);
}
@override
Widget build(BuildContext context) {
return StreamBuilder<void>(
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,
),
),
],
),
),
],
),
),
),
),
);
},
);
}
}

View file

@ -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: () {},
),
],
);
}
}

View file

@ -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<bool> 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<Color>(Colors.white),
),
)
: Text(
currentPage.isLast ? context.lang.finishSetup : context.lang.next,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
);
}
}

View file

@ -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<bool> 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),
),
),
],
),
);
}
}

View file

@ -1,15 +0,0 @@
import 'package:flutter/material.dart';
class FinishSetupComp extends StatefulWidget {
const FinishSetupComp({super.key});
@override
State<FinishSetupComp> createState() => _FinishSetupCompState();
}
class _FinishSetupCompState extends State<FinishSetupComp> {
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}

View file

@ -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<ProfileSetupPage> {
@override
void initState() {
super.initState();
_displayNameController = TextEditingController(
text: userService.currentUser.displayName,
);
_displayNameController = TextEditingController();
}
@override
@ -55,32 +53,39 @@ class _ProfileSetupPageState extends State<ProfileSetupPage> {
),
),
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<ProfileSetupPage> {
),
),
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,
),
),
),
],
);

View file

@ -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<UserDiscoverySetupPage> createState() => _UserDiscoverySetupPageState();
}
class _UserDiscoverySetupPageState extends State<UserDiscoverySetupPage> {
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),
),
],
),

View file

@ -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(),
],
);
}

View file

@ -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<UserDiscoveryDisabledComp> {
Future<void> 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<UserDiscoveryDisabledComp> {
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),
],
),
);

View file

@ -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<bool> 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<int>(
value: state.requiredSendImages,
items: List.generate(
9,
(index) {
final value = index + 2;
return DropdownMenuItem<int>(
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<int>(
value: state.threshold,
items: List.generate(
9,
(index) {
final value = index + 2;
return DropdownMenuItem<int>(
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(),
],
),
),
),
],
),
),
),
],
),
);
}
}

View file

@ -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<UserDiscoverySettingsView> {
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<void> _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<bool?> _showBackDialog() {
return showDialog<bool>(
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<bool?>(
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),
],
),
),
),
);

View file

@ -8,12 +8,13 @@ impl FlutterUserDiscovery {
threshold: u8,
user_id: i64,
public_key: Vec<u8>,
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?)
}

View file

@ -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<Self> {
let db_url = format!("sqlite://{}", db_path);
// impl Database {
// pub(crate) async fn new(db_path: &String, read_only: bool) -> Result<Self> {
// 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::<SqliteConnectOptions>()?
.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::<SqliteConnectOptions>()?
// .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 })
// }
// }

View file

@ -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 = <u8>::sse_decode(&mut deserializer);
let api_user_id = <i64>::sse_decode(&mut deserializer);
let api_public_key = <Vec<u8>>::sse_decode(&mut deserializer);deserializer.end(); move |context| async move {
let api_public_key = <Vec<u8>>::sse_decode(&mut deserializer);
let api_share_promotion = <bool>::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)
} })
}

View file

@ -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<Database>,
}
// 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<Database>,
// }

View file

@ -40,6 +40,8 @@ struct UserDiscoveryConfig {
verification_shares: Vec<Vec<u8>>,
// The users' id:
user_id: UserID,
// If others user should promote the promotion to other users
share_promotion: bool,
}
///
@ -87,6 +89,7 @@ impl<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store,
threshold: u8,
user_id: UserID,
public_key: Vec<u8>,
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<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store,
config.public_id = public_id;
config.announcement_version += 1;
config.verification_shares = verification_shares;
config.share_promotion = share_promotion;
self.update_config(config, config_lock).await?;
@ -203,6 +207,7 @@ impl<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store,
threshold: config.threshold as u32,
announcement_share,
verification_shares: config.verification_shares,
share_promotion: config.share_promotion,
});
messages.push(
@ -519,33 +524,36 @@ impl<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store,
)));
}
let (mut config, config_lock) = self.get_config().await?;
config.promotion_version += 1;
// Only add this user to the promotions if the users enabled this feature
if uda.share_promotion {
let (mut config, config_lock) = self.get_config().await?;
config.promotion_version += 1;
let message = UserDiscoveryMessage {
version: Some(UserDiscoveryVersion {
announcement: config.announcement_version,
promotion: config.promotion_version,
}),
user_discovery_promotion: Some(UserDiscoveryPromotion {
promotion_id: rand::random(),
public_id: signed_data.public_id,
threshold: uda.threshold,
announcement_share: uda.announcement_share,
public_key_verified_timestamp,
}),
..Default::default()
};
let message = UserDiscoveryMessage {
version: Some(UserDiscoveryVersion {
announcement: config.announcement_version,
promotion: config.promotion_version,
}),
user_discovery_promotion: Some(UserDiscoveryPromotion {
promotion_id: rand::random(),
public_id: signed_data.public_id,
threshold: uda.threshold,
announcement_share: uda.announcement_share,
public_key_verified_timestamp,
}),
..Default::default()
};
self.store
.push_own_promotion_and_clear_old_version(
contact_id,
config.promotion_version,
message.encode_to_vec(),
)
.await?;
self.store
.push_own_promotion_and_clear_old_version(
contact_id,
config.promotion_version,
message.encode_to_vec(),
)
.await?;
self.update_config(config, config_lock).await?;
self.update_config(config, config_lock).await?;
}
let announced_user = AnnouncedUser {
user_id: signed_data.user_id,
@ -721,6 +729,7 @@ impl Default for UserDiscoveryConfig {
promotion_version: 0,
verification_shares: vec![],
public_id: 0,
share_promotion: true,
user_id: 0,
}
}

View file

@ -161,7 +161,7 @@ async fn test_user_discovery_decreased_threshold_in_memory_store() {
// Phase 2: Update ALICE's threshold to 2
network.uds[alice_idx]
.initialize_or_update(2, alice_idx as UserID, vec![alice_idx as u8; 32])
.initialize_or_update(2, alice_idx as UserID, vec![alice_idx as u8; 32], true)
.await
.unwrap();
@ -241,7 +241,7 @@ async fn test_user_discovery_increased_threshold_in_memory_store() {
// Phase 2: Update ALICE's threshold to 3
network.uds[alice_idx]
.initialize_or_update(3, alice_idx as UserID, vec![alice_idx as u8; 32])
.initialize_or_update(3, alice_idx as UserID, vec![alice_idx as u8; 32], true)
.await
.unwrap();
@ -414,7 +414,7 @@ async fn get_ud<S: UserDiscoveryStore + Clone + Default>(
) -> UserDiscovery<S, TestingUtils> {
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();

View file

@ -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 {

121
scripts/check_arb.py Normal file
View file

@ -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()

View file

@ -145,6 +145,17 @@ class Contacts extends Table with TableInfo<Contacts, ContactsData> {
'NOT NULL DEFAULT 0 CHECK (user_discovery_excluded IN (0, 1))',
defaultValue: const CustomExpression('0'),
);
late final GeneratedColumn<int>
userDiscoveryManualApproved = GeneratedColumn<int>(
'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<int> mediaSendCounter = GeneratedColumn<int>(
'media_send_counter',
aliasedName,
@ -180,6 +191,7 @@ class Contacts extends Table with TableInfo<Contacts, ContactsData> {
createdAt,
userDiscoveryVersion,
userDiscoveryExcluded,
userDiscoveryManualApproved,
mediaSendCounter,
mediaReceivedCounter,
];
@ -254,6 +266,10 @@ class Contacts extends Table with TableInfo<Contacts, ContactsData> {
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<ContactsData> {
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<ContactsData> {
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<ContactsData> {
);
}
map['user_discovery_excluded'] = Variable<int>(userDiscoveryExcluded);
map['user_discovery_manual_approved'] = Variable<int>(
userDiscoveryManualApproved,
);
map['media_send_counter'] = Variable<int>(mediaSendCounter);
map['media_received_counter'] = Variable<int>(mediaReceivedCounter);
return map;
@ -373,6 +394,7 @@ class ContactsData extends DataClass implements Insertable<ContactsData> {
? 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<ContactsData> {
userDiscoveryExcluded: serializer.fromJson<int>(
json['userDiscoveryExcluded'],
),
userDiscoveryManualApproved: serializer.fromJson<int>(
json['userDiscoveryManualApproved'],
),
mediaSendCounter: serializer.fromJson<int>(json['mediaSendCounter']),
mediaReceivedCounter: serializer.fromJson<int>(
json['mediaReceivedCounter'],
@ -436,6 +461,9 @@ class ContactsData extends DataClass implements Insertable<ContactsData> {
userDiscoveryVersion,
),
'userDiscoveryExcluded': serializer.toJson<int>(userDiscoveryExcluded),
'userDiscoveryManualApproved': serializer.toJson<int>(
userDiscoveryManualApproved,
),
'mediaSendCounter': serializer.toJson<int>(mediaSendCounter),
'mediaReceivedCounter': serializer.toJson<int>(mediaReceivedCounter),
};
@ -457,6 +485,7 @@ class ContactsData extends DataClass implements Insertable<ContactsData> {
int? createdAt,
Value<i2.Uint8List?> userDiscoveryVersion = const Value.absent(),
int? userDiscoveryExcluded,
int? userDiscoveryManualApproved,
int? mediaSendCounter,
int? mediaReceivedCounter,
}) => ContactsData(
@ -479,6 +508,8 @@ class ContactsData extends DataClass implements Insertable<ContactsData> {
? 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<ContactsData> {
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<ContactsData> {
..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<ContactsData> {
createdAt,
$driftBlobEquality.hash(userDiscoveryVersion),
userDiscoveryExcluded,
userDiscoveryManualApproved,
mediaSendCounter,
mediaReceivedCounter,
);
@ -591,6 +627,8 @@ class ContactsData extends DataClass implements Insertable<ContactsData> {
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<ContactsData> {
final Value<int> createdAt;
final Value<i2.Uint8List?> userDiscoveryVersion;
final Value<int> userDiscoveryExcluded;
final Value<int> userDiscoveryManualApproved;
final Value<int> mediaSendCounter;
final Value<int> mediaReceivedCounter;
const ContactsCompanion({
@ -629,6 +668,7 @@ class ContactsCompanion extends UpdateCompanion<ContactsData> {
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<ContactsData> {
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<ContactsData> {
Expression<int>? createdAt,
Expression<i2.Uint8List>? userDiscoveryVersion,
Expression<int>? userDiscoveryExcluded,
Expression<int>? userDiscoveryManualApproved,
Expression<int>? mediaSendCounter,
Expression<int>? mediaReceivedCounter,
}) {
@ -690,6 +732,8 @@ class ContactsCompanion extends UpdateCompanion<ContactsData> {
'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<ContactsData> {
Value<int>? createdAt,
Value<i2.Uint8List?>? userDiscoveryVersion,
Value<int>? userDiscoveryExcluded,
Value<int>? userDiscoveryManualApproved,
Value<int>? mediaSendCounter,
Value<int>? mediaReceivedCounter,
}) {
@ -732,6 +777,8 @@ class ContactsCompanion extends UpdateCompanion<ContactsData> {
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<ContactsData> {
userDiscoveryExcluded.value,
);
}
if (userDiscoveryManualApproved.present) {
map['user_discovery_manual_approved'] = Variable<int>(
userDiscoveryManualApproved.value,
);
}
if (mediaSendCounter.present) {
map['media_send_counter'] = Variable<int>(mediaSendCounter.value);
}
@ -818,6 +870,7 @@ class ContactsCompanion extends UpdateCompanion<ContactsData> {
..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<int> verificationId = GeneratedColumn<int>(
'verification_id',
aliasedName,
false,
hasAutoIncrement: true,
type: DriftSqlType.int,
requiredDuringInsert: false,
$customConstraints: 'NOT NULL PRIMARY KEY AUTOINCREMENT',
);
late final GeneratedColumn<int> contactId = GeneratedColumn<int>(
'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<GeneratedColumn> get $columns => [contactId, type, createdAt];
List<GeneratedColumn> 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<GeneratedColumn> get $primaryKey => {contactId};
Set<GeneratedColumn> get $primaryKey => {verificationId};
@override
KeyVerificationsData map(Map<String, dynamic> 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<String> get customConstraints => const ['PRIMARY KEY(contact_id)'];
@override
bool get dontWriteConstraints => true;
}
class KeyVerificationsData extends DataClass
implements Insertable<KeyVerificationsData> {
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<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
map['verification_id'] = Variable<int>(verificationId);
map['contact_id'] = Variable<int>(contactId);
map['type'] = Variable<String>(type);
map['created_at'] = Variable<int>(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<int>(json['verificationId']),
contactId: serializer.fromJson<int>(json['contactId']),
type: serializer.fromJson<String>(json['type']),
createdAt: serializer.fromJson<int>(json['createdAt']),
@ -7572,6 +7646,7 @@ class KeyVerificationsData extends DataClass
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'verificationId': serializer.toJson<int>(verificationId),
'contactId': serializer.toJson<int>(contactId),
'type': serializer.toJson<String>(type),
'createdAt': serializer.toJson<int>(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<KeyVerificationsData> {
final Value<int> verificationId;
final Value<int> contactId;
final Value<String> type;
final Value<int> 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<KeyVerificationsData> custom({
Expression<int>? verificationId,
Expression<int>? contactId,
Expression<String>? type,
Expression<int>? 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<KeyVerificationsData> {
}
KeyVerificationsCompanion copyWith({
Value<int>? verificationId,
Value<int>? contactId,
Value<String>? type,
Value<int>? 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<KeyVerificationsData> {
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
if (verificationId.present) {
map['verification_id'] = Variable<int>(verificationId.value);
}
if (contactId.present) {
map['contact_id'] = Variable<int>(contactId.value);
}
@ -7672,6 +7765,7 @@ class KeyVerificationsCompanion extends UpdateCompanion<KeyVerificationsData> {
@override
String toString() {
return (StringBuffer('KeyVerificationsCompanion(')
..write('verificationId: $verificationId, ')
..write('contactId: $contactId, ')
..write('type: $type, ')
..write('createdAt: $createdAt')