first working state

This commit is contained in:
otsmr 2026-04-21 01:31:50 +02:00
parent 93d5f682fc
commit 693c74df46
34 changed files with 14259 additions and 326 deletions

View file

@ -16,9 +16,11 @@ analyzer:
- "lib/src/model/protobuf/**" - "lib/src/model/protobuf/**"
- "lib/src/model/protobuf/api/websocket/**" - "lib/src/model/protobuf/api/websocket/**"
- "lib/generated/**" - "lib/generated/**"
- "lib/core/**"
- "lib/src/localization/**"
- "dependencies/**" - "dependencies/**"
- "pubspec.yaml" - "pubspec.yaml"
- "*.arb" - "**.arb"
- "test/drift/**" - "test/drift/**"
- "**.g.dart" - "**.g.dart"

View file

@ -1,12 +1,31 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart'; import 'package:integration_test/integration_test.dart';
import 'package:twonly/core/frb_generated.dart'; import 'package:twonly/core/frb_generated.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/services/background/callback_dispatcher.background.dart';
void main() { void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized(); IntegrationTestWidgetsFlutterBinding.ensureInitialized();
setUpAll(() async => RustLib.init()); setUpAll(() async => RustLib.init());
// testWidgets('Can call rust function', (tester) async {
// await tester.pumpWidget(const MyApp()); test('Can initialize twonlyDB and connect to api server', () async {
// expect(find.textContaining('Result: `Hello, Tom!`'), findsOneWidget); // Initialize global variables
// }); await initBackgroundExecution();
// Try to connect to the API server
final connected = await apiService.connect();
// Print out the result or test it
expect(connected, isA<bool>());
// We can also check if it's connected
// Depending on your test environment, this might be true or false
// if the server is unreachable without further setup
// expect(apiService.isConnected, isA<bool>());
// Close the connection after the test
if (apiService.isConnected) {
await apiService.close(() {});
}
});
} }

View file

@ -1,3 +1,4 @@
import 'package:collection/collection.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart' import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'
show Curve, IdentityKey; show Curve, IdentityKey;
@ -45,7 +46,7 @@ class UserDiscoveryCallbacks {
) async { ) async {
final storedPublicKey = await getPublicKeyFromContact(contactId); final storedPublicKey = await getPublicKeyFromContact(contactId);
if (storedPublicKey != null) { if (storedPublicKey != null) {
return storedPublicKey == pubKey; return storedPublicKey.equals(pubKey);
} else { } else {
return false; return false;
} }

View file

@ -138,6 +138,7 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
return (select(contacts)..where( return (select(contacts)..where(
(t) => (t) =>
t.userDiscoveryVersion.isNotNull() & t.userDiscoveryVersion.isNotNull() &
t.userDiscoveryExcluded.equals(false) &
t.mediaSendCounter.isBiggerOrEqualValue( t.mediaSendCounter.isBiggerOrEqualValue(
gUser.minimumRequiredImagesExchanged, gUser.minimumRequiredImagesExchanged,
), ),

View file

@ -311,4 +311,19 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
)) ))
.write(GroupsCompanion(lastMessageExchange: Value(newLastMessage))); .write(GroupsCompanion(lastMessageExchange: Value(newLastMessage)));
} }
Stream<List<Group>> watchNonDirectGroupsForMember(int contactId) {
final query =
select(groups).join([
innerJoin(
groupMembers,
groupMembers.groupId.equalsExp(groups.groupId),
),
])..where(
groups.isDirectChat.equals(false) &
groupMembers.contactId.equals(contactId),
);
return query.map((row) => row.readTable(groups)).watch();
}
} }

View file

@ -1,15 +1,20 @@
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/tables/user_discovery.table.dart'; import 'package:twonly/src/database/tables/user_discovery.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
part 'user_discovery.dao.g.dart'; part 'user_discovery.dao.g.dart';
typedef AnnouncedUsersWithRelations =
Map<UserDiscoveryAnnouncedUser, List<(Contact, DateTime?)>>;
@DriftAccessor( @DriftAccessor(
tables: [ tables: [
UserDiscoveryAnnouncedUsers, UserDiscoveryAnnouncedUsers,
UserDiscoveryUserRelations, UserDiscoveryUserRelations,
UserDiscoveryOwnPromotions, UserDiscoveryOwnPromotions,
UserDiscoveryShares, UserDiscoveryShares,
Contacts,
], ],
) )
class UserDiscoveryDao extends DatabaseAccessor<TwonlyDB> class UserDiscoveryDao extends DatabaseAccessor<TwonlyDB>
@ -46,8 +51,7 @@ class UserDiscoveryDao extends DatabaseAccessor<TwonlyDB>
.toList(); .toList();
} }
Future<Map<UserDiscoveryAnnouncedUser, List<(int, DateTime?)>>> Stream<AnnouncedUsersWithRelations> watchAllAnnouncedUsersWithRelations() {
getAnnouncedUsersWithRelations() async {
final query = select(userDiscoveryAnnouncedUsers).join([ final query = select(userDiscoveryAnnouncedUsers).join([
innerJoin( innerJoin(
userDiscoveryUserRelations, userDiscoveryUserRelations,
@ -55,28 +59,89 @@ class UserDiscoveryDao extends DatabaseAccessor<TwonlyDB>
userDiscoveryAnnouncedUsers.announcedUserId, userDiscoveryAnnouncedUsers.announcedUserId,
), ),
), ),
]); innerJoin(
contacts,
contacts.userId.equalsExp(
userDiscoveryUserRelations.fromContactId,
),
),
])..where(userDiscoveryAnnouncedUsers.username.isNotNull());
final rows = await query.get(); return query.watch().map((rows) {
// ignore: omit_local_variable_types
final AnnouncedUsersWithRelations results = {};
final results = <UserDiscoveryAnnouncedUser, List<(int, DateTime?)>>{}; for (final row in rows) {
final user = row.readTable(userDiscoveryAnnouncedUsers);
final relation = row.readTable(userDiscoveryUserRelations);
final contact = row.readTable(contacts);
for (final row in rows) { final relationData = (
final user = row.readTable(userDiscoveryAnnouncedUsers); contact,
final relation = row.readTable(userDiscoveryUserRelations); relation.publicKeyVerifiedTimestamp,
);
final relationData = ( if (!results.containsKey(user)) {
relation.fromContactId, results[user] = [];
relation.publicKeyVerifiedTimestamp, }
); results[user]!.add(relationData);
if (!results.containsKey(user)) {
results[user] = [];
} }
results[user]!.add(relationData);
}
return results; return results;
});
}
Stream<AnnouncedUsersWithRelations> watchNewAnnouncedUsersWithRelations() {
final announcedContact = alias(contacts, 'announcedContact');
final query =
select(userDiscoveryAnnouncedUsers).join([
innerJoin(
userDiscoveryUserRelations,
userDiscoveryUserRelations.announcedUserId.equalsExp(
userDiscoveryAnnouncedUsers.announcedUserId,
),
),
innerJoin(
contacts,
contacts.userId.equalsExp(
userDiscoveryUserRelations.fromContactId,
),
),
leftOuterJoin(
announcedContact,
announcedContact.userId.equalsExp(
userDiscoveryAnnouncedUsers.announcedUserId,
),
),
])..where(
userDiscoveryAnnouncedUsers.username.isNotNull() &
userDiscoveryAnnouncedUsers.isHidden.equals(false) &
(announcedContact.userId.isNull() |
announcedContact.deletedByUser.equals(true)),
);
return query.watch().map((rows) {
// ignore: omit_local_variable_types
final AnnouncedUsersWithRelations results = {};
for (final row in rows) {
final user = row.readTable(userDiscoveryAnnouncedUsers);
final relation = row.readTable(userDiscoveryUserRelations);
final contact = row.readTable(contacts);
final relationData = (
contact,
relation.publicKeyVerifiedTimestamp,
);
if (!results.containsKey(user)) {
results[user] = [];
}
results[user]!.add(relationData);
}
return results;
});
} }
Stream<int> watchNewAnnouncementsWithDataCount() { Stream<int> watchNewAnnouncementsWithDataCount() {
@ -94,6 +159,20 @@ class UserDiscoveryDao extends DatabaseAccessor<TwonlyDB>
return query.watchSingle().map((row) => row.read(countExp) ?? 0); return query.watchSingle().map((row) => row.read(countExp) ?? 0);
} }
Future<void> markAllValidAnnouncedUsersAsShown() async {
await (update(userDiscoveryAnnouncedUsers)..where(
(t) =>
t.username.isNotNull() &
t.wasShownToTheUser.equals(false) &
t.isHidden.equals(false),
))
.write(
const UserDiscoveryAnnouncedUsersCompanion(
wasShownToTheUser: Value(true),
),
);
}
Future<void> updateAnnouncedUser( Future<void> updateAnnouncedUser(
int announcedUserId, int announcedUserId,
UserDiscoveryAnnouncedUsersCompanion updatedValues, UserDiscoveryAnnouncedUsersCompanion updatedValues,

File diff suppressed because it is too large Load diff

View file

@ -26,6 +26,9 @@ class Contacts extends Table {
// contact_versions: HashMap<UserID, Vec<u8>>, // contact_versions: HashMap<UserID, Vec<u8>>,
BlobColumn get userDiscoveryVersion => blob().nullable()(); BlobColumn get userDiscoveryVersion => blob().nullable()();
BoolColumn get userDiscoveryExcluded =>
boolean().withDefault(const Constant(false))();
IntColumn get mediaSendCounter => integer().withDefault(const Constant(0))(); IntColumn get mediaSendCounter => integer().withDefault(const Constant(0))();
IntColumn get mediaReceivedCounter => IntColumn get mediaReceivedCounter =>
integer().withDefault(const Constant(0))(); integer().withDefault(const Constant(0))();

View file

@ -72,7 +72,7 @@ class TwonlyDB extends _$TwonlyDB {
TwonlyDB.forTesting(DatabaseConnection super.connection); TwonlyDB.forTesting(DatabaseConnection super.connection);
@override @override
int get schemaVersion => 14; int get schemaVersion => 15;
static QueryExecutor _openConnection() { static QueryExecutor _openConnection() {
return driftDatabase( return driftDatabase(
@ -108,7 +108,6 @@ class TwonlyDB extends _$TwonlyDB {
}, },
from3To4: (m, schema) async { from3To4: (m, schema) async {
await m.alterTable( await m.alterTable(
// ignore: experimental_member_use
TableMigration( TableMigration(
schema.groupHistories, schema.groupHistories,
columnTransformer: { columnTransformer: {
@ -141,9 +140,7 @@ class TwonlyDB extends _$TwonlyDB {
await m.deleteTable('signal_contact_pre_keys'); await m.deleteTable('signal_contact_pre_keys');
await m.deleteTable('signal_contact_signed_pre_keys'); await m.deleteTable('signal_contact_signed_pre_keys');
// For message_actions // For message_actions
// ignore: experimental_member_use
await m.alterTable(TableMigration(schema.messageHistories)); await m.alterTable(TableMigration(schema.messageHistories));
// ignore: experimental_member_use
await m.alterTable(TableMigration(schema.messageActions)); await m.alterTable(TableMigration(schema.messageActions));
}, },
from8To9: (m, schema) async { from8To9: (m, schema) async {
@ -205,6 +202,12 @@ class TwonlyDB extends _$TwonlyDB {
schema.userDiscoveryAnnouncedUsers.username, schema.userDiscoveryAnnouncedUsers.username,
); );
}, },
from14To15: (m, schema) async {
await m.addColumn(
schema.contacts,
schema.contacts.userDiscoveryExcluded,
);
},
)(m, from, to); )(m, from, to);
}, },
); );

View file

@ -185,6 +185,21 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> {
type: DriftSqlType.blob, type: DriftSqlType.blob,
requiredDuringInsert: false, requiredDuringInsert: false,
); );
static const VerificationMeta _userDiscoveryExcludedMeta =
const VerificationMeta('userDiscoveryExcluded');
@override
late final GeneratedColumn<bool> userDiscoveryExcluded =
GeneratedColumn<bool>(
'user_discovery_excluded',
aliasedName,
false,
type: DriftSqlType.bool,
requiredDuringInsert: false,
defaultConstraints: GeneratedColumn.constraintIsAlways(
'CHECK ("user_discovery_excluded" IN (0, 1))',
),
defaultValue: const Constant(false),
);
static const VerificationMeta _mediaSendCounterMeta = const VerificationMeta( static const VerificationMeta _mediaSendCounterMeta = const VerificationMeta(
'mediaSendCounter', 'mediaSendCounter',
); );
@ -224,6 +239,7 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> {
accountDeleted, accountDeleted,
createdAt, createdAt,
userDiscoveryVersion, userDiscoveryVersion,
userDiscoveryExcluded,
mediaSendCounter, mediaSendCounter,
mediaReceivedCounter, mediaReceivedCounter,
]; ];
@ -343,6 +359,15 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> {
), ),
); );
} }
if (data.containsKey('user_discovery_excluded')) {
context.handle(
_userDiscoveryExcludedMeta,
userDiscoveryExcluded.isAcceptableOrUnknown(
data['user_discovery_excluded']!,
_userDiscoveryExcludedMeta,
),
);
}
if (data.containsKey('media_send_counter')) { if (data.containsKey('media_send_counter')) {
context.handle( context.handle(
_mediaSendCounterMeta, _mediaSendCounterMeta,
@ -426,6 +451,10 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> {
DriftSqlType.blob, DriftSqlType.blob,
data['${effectivePrefix}user_discovery_version'], data['${effectivePrefix}user_discovery_version'],
), ),
userDiscoveryExcluded: attachedDatabase.typeMapping.read(
DriftSqlType.bool,
data['${effectivePrefix}user_discovery_excluded'],
)!,
mediaSendCounter: attachedDatabase.typeMapping.read( mediaSendCounter: attachedDatabase.typeMapping.read(
DriftSqlType.int, DriftSqlType.int,
data['${effectivePrefix}media_send_counter'], data['${effectivePrefix}media_send_counter'],
@ -458,6 +487,7 @@ class Contact extends DataClass implements Insertable<Contact> {
final bool accountDeleted; final bool accountDeleted;
final DateTime createdAt; final DateTime createdAt;
final Uint8List? userDiscoveryVersion; final Uint8List? userDiscoveryVersion;
final bool userDiscoveryExcluded;
final int mediaSendCounter; final int mediaSendCounter;
final int mediaReceivedCounter; final int mediaReceivedCounter;
const Contact({ const Contact({
@ -475,6 +505,7 @@ class Contact extends DataClass implements Insertable<Contact> {
required this.accountDeleted, required this.accountDeleted,
required this.createdAt, required this.createdAt,
this.userDiscoveryVersion, this.userDiscoveryVersion,
required this.userDiscoveryExcluded,
required this.mediaSendCounter, required this.mediaSendCounter,
required this.mediaReceivedCounter, required this.mediaReceivedCounter,
}); });
@ -503,6 +534,7 @@ class Contact extends DataClass implements Insertable<Contact> {
if (!nullToAbsent || userDiscoveryVersion != null) { if (!nullToAbsent || userDiscoveryVersion != null) {
map['user_discovery_version'] = Variable<Uint8List>(userDiscoveryVersion); map['user_discovery_version'] = Variable<Uint8List>(userDiscoveryVersion);
} }
map['user_discovery_excluded'] = Variable<bool>(userDiscoveryExcluded);
map['media_send_counter'] = Variable<int>(mediaSendCounter); map['media_send_counter'] = Variable<int>(mediaSendCounter);
map['media_received_counter'] = Variable<int>(mediaReceivedCounter); map['media_received_counter'] = Variable<int>(mediaReceivedCounter);
return map; return map;
@ -532,6 +564,7 @@ class Contact extends DataClass implements Insertable<Contact> {
userDiscoveryVersion: userDiscoveryVersion == null && nullToAbsent userDiscoveryVersion: userDiscoveryVersion == null && nullToAbsent
? const Value.absent() ? const Value.absent()
: Value(userDiscoveryVersion), : Value(userDiscoveryVersion),
userDiscoveryExcluded: Value(userDiscoveryExcluded),
mediaSendCounter: Value(mediaSendCounter), mediaSendCounter: Value(mediaSendCounter),
mediaReceivedCounter: Value(mediaReceivedCounter), mediaReceivedCounter: Value(mediaReceivedCounter),
); );
@ -563,6 +596,9 @@ class Contact extends DataClass implements Insertable<Contact> {
userDiscoveryVersion: serializer.fromJson<Uint8List?>( userDiscoveryVersion: serializer.fromJson<Uint8List?>(
json['userDiscoveryVersion'], json['userDiscoveryVersion'],
), ),
userDiscoveryExcluded: serializer.fromJson<bool>(
json['userDiscoveryExcluded'],
),
mediaSendCounter: serializer.fromJson<int>(json['mediaSendCounter']), mediaSendCounter: serializer.fromJson<int>(json['mediaSendCounter']),
mediaReceivedCounter: serializer.fromJson<int>( mediaReceivedCounter: serializer.fromJson<int>(
json['mediaReceivedCounter'], json['mediaReceivedCounter'],
@ -589,6 +625,7 @@ class Contact extends DataClass implements Insertable<Contact> {
'userDiscoveryVersion': serializer.toJson<Uint8List?>( 'userDiscoveryVersion': serializer.toJson<Uint8List?>(
userDiscoveryVersion, userDiscoveryVersion,
), ),
'userDiscoveryExcluded': serializer.toJson<bool>(userDiscoveryExcluded),
'mediaSendCounter': serializer.toJson<int>(mediaSendCounter), 'mediaSendCounter': serializer.toJson<int>(mediaSendCounter),
'mediaReceivedCounter': serializer.toJson<int>(mediaReceivedCounter), 'mediaReceivedCounter': serializer.toJson<int>(mediaReceivedCounter),
}; };
@ -609,6 +646,7 @@ class Contact extends DataClass implements Insertable<Contact> {
bool? accountDeleted, bool? accountDeleted,
DateTime? createdAt, DateTime? createdAt,
Value<Uint8List?> userDiscoveryVersion = const Value.absent(), Value<Uint8List?> userDiscoveryVersion = const Value.absent(),
bool? userDiscoveryExcluded,
int? mediaSendCounter, int? mediaSendCounter,
int? mediaReceivedCounter, int? mediaReceivedCounter,
}) => Contact( }) => Contact(
@ -630,6 +668,7 @@ class Contact extends DataClass implements Insertable<Contact> {
userDiscoveryVersion: userDiscoveryVersion.present userDiscoveryVersion: userDiscoveryVersion.present
? userDiscoveryVersion.value ? userDiscoveryVersion.value
: this.userDiscoveryVersion, : this.userDiscoveryVersion,
userDiscoveryExcluded: userDiscoveryExcluded ?? this.userDiscoveryExcluded,
mediaSendCounter: mediaSendCounter ?? this.mediaSendCounter, mediaSendCounter: mediaSendCounter ?? this.mediaSendCounter,
mediaReceivedCounter: mediaReceivedCounter ?? this.mediaReceivedCounter, mediaReceivedCounter: mediaReceivedCounter ?? this.mediaReceivedCounter,
); );
@ -661,6 +700,9 @@ class Contact extends DataClass implements Insertable<Contact> {
userDiscoveryVersion: data.userDiscoveryVersion.present userDiscoveryVersion: data.userDiscoveryVersion.present
? data.userDiscoveryVersion.value ? data.userDiscoveryVersion.value
: this.userDiscoveryVersion, : this.userDiscoveryVersion,
userDiscoveryExcluded: data.userDiscoveryExcluded.present
? data.userDiscoveryExcluded.value
: this.userDiscoveryExcluded,
mediaSendCounter: data.mediaSendCounter.present mediaSendCounter: data.mediaSendCounter.present
? data.mediaSendCounter.value ? data.mediaSendCounter.value
: this.mediaSendCounter, : this.mediaSendCounter,
@ -687,6 +729,7 @@ class Contact extends DataClass implements Insertable<Contact> {
..write('accountDeleted: $accountDeleted, ') ..write('accountDeleted: $accountDeleted, ')
..write('createdAt: $createdAt, ') ..write('createdAt: $createdAt, ')
..write('userDiscoveryVersion: $userDiscoveryVersion, ') ..write('userDiscoveryVersion: $userDiscoveryVersion, ')
..write('userDiscoveryExcluded: $userDiscoveryExcluded, ')
..write('mediaSendCounter: $mediaSendCounter, ') ..write('mediaSendCounter: $mediaSendCounter, ')
..write('mediaReceivedCounter: $mediaReceivedCounter') ..write('mediaReceivedCounter: $mediaReceivedCounter')
..write(')')) ..write(')'))
@ -709,6 +752,7 @@ class Contact extends DataClass implements Insertable<Contact> {
accountDeleted, accountDeleted,
createdAt, createdAt,
$driftBlobEquality.hash(userDiscoveryVersion), $driftBlobEquality.hash(userDiscoveryVersion),
userDiscoveryExcluded,
mediaSendCounter, mediaSendCounter,
mediaReceivedCounter, mediaReceivedCounter,
); );
@ -736,6 +780,7 @@ class Contact extends DataClass implements Insertable<Contact> {
other.userDiscoveryVersion, other.userDiscoveryVersion,
this.userDiscoveryVersion, this.userDiscoveryVersion,
) && ) &&
other.userDiscoveryExcluded == this.userDiscoveryExcluded &&
other.mediaSendCounter == this.mediaSendCounter && other.mediaSendCounter == this.mediaSendCounter &&
other.mediaReceivedCounter == this.mediaReceivedCounter); other.mediaReceivedCounter == this.mediaReceivedCounter);
} }
@ -755,6 +800,7 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
final Value<bool> accountDeleted; final Value<bool> accountDeleted;
final Value<DateTime> createdAt; final Value<DateTime> createdAt;
final Value<Uint8List?> userDiscoveryVersion; final Value<Uint8List?> userDiscoveryVersion;
final Value<bool> userDiscoveryExcluded;
final Value<int> mediaSendCounter; final Value<int> mediaSendCounter;
final Value<int> mediaReceivedCounter; final Value<int> mediaReceivedCounter;
const ContactsCompanion({ const ContactsCompanion({
@ -772,6 +818,7 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
this.accountDeleted = const Value.absent(), this.accountDeleted = const Value.absent(),
this.createdAt = const Value.absent(), this.createdAt = const Value.absent(),
this.userDiscoveryVersion = const Value.absent(), this.userDiscoveryVersion = const Value.absent(),
this.userDiscoveryExcluded = const Value.absent(),
this.mediaSendCounter = const Value.absent(), this.mediaSendCounter = const Value.absent(),
this.mediaReceivedCounter = const Value.absent(), this.mediaReceivedCounter = const Value.absent(),
}); });
@ -790,6 +837,7 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
this.accountDeleted = const Value.absent(), this.accountDeleted = const Value.absent(),
this.createdAt = const Value.absent(), this.createdAt = const Value.absent(),
this.userDiscoveryVersion = const Value.absent(), this.userDiscoveryVersion = const Value.absent(),
this.userDiscoveryExcluded = const Value.absent(),
this.mediaSendCounter = const Value.absent(), this.mediaSendCounter = const Value.absent(),
this.mediaReceivedCounter = const Value.absent(), this.mediaReceivedCounter = const Value.absent(),
}) : username = Value(username); }) : username = Value(username);
@ -808,6 +856,7 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
Expression<bool>? accountDeleted, Expression<bool>? accountDeleted,
Expression<DateTime>? createdAt, Expression<DateTime>? createdAt,
Expression<Uint8List>? userDiscoveryVersion, Expression<Uint8List>? userDiscoveryVersion,
Expression<bool>? userDiscoveryExcluded,
Expression<int>? mediaSendCounter, Expression<int>? mediaSendCounter,
Expression<int>? mediaReceivedCounter, Expression<int>? mediaReceivedCounter,
}) { }) {
@ -829,6 +878,8 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
if (createdAt != null) 'created_at': createdAt, if (createdAt != null) 'created_at': createdAt,
if (userDiscoveryVersion != null) if (userDiscoveryVersion != null)
'user_discovery_version': userDiscoveryVersion, 'user_discovery_version': userDiscoveryVersion,
if (userDiscoveryExcluded != null)
'user_discovery_excluded': userDiscoveryExcluded,
if (mediaSendCounter != null) 'media_send_counter': mediaSendCounter, if (mediaSendCounter != null) 'media_send_counter': mediaSendCounter,
if (mediaReceivedCounter != null) if (mediaReceivedCounter != null)
'media_received_counter': mediaReceivedCounter, 'media_received_counter': mediaReceivedCounter,
@ -850,6 +901,7 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
Value<bool>? accountDeleted, Value<bool>? accountDeleted,
Value<DateTime>? createdAt, Value<DateTime>? createdAt,
Value<Uint8List?>? userDiscoveryVersion, Value<Uint8List?>? userDiscoveryVersion,
Value<bool>? userDiscoveryExcluded,
Value<int>? mediaSendCounter, Value<int>? mediaSendCounter,
Value<int>? mediaReceivedCounter, Value<int>? mediaReceivedCounter,
}) { }) {
@ -868,6 +920,8 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
accountDeleted: accountDeleted ?? this.accountDeleted, accountDeleted: accountDeleted ?? this.accountDeleted,
createdAt: createdAt ?? this.createdAt, createdAt: createdAt ?? this.createdAt,
userDiscoveryVersion: userDiscoveryVersion ?? this.userDiscoveryVersion, userDiscoveryVersion: userDiscoveryVersion ?? this.userDiscoveryVersion,
userDiscoveryExcluded:
userDiscoveryExcluded ?? this.userDiscoveryExcluded,
mediaSendCounter: mediaSendCounter ?? this.mediaSendCounter, mediaSendCounter: mediaSendCounter ?? this.mediaSendCounter,
mediaReceivedCounter: mediaReceivedCounter ?? this.mediaReceivedCounter, mediaReceivedCounter: mediaReceivedCounter ?? this.mediaReceivedCounter,
); );
@ -922,6 +976,11 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
userDiscoveryVersion.value, userDiscoveryVersion.value,
); );
} }
if (userDiscoveryExcluded.present) {
map['user_discovery_excluded'] = Variable<bool>(
userDiscoveryExcluded.value,
);
}
if (mediaSendCounter.present) { if (mediaSendCounter.present) {
map['media_send_counter'] = Variable<int>(mediaSendCounter.value); map['media_send_counter'] = Variable<int>(mediaSendCounter.value);
} }
@ -948,6 +1007,7 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
..write('accountDeleted: $accountDeleted, ') ..write('accountDeleted: $accountDeleted, ')
..write('createdAt: $createdAt, ') ..write('createdAt: $createdAt, ')
..write('userDiscoveryVersion: $userDiscoveryVersion, ') ..write('userDiscoveryVersion: $userDiscoveryVersion, ')
..write('userDiscoveryExcluded: $userDiscoveryExcluded, ')
..write('mediaSendCounter: $mediaSendCounter, ') ..write('mediaSendCounter: $mediaSendCounter, ')
..write('mediaReceivedCounter: $mediaReceivedCounter') ..write('mediaReceivedCounter: $mediaReceivedCounter')
..write(')')) ..write(')'))
@ -11497,6 +11557,7 @@ typedef $$ContactsTableCreateCompanionBuilder =
Value<bool> accountDeleted, Value<bool> accountDeleted,
Value<DateTime> createdAt, Value<DateTime> createdAt,
Value<Uint8List?> userDiscoveryVersion, Value<Uint8List?> userDiscoveryVersion,
Value<bool> userDiscoveryExcluded,
Value<int> mediaSendCounter, Value<int> mediaSendCounter,
Value<int> mediaReceivedCounter, Value<int> mediaReceivedCounter,
}); });
@ -11516,6 +11577,7 @@ typedef $$ContactsTableUpdateCompanionBuilder =
Value<bool> accountDeleted, Value<bool> accountDeleted,
Value<DateTime> createdAt, Value<DateTime> createdAt,
Value<Uint8List?> userDiscoveryVersion, Value<Uint8List?> userDiscoveryVersion,
Value<bool> userDiscoveryExcluded,
Value<int> mediaSendCounter, Value<int> mediaSendCounter,
Value<int> mediaReceivedCounter, Value<int> mediaReceivedCounter,
}); });
@ -11892,6 +11954,11 @@ class $$ContactsTableFilterComposer
builder: (column) => ColumnFilters(column), builder: (column) => ColumnFilters(column),
); );
ColumnFilters<bool> get userDiscoveryExcluded => $composableBuilder(
column: $table.userDiscoveryExcluded,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<int> get mediaSendCounter => $composableBuilder( ColumnFilters<int> get mediaSendCounter => $composableBuilder(
column: $table.mediaSendCounter, column: $table.mediaSendCounter,
builder: (column) => ColumnFilters(column), builder: (column) => ColumnFilters(column),
@ -12290,6 +12357,11 @@ class $$ContactsTableOrderingComposer
builder: (column) => ColumnOrderings(column), builder: (column) => ColumnOrderings(column),
); );
ColumnOrderings<bool> get userDiscoveryExcluded => $composableBuilder(
column: $table.userDiscoveryExcluded,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<int> get mediaSendCounter => $composableBuilder( ColumnOrderings<int> get mediaSendCounter => $composableBuilder(
column: $table.mediaSendCounter, column: $table.mediaSendCounter,
builder: (column) => ColumnOrderings(column), builder: (column) => ColumnOrderings(column),
@ -12364,6 +12436,11 @@ class $$ContactsTableAnnotationComposer
builder: (column) => column, builder: (column) => column,
); );
GeneratedColumn<bool> get userDiscoveryExcluded => $composableBuilder(
column: $table.userDiscoveryExcluded,
builder: (column) => column,
);
GeneratedColumn<int> get mediaSendCounter => $composableBuilder( GeneratedColumn<int> get mediaSendCounter => $composableBuilder(
column: $table.mediaSendCounter, column: $table.mediaSendCounter,
builder: (column) => column, builder: (column) => column,
@ -12743,6 +12820,7 @@ class $$ContactsTableTableManager
Value<bool> accountDeleted = const Value.absent(), Value<bool> accountDeleted = const Value.absent(),
Value<DateTime> createdAt = const Value.absent(), Value<DateTime> createdAt = const Value.absent(),
Value<Uint8List?> userDiscoveryVersion = const Value.absent(), Value<Uint8List?> userDiscoveryVersion = const Value.absent(),
Value<bool> userDiscoveryExcluded = const Value.absent(),
Value<int> mediaSendCounter = const Value.absent(), Value<int> mediaSendCounter = const Value.absent(),
Value<int> mediaReceivedCounter = const Value.absent(), Value<int> mediaReceivedCounter = const Value.absent(),
}) => ContactsCompanion( }) => ContactsCompanion(
@ -12760,6 +12838,7 @@ class $$ContactsTableTableManager
accountDeleted: accountDeleted, accountDeleted: accountDeleted,
createdAt: createdAt, createdAt: createdAt,
userDiscoveryVersion: userDiscoveryVersion, userDiscoveryVersion: userDiscoveryVersion,
userDiscoveryExcluded: userDiscoveryExcluded,
mediaSendCounter: mediaSendCounter, mediaSendCounter: mediaSendCounter,
mediaReceivedCounter: mediaReceivedCounter, mediaReceivedCounter: mediaReceivedCounter,
), ),
@ -12779,6 +12858,7 @@ class $$ContactsTableTableManager
Value<bool> accountDeleted = const Value.absent(), Value<bool> accountDeleted = const Value.absent(),
Value<DateTime> createdAt = const Value.absent(), Value<DateTime> createdAt = const Value.absent(),
Value<Uint8List?> userDiscoveryVersion = const Value.absent(), Value<Uint8List?> userDiscoveryVersion = const Value.absent(),
Value<bool> userDiscoveryExcluded = const Value.absent(),
Value<int> mediaSendCounter = const Value.absent(), Value<int> mediaSendCounter = const Value.absent(),
Value<int> mediaReceivedCounter = const Value.absent(), Value<int> mediaReceivedCounter = const Value.absent(),
}) => ContactsCompanion.insert( }) => ContactsCompanion.insert(
@ -12796,6 +12876,7 @@ class $$ContactsTableTableManager
accountDeleted: accountDeleted, accountDeleted: accountDeleted,
createdAt: createdAt, createdAt: createdAt,
userDiscoveryVersion: userDiscoveryVersion, userDiscoveryVersion: userDiscoveryVersion,
userDiscoveryExcluded: userDiscoveryExcluded,
mediaSendCounter: mediaSendCounter, mediaSendCounter: mediaSendCounter,
mediaReceivedCounter: mediaReceivedCounter, mediaReceivedCounter: mediaReceivedCounter,
), ),

View file

@ -7380,6 +7380,461 @@ i1.GeneratedColumn<int> _column_232(String aliasedName) =>
$customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_hidden IN (0, 1))', $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_hidden IN (0, 1))',
defaultValue: const i1.CustomExpression('0'), defaultValue: const i1.CustomExpression('0'),
); );
final class Schema15 extends i0.VersionedSchema {
Schema15({required super.database}) : super(version: 15);
@override
late final List<i1.DatabaseSchemaEntity> entities = [
contacts,
groups,
mediaFiles,
messages,
messageHistories,
reactions,
groupMembers,
receipts,
receivedReceipts,
signalIdentityKeyStores,
signalPreKeyStores,
signalSenderKeyStores,
signalSessionStores,
messageActions,
groupHistories,
keyVerifications,
verificationTokens,
userDiscoveryAnnouncedUsers,
userDiscoveryUserRelations,
userDiscoveryOtherPromotions,
userDiscoveryOwnPromotions,
userDiscoveryShares,
];
late final Shape49 contacts = Shape49(
source: i0.VersionedTable(
entityName: 'contacts',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(user_id)'],
columns: [
_column_106,
_column_107,
_column_108,
_column_109,
_column_110,
_column_111,
_column_112,
_column_113,
_column_114,
_column_115,
_column_116,
_column_117,
_column_118,
_column_211,
_column_233,
_column_228,
_column_229,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape23 groups = Shape23(
source: i0.VersionedTable(
entityName: 'groups',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(group_id)'],
columns: [
_column_119,
_column_120,
_column_121,
_column_122,
_column_123,
_column_124,
_column_125,
_column_126,
_column_127,
_column_128,
_column_129,
_column_130,
_column_131,
_column_132,
_column_133,
_column_134,
_column_118,
_column_135,
_column_136,
_column_137,
_column_138,
_column_139,
_column_140,
_column_141,
_column_142,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape36 mediaFiles = Shape36(
source: i0.VersionedTable(
entityName: 'media_files',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(media_id)'],
columns: [
_column_143,
_column_144,
_column_145,
_column_146,
_column_147,
_column_148,
_column_149,
_column_207,
_column_150,
_column_151,
_column_152,
_column_153,
_column_154,
_column_155,
_column_156,
_column_157,
_column_118,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape25 messages = Shape25(
source: i0.VersionedTable(
entityName: 'messages',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(message_id)'],
columns: [
_column_158,
_column_159,
_column_160,
_column_144,
_column_161,
_column_162,
_column_163,
_column_164,
_column_165,
_column_153,
_column_166,
_column_167,
_column_168,
_column_169,
_column_118,
_column_170,
_column_171,
_column_172,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape26 messageHistories = Shape26(
source: i0.VersionedTable(
entityName: 'message_histories',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_173,
_column_174,
_column_175,
_column_161,
_column_118,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape27 reactions = Shape27(
source: i0.VersionedTable(
entityName: 'reactions',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(message_id, sender_id, emoji)'],
columns: [_column_174, _column_176, _column_177, _column_118],
attachedDatabase: database,
),
alias: null,
);
late final Shape38 groupMembers = Shape38(
source: i0.VersionedTable(
entityName: 'group_members',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(group_id, contact_id)'],
columns: [
_column_158,
_column_178,
_column_179,
_column_180,
_column_209,
_column_210,
_column_181,
_column_118,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape37 receipts = Shape37(
source: i0.VersionedTable(
entityName: 'receipts',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(receipt_id)'],
columns: [
_column_182,
_column_183,
_column_184,
_column_185,
_column_186,
_column_208,
_column_187,
_column_188,
_column_189,
_column_190,
_column_191,
_column_118,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape30 receivedReceipts = Shape30(
source: i0.VersionedTable(
entityName: 'received_receipts',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(receipt_id)'],
columns: [_column_182, _column_118],
attachedDatabase: database,
),
alias: null,
);
late final Shape31 signalIdentityKeyStores = Shape31(
source: i0.VersionedTable(
entityName: 'signal_identity_key_stores',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(device_id, name)'],
columns: [_column_192, _column_193, _column_194, _column_118],
attachedDatabase: database,
),
alias: null,
);
late final Shape32 signalPreKeyStores = Shape32(
source: i0.VersionedTable(
entityName: 'signal_pre_key_stores',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(pre_key_id)'],
columns: [_column_195, _column_196, _column_118],
attachedDatabase: database,
),
alias: null,
);
late final Shape11 signalSenderKeyStores = Shape11(
source: i0.VersionedTable(
entityName: 'signal_sender_key_stores',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(sender_key_name)'],
columns: [_column_197, _column_198],
attachedDatabase: database,
),
alias: null,
);
late final Shape33 signalSessionStores = Shape33(
source: i0.VersionedTable(
entityName: 'signal_session_stores',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(device_id, name)'],
columns: [_column_192, _column_193, _column_199, _column_118],
attachedDatabase: database,
),
alias: null,
);
late final Shape34 messageActions = Shape34(
source: i0.VersionedTable(
entityName: 'message_actions',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(message_id, contact_id, type)'],
columns: [_column_174, _column_183, _column_144, _column_200],
attachedDatabase: database,
),
alias: null,
);
late final Shape35 groupHistories = Shape35(
source: i0.VersionedTable(
entityName: 'group_histories',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(group_history_id)'],
columns: [
_column_201,
_column_158,
_column_202,
_column_203,
_column_204,
_column_205,
_column_206,
_column_144,
_column_200,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape40 keyVerifications = Shape40(
source: i0.VersionedTable(
entityName: 'key_verifications',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(contact_id)'],
columns: [_column_183, _column_144, _column_118],
attachedDatabase: database,
),
alias: null,
);
late final Shape41 verificationTokens = Shape41(
source: i0.VersionedTable(
entityName: 'verification_tokens',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [_column_212, _column_213, _column_118],
attachedDatabase: database,
),
alias: null,
);
late final Shape48 userDiscoveryAnnouncedUsers = Shape48(
source: i0.VersionedTable(
entityName: 'user_discovery_announced_users',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(announced_user_id)'],
columns: [
_column_214,
_column_215,
_column_216,
_column_230,
_column_231,
_column_232,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape43 userDiscoveryUserRelations = Shape43(
source: i0.VersionedTable(
entityName: 'user_discovery_user_relations',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(announced_user_id, from_contact_id)'],
columns: [_column_217, _column_218, _column_219],
attachedDatabase: database,
),
alias: null,
);
late final Shape44 userDiscoveryOtherPromotions = Shape44(
source: i0.VersionedTable(
entityName: 'user_discovery_other_promotions',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(from_contact_id, promotion_id)'],
columns: [
_column_218,
_column_220,
_column_221,
_column_222,
_column_223,
_column_219,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape45 userDiscoveryOwnPromotions = Shape45(
source: i0.VersionedTable(
entityName: 'user_discovery_own_promotions',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [_column_224, _column_183, _column_225],
attachedDatabase: database,
),
alias: null,
);
late final Shape46 userDiscoveryShares = Shape46(
source: i0.VersionedTable(
entityName: 'user_discovery_shares',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [_column_226, _column_227, _column_175],
attachedDatabase: database,
),
alias: null,
);
}
class Shape49 extends i0.VersionedTable {
Shape49({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<int> get userId =>
columnsByName['user_id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get username =>
columnsByName['username']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get displayName =>
columnsByName['display_name']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get nickName =>
columnsByName['nick_name']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<i2.Uint8List> get avatarSvgCompressed =>
columnsByName['avatar_svg_compressed']!
as i1.GeneratedColumn<i2.Uint8List>;
i1.GeneratedColumn<int> get senderProfileCounter =>
columnsByName['sender_profile_counter']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get accepted =>
columnsByName['accepted']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get deletedByUser =>
columnsByName['deleted_by_user']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get requested =>
columnsByName['requested']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get blocked =>
columnsByName['blocked']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get verified =>
columnsByName['verified']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get accountDeleted =>
columnsByName['account_deleted']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get createdAt =>
columnsByName['created_at']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<i2.Uint8List> get userDiscoveryVersion =>
columnsByName['user_discovery_version']!
as i1.GeneratedColumn<i2.Uint8List>;
i1.GeneratedColumn<int> get userDiscoveryExcluded =>
columnsByName['user_discovery_excluded']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get mediaSendCounter =>
columnsByName['media_send_counter']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get mediaReceivedCounter =>
columnsByName['media_received_counter']! as i1.GeneratedColumn<int>;
}
i1.GeneratedColumn<int> _column_233(String aliasedName) =>
i1.GeneratedColumn<int>(
'user_discovery_excluded',
aliasedName,
false,
type: i1.DriftSqlType.int,
$customConstraints:
'NOT NULL DEFAULT 0 CHECK (user_discovery_excluded IN (0, 1))',
defaultValue: const i1.CustomExpression('0'),
);
i0.MigrationStepWithVersion migrationSteps({ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2, required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3, required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
@ -7394,6 +7849,7 @@ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema12 schema) from11To12, required Future<void> Function(i1.Migrator m, Schema12 schema) from11To12,
required Future<void> Function(i1.Migrator m, Schema13 schema) from12To13, required Future<void> Function(i1.Migrator m, Schema13 schema) from12To13,
required Future<void> Function(i1.Migrator m, Schema14 schema) from13To14, required Future<void> Function(i1.Migrator m, Schema14 schema) from13To14,
required Future<void> Function(i1.Migrator m, Schema15 schema) from14To15,
}) { }) {
return (currentVersion, database) async { return (currentVersion, database) async {
switch (currentVersion) { switch (currentVersion) {
@ -7462,6 +7918,11 @@ i0.MigrationStepWithVersion migrationSteps({
final migrator = i1.Migrator(database, schema); final migrator = i1.Migrator(database, schema);
await from13To14(migrator, schema); await from13To14(migrator, schema);
return 14; return 14;
case 14:
final schema = Schema15(database: database);
final migrator = i1.Migrator(database, schema);
await from14To15(migrator, schema);
return 15;
default: default:
throw ArgumentError.value('Unknown migration from $currentVersion'); throw ArgumentError.value('Unknown migration from $currentVersion');
} }
@ -7482,6 +7943,7 @@ i1.OnUpgrade stepByStep({
required Future<void> Function(i1.Migrator m, Schema12 schema) from11To12, required Future<void> Function(i1.Migrator m, Schema12 schema) from11To12,
required Future<void> Function(i1.Migrator m, Schema13 schema) from12To13, required Future<void> Function(i1.Migrator m, Schema13 schema) from12To13,
required Future<void> Function(i1.Migrator m, Schema14 schema) from13To14, required Future<void> Function(i1.Migrator m, Schema14 schema) from13To14,
required Future<void> Function(i1.Migrator m, Schema15 schema) from14To15,
}) => i0.VersionedSchema.stepByStepHelper( }) => i0.VersionedSchema.stepByStepHelper(
step: migrationSteps( step: migrationSteps(
from1To2: from1To2, from1To2: from1To2,
@ -7497,5 +7959,6 @@ i1.OnUpgrade stepByStep({
from11To12: from11To12, from11To12: from11To12,
from12To13: from12To13, from12To13: from12To13,
from13To14: from13To14, from13To14: from13To14,
from14To15: from14To15,
), ),
); );

View file

@ -397,27 +397,9 @@ abstract class AppLocalizations {
/// No description provided for @searchUserNamePending. /// No description provided for @searchUserNamePending.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Pending'** /// **'Request pending'**
String get searchUserNamePending; String get searchUserNamePending;
/// No description provided for @searchUserNameBlockUserTooltip.
///
/// In en, this message translates to:
/// **'Block the user without informing.'**
String get searchUserNameBlockUserTooltip;
/// No description provided for @searchUserNameRejectUserTooltip.
///
/// In en, this message translates to:
/// **'Reject the request and let the requester know.'**
String get searchUserNameRejectUserTooltip;
/// No description provided for @searchUserNameArchiveUserTooltip.
///
/// In en, this message translates to:
/// **'Archive the user. He will appear again as soon as he accepts your request.'**
String get searchUserNameArchiveUserTooltip;
/// No description provided for @searchUsernameNotFound. /// No description provided for @searchUsernameNotFound.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -433,7 +415,7 @@ abstract class AppLocalizations {
/// No description provided for @searchUsernameNewFollowerTitle. /// No description provided for @searchUsernameNewFollowerTitle.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Follow requests'** /// **'Open requests'**
String get searchUsernameNewFollowerTitle; String get searchUsernameNewFollowerTitle;
/// No description provided for @searchUsernameQrCodeBtn. /// No description provided for @searchUsernameQrCodeBtn.
@ -3147,6 +3129,198 @@ abstract class AppLocalizations {
/// In en, this message translates to: /// In en, this message translates to:
/// **'Scan / Show QR'** /// **'Scan / Show QR'**
String get scanQrOrShow; String get scanQrOrShow;
/// No description provided for @contactActionBlock.
///
/// In en, this message translates to:
/// **'Block'**
String get contactActionBlock;
/// No description provided for @contactActionAccept.
///
/// In en, this message translates to:
/// **'Accept'**
String get contactActionAccept;
/// No description provided for @userDiscoverySettingsMinImages.
///
/// In en, this message translates to:
/// **'Choose the minimum number of images you must have exchanged with a person before you securely share your friends with them.'**
String get userDiscoverySettingsMinImages;
/// No description provided for @userDiscoverySettingsMutualFriends.
///
/// In en, this message translates to:
/// **'Choose how many mutual friends a person must have for you to be suggested to them.'**
String get userDiscoverySettingsMutualFriends;
/// No description provided for @userDiscoverySettingsApply.
///
/// In en, this message translates to:
/// **'Apply changes'**
String get userDiscoverySettingsApply;
/// No description provided for @userDiscoveryEnabledDisableWarning.
///
/// In en, this message translates to:
/// **'If you disable the \"Find friends\" feature, you will no longer see suggestions. You will also stop sharing your friends with new contacts.'**
String get userDiscoveryEnabledDisableWarning;
/// No description provided for @userDiscoveryEnabledChangeSettings.
///
/// In en, this message translates to:
/// **'Change settings'**
String get userDiscoveryEnabledChangeSettings;
/// No description provided for @userDiscoveryEnabledFaq.
///
/// In en, this message translates to:
/// **'In our FAQ we explain how the \"Find friends\" feature works.'**
String get userDiscoveryEnabledFaq;
/// No description provided for @userDiscoveryDisabledIntro.
///
/// In en, this message translates to:
/// **'twonly doesn\'t use phone numbers, so we suggest friends based on mutual contacts instead securely and privately.'**
String get userDiscoveryDisabledIntro;
/// No description provided for @userDiscoveryDisabledInvisible.
///
/// In en, this message translates to:
/// **'Your friend list is *completely invisible to strangers*. Only your friends can see parts of it and only those people with whom they have *mutual friends* themselves.'**
String get userDiscoveryDisabledInvisible;
/// No description provided for @userDiscoveryDisabledDecide.
///
/// In en, this message translates to:
/// **'Decide for yourself who can see your friends. You can change your mind at any time or hide specific people.'**
String get userDiscoveryDisabledDecide;
/// No description provided for @userDiscoverySettingsTitle.
///
/// In en, this message translates to:
/// **'Find friends'**
String get userDiscoverySettingsTitle;
/// No description provided for @userDiscoverySettingsMinImagesTitle.
///
/// In en, this message translates to:
/// **'Number of shared images'**
String get userDiscoverySettingsMinImagesTitle;
/// No description provided for @userDiscoverySettingsMutualFriendsTitle.
///
/// In en, this message translates to:
/// **'Number of mutual friends'**
String get userDiscoverySettingsMutualFriendsTitle;
/// No description provided for @userDiscoveryDisabledYouHaveControl.
///
/// In en, this message translates to:
/// **'You are in control'**
String get userDiscoveryDisabledYouHaveControl;
/// No description provided for @userDiscoveryDisabledEnableWithDefault.
///
/// In en, this message translates to:
/// **'Enable with default settings'**
String get userDiscoveryDisabledEnableWithDefault;
/// No description provided for @userDiscoveryDisabledCustomizeSettings.
///
/// In en, this message translates to:
/// **'Customize settings'**
String get userDiscoveryDisabledCustomizeSettings;
/// No description provided for @userDiscoveryDisabledLearnMore.
///
/// In en, this message translates to:
/// **'Learn more'**
String get userDiscoveryDisabledLearnMore;
/// No description provided for @userDiscoveryEnabledDialogTitle.
///
/// In en, this message translates to:
/// **'Really disable?'**
String get userDiscoveryEnabledDialogTitle;
/// No description provided for @userDiscoveryEnabledFriendsShared.
///
/// In en, this message translates to:
/// **'Friends you share'**
String get userDiscoveryEnabledFriendsShared;
/// No description provided for @userDiscoveryEnabledFriendsSharedDesc.
///
/// In en, this message translates to:
/// **'You only share friends who have also activated this feature and who have reached the threshold you set.'**
String get userDiscoveryEnabledFriendsSharedDesc;
/// No description provided for @userDiscoveryEnabledNoFriendsShared.
///
/// In en, this message translates to:
/// **'You are not sharing anyone yet.'**
String get userDiscoveryEnabledNoFriendsShared;
/// No description provided for @userDiscoveryActionDisable.
///
/// In en, this message translates to:
/// **'Disable'**
String get userDiscoveryActionDisable;
/// No description provided for @friendSuggestionsTitle.
///
/// In en, this message translates to:
/// **'Friend suggestions'**
String get friendSuggestionsTitle;
/// No description provided for @andWord.
///
/// In en, this message translates to:
/// **'and'**
String get andWord;
/// No description provided for @friendSuggestionsFriendsWith.
///
/// In en, this message translates to:
/// **'Friends with {friends}.'**
String friendSuggestionsFriendsWith(Object friends);
/// No description provided for @friendSuggestionsGroupMemberIn.
///
/// In en, this message translates to:
/// **' Group member in {groups}.'**
String friendSuggestionsGroupMemberIn(Object groups);
/// No description provided for @friendSuggestionsRequest.
///
/// In en, this message translates to:
/// **'Request'**
String get friendSuggestionsRequest;
/// No description provided for @contactUserDiscoveryImagesLeft.
///
/// In en, this message translates to:
/// **'{imagesLeft} more images are needed until your friends are shared with {username}.'**
String contactUserDiscoveryImagesLeft(Object imagesLeft, Object username);
/// No description provided for @userDiscoveryEnabledVersion.
///
/// In en, this message translates to:
/// **'Version: {version}'**
String userDiscoveryEnabledVersion(Object version);
/// No description provided for @userDiscoveryEnabledYourVersion.
///
/// In en, this message translates to:
/// **'Your version: {version}'**
String userDiscoveryEnabledYourVersion(Object version);
/// No description provided for @userDiscoveryEnabledStopSharing.
///
/// In en, this message translates to:
/// **'Stop sharing'**
String get userDiscoveryEnabledStopSharing;
} }
class _AppLocalizationsDelegate class _AppLocalizationsDelegate

View file

@ -172,19 +172,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get selectSubscription => 'Abo auswählen'; String get selectSubscription => 'Abo auswählen';
@override @override
String get searchUserNamePending => 'Ausstehend'; String get searchUserNamePending => 'Anfrage ausstehend';
@override
String get searchUserNameBlockUserTooltip =>
'Benutzer ohne Benachrichtigung blockieren.';
@override
String get searchUserNameRejectUserTooltip =>
'Die Anfrage ablehnen und den Anfragenden informieren.';
@override
String get searchUserNameArchiveUserTooltip =>
'Benutzer archivieren. Du wirst informiert sobald er deine Anfrage akzeptiert.';
@override @override
String get searchUsernameNotFound => 'Benutzername nicht gefunden'; String get searchUsernameNotFound => 'Benutzername nicht gefunden';
@ -195,7 +183,7 @@ class AppLocalizationsDe extends AppLocalizations {
} }
@override @override
String get searchUsernameNewFollowerTitle => 'Folgeanfragen'; String get searchUsernameNewFollowerTitle => 'Offene Anfragen';
@override @override
String get searchUsernameQrCodeBtn => 'QR-Code scannen'; String get searchUsernameQrCodeBtn => 'QR-Code scannen';
@ -1764,4 +1752,122 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get scanQrOrShow => 'QR scannen / anzeigen'; String get scanQrOrShow => 'QR scannen / anzeigen';
@override
String get contactActionBlock => 'Blockieren';
@override
String get contactActionAccept => 'Annehmen';
@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.';
@override
String get userDiscoverySettingsMutualFriends =>
'Wähle aus, wie viele gemeinsame Freunde eine Person haben muss, damit du ihr vorgeschlagen wirst.';
@override
String get userDiscoverySettingsApply => 'Änderungen übernehmen';
@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.';
@override
String get userDiscoveryEnabledChangeSettings => 'Einstellungen ändern';
@override
String get userDiscoveryEnabledFaq =>
'In unserem FAQ erklären wir dir wie das Feature \"Freunde finden\" 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.';
@override
String get userDiscoveryDisabledDecide =>
'Entscheide selbst, wer deine Freunde sehen darf. Du kannst deine Meinung jederzeit ändern oder bestimmte Personen verstecken.';
@override
String get userDiscoverySettingsTitle => 'Freunde finden';
@override
String get userDiscoverySettingsMinImagesTitle =>
'Anzahl an geteilten Bildern';
@override
String get userDiscoverySettingsMutualFriendsTitle =>
'Anzahl an gemeinsame Freunde';
@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';
@override
String get userDiscoveryEnabledDialogTitle => 'Wirklich deaktivieren?';
@override
String get userDiscoveryEnabledFriendsShared => 'Freunde die du teilst';
@override
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 userDiscoveryEnabledNoFriendsShared =>
'Bisher teilst du noch niemanden.';
@override
String get userDiscoveryActionDisable => 'Deaktivieren';
@override
String get friendSuggestionsTitle => 'Freundschaftsvorschläge';
@override
String get andWord => 'und';
@override
String friendSuggestionsFriendsWith(Object friends) {
return 'Befreundet mit $friends.';
}
@override
String friendSuggestionsGroupMemberIn(Object groups) {
return ' Gruppenmitglied in $groups.';
}
@override
String get friendSuggestionsRequest => 'Anfragen';
@override
String contactUserDiscoveryImagesLeft(Object imagesLeft, Object username) {
return 'Es fehlen noch $imagesLeft Bilder bis deine Freunde mit $username geteilt werden.';
}
@override
String userDiscoveryEnabledVersion(Object version) {
return 'Version: $version';
}
@override
String userDiscoveryEnabledYourVersion(Object version) {
return 'Deine Version: $version';
}
@override
String get userDiscoveryEnabledStopSharing => 'Nicht mehr teilen';
} }

View file

@ -171,19 +171,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get selectSubscription => 'Select subscription'; String get selectSubscription => 'Select subscription';
@override @override
String get searchUserNamePending => 'Pending'; String get searchUserNamePending => 'Request pending';
@override
String get searchUserNameBlockUserTooltip =>
'Block the user without informing.';
@override
String get searchUserNameRejectUserTooltip =>
'Reject the request and let the requester know.';
@override
String get searchUserNameArchiveUserTooltip =>
'Archive the user. He will appear again as soon as he accepts your request.';
@override @override
String get searchUsernameNotFound => 'Username not found'; String get searchUsernameNotFound => 'Username not found';
@ -194,7 +182,7 @@ class AppLocalizationsEn extends AppLocalizations {
} }
@override @override
String get searchUsernameNewFollowerTitle => 'Follow requests'; String get searchUsernameNewFollowerTitle => 'Open requests';
@override @override
String get searchUsernameQrCodeBtn => 'Scan QR code'; String get searchUsernameQrCodeBtn => 'Scan QR code';
@ -1752,4 +1740,121 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get scanQrOrShow => 'Scan / Show QR'; String get scanQrOrShow => 'Scan / Show QR';
@override
String get contactActionBlock => 'Block';
@override
String get contactActionAccept => 'Accept';
@override
String get userDiscoverySettingsMinImages =>
'Choose the minimum number of images you must have exchanged with a person before you securely share your friends with them.';
@override
String get userDiscoverySettingsMutualFriends =>
'Choose how many mutual friends a person must have for you to be suggested to them.';
@override
String get userDiscoverySettingsApply => 'Apply changes';
@override
String get userDiscoveryEnabledDisableWarning =>
'If you disable the \"Find friends\" feature, you will no longer see suggestions. You will also stop sharing your friends with new contacts.';
@override
String get userDiscoveryEnabledChangeSettings => 'Change settings';
@override
String get userDiscoveryEnabledFaq =>
'In our FAQ we explain how the \"Find friends\" feature works.';
@override
String get userDiscoveryDisabledIntro =>
'twonly doesn\'t use phone numbers, so we suggest friends based on mutual contacts instead securely and privately.';
@override
String get userDiscoveryDisabledInvisible =>
'Your friend list is *completely invisible to strangers*. Only your friends can see parts of it and only those people with whom they have *mutual friends* themselves.';
@override
String get userDiscoveryDisabledDecide =>
'Decide for yourself who can see your friends. You can change your mind at any time or hide specific people.';
@override
String get userDiscoverySettingsTitle => 'Find friends';
@override
String get userDiscoverySettingsMinImagesTitle => 'Number of shared images';
@override
String get userDiscoverySettingsMutualFriendsTitle =>
'Number of mutual friends';
@override
String get userDiscoveryDisabledYouHaveControl => 'You are in control';
@override
String get userDiscoveryDisabledEnableWithDefault =>
'Enable with default settings';
@override
String get userDiscoveryDisabledCustomizeSettings => 'Customize settings';
@override
String get userDiscoveryDisabledLearnMore => 'Learn more';
@override
String get userDiscoveryEnabledDialogTitle => 'Really disable?';
@override
String get userDiscoveryEnabledFriendsShared => 'Friends you share';
@override
String get userDiscoveryEnabledFriendsSharedDesc =>
'You only share friends who have also activated this feature and who have reached the threshold you set.';
@override
String get userDiscoveryEnabledNoFriendsShared =>
'You are not sharing anyone yet.';
@override
String get userDiscoveryActionDisable => 'Disable';
@override
String get friendSuggestionsTitle => 'Friend suggestions';
@override
String get andWord => 'and';
@override
String friendSuggestionsFriendsWith(Object friends) {
return 'Friends with $friends.';
}
@override
String friendSuggestionsGroupMemberIn(Object groups) {
return ' Group member in $groups.';
}
@override
String get friendSuggestionsRequest => 'Request';
@override
String contactUserDiscoveryImagesLeft(Object imagesLeft, Object username) {
return '$imagesLeft more images are needed until your friends are shared with $username.';
}
@override
String userDiscoveryEnabledVersion(Object version) {
return 'Version: $version';
}
@override
String userDiscoveryEnabledYourVersion(Object version) {
return 'Your version: $version';
}
@override
String get userDiscoveryEnabledStopSharing => 'Stop sharing';
} }

View file

@ -171,19 +171,7 @@ class AppLocalizationsSv extends AppLocalizations {
String get selectSubscription => 'Select subscription'; String get selectSubscription => 'Select subscription';
@override @override
String get searchUserNamePending => 'Pending'; String get searchUserNamePending => 'Request pending';
@override
String get searchUserNameBlockUserTooltip =>
'Block the user without informing.';
@override
String get searchUserNameRejectUserTooltip =>
'Reject the request and let the requester know.';
@override
String get searchUserNameArchiveUserTooltip =>
'Archive the user. He will appear again as soon as he accepts your request.';
@override @override
String get searchUsernameNotFound => 'Username not found'; String get searchUsernameNotFound => 'Username not found';
@ -194,7 +182,7 @@ class AppLocalizationsSv extends AppLocalizations {
} }
@override @override
String get searchUsernameNewFollowerTitle => 'Follow requests'; String get searchUsernameNewFollowerTitle => 'Open requests';
@override @override
String get searchUsernameQrCodeBtn => 'Scan QR code'; String get searchUsernameQrCodeBtn => 'Scan QR code';
@ -1752,4 +1740,121 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get scanQrOrShow => 'Scan / Show QR'; String get scanQrOrShow => 'Scan / Show QR';
@override
String get contactActionBlock => 'Block';
@override
String get contactActionAccept => 'Accept';
@override
String get userDiscoverySettingsMinImages =>
'Choose the minimum number of images you must have exchanged with a person before you securely share your friends with them.';
@override
String get userDiscoverySettingsMutualFriends =>
'Choose how many mutual friends a person must have for you to be suggested to them.';
@override
String get userDiscoverySettingsApply => 'Apply changes';
@override
String get userDiscoveryEnabledDisableWarning =>
'If you disable the \"Find friends\" feature, you will no longer see suggestions. You will also stop sharing your friends with new contacts.';
@override
String get userDiscoveryEnabledChangeSettings => 'Change settings';
@override
String get userDiscoveryEnabledFaq =>
'In our FAQ we explain how the \"Find friends\" feature works.';
@override
String get userDiscoveryDisabledIntro =>
'twonly doesn\'t use phone numbers, so we suggest friends based on mutual contacts instead securely and privately.';
@override
String get userDiscoveryDisabledInvisible =>
'Your friend list is *completely invisible to strangers*. Only your friends can see parts of it and only those people with whom they have *mutual friends* themselves.';
@override
String get userDiscoveryDisabledDecide =>
'Decide for yourself who can see your friends. You can change your mind at any time or hide specific people.';
@override
String get userDiscoverySettingsTitle => 'Find friends';
@override
String get userDiscoverySettingsMinImagesTitle => 'Number of shared images';
@override
String get userDiscoverySettingsMutualFriendsTitle =>
'Number of mutual friends';
@override
String get userDiscoveryDisabledYouHaveControl => 'You are in control';
@override
String get userDiscoveryDisabledEnableWithDefault =>
'Enable with default settings';
@override
String get userDiscoveryDisabledCustomizeSettings => 'Customize settings';
@override
String get userDiscoveryDisabledLearnMore => 'Learn more';
@override
String get userDiscoveryEnabledDialogTitle => 'Really disable?';
@override
String get userDiscoveryEnabledFriendsShared => 'Friends you share';
@override
String get userDiscoveryEnabledFriendsSharedDesc =>
'You only share friends who have also activated this feature and who have reached the threshold you set.';
@override
String get userDiscoveryEnabledNoFriendsShared =>
'You are not sharing anyone yet.';
@override
String get userDiscoveryActionDisable => 'Disable';
@override
String get friendSuggestionsTitle => 'Friend suggestions';
@override
String get andWord => 'and';
@override
String friendSuggestionsFriendsWith(Object friends) {
return 'Friends with $friends.';
}
@override
String friendSuggestionsGroupMemberIn(Object groups) {
return ' Group member in $groups.';
}
@override
String get friendSuggestionsRequest => 'Request';
@override
String contactUserDiscoveryImagesLeft(Object imagesLeft, Object username) {
return '$imagesLeft more images are needed until your friends are shared with $username.';
}
@override
String userDiscoveryEnabledVersion(Object version) {
return 'Version: $version';
}
@override
String userDiscoveryEnabledYourVersion(Object version) {
return 'Your version: $version';
}
@override
String get userDiscoveryEnabledStopSharing => 'Stop sharing';
} }

View file

@ -117,15 +117,20 @@ Future<void> handleContactUpdate(
case EncryptedContent_ContactUpdate_Type.UPDATE: case EncryptedContent_ContactUpdate_Type.UPDATE:
Log.info('Got a contact update $fromUserId'); Log.info('Got a contact update $fromUserId');
if (contactUpdate.hasAvatarSvgCompressed() && Uint8List? avatarSvgCompressed;
contactUpdate.hasDisplayName() && if (contactUpdate.hasAvatarSvgCompressed()) {
avatarSvgCompressed = Uint8List.fromList(
contactUpdate.avatarSvgCompressed,
);
}
if (contactUpdate.hasDisplayName() &&
contactUpdate.hasUsername() && contactUpdate.hasUsername() &&
senderProfileCounter != null) { senderProfileCounter != null) {
await twonlyDB.contactsDao.updateContact( await twonlyDB.contactsDao.updateContact(
fromUserId, fromUserId,
ContactsCompanion( ContactsCompanion(
avatarSvgCompressed: Value( avatarSvgCompressed: Value(
Uint8List.fromList(contactUpdate.avatarSvgCompressed), avatarSvgCompressed,
), ),
displayName: Value(contactUpdate.displayName), displayName: Value(contactUpdate.displayName),
username: Value(contactUpdate.username), username: Value(contactUpdate.username),
@ -180,6 +185,7 @@ Future<int?> checkForProfileUpdate(
.getSingleOrNull(); .getSingleOrNull();
if (contact != null) { if (contact != null) {
if (contact.senderProfileCounter < senderProfileCounter) { if (contact.senderProfileCounter < senderProfileCounter) {
Log.info('${contact.senderProfileCounter} < $senderProfileCounter');
await sendCipherText( await sendCipherText(
fromUserId, fromUserId,
EncryptedContent( EncryptedContent(

View file

@ -16,6 +16,7 @@ Future<void> checkForUserDiscoveryChanges(
); );
if (currentVersion != null) { if (currentVersion != null) {
Log.info('Having old version from contact. Requesting new version.');
await sendCipherText( await sendCipherText(
fromUserId, fromUserId,
EncryptedContent( EncryptedContent(
@ -31,6 +32,8 @@ Future<void> handleUserDiscoveryRequest(
int fromUserId, int fromUserId,
EncryptedContent_UserDiscoveryRequest request, EncryptedContent_UserDiscoveryRequest request,
) async { ) async {
Log.info('Got a user discovery request');
if (!gUser.isUserDiscoveryEnabled) { if (!gUser.isUserDiscoveryEnabled) {
Log.warn('Got a user discovery request while it is disabled'); Log.warn('Got a user discovery request while it is disabled');
return; return;
@ -38,9 +41,10 @@ Future<void> handleUserDiscoveryRequest(
final contact = await twonlyDB.contactsDao.getContactById(fromUserId); final contact = await twonlyDB.contactsDao.getContactById(fromUserId);
if (contact == null) return; if (contact == null) return;
if (contact.mediaSendCounter < gUser.minimumRequiredImagesExchanged) { if (contact.mediaSendCounter < gUser.minimumRequiredImagesExchanged ||
contact.userDiscoveryExcluded) {
Log.warn( Log.warn(
'Got a request to update user discovery, but mediaSendCounter (${contact.mediaSendCounter}) < ${gUser.minimumRequiredImagesExchanged}', 'Got a request to update user discovery, but mediaSendCounter (${contact.mediaSendCounter}) < ${gUser.minimumRequiredImagesExchanged} or user is excluded ${contact.userDiscoveryExcluded}',
); );
return; return;
} }
@ -50,6 +54,7 @@ Future<void> handleUserDiscoveryRequest(
request.currentVersion, request.currentVersion,
); );
if (newMessages != null && newMessages.isNotEmpty) { if (newMessages != null && newMessages.isNotEmpty) {
Log.info('Sending ${newMessages.length} user discovery messages');
await sendCipherText( await sendCipherText(
fromUserId, fromUserId,
EncryptedContent( EncryptedContent(
@ -71,6 +76,7 @@ Future<void> handleUserDiscoveryUpdate(
Log.warn('Got a user discovery update while it is disabled'); Log.warn('Got a user discovery update while it is disabled');
return; return;
} }
Log.info('Got ${update.messages.length} user discovery messages');
await UserDiscoveryService.handleNewMessages( await UserDiscoveryService.handleNewMessages(
fromUserId, fromUserId,
update.messages.map(Uint8List.fromList).toList(), update.messages.map(Uint8List.fromList).toList(),

View file

@ -347,10 +347,11 @@ Future<(Uint8List, Uint8List?)?> sendCipherText(
} }
encryptedContent.senderProfileCounter = Int64(gUser.avatarCounter); encryptedContent.senderProfileCounter = Int64(gUser.avatarCounter);
if (gUser.isUserDiscoveryEnabled) { if (gUser.isUserDiscoveryEnabled && messageId != null) {
final contact = await twonlyDB.contactsDao.getContactById(contactId); final contact = await twonlyDB.contactsDao.getContactById(contactId);
if (contact != null && if (contact != null &&
contact.mediaSendCounter >= gUser.minimumRequiredImagesExchanged) { contact.mediaSendCounter >= gUser.minimumRequiredImagesExchanged &&
!contact.userDiscoveryExcluded) {
final version = await UserDiscoveryService.getCurrentVersion(); final version = await UserDiscoveryService.getCurrentVersion();
if (version != null) { if (version != null) {
encryptedContent.senderUserDiscoveryVersion = version; encryptedContent.senderUserDiscoveryVersion = version;

View file

@ -355,7 +355,10 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
final contact = await twonlyDB.contactsDao final contact = await twonlyDB.contactsDao
.getContactByUserId(fromUserId) .getContactByUserId(fromUserId)
.getSingleOrNull(); .getSingleOrNull();
if (contact == null || contact.deletedByUser) { Log.info(
'Contact exists?: ${contact != null} Is deleted? ${contact?.deletedByUser} Accepted? (${contact?.accepted})',
);
if (contact == null || !contact.accepted || contact.deletedByUser) {
await handleNewContactRequest(fromUserId); await handleNewContactRequest(fromUserId);
Log.error( Log.error(
'User tries to send message to direct chat while the user does not exists !', 'User tries to send message to direct chat while the user does not exists !',

View file

@ -1,5 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:flutter/foundation.dart';
import 'package:twonly/core/bridge/wrapper/user_discovery.dart'; import 'package:twonly/core/bridge/wrapper/user_discovery.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
@ -18,8 +20,14 @@ class UserDiscoveryService {
announcedUser.announcedUserId, announcedUser.announcedUserId,
); );
if (userdata == null) continue; if (userdata == null) continue;
if (userdata.publicIdentityKey != if (!userdata.publicIdentityKey.equals(
announcedUser.announcedPublicKey.toList()) { announcedUser.announcedPublicKey.toList(),
)) {
if (kDebugMode) {
Log.warn(
'${userdata.publicIdentityKey} != ${announcedUser.announcedPublicKey.toList()}',
);
}
Log.error( Log.error(
'Server delivered a different public key then received from the announcement.', 'Server delivered a different public key then received from the announcement.',
); );
@ -74,6 +82,21 @@ class UserDiscoveryService {
return UserDiscoveryVersion.fromBuffer(version); return UserDiscoveryVersion.fromBuffer(version);
} }
static Future<UserDiscoveryVersion?> getContactVersionTyped(
int contactId,
) async {
final contact = await twonlyDB.contactsDao.getContactById(contactId);
if (contact == null || contact.userDiscoveryVersion == null) return null;
return UserDiscoveryVersion.fromBuffer(contact.userDiscoveryVersion!);
}
static UserDiscoveryVersion? getContactVersionTypedFromContact(
Contact contact,
) {
if (contact.userDiscoveryVersion == null) return null;
return UserDiscoveryVersion.fromBuffer(contact.userDiscoveryVersion!);
}
static Future<Uint8List?> shouldRequestNewMessages( static Future<Uint8List?> shouldRequestNewMessages(
int fromUserId, int fromUserId,
List<int> receivedVersion, List<int> receivedVersion,

View file

@ -405,13 +405,15 @@ Future<List<int>> sha256File(File file) async {
return sha256Sink.events.single.bytes; return sha256Sink.events.single.bytes;
} }
List<TextSpan> formattedText(String input) { List<TextSpan> formattedText(BuildContext context, String input) {
// Pattern to find text between asterisks // Access the current theme's text color
final regex = RegExp(r'\*(.*?)\*'); // Defaulting to bodyMedium color, but you can use labelLarge, displaySmall, etc.
final List<TextSpan> spans = []; final defaultColor = Theme.of(context).colorScheme.onSurface;
// Track the current position in the string final regex = RegExp(r'\*(.*?)\*');
int lastMatchEnd = 0; final spans = <TextSpan>[];
var lastMatchEnd = 0;
for (final match in regex.allMatches(input)) { for (final match in regex.allMatches(input)) {
// Add text before the match (Normal style) // Add text before the match (Normal style)
@ -419,17 +421,18 @@ List<TextSpan> formattedText(String input) {
spans.add( spans.add(
TextSpan( TextSpan(
text: input.substring(lastMatchEnd, match.start), text: input.substring(lastMatchEnd, match.start),
style: TextStyle(color: defaultColor),
), ),
); );
} }
// Add the matched text (Bold style) // Add the matched text (Bold style)
// match.group(1) is the text without the asterisks
spans.add( spans.add(
TextSpan( TextSpan(
text: match.group(1), text: match.group(1),
style: const TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: defaultColor, // Ensures bold text also uses the theme color
), ),
), ),
); );
@ -442,9 +445,16 @@ List<TextSpan> formattedText(String input) {
spans.add( spans.add(
TextSpan( TextSpan(
text: input.substring(lastMatchEnd), text: input.substring(lastMatchEnd),
style: TextStyle(color: defaultColor),
), ),
); );
} }
return spans; return spans;
} }
String joinWithAnd(List<String> items, String andWord) {
if (items.isEmpty) return '';
if (items.length == 1) return items.first;
return '${items.sublist(0, items.length - 1).join(', ')} $andWord ${items.last}';
}

View file

@ -7,15 +7,13 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/daos/user_discovery.dao.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/services/api/utils.dart'; import 'package:twonly/src/services/api/utils.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/chats/add_new_user_components/friend_suggestions.dart';
import 'package:twonly/src/views/chats/add_new_user_components/open_requests_list.dart';
import 'package:twonly/src/views/components/alert_dialog.dart'; import 'package:twonly/src/views/components/alert_dialog.dart';
import 'package:twonly/src/views/components/avatar_icon.component.dart';
import 'package:twonly/src/views/components/headline.dart';
class AddNewUserView extends StatefulWidget { class AddNewUserView extends StatefulWidget {
const AddNewUserView({ const AddNewUserView({
@ -32,47 +30,76 @@ class AddNewUserView extends StatefulWidget {
} }
class _SearchUsernameView extends State<AddNewUserView> { class _SearchUsernameView extends State<AddNewUserView> {
final TextEditingController searchUserName = TextEditingController(); final TextEditingController _usernameController = TextEditingController();
bool _isLoading = false; bool _isLoading = false;
bool hasRequestedUsers = false; bool hasRequestedUsers = false;
List<Contact> contacts = []; List<Contact> _openRequestsContacts = [];
late StreamSubscription<List<Contact>> contactsStream; late StreamSubscription<List<Contact>> _contactsStream;
AnnouncedUsersWithRelations _newAnnouncedUsers = {};
late StreamSubscription<AnnouncedUsersWithRelations> _newAnnouncedUsersStream;
AnnouncedUsersWithRelations _allAnnouncedUsers = {};
late StreamSubscription<AnnouncedUsersWithRelations> _allAnnouncedUsersStream;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
contactsStream = twonlyDB.contactsDao.watchNotAcceptedContacts().listen( _contactsStream = twonlyDB.contactsDao.watchNotAcceptedContacts().listen(
(update) => setState(() { (update) {
contacts = update; if (mounted) {
}), setState(() {
_openRequestsContacts = update;
});
}
},
); );
_newAnnouncedUsersStream = twonlyDB.userDiscoveryDao
.watchNewAnnouncedUsersWithRelations()
.listen((update) {
if (mounted) {
setState(() {
_newAnnouncedUsers = update;
});
}
});
_allAnnouncedUsersStream = twonlyDB.userDiscoveryDao
.watchAllAnnouncedUsersWithRelations()
.listen((update) {
if (mounted) {
setState(() {
_allAnnouncedUsers = update;
});
}
});
if (widget.username != null) { if (widget.username != null) {
searchUserName.text = widget.username!; _usernameController.text = widget.username!;
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
_addNewUser(context); _requestNewUserByUsername(widget.username!);
}); });
} }
twonlyDB.userDiscoveryDao.markAllValidAnnouncedUsersAsShown();
} }
@override @override
void dispose() { void dispose() {
unawaited(contactsStream.cancel()); _contactsStream.cancel();
_newAnnouncedUsersStream.cancel();
_allAnnouncedUsersStream.cancel();
super.dispose(); super.dispose();
} }
Future<void> _addNewUser(BuildContext context) async { Future<void> _requestNewUserByUsername(String username) async {
if (gUser.username == searchUserName.text) { if (gUser.username == username) return;
return;
}
setState(() { setState(() {
_isLoading = true; _isLoading = true;
}); });
final userdata = await apiService.getUserData(searchUserName.text); final userdata = await apiService.getUserData(username);
if (!context.mounted) return; if (!mounted) return;
setState(() { setState(() {
_isLoading = false; _isLoading = false;
@ -82,24 +109,22 @@ class _SearchUsernameView extends State<AddNewUserView> {
await showAlertDialog( await showAlertDialog(
context, context,
context.lang.searchUsernameNotFound, context.lang.searchUsernameNotFound,
context.lang.searchUsernameNotFoundBody(searchUserName.text), context.lang.searchUsernameNotFoundBody(username),
); );
return; return;
} }
final addUser = await showAlertDialog( final addUser = await showAlertDialog(
context, context,
context.lang.userFound(searchUserName.text), context.lang.userFound(username),
context.lang.userFoundBody, context.lang.userFoundBody,
); );
if (!addUser || !context.mounted) { if (!addUser || !mounted) return;
return;
}
final added = await twonlyDB.contactsDao.insertOnConflictUpdate( final added = await twonlyDB.contactsDao.insertOnConflictUpdate(
ContactsCompanion( ContactsCompanion(
username: Value(searchUserName.text), username: Value(username),
userId: Value(userdata.userId.toInt()), userId: Value(userdata.userId.toInt()),
requested: const Value(false), requested: const Value(false),
blocked: const Value(false), blocked: const Value(false),
@ -114,7 +139,7 @@ class _SearchUsernameView extends State<AddNewUserView> {
if (added > 0) await importSignalContactAndCreateRequest(userdata); if (added > 0) await importSignalContactAndCreateRequest(userdata);
} }
InputDecoration getInputDecoration(String hintText) { InputDecoration _getInputDecoration(String hintText) {
return InputDecoration( return InputDecoration(
hintText: hintText, hintText: hintText,
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
@ -146,14 +171,16 @@ class _SearchUsernameView extends State<AddNewUserView> {
children: [ children: [
Expanded( Expanded(
child: TextField( child: TextField(
onSubmitted: (_) async { onSubmitted: _requestNewUserByUsername,
await _addNewUser(context);
},
onChanged: (value) { onChanged: (value) {
searchUserName.text = value.toLowerCase(); _usernameController.text = value.toLowerCase();
searchUserName.selection = TextSelection.fromPosition( _usernameController.selection =
TextPosition(offset: searchUserName.text.length), TextSelection.fromPosition(
); TextPosition(
offset: _usernameController.text.length,
),
);
setState(() {});
}, },
inputFormatters: [ inputFormatters: [
LengthLimitingTextInputFormatter(12), LengthLimitingTextInputFormatter(12),
@ -161,8 +188,8 @@ class _SearchUsernameView extends State<AddNewUserView> {
RegExp('[a-z0-9A-Z._]'), RegExp('[a-z0-9A-Z._]'),
), ),
], ],
controller: searchUserName, controller: _usernameController,
decoration: getInputDecoration( decoration: _getInputDecoration(
context.lang.searchUsernameInput, context.lang.searchUsernameInput,
), ),
), ),
@ -173,18 +200,24 @@ class _SearchUsernameView extends State<AddNewUserView> {
const SizedBox( const SizedBox(
height: 20, height: 20,
), ),
OutlinedButton.icon(
onPressed: () => context.push(Routes.settingsPublicProfile),
icon: const FaIcon(FontAwesomeIcons.qrcode),
label: Text(context.lang.scanQrOrShow),
),
const SizedBox(height: 20),
if (contacts.isNotEmpty)
HeadLineComponent(
context.lang.searchUsernameNewFollowerTitle,
),
Expanded( Expanded(
child: ContactsListView(contacts), child: ListView(
children: [
Center(
child: OutlinedButton.icon(
onPressed: () =>
context.push(Routes.settingsPublicProfile),
icon: const FaIcon(FontAwesomeIcons.qrcode),
label: Text(context.lang.scanQrOrShow),
),
),
OpenRequestsList(
contacts: _openRequestsContacts,
relations: _allAnnouncedUsers,
),
FriendSuggestions(_newAnnouncedUsers),
],
),
), ),
], ],
), ),
@ -193,9 +226,9 @@ class _SearchUsernameView extends State<AddNewUserView> {
floatingActionButton: Padding( floatingActionButton: Padding(
padding: const EdgeInsets.only(bottom: 30), padding: const EdgeInsets.only(bottom: 30),
child: FloatingActionButton( child: FloatingActionButton(
onPressed: _isLoading || searchUserName.text.isEmpty onPressed: _isLoading || _usernameController.text.isEmpty
? null ? null
: () async => _addNewUser(context), : () => _requestNewUserByUsername(_usernameController.text),
child: _isLoading child: _isLoading
? const Center(child: CircularProgressIndicator()) ? const Center(child: CircularProgressIndicator())
: const FaIcon(FontAwesomeIcons.magnifyingGlassPlus), : const FaIcon(FontAwesomeIcons.magnifyingGlassPlus),
@ -204,114 +237,3 @@ class _SearchUsernameView extends State<AddNewUserView> {
); );
} }
} }
class ContactsListView extends StatelessWidget {
const ContactsListView(this.contacts, {super.key});
final List<Contact> contacts;
List<Widget> sendRequestActions(BuildContext context, Contact contact) {
return [
Tooltip(
message: context.lang.searchUserNameArchiveUserTooltip,
child: IconButton(
icon: const FaIcon(Icons.archive_outlined, size: 15),
onPressed: () async {
const update = ContactsCompanion(deletedByUser: Value(true));
await twonlyDB.contactsDao.updateContact(contact.userId, update);
},
),
),
Text(context.lang.searchUserNamePending),
];
}
List<Widget> requestedActions(BuildContext context, Contact contact) {
return [
Tooltip(
message: context.lang.searchUserNameBlockUserTooltip,
child: IconButton(
icon: const Icon(
Icons.person_off_rounded,
color: Color.fromARGB(164, 244, 67, 54),
),
onPressed: () async {
const update = ContactsCompanion(blocked: Value(true));
await twonlyDB.contactsDao.updateContact(contact.userId, update);
},
),
),
Tooltip(
message: context.lang.searchUserNameRejectUserTooltip,
child: IconButton(
icon: const Icon(Icons.close, color: Colors.red),
onPressed: () async {
await sendCipherText(
contact.userId,
EncryptedContent(
contactRequest: EncryptedContent_ContactRequest(
type: EncryptedContent_ContactRequest_Type.REJECT,
),
),
);
await twonlyDB.contactsDao.updateContact(
contact.userId,
const ContactsCompanion(
accepted: Value(false),
requested: Value(false),
deletedByUser: Value(true),
),
);
},
),
),
IconButton(
icon: const Icon(Icons.check, color: Colors.green),
onPressed: () async {
await twonlyDB.contactsDao.updateContact(
contact.userId,
const ContactsCompanion(
accepted: Value(true),
requested: Value(false),
),
);
await twonlyDB.groupsDao.createNewDirectChat(
contact.userId,
GroupsCompanion(
groupName: Value(getContactDisplayName(contact)),
),
);
await sendCipherText(
contact.userId,
EncryptedContent(
contactRequest: EncryptedContent_ContactRequest(
type: EncryptedContent_ContactRequest_Type.ACCEPT,
),
),
);
await sendContactMyProfileData(contact.userId);
},
),
];
}
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: contacts.length,
itemBuilder: (context, index) {
final contact = contacts[index];
return ListTile(
key: ValueKey(contact.userId),
title: Text(substringBy(contact.username, 25)),
leading: AvatarIcon(contactId: contact.userId),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: contact.requested
? requestedActions(context, contact)
: sendRequestActions(context, contact),
),
);
},
);
}
}

View file

@ -0,0 +1,162 @@
import 'package:drift/drift.dart' show Value;
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/daos/user_discovery.dao.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/api/utils.dart';
import 'package:twonly/src/themes/light.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/components/avatar_icon.component.dart';
import 'package:twonly/src/views/components/headline.dart';
import 'package:twonly/src/views/groups/group.view.dart';
List<TextSpan> buildFriendsListText(
BuildContext context,
List<(Contact, DateTime?)> friends,
) {
final names = friends.map((f) => '*${getContactDisplayName(f.$1)}*').toList();
return formattedText(
context,
context.lang.friendSuggestionsFriendsWith(
joinWithAnd(names, context.lang.andWord),
),
);
}
class FriendSuggestions extends StatelessWidget {
const FriendSuggestions(this.announcedUsers, {super.key});
final AnnouncedUsersWithRelations announcedUsers;
Future<void> _requestAnnouncedUser(
BuildContext context,
UserDiscoveryAnnouncedUser user,
) async {
Log.info('Requesting user via friend suggestions');
final userdata = await apiService.getUserById(user.announcedUserId);
if (userdata == null) {
if (context.mounted) {
showNetworkIssue(context);
}
return;
}
final added = await twonlyDB.contactsDao.insertOnConflictUpdate(
ContactsCompanion(
username: Value(user.username!),
userId: Value(userdata.userId.toInt()),
requested: const Value(false),
blocked: const Value(false),
deletedByUser: const Value(false),
),
);
if (added > 0) await importSignalContactAndCreateRequest(userdata);
}
Future<void> _hideAnnouncedUser(int userId) async {
await twonlyDB.userDiscoveryDao.updateAnnouncedUser(
userId,
const UserDiscoveryAnnouncedUsersCompanion(
isHidden: Value(true),
),
);
}
@override
Widget build(BuildContext context) {
if (announcedUsers.isEmpty) return Container();
return Column(
children: [
const SizedBox(height: 20),
HeadLineComponent(
context.lang.friendSuggestionsTitle,
),
...announcedUsers.entries.map((
announcedUser,
) {
final user = announcedUser.key;
final friends = announcedUser.value;
final friendsList = buildFriendsListText(context, friends);
return ListTile(
key: ValueKey(user.announcedUserId),
contentPadding: EdgeInsets.zero,
title: Text(substringBy(user.username!, 25)),
subtitle: StreamBuilder(
stream: twonlyDB.groupsDao.watchNonDirectGroupsForMember(
user.announcedUserId,
),
builder: (context, snapshot) {
var text = friendsList;
if (snapshot.hasData && snapshot.data!.isNotEmpty) {
text += formattedText(
context,
context.lang.friendSuggestionsGroupMemberIn(
joinWithAnd(
snapshot.data!.map((g) => '*${g.groupName}*').toList(),
context.lang.andWord,
),
),
);
}
return RichText(
text: TextSpan(
children: text,
style: const TextStyle(fontSize: 11),
),
);
},
),
leading: const AvatarIcon(
fontSize: 17,
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
height: 26,
child: FilledButton(
style: FilledButton.styleFrom(
padding: const EdgeInsets.only(right: 8, left: 4),
).merge(secondaryGreyButtonStyle(context)),
child: Row(
children: [
const Padding(
padding: EdgeInsets.symmetric(horizontal: 6),
child: FaIcon(FontAwesomeIcons.userPlus, size: 12),
),
Text(
context.lang.friendSuggestionsRequest,
style: const TextStyle(fontSize: 10),
),
],
),
onPressed: () => _requestAnnouncedUser(context, user),
),
),
IconButton(
style: IconButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 8),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
constraints: const BoxConstraints(),
icon: const Icon(Icons.close, size: 18),
onPressed: () => _hideAnnouncedUser(user.announcedUserId),
),
],
),
);
}),
],
);
}
}

View file

@ -0,0 +1,189 @@
import 'package:drift/drift.dart' hide Column;
import 'package:flutter/material.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/daos/user_discovery.dao.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/themes/light.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/chats/add_new_user_components/friend_suggestions.dart';
import 'package:twonly/src/views/components/avatar_icon.component.dart';
import 'package:twonly/src/views/components/headline.dart';
class OpenRequestsList extends StatelessWidget {
const OpenRequestsList({
required this.contacts,
required this.relations,
super.key,
});
final List<Contact> contacts;
final AnnouncedUsersWithRelations relations;
List<Widget> sendRequestActions(BuildContext context, Contact contact) {
return [
Text(context.lang.searchUserNamePending),
IconButton(
style: IconButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 8),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
constraints: const BoxConstraints(),
icon: const Icon(Icons.close, size: 18),
onPressed: () async {
const update = ContactsCompanion(deletedByUser: Value(true));
await twonlyDB.contactsDao.updateContact(contact.userId, update);
},
),
];
}
List<Widget> requestedActions(BuildContext context, Contact contact) {
return [
SizedBox(
height: 26,
child: FilledButton(
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 4,
),
).merge(secondaryGreyButtonStyle(context)),
child: Row(
children: [
const Icon(
Icons.person_off_rounded,
color: Color.fromARGB(164, 244, 67, 54),
),
Text(
context.lang.contactActionBlock,
style: const TextStyle(fontSize: 10),
),
],
),
onPressed: () async {
const update = ContactsCompanion(blocked: Value(true));
await twonlyDB.contactsDao.updateContact(contact.userId, update);
},
),
),
const SizedBox(width: 9),
SizedBox(
height: 26,
child: FilledButton(
style: FilledButton.styleFrom(
padding: const EdgeInsets.only(right: 8, left: 4),
).merge(secondaryGreyButtonStyle(context)),
child: Row(
children: [
const Icon(Icons.check, color: Colors.green),
Text(
context.lang.contactActionAccept,
style: const TextStyle(fontSize: 10),
),
],
),
onPressed: () async {
await twonlyDB.contactsDao.updateContact(
contact.userId,
const ContactsCompanion(
accepted: Value(true),
requested: Value(false),
),
);
await twonlyDB.groupsDao.createNewDirectChat(
contact.userId,
GroupsCompanion(
groupName: Value(getContactDisplayName(contact)),
),
);
await sendCipherText(
contact.userId,
EncryptedContent(
contactRequest: EncryptedContent_ContactRequest(
type: EncryptedContent_ContactRequest_Type.ACCEPT,
),
),
);
await sendContactMyProfileData(contact.userId);
},
),
),
IconButton(
style: IconButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 8),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
constraints: const BoxConstraints(),
icon: const Icon(Icons.close, size: 18),
onPressed: () async {
await sendCipherText(
contact.userId,
EncryptedContent(
contactRequest: EncryptedContent_ContactRequest(
type: EncryptedContent_ContactRequest_Type.REJECT,
),
),
);
await twonlyDB.contactsDao.updateContact(
contact.userId,
const ContactsCompanion(
accepted: Value(false),
requested: Value(false),
deletedByUser: Value(true),
),
);
},
),
];
}
@override
Widget build(BuildContext context) {
if (contacts.isEmpty) return Container();
return Column(
children: [
const SizedBox(height: 20),
HeadLineComponent(
context.lang.searchUsernameNewFollowerTitle,
),
...contacts.map((contact) {
Widget? subtitle;
Log.info('Relations count: ${relations.entries.length}');
for (final relation in relations.entries) {
if (relation.key.announcedUserId == contact.userId) {
subtitle = RichText(
text: TextSpan(
children: buildFriendsListText(context, relation.value),
style: const TextStyle(fontSize: 11),
),
);
break;
}
}
return ListTile(
key: ValueKey(contact.userId),
contentPadding: EdgeInsets.zero,
title: Text(substringBy(contact.username, 25)),
subtitle: subtitle,
leading: AvatarIcon(
contactId: contact.userId,
fontSize: 17,
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: contact.requested
? requestedActions(context, contact)
: sendRequestActions(context, contact),
),
);
}),
],
);
}
}

View file

@ -11,6 +11,7 @@ import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/providers/purchases.provider.dart'; import 'package:twonly/src/providers/purchases.provider.dart';
import 'package:twonly/src/services/subscription.service.dart'; import 'package:twonly/src/services/subscription.service.dart';
import 'package:twonly/src/themes/light.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/chats/chat_list_components/feedback_btn.dart'; import 'package:twonly/src/views/chats/chat_list_components/feedback_btn.dart';
@ -48,6 +49,7 @@ class _ChatListViewState extends State<ChatListView> {
Future<void> initAsync() async { Future<void> initAsync() async {
final stream = twonlyDB.groupsDao.watchGroupsForChatList(); final stream = twonlyDB.groupsDao.watchGroupsForChatList();
_contactsSub = stream.listen((groups) { _contactsSub = stream.listen((groups) {
if (!mounted) return;
setState(() { setState(() {
_groupsNotPinned = groups _groupsNotPinned = groups
.where((x) => !x.pinned && !x.archived) .where((x) => !x.pinned && !x.archived)
@ -61,15 +63,17 @@ class _ChatListViewState extends State<ChatListView> {
.watchContactsRequestedCount() .watchContactsRequestedCount()
.listen((update) { .listen((update) {
if (update != null) { if (update != null) {
if (!mounted) return;
setState(() { setState(() {
_countContactRequest = update; _countContactRequest = update;
}); });
} }
}); });
_countContactRequestStream = twonlyDB.userDiscoveryDao _countAnnouncedStream = twonlyDB.userDiscoveryDao
.watchNewAnnouncementsWithDataCount() .watchNewAnnouncementsWithDataCount()
.listen((update) { .listen((update) {
if (!mounted) return;
setState(() { setState(() {
_countAnnouncedUsers = update; _countAnnouncedUsers = update;
}); });
@ -165,8 +169,8 @@ class _ChatListViewState extends State<ChatListView> {
child: Container( child: Container(
width: 40, width: 40,
height: 40, height: 40,
decoration: BoxDecoration( decoration: const BoxDecoration(
color: context.color.primary, color: primaryColor,
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
), ),
@ -174,6 +178,10 @@ class _ChatListViewState extends State<ChatListView> {
), ),
Center( Center(
child: NotificationBadge( child: NotificationBadge(
backgroundColor: isDarkMode(context)
? Colors.white
: Colors.black,
textColor: isDarkMode(context) ? Colors.black : Colors.white,
count: (_countAnnouncedUsers + _countContactRequest) count: (_countAnnouncedUsers + _countContactRequest)
.toString(), .toString(),
child: IconButton( child: IconButton(

View file

@ -41,8 +41,6 @@ class TypingIndicator extends StatefulWidget {
} }
class _TypingIndicatorState extends State<TypingIndicator> { class _TypingIndicatorState extends State<TypingIndicator> {
late AnimationController _controller;
List<GroupMember> _groupMembers = []; List<GroupMember> _groupMembers = [];
late StreamSubscription<List<(Contact, GroupMember)>> membersSub; late StreamSubscription<List<(Contact, GroupMember)>> membersSub;
@ -75,7 +73,6 @@ class _TypingIndicatorState extends State<TypingIndicator> {
@override @override
void dispose() { void dispose() {
_controller.dispose();
membersSub.cancel(); membersSub.cancel();
_periodicUpdate.cancel(); _periodicUpdate.cancel();
super.dispose(); super.dispose();
@ -183,6 +180,12 @@ class _AnimatedTypingDotsState extends State<AnimatedTypingDots>
super.initState(); super.initState();
} }
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Row( return Row(

View file

@ -4,9 +4,13 @@ class NotificationBadge extends StatelessWidget {
const NotificationBadge({ const NotificationBadge({
required this.count, required this.count,
required this.child, required this.child,
this.backgroundColor = Colors.red,
this.textColor = Colors.white,
super.key, super.key,
}); });
final String count; final String count;
final Color backgroundColor;
final Color textColor;
final Widget child; final Widget child;
@override @override
@ -23,14 +27,14 @@ class NotificationBadge extends StatelessWidget {
height: 18, height: 18,
width: 18, width: 18,
child: CircleAvatar( child: CircleAvatar(
backgroundColor: Colors.red, backgroundColor: backgroundColor,
child: Center( child: Center(
child: Transform.rotate( child: Transform.rotate(
angle: infinity ? 90 * (3.141592653589793 / 180) : 0, angle: infinity ? 90 * (3.141592653589793 / 180) : 0,
child: Text( child: Text(
infinity ? '8' : count, infinity ? '8' : count,
style: const TextStyle( style: TextStyle(
color: Colors.white, // Text color color: textColor,
fontSize: 10, fontSize: 10,
), ),
), ),

View file

@ -87,14 +87,15 @@ class _ContactViewState extends State<ContactView> {
context.lang.contactRemoveBody, context.lang.contactRemoveBody,
); );
if (remove) { if (remove) {
await twonlyDB.contactsDao.updateContact( await twonlyDB.contactsDao.deleteContactByUserId(contact.userId);
contact.userId, // await twonlyDB.contactsDao.updateContact(
const ContactsCompanion( // contact.userId,
accepted: Value(false), // const ContactsCompanion(
requested: Value(false), // accepted: Value(false),
deletedByUser: Value(true), // requested: Value(false),
), // deletedByUser: Value(true),
); // ),
// );
if (mounted) { if (mounted) {
Navigator.popUntil(context, (route) => route.isFirst); Navigator.popUntil(context, (route) => route.isFirst);
} }
@ -221,6 +222,36 @@ class _ContactViewState extends State<ContactView> {
setState(() {}); setState(() {});
}, },
), ),
if (gUser.isUserDiscoveryEnabled)
BetterListTile(
icon: FontAwesomeIcons.usersViewfinder,
text: context.lang.userDiscoverySettingsTitle,
subtitle:
!contact.userDiscoveryExcluded &&
contact.mediaSendCounter <
gUser.minimumRequiredImagesExchanged
? Text(
context.lang.contactUserDiscoveryImagesLeft(
gUser.minimumRequiredImagesExchanged -
contact.mediaSendCounter,
getContactDisplayName(contact),
),
style: const TextStyle(fontSize: 9),
)
: null,
trailing: Transform.scale(
scale: 0.8,
child: Switch(
value: !contact.userDiscoveryExcluded,
onChanged: (a) async {
await twonlyDB.contactsDao.updateContact(
contact.userId,
ContactsCompanion(userDiscoveryExcluded: Value(!a)),
);
},
),
),
),
BetterListTile( BetterListTile(
icon: FontAwesomeIcons.flag, icon: FontAwesomeIcons.flag,
text: context.lang.reportUser, text: context.lang.reportUser,

View file

@ -30,28 +30,29 @@ class _UserDiscoveryDisabledComponentState
child: ListView( child: ListView(
children: [ children: [
const SizedBox(height: 45), const SizedBox(height: 45),
const Text( Text(
'twonly verzichten auf Telefonnummern, daher schlagen wir dir Freunde stattdessen über gemeinsame Kontakte vor sicher und privat.', context.lang.userDiscoveryDisabledIntro,
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
const SizedBox(height: 30), const SizedBox(height: 30),
RichText( RichText(
text: TextSpan( text: TextSpan(
children: formattedText( children: formattedText(
'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.', context,
context.lang.userDiscoveryDisabledInvisible,
), ),
), ),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
const SizedBox(height: 35), const SizedBox(height: 35),
const Text( Text(
'Du hast die Kontrolle', context.lang.userDiscoveryDisabledYouHaveControl,
style: TextStyle(fontSize: 17), style: const TextStyle(fontSize: 17),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
const Text( Text(
'Entscheide selbst, wer deine Freunde sehen darf. Du kannst deine Meinung jederzeit ändern oder bestimmte Personen verstecken.', context.lang.userDiscoveryDisabledDecide,
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
@ -61,10 +62,13 @@ class _UserDiscoveryDisabledComponentState
onPressed: initializeUserDiscoveryWithDefaultSettings, onPressed: initializeUserDiscoveryWithDefaultSettings,
style: primaryColorButtonStyle.merge( style: primaryColorButtonStyle.merge(
FilledButton.styleFrom( FilledButton.styleFrom(
padding: EdgeInsets.symmetric(horizontal: 32, vertical: 24), padding: const EdgeInsets.symmetric(
horizontal: 32,
vertical: 24,
),
), ),
), ),
child: const Text('Mit Standardeinstellungen aktivieren'), child: Text(context.lang.userDiscoveryDisabledEnableWithDefault),
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
@ -74,7 +78,7 @@ class _UserDiscoveryDisabledComponentState
child: FilledButton( child: FilledButton(
onPressed: () {}, onPressed: () {},
style: secondaryGreyButtonStyle(context), style: secondaryGreyButtonStyle(context),
child: const Text('Einstellungen anpassen'), child: Text(context.lang.userDiscoveryDisabledCustomizeSettings),
), ),
), ),
const SizedBox(height: 15), const SizedBox(height: 15),
@ -83,7 +87,7 @@ class _UserDiscoveryDisabledComponentState
child: FilledButton( child: FilledButton(
onPressed: () {}, onPressed: () {},
style: secondaryGreyButtonStyle(context), style: secondaryGreyButtonStyle(context),
child: const Text('Mehr erfahren'), child: Text(context.lang.userDiscoveryDisabledLearnMore),
), ),
), ),
], ],

View file

@ -1,12 +1,17 @@
import 'dart:async'; import 'dart:async';
import 'package:drift/drift.dart' show Value;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/user_discovery/types.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/user_discovery/types.pb.dart';
import 'package:twonly/src/services/user_discovery.service.dart'; import 'package:twonly/src/services/user_discovery.service.dart';
import 'package:twonly/src/themes/light.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/components/alert_dialog.dart'; import 'package:twonly/src/views/components/alert_dialog.dart';
import 'package:twonly/src/views/components/avatar_icon.component.dart';
import 'package:twonly/src/views/components/user_context_menu.component.dart';
import 'package:twonly/src/views/settings/privacy/user_discovery/user_discovery_settings.view.dart'; import 'package:twonly/src/views/settings/privacy/user_discovery/user_discovery_settings.view.dart';
class UserDiscoveryEnabledComponent extends StatefulWidget { class UserDiscoveryEnabledComponent extends StatefulWidget {
@ -57,8 +62,8 @@ class _UserDiscoveryEnabledComponentState
Future<void> _disableUserDiscovery() async { Future<void> _disableUserDiscovery() async {
final ok = await showAlertDialog( final ok = await showAlertDialog(
context, context,
'Wirklich deaktivieren?', context.lang.userDiscoveryEnabledDialogTitle,
'Wenn du das Feature „Freunde finden“ deaktivierst, werden dir keine Vorschläge mehr angezeigt. Du teilst neuen Kontakten dann auch nicht mehr deine Freunde.', context.lang.userDiscoveryEnabledDisableWarning,
); );
if (ok) { if (ok) {
@ -80,48 +85,98 @@ class _UserDiscoveryEnabledComponentState
backgroundColor: context.color.surfaceContainer, backgroundColor: context.color.surfaceContainer,
collapsedShape: const RoundedRectangleBorder(), collapsedShape: const RoundedRectangleBorder(),
tilePadding: const EdgeInsets.symmetric(horizontal: 17), tilePadding: const EdgeInsets.symmetric(horizontal: 17),
title: const Text('Freunde die du teilst'), title: Text(context.lang.userDiscoveryEnabledFriendsShared),
subtitle: const Text( subtitle: Text(
'Du teilst nur Freunde, die diese Funktion ebenfalls aktiviert haben und die den von dir festgelegten Schwellenwert erreicht haben.', context.lang.userDiscoveryEnabledFriendsSharedDesc,
style: TextStyle(fontSize: 10), style: const TextStyle(fontSize: 10),
), ),
children: _contactsGettingAnnounced.isEmpty children: _contactsGettingAnnounced.isEmpty
? [ ? [
const Padding( Padding(
padding: EdgeInsetsGeometry.symmetric(vertical: 12), padding: const EdgeInsetsGeometry.symmetric(vertical: 12),
child: Text( child: Text(
'Bisher teilst du noch niemanden.', context.lang.userDiscoveryEnabledNoFriendsShared,
), ),
), ),
] ]
: _contactsGettingAnnounced.map((contact) { : _contactsGettingAnnounced.map((contact) {
return Text(getContactDisplayName(contact)); final version =
UserDiscoveryService.getContactVersionTypedFromContact(
contact,
);
return UserContextMenu(
key: ValueKey(contact.userId),
contact: contact,
child: ListTile(
dense: true,
visualDensity: VisualDensity.compact,
minVerticalPadding: 0,
title: Text(getContactDisplayName(contact)),
leading: AvatarIcon(
contactId: contact.userId,
fontSize: 17,
),
subtitle:
(version != null &&
(gUser.isDeveloper || !kReleaseMode))
? Text(
context.lang.userDiscoveryEnabledVersion(
'${version.announcement}.${version.promotion}',
),
style: const TextStyle(fontSize: 10),
)
: null,
trailing: SizedBox(
height: 26,
child: FilledButton(
style: FilledButton.styleFrom(
padding: const EdgeInsets.only(right: 8, left: 8),
).merge(secondaryGreyButtonStyle(context)),
child: Text(
context.lang.userDiscoveryEnabledStopSharing,
style: const TextStyle(fontSize: 10),
),
onPressed: () async {
await twonlyDB.contactsDao.updateContact(
contact.userId,
const ContactsCompanion(
userDiscoveryExcluded: Value(true),
),
);
},
),
),
),
);
}).toList(), }).toList(),
), ),
ListTile( ListTile(
title: const Text('Einstellungen ändern'), title: Text(context.lang.userDiscoveryEnabledChangeSettings),
onTap: () async { onTap: () async {
await context.navPush(const UserDiscoverySettingsView()); await context.navPush(const UserDiscoverySettingsView());
await _initAsync(); await _initAsync();
}, },
), ),
const Divider(), const Divider(),
const ListTile( ListTile(
title: Text('Mehr erfahren'), title: Text(context.lang.userDiscoveryDisabledLearnMore),
subtitle: Text( subtitle: Text(
'In unserem FAQ erklären wir dir wie das Feature "Freunde finden" funktioniert.', context.lang.userDiscoveryEnabledFaq,
), ),
// onTap: _disableUserDiscovery, // onTap: _disableUserDiscovery,
), ),
const Divider(), const Divider(),
ListTile( ListTile(
title: const Text('Deaktivieren'), title: Text(context.lang.userDiscoveryActionDisable),
onTap: _disableUserDiscovery, onTap: _disableUserDiscovery,
), ),
if (_version != null) if (_version != null && (gUser.isDeveloper || !kReleaseMode))
ListTile( ListTile(
title: Text( title: Text(
'Your version: ${_version!.announcement}.${_version!.promotion}', context.lang.userDiscoveryEnabledYourVersion(
'${_version!.announcement}.${_version!.promotion}',
),
style: const TextStyle(color: Colors.grey, fontSize: 13), style: const TextStyle(color: Colors.grey, fontSize: 13),
), ),
), ),

View file

@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/services/user_discovery.service.dart'; import 'package:twonly/src/services/user_discovery.service.dart';
import 'package:twonly/src/themes/light.dart'; import 'package:twonly/src/themes/light.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
class UserDiscoverySettingsView extends StatefulWidget { class UserDiscoverySettingsView extends StatefulWidget {
@ -50,16 +51,16 @@ class _UserDiscoverySettingsViewState extends State<UserDiscoverySettingsView> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Freunde finden'), title: Text(context.lang.userDiscoverySettingsTitle),
), ),
body: Padding( body: Padding(
padding: const EdgeInsets.only(top: 10), padding: const EdgeInsets.only(top: 10),
child: ListView( child: ListView(
children: [ children: [
ListTile( ListTile(
title: const Text('Anzahl an geteilten Bildern'), title: Text(context.lang.userDiscoverySettingsMinImagesTitle),
subtitle: const Text( subtitle: Text(
'Wähle die Mindestanzahl an Bildern, die du mit einer Person ausgetauscht haben musst, bevor du ihr deine Freunde sicher teilst.', context.lang.userDiscoverySettingsMinImages,
), ),
trailing: SizedBox( trailing: SizedBox(
width: 60, width: 60,
@ -83,9 +84,9 @@ class _UserDiscoverySettingsViewState extends State<UserDiscoverySettingsView> {
), ),
), ),
ListTile( ListTile(
title: const Text('Anzahl an gemeinsame Freunde'), title: Text(context.lang.userDiscoverySettingsMutualFriendsTitle),
subtitle: const Text( subtitle: Text(
'Wähle aus, wie viele gemeinsame Freunde eine Person haben muss, damit du ihr vorgeschlagen wirst.', context.lang.userDiscoverySettingsMutualFriends,
), ),
trailing: SizedBox( trailing: SizedBox(
width: 60, width: 60,
@ -120,13 +121,13 @@ class _UserDiscoverySettingsViewState extends State<UserDiscoverySettingsView> {
onPressed: _saveChanges, onPressed: _saveChanges,
style: primaryColorButtonStyle.merge( style: primaryColorButtonStyle.merge(
FilledButton.styleFrom( FilledButton.styleFrom(
padding: EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 32, horizontal: 32,
vertical: 24, vertical: 24,
), ),
), ),
), ),
child: const Text('Änderungen übernehmen'), child: Text(context.lang.userDiscoverySettingsApply),
), ),
), ),
], ],

View file

@ -15,9 +15,9 @@ dependencies:
dev_dependencies: dev_dependencies:
ffi: ^2.0.2 ffi: ^2.0.2
ffigen: ^11.0.0 ffigen: ^11.0.0
flutter_lints: ^2.0.0
flutter_test: flutter_test:
sdk: flutter sdk: flutter
flutter_lints: ^2.0.0
flutter: flutter:
plugin: plugin:

View file

@ -18,6 +18,7 @@ import 'schema_v11.dart' as v11;
import 'schema_v12.dart' as v12; import 'schema_v12.dart' as v12;
import 'schema_v13.dart' as v13; import 'schema_v13.dart' as v13;
import 'schema_v14.dart' as v14; import 'schema_v14.dart' as v14;
import 'schema_v15.dart' as v15;
class GeneratedHelper implements SchemaInstantiationHelper { class GeneratedHelper implements SchemaInstantiationHelper {
@override @override
@ -51,10 +52,28 @@ class GeneratedHelper implements SchemaInstantiationHelper {
return v13.DatabaseAtV13(db); return v13.DatabaseAtV13(db);
case 14: case 14:
return v14.DatabaseAtV14(db); return v14.DatabaseAtV14(db);
case 15:
return v15.DatabaseAtV15(db);
default: default:
throw MissingSchemaException(version, versions); throw MissingSchemaException(version, versions);
} }
} }
static const versions = const [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]; static const versions = const [
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
];
} }

File diff suppressed because it is too large Load diff