handling server messages

This commit is contained in:
otsmr 2026-04-20 01:13:11 +02:00
parent 6517473603
commit f2493a2b56
27 changed files with 13369 additions and 70 deletions

View file

@ -1,13 +1,12 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:twonly/core/frb_generated.dart';
import 'package:twonly/main.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
setUpAll(() async => RustLib.init());
testWidgets('Can call rust function', (tester) async {
await tester.pumpWidget(const MyApp());
expect(find.textContaining('Result: `Hello, Tom!`'), findsOneWidget);
});
// testWidgets('Can call rust function', (tester) async {
// await tester.pumpWidget(const MyApp());
// expect(find.textContaining('Result: `Hello, Tom!`'), findsOneWidget);
// });
}

View file

@ -22,11 +22,11 @@ class FlutterUserDiscovery {
receivedVersion: receivedVersion,
);
static Future<void> handleUserDiscoveryMessages({
static Future<void> handleNewMessages({
required PlatformInt64 contactId,
required List<Uint8List> messages,
}) => RustLib.instance.api
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleUserDiscoveryMessages(
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessages(
contactId: contactId,
messages: messages,
);
@ -42,7 +42,7 @@ class FlutterUserDiscovery {
publicKey: publicKey,
);
static Future<bool> shouldRequestNewMessages({
static Future<Uint8List?> shouldRequestNewMessages({
required PlatformInt64 contactId,
required List<int> version,
}) => RustLib.instance.api

View file

@ -71,7 +71,7 @@ class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
String get codegenVersion => '2.12.0';
@override
int get rustContentHash => 523281685;
int get rustContentHash => -630534473;
static const kDefaultExternalLibraryLoaderConfig =
ExternalLibraryLoaderConfig(
@ -92,7 +92,7 @@ abstract class RustLibApi extends BaseApi {
});
Future<void>
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleUserDiscoveryMessages({
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessages({
required PlatformInt64 contactId,
required List<Uint8List> messages,
});
@ -104,7 +104,7 @@ abstract class RustLibApi extends BaseApi {
required List<int> publicKey,
});
Future<bool>
Future<Uint8List?>
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryShouldRequestNewMessages({
required PlatformInt64 contactId,
required List<int> version,
@ -228,7 +228,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
@override
Future<void>
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleUserDiscoveryMessages({
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessages({
required PlatformInt64 contactId,
required List<Uint8List> messages,
}) {
@ -250,7 +250,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
decodeErrorData: sse_decode_AnyhowException,
),
constMeta:
kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleUserDiscoveryMessagesConstMeta,
kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessagesConstMeta,
argValues: [contactId, messages],
apiImpl: this,
),
@ -258,9 +258,9 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
}
TaskConstMeta
get kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleUserDiscoveryMessagesConstMeta =>
get kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessagesConstMeta =>
const TaskConstMeta(
debugName: 'flutter_user_discovery_handle_user_discovery_messages',
debugName: 'flutter_user_discovery_handle_new_messages',
argNames: ['contactId', 'messages'],
);
@ -305,7 +305,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
);
@override
Future<bool>
Future<Uint8List?>
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryShouldRequestNewMessages({
required PlatformInt64 contactId,
required List<int> version,
@ -324,7 +324,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
);
},
codec: SseCodec(
decodeSuccessData: sse_decode_bool,
decodeSuccessData: sse_decode_opt_list_prim_u_8_strict,
decodeErrorData: sse_decode_AnyhowException,
),
constMeta:

View file

@ -216,10 +216,10 @@ class UserDiscoveryCallbacks {
await twonlyDB
.into(twonlyDB.userDiscoveryAnnouncedUsers)
.insertOnConflictUpdate(
UserDiscoveryAnnouncedUser(
announcedUserId: announcedUser.userId,
announcedPublicKey: announcedUser.publicKey,
publicId: announcedUser.publicId,
UserDiscoveryAnnouncedUsersCompanion(
announcedUserId: Value(announcedUser.userId),
announcedPublicKey: Value(announcedUser.publicKey),
publicId: Value(announcedUser.publicId),
),
);

View file

@ -18,4 +18,90 @@ class UserDiscoveryDao extends DatabaseAccessor<TwonlyDB>
// of this object.
// ignore: matching_super_parameters
UserDiscoveryDao(super.db);
/// 1. Get count for contacts which are in announced but not in the contacts table
/// Returns all users which are not yet in the contacts table but have no data loaded (e.g. Avatar, username and display name)
Future<List<UserDiscoveryAnnouncedUser>>
getNewAnnouncementsWithoutData() async {
final query =
select(userDiscoveryAnnouncedUsers).join([
leftOuterJoin(
contacts,
contacts.userId.equalsExp(
userDiscoveryAnnouncedUsers.announcedUserId,
),
),
])
// Apply filters:
// 1. The user must NOT exist in the contacts table
// 2. The username must be null
..where(
contacts.userId.isNull() &
userDiscoveryAnnouncedUsers.username.isNull(),
);
return (await query.get())
.map((row) => row.readTable(userDiscoveryAnnouncedUsers))
.toList();
}
Future<Map<UserDiscoveryAnnouncedUser, List<(int, DateTime?)>>>
getAnnouncedUsersWithRelations() async {
final query = select(userDiscoveryAnnouncedUsers).join([
innerJoin(
userDiscoveryUserRelations,
userDiscoveryUserRelations.announcedUserId.equalsExp(
userDiscoveryAnnouncedUsers.announcedUserId,
),
),
]);
final rows = await query.get();
final results = <UserDiscoveryAnnouncedUser, List<(int, DateTime?)>>{};
for (final row in rows) {
final user = row.readTable(userDiscoveryAnnouncedUsers);
final relation = row.readTable(userDiscoveryUserRelations);
final relationData = (
relation.fromContactId,
relation.publicKeyVerifiedTimestamp,
);
if (!results.containsKey(user)) {
results[user] = [];
}
results[user]!.add(relationData);
}
return results;
}
Stream<int> watchNewAnnouncementsWithDataCount() {
final countExp = userDiscoveryAnnouncedUsers.announcedUserId.count();
final query = selectOnly(userDiscoveryAnnouncedUsers)
..addColumns([countExp])
..where(
// Filters: Has a username AND has not been shown to the user yet
userDiscoveryAnnouncedUsers.username.isNotNull() &
userDiscoveryAnnouncedUsers.wasShownToTheUser.equals(false) &
userDiscoveryAnnouncedUsers.isHidden.equals(false),
);
return query.watchSingle().map((row) => row.read(countExp) ?? 0);
}
Future<void> updateAnnouncedUser(
int announcedUserId,
UserDiscoveryAnnouncedUsersCompanion updatedValues,
) async {
await (update(
userDiscoveryAnnouncedUsers,
)..where((c) => c.announcedUserId.equals(announcedUserId))).write(
updatedValues,
);
}
}

File diff suppressed because it is too large Load diff

View file

@ -10,6 +10,13 @@ class UserDiscoveryAnnouncedUsers extends Table {
BlobColumn get announcedPublicKey => blob()();
IntColumn get publicId => integer().unique()();
// When a new user got announced this data will be requested without adding the users to the contacts...
TextColumn get username => text().nullable()();
BoolColumn get wasShownToTheUser =>
boolean().withDefault(const Constant(false))();
BoolColumn get isHidden => boolean().withDefault(const Constant(false))();
@override
Set<Column> get primaryKey => {announcedUserId};
}

View file

@ -72,7 +72,7 @@ class TwonlyDB extends _$TwonlyDB {
TwonlyDB.forTesting(DatabaseConnection super.connection);
@override
int get schemaVersion => 13;
int get schemaVersion => 14;
static QueryExecutor _openConnection() {
return driftDatabase(
@ -191,6 +191,20 @@ class TwonlyDB extends _$TwonlyDB {
schema.contacts.mediaSendCounter,
);
},
from13To14: (m, schema) async {
await m.addColumn(
schema.userDiscoveryAnnouncedUsers,
schema.userDiscoveryAnnouncedUsers.wasShownToTheUser,
);
await m.addColumn(
schema.userDiscoveryAnnouncedUsers,
schema.userDiscoveryAnnouncedUsers.isHidden,
);
await m.addColumn(
schema.userDiscoveryAnnouncedUsers,
schema.userDiscoveryAnnouncedUsers.username,
);
},
)(m, from, to);
},
);

View file

@ -9537,11 +9537,55 @@ class $UserDiscoveryAnnouncedUsersTable extends UserDiscoveryAnnouncedUsers
requiredDuringInsert: true,
defaultConstraints: GeneratedColumn.constraintIsAlways('UNIQUE'),
);
static const VerificationMeta _usernameMeta = const VerificationMeta(
'username',
);
@override
late final GeneratedColumn<String> username = GeneratedColumn<String>(
'username',
aliasedName,
true,
type: DriftSqlType.string,
requiredDuringInsert: false,
);
static const VerificationMeta _wasShownToTheUserMeta = const VerificationMeta(
'wasShownToTheUser',
);
@override
late final GeneratedColumn<bool> wasShownToTheUser = GeneratedColumn<bool>(
'was_shown_to_the_user',
aliasedName,
false,
type: DriftSqlType.bool,
requiredDuringInsert: false,
defaultConstraints: GeneratedColumn.constraintIsAlways(
'CHECK ("was_shown_to_the_user" IN (0, 1))',
),
defaultValue: const Constant(false),
);
static const VerificationMeta _isHiddenMeta = const VerificationMeta(
'isHidden',
);
@override
late final GeneratedColumn<bool> isHidden = GeneratedColumn<bool>(
'is_hidden',
aliasedName,
false,
type: DriftSqlType.bool,
requiredDuringInsert: false,
defaultConstraints: GeneratedColumn.constraintIsAlways(
'CHECK ("is_hidden" IN (0, 1))',
),
defaultValue: const Constant(false),
);
@override
List<GeneratedColumn> get $columns => [
announcedUserId,
announcedPublicKey,
publicId,
username,
wasShownToTheUser,
isHidden,
];
@override
String get aliasedName => _alias ?? actualTableName;
@ -9583,6 +9627,27 @@ class $UserDiscoveryAnnouncedUsersTable extends UserDiscoveryAnnouncedUsers
} else if (isInserting) {
context.missing(_publicIdMeta);
}
if (data.containsKey('username')) {
context.handle(
_usernameMeta,
username.isAcceptableOrUnknown(data['username']!, _usernameMeta),
);
}
if (data.containsKey('was_shown_to_the_user')) {
context.handle(
_wasShownToTheUserMeta,
wasShownToTheUser.isAcceptableOrUnknown(
data['was_shown_to_the_user']!,
_wasShownToTheUserMeta,
),
);
}
if (data.containsKey('is_hidden')) {
context.handle(
_isHiddenMeta,
isHidden.isAcceptableOrUnknown(data['is_hidden']!, _isHiddenMeta),
);
}
return context;
}
@ -9607,6 +9672,18 @@ class $UserDiscoveryAnnouncedUsersTable extends UserDiscoveryAnnouncedUsers
DriftSqlType.int,
data['${effectivePrefix}public_id'],
)!,
username: attachedDatabase.typeMapping.read(
DriftSqlType.string,
data['${effectivePrefix}username'],
),
wasShownToTheUser: attachedDatabase.typeMapping.read(
DriftSqlType.bool,
data['${effectivePrefix}was_shown_to_the_user'],
)!,
isHidden: attachedDatabase.typeMapping.read(
DriftSqlType.bool,
data['${effectivePrefix}is_hidden'],
)!,
);
}
@ -9621,10 +9698,16 @@ class UserDiscoveryAnnouncedUser extends DataClass
final int announcedUserId;
final Uint8List announcedPublicKey;
final int publicId;
final String? username;
final bool wasShownToTheUser;
final bool isHidden;
const UserDiscoveryAnnouncedUser({
required this.announcedUserId,
required this.announcedPublicKey,
required this.publicId,
this.username,
required this.wasShownToTheUser,
required this.isHidden,
});
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
@ -9632,6 +9715,11 @@ class UserDiscoveryAnnouncedUser extends DataClass
map['announced_user_id'] = Variable<int>(announcedUserId);
map['announced_public_key'] = Variable<Uint8List>(announcedPublicKey);
map['public_id'] = Variable<int>(publicId);
if (!nullToAbsent || username != null) {
map['username'] = Variable<String>(username);
}
map['was_shown_to_the_user'] = Variable<bool>(wasShownToTheUser);
map['is_hidden'] = Variable<bool>(isHidden);
return map;
}
@ -9640,6 +9728,11 @@ class UserDiscoveryAnnouncedUser extends DataClass
announcedUserId: Value(announcedUserId),
announcedPublicKey: Value(announcedPublicKey),
publicId: Value(publicId),
username: username == null && nullToAbsent
? const Value.absent()
: Value(username),
wasShownToTheUser: Value(wasShownToTheUser),
isHidden: Value(isHidden),
);
}
@ -9654,6 +9747,9 @@ class UserDiscoveryAnnouncedUser extends DataClass
json['announcedPublicKey'],
),
publicId: serializer.fromJson<int>(json['publicId']),
username: serializer.fromJson<String?>(json['username']),
wasShownToTheUser: serializer.fromJson<bool>(json['wasShownToTheUser']),
isHidden: serializer.fromJson<bool>(json['isHidden']),
);
}
@override
@ -9663,6 +9759,9 @@ class UserDiscoveryAnnouncedUser extends DataClass
'announcedUserId': serializer.toJson<int>(announcedUserId),
'announcedPublicKey': serializer.toJson<Uint8List>(announcedPublicKey),
'publicId': serializer.toJson<int>(publicId),
'username': serializer.toJson<String?>(username),
'wasShownToTheUser': serializer.toJson<bool>(wasShownToTheUser),
'isHidden': serializer.toJson<bool>(isHidden),
};
}
@ -9670,10 +9769,16 @@ class UserDiscoveryAnnouncedUser extends DataClass
int? announcedUserId,
Uint8List? announcedPublicKey,
int? publicId,
Value<String?> username = const Value.absent(),
bool? wasShownToTheUser,
bool? isHidden,
}) => UserDiscoveryAnnouncedUser(
announcedUserId: announcedUserId ?? this.announcedUserId,
announcedPublicKey: announcedPublicKey ?? this.announcedPublicKey,
publicId: publicId ?? this.publicId,
username: username.present ? username.value : this.username,
wasShownToTheUser: wasShownToTheUser ?? this.wasShownToTheUser,
isHidden: isHidden ?? this.isHidden,
);
UserDiscoveryAnnouncedUser copyWithCompanion(
UserDiscoveryAnnouncedUsersCompanion data,
@ -9686,6 +9791,11 @@ class UserDiscoveryAnnouncedUser extends DataClass
? data.announcedPublicKey.value
: this.announcedPublicKey,
publicId: data.publicId.present ? data.publicId.value : this.publicId,
username: data.username.present ? data.username.value : this.username,
wasShownToTheUser: data.wasShownToTheUser.present
? data.wasShownToTheUser.value
: this.wasShownToTheUser,
isHidden: data.isHidden.present ? data.isHidden.value : this.isHidden,
);
}
@ -9694,7 +9804,10 @@ class UserDiscoveryAnnouncedUser extends DataClass
return (StringBuffer('UserDiscoveryAnnouncedUser(')
..write('announcedUserId: $announcedUserId, ')
..write('announcedPublicKey: $announcedPublicKey, ')
..write('publicId: $publicId')
..write('publicId: $publicId, ')
..write('username: $username, ')
..write('wasShownToTheUser: $wasShownToTheUser, ')
..write('isHidden: $isHidden')
..write(')'))
.toString();
}
@ -9704,6 +9817,9 @@ class UserDiscoveryAnnouncedUser extends DataClass
announcedUserId,
$driftBlobEquality.hash(announcedPublicKey),
publicId,
username,
wasShownToTheUser,
isHidden,
);
@override
bool operator ==(Object other) =>
@ -9714,7 +9830,10 @@ class UserDiscoveryAnnouncedUser extends DataClass
other.announcedPublicKey,
this.announcedPublicKey,
) &&
other.publicId == this.publicId);
other.publicId == this.publicId &&
other.username == this.username &&
other.wasShownToTheUser == this.wasShownToTheUser &&
other.isHidden == this.isHidden);
}
class UserDiscoveryAnnouncedUsersCompanion
@ -9722,27 +9841,42 @@ class UserDiscoveryAnnouncedUsersCompanion
final Value<int> announcedUserId;
final Value<Uint8List> announcedPublicKey;
final Value<int> publicId;
final Value<String?> username;
final Value<bool> wasShownToTheUser;
final Value<bool> isHidden;
const UserDiscoveryAnnouncedUsersCompanion({
this.announcedUserId = const Value.absent(),
this.announcedPublicKey = const Value.absent(),
this.publicId = const Value.absent(),
this.username = const Value.absent(),
this.wasShownToTheUser = const Value.absent(),
this.isHidden = const Value.absent(),
});
UserDiscoveryAnnouncedUsersCompanion.insert({
this.announcedUserId = const Value.absent(),
required Uint8List announcedPublicKey,
required int publicId,
this.username = const Value.absent(),
this.wasShownToTheUser = const Value.absent(),
this.isHidden = const Value.absent(),
}) : announcedPublicKey = Value(announcedPublicKey),
publicId = Value(publicId);
static Insertable<UserDiscoveryAnnouncedUser> custom({
Expression<int>? announcedUserId,
Expression<Uint8List>? announcedPublicKey,
Expression<int>? publicId,
Expression<String>? username,
Expression<bool>? wasShownToTheUser,
Expression<bool>? isHidden,
}) {
return RawValuesInsertable({
if (announcedUserId != null) 'announced_user_id': announcedUserId,
if (announcedPublicKey != null)
'announced_public_key': announcedPublicKey,
if (publicId != null) 'public_id': publicId,
if (username != null) 'username': username,
if (wasShownToTheUser != null) 'was_shown_to_the_user': wasShownToTheUser,
if (isHidden != null) 'is_hidden': isHidden,
});
}
@ -9750,11 +9884,17 @@ class UserDiscoveryAnnouncedUsersCompanion
Value<int>? announcedUserId,
Value<Uint8List>? announcedPublicKey,
Value<int>? publicId,
Value<String?>? username,
Value<bool>? wasShownToTheUser,
Value<bool>? isHidden,
}) {
return UserDiscoveryAnnouncedUsersCompanion(
announcedUserId: announcedUserId ?? this.announcedUserId,
announcedPublicKey: announcedPublicKey ?? this.announcedPublicKey,
publicId: publicId ?? this.publicId,
username: username ?? this.username,
wasShownToTheUser: wasShownToTheUser ?? this.wasShownToTheUser,
isHidden: isHidden ?? this.isHidden,
);
}
@ -9772,6 +9912,15 @@ class UserDiscoveryAnnouncedUsersCompanion
if (publicId.present) {
map['public_id'] = Variable<int>(publicId.value);
}
if (username.present) {
map['username'] = Variable<String>(username.value);
}
if (wasShownToTheUser.present) {
map['was_shown_to_the_user'] = Variable<bool>(wasShownToTheUser.value);
}
if (isHidden.present) {
map['is_hidden'] = Variable<bool>(isHidden.value);
}
return map;
}
@ -9780,7 +9929,10 @@ class UserDiscoveryAnnouncedUsersCompanion
return (StringBuffer('UserDiscoveryAnnouncedUsersCompanion(')
..write('announcedUserId: $announcedUserId, ')
..write('announcedPublicKey: $announcedPublicKey, ')
..write('publicId: $publicId')
..write('publicId: $publicId, ')
..write('username: $username, ')
..write('wasShownToTheUser: $wasShownToTheUser, ')
..write('isHidden: $isHidden')
..write(')'))
.toString();
}
@ -19690,12 +19842,18 @@ typedef $$UserDiscoveryAnnouncedUsersTableCreateCompanionBuilder =
Value<int> announcedUserId,
required Uint8List announcedPublicKey,
required int publicId,
Value<String?> username,
Value<bool> wasShownToTheUser,
Value<bool> isHidden,
});
typedef $$UserDiscoveryAnnouncedUsersTableUpdateCompanionBuilder =
UserDiscoveryAnnouncedUsersCompanion Function({
Value<int> announcedUserId,
Value<Uint8List> announcedPublicKey,
Value<int> publicId,
Value<String?> username,
Value<bool> wasShownToTheUser,
Value<bool> isHidden,
});
final class $$UserDiscoveryAnnouncedUsersTableReferences
@ -19769,6 +19927,21 @@ class $$UserDiscoveryAnnouncedUsersTableFilterComposer
builder: (column) => ColumnFilters(column),
);
ColumnFilters<String> get username => $composableBuilder(
column: $table.username,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<bool> get wasShownToTheUser => $composableBuilder(
column: $table.wasShownToTheUser,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<bool> get isHidden => $composableBuilder(
column: $table.isHidden,
builder: (column) => ColumnFilters(column),
);
Expression<bool> userDiscoveryUserRelationsRefs(
Expression<bool> Function($$UserDiscoveryUserRelationsTableFilterComposer f)
f,
@ -19820,6 +19993,21 @@ class $$UserDiscoveryAnnouncedUsersTableOrderingComposer
column: $table.publicId,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<String> get username => $composableBuilder(
column: $table.username,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<bool> get wasShownToTheUser => $composableBuilder(
column: $table.wasShownToTheUser,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<bool> get isHidden => $composableBuilder(
column: $table.isHidden,
builder: (column) => ColumnOrderings(column),
);
}
class $$UserDiscoveryAnnouncedUsersTableAnnotationComposer
@ -19844,6 +20032,17 @@ class $$UserDiscoveryAnnouncedUsersTableAnnotationComposer
GeneratedColumn<int> get publicId =>
$composableBuilder(column: $table.publicId, builder: (column) => column);
GeneratedColumn<String> get username =>
$composableBuilder(column: $table.username, builder: (column) => column);
GeneratedColumn<bool> get wasShownToTheUser => $composableBuilder(
column: $table.wasShownToTheUser,
builder: (column) => column,
);
GeneratedColumn<bool> get isHidden =>
$composableBuilder(column: $table.isHidden, builder: (column) => column);
Expression<T> userDiscoveryUserRelationsRefs<T extends Object>(
Expression<T> Function(
$$UserDiscoveryUserRelationsTableAnnotationComposer a,
@ -19919,20 +20118,32 @@ class $$UserDiscoveryAnnouncedUsersTableTableManager
Value<int> announcedUserId = const Value.absent(),
Value<Uint8List> announcedPublicKey = const Value.absent(),
Value<int> publicId = const Value.absent(),
Value<String?> username = const Value.absent(),
Value<bool> wasShownToTheUser = const Value.absent(),
Value<bool> isHidden = const Value.absent(),
}) => UserDiscoveryAnnouncedUsersCompanion(
announcedUserId: announcedUserId,
announcedPublicKey: announcedPublicKey,
publicId: publicId,
username: username,
wasShownToTheUser: wasShownToTheUser,
isHidden: isHidden,
),
createCompanionCallback:
({
Value<int> announcedUserId = const Value.absent(),
required Uint8List announcedPublicKey,
required int publicId,
Value<String?> username = const Value.absent(),
Value<bool> wasShownToTheUser = const Value.absent(),
Value<bool> isHidden = const Value.absent(),
}) => UserDiscoveryAnnouncedUsersCompanion.insert(
announcedUserId: announcedUserId,
announcedPublicKey: announcedPublicKey,
publicId: publicId,
username: username,
wasShownToTheUser: wasShownToTheUser,
isHidden: isHidden,
),
withReferenceMapper: (p0) => p0
.map(

View file

@ -6932,6 +6932,454 @@ i1.GeneratedColumn<int> _column_229(String aliasedName) =>
$customConstraints: 'NOT NULL DEFAULT 0',
defaultValue: const i1.CustomExpression('0'),
);
final class Schema14 extends i0.VersionedSchema {
Schema14({required super.database}) : super(version: 14);
@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 Shape47 contacts = Shape47(
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_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 Shape48 extends i0.VersionedTable {
Shape48({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<int> get announcedUserId =>
columnsByName['announced_user_id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<i2.Uint8List> get announcedPublicKey =>
columnsByName['announced_public_key']!
as i1.GeneratedColumn<i2.Uint8List>;
i1.GeneratedColumn<int> get publicId =>
columnsByName['public_id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get username =>
columnsByName['username']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get wasShownToTheUser =>
columnsByName['was_shown_to_the_user']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get isHidden =>
columnsByName['is_hidden']! as i1.GeneratedColumn<int>;
}
i1.GeneratedColumn<String> _column_230(String aliasedName) =>
i1.GeneratedColumn<String>(
'username',
aliasedName,
true,
type: i1.DriftSqlType.string,
$customConstraints: 'NULL',
);
i1.GeneratedColumn<int> _column_231(String aliasedName) =>
i1.GeneratedColumn<int>(
'was_shown_to_the_user',
aliasedName,
false,
type: i1.DriftSqlType.int,
$customConstraints:
'NOT NULL DEFAULT 0 CHECK (was_shown_to_the_user IN (0, 1))',
defaultValue: const i1.CustomExpression('0'),
);
i1.GeneratedColumn<int> _column_232(String aliasedName) =>
i1.GeneratedColumn<int>(
'is_hidden',
aliasedName,
false,
type: i1.DriftSqlType.int,
$customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_hidden IN (0, 1))',
defaultValue: const i1.CustomExpression('0'),
);
i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
@ -6945,6 +7393,7 @@ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema11 schema) from10To11,
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, Schema14 schema) from13To14,
}) {
return (currentVersion, database) async {
switch (currentVersion) {
@ -7008,6 +7457,11 @@ i0.MigrationStepWithVersion migrationSteps({
final migrator = i1.Migrator(database, schema);
await from12To13(migrator, schema);
return 13;
case 13:
final schema = Schema14(database: database);
final migrator = i1.Migrator(database, schema);
await from13To14(migrator, schema);
return 14;
default:
throw ArgumentError.value('Unknown migration from $currentVersion');
}
@ -7027,6 +7481,7 @@ i1.OnUpgrade stepByStep({
required Future<void> Function(i1.Migrator m, Schema11 schema) from10To11,
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, Schema14 schema) from13To14,
}) => i0.VersionedSchema.stepByStepHelper(
step: migrationSteps(
from1To2: from1To2,
@ -7041,5 +7496,6 @@ i1.OnUpgrade stepByStep({
from10To11: from10To11,
from11To12: from11To12,
from12To13: from12To13,
from13To14: from13To14,
),
);

View file

@ -3141,6 +3141,12 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'When the typing indicator is turned off, you can\'t see when others are typing a message.'**
String get settingsTypingIndicationSubtitle;
/// No description provided for @scanQrOrShow.
///
/// In en, this message translates to:
/// **'Scan / Show QR'**
String get scanQrOrShow;
}
class _AppLocalizationsDelegate

View file

@ -1761,4 +1761,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get settingsTypingIndicationSubtitle =>
'Bei deaktivierten Tipp-Indikatoren kannst du nicht sehen, wenn andere gerade eine Nachricht tippen.';
@override
String get scanQrOrShow => 'QR scannen / anzeigen';
}

View file

@ -1749,4 +1749,7 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get settingsTypingIndicationSubtitle =>
'When the typing indicator is turned off, you can\'t see when others are typing a message.';
@override
String get scanQrOrShow => 'Scan / Show QR';
}

View file

@ -1749,4 +1749,7 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get settingsTypingIndicationSubtitle =>
'When the typing indicator is turned off, you can\'t see when others are typing a message.';
@override
String get scanQrOrShow => 'Scan / Show QR';
}

View file

@ -37,6 +37,7 @@ import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
import 'package:twonly/src/services/signal/identity.signal.dart';
import 'package:twonly/src/services/signal/utils.signal.dart';
import 'package:twonly/src/services/subscription.service.dart';
import 'package:twonly/src/services/user_discovery.service.dart';
import 'package:twonly/src/utils/keyvalue.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
@ -100,6 +101,7 @@ class ApiService {
unawaited(retransmitAllMessages());
unawaited(tryDownloadAllMediaFiles());
unawaited(reuploadMediaFiles());
twonlyDB.markUpdated();
unawaited(syncFlameCounters());
unawaited(setupNotificationWithUsers());
@ -108,6 +110,8 @@ class ApiService {
unawaited(fetchMissingGroupPublicKey());
unawaited(checkForDeletedUsernames());
unawaited(UserDiscoveryService.checkForNewAnnouncedUsers());
if (gUser.userStudyParticipantsToken != null) {
// In case the user participates in the user study, call the handler after authenticated, to be sure there is a internet connection
unawaited(handleUserStudyUpload());

View file

@ -0,0 +1,78 @@
import 'dart:typed_data';
import 'package:twonly/globals.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/user_discovery.service.dart';
import 'package:twonly/src/utils/log.dart';
Future<void> checkForUserDiscoveryChanges(
int fromUserId,
List<int> receivedVersion,
) async {
final currentVersion = await UserDiscoveryService.shouldRequestNewMessages(
fromUserId,
receivedVersion,
);
if (currentVersion != null) {
await sendCipherText(
fromUserId,
EncryptedContent(
userDiscoveryRequest: EncryptedContent_UserDiscoveryRequest(
currentVersion: currentVersion.toList(),
),
),
);
}
}
Future<void> handleUserDiscoveryRequest(
int fromUserId,
EncryptedContent_UserDiscoveryRequest request,
) async {
if (!gUser.isUserDiscoveryEnabled) {
Log.warn('Got a user discovery request while it is disabled');
return;
}
final contact = await twonlyDB.contactsDao.getContactById(fromUserId);
if (contact == null) return;
if (contact.mediaSendCounter < gUser.minimumRequiredImagesExchanged) {
Log.warn(
'Got a request to update user discovery, but mediaSendCounter (${contact.mediaSendCounter}) < ${gUser.minimumRequiredImagesExchanged}',
);
return;
}
final newMessages = await UserDiscoveryService.getNewMessages(
fromUserId,
request.currentVersion,
);
if (newMessages != null && newMessages.isNotEmpty) {
await sendCipherText(
fromUserId,
EncryptedContent(
userDiscoveryUpdate: EncryptedContent_UserDiscoveryUpdate(
messages: newMessages,
),
),
);
} else {
Log.info('Got update request, but there are no new updates for the user');
}
}
Future<void> handleUserDiscoveryUpdate(
int fromUserId,
EncryptedContent_UserDiscoveryUpdate update,
) async {
if (!gUser.isUserDiscoveryEnabled) {
Log.warn('Got a user discovery update while it is disabled');
return;
}
await UserDiscoveryService.handleNewMessages(
fromUserId,
update.messages.map(Uint8List.fromList).toList(),
);
}

View file

@ -18,6 +18,7 @@ import 'package:twonly/src/model/protobuf/client/generated/push_notification.pb.
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
import 'package:twonly/src/services/signal/encryption.signal.dart';
import 'package:twonly/src/services/signal/session.signal.dart';
import 'package:twonly/src/services/user_discovery.service.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
@ -344,6 +345,17 @@ Future<(Uint8List, Uint8List?)?> sendCipherText(
}
encryptedContent.senderProfileCounter = Int64(gUser.avatarCounter);
if (gUser.isUserDiscoveryEnabled) {
final contact = await twonlyDB.contactsDao.getContactById(contactId);
if (contact != null &&
contact.mediaSendCounter >= gUser.minimumRequiredImagesExchanged) {
final version = await UserDiscoveryService.getCurrentVersion();
if (version != null) {
encryptedContent.senderUserDiscoveryVersion = version;
}
}
}
final response = pb.Message()
..type = pb.Message_Type.CIPHERTEXT
..encryptedContent = encryptedContent.writeToBuffer();

View file

@ -24,6 +24,7 @@ import 'package:twonly/src/services/api/client2client/prekeys.c2c.dart';
import 'package:twonly/src/services/api/client2client/pushkeys.c2c.dart';
import 'package:twonly/src/services/api/client2client/reaction.c2c.dart';
import 'package:twonly/src/services/api/client2client/text_message.c2c.dart';
import 'package:twonly/src/services/api/client2client/user_discovery.c2c.dart';
import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/services/group.services.dart';
import 'package:twonly/src/services/notifications/background.notifications.dart';
@ -262,6 +263,12 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
await twonlyDB.receiptsDao.markMessagesForRetry(fromUserId);
final senderProfileCounter = await checkForProfileUpdate(fromUserId, content);
if (gUser.isUserDiscoveryEnabled && content.hasSenderUserDiscoveryVersion()) {
await checkForUserDiscoveryChanges(
fromUserId,
content.senderUserDiscoveryVersion,
);
}
if (content.hasContactRequest()) {
if (!await handleContactRequest(fromUserId, content.contactRequest)) {
@ -291,6 +298,22 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
return (null, null);
}
if (content.hasUserDiscoveryRequest()) {
await handleUserDiscoveryRequest(
fromUserId,
content.userDiscoveryRequest,
);
return (null, null);
}
if (content.hasUserDiscoveryUpdate()) {
await handleUserDiscoveryUpdate(
fromUserId,
content.userDiscoveryUpdate,
);
return (null, null);
}
if (content.hasPushKeys()) {
await handlePushKey(fromUserId, content.pushKeys);
return (null, null);

View file

@ -1,13 +1,43 @@
import 'dart:typed_data';
import 'dart:convert';
import 'package:drift/drift.dart';
import 'package:twonly/core/bridge/wrapper/user_discovery.dart';
import 'package:twonly/globals.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/utils/log.dart';
import 'package:twonly/src/utils/qr.dart';
import 'package:twonly/src/utils/storage.dart';
class UserDiscoveryService {
static Future<void> checkForNewAnnouncedUsers() async {
final announcedUsers = await twonlyDB.userDiscoveryDao
.getNewAnnouncementsWithoutData();
for (final announcedUser in announcedUsers) {
final userdata = await apiService.getUserById(
announcedUser.announcedUserId,
);
if (userdata == null) continue;
if (userdata.publicIdentityKey !=
announcedUser.announcedPublicKey.toList()) {
Log.error(
'Server delivered a different public key then received from the announcement.',
);
continue;
}
Log.info('Updating the username from the announced user');
// Updating the username, so the data will not be requested again..
await twonlyDB.userDiscoveryDao.updateAnnouncedUser(
announcedUser.announcedUserId,
UserDiscoveryAnnouncedUsersCompanion(
username: Value(utf8.decode(userdata.username)),
),
);
}
}
static Future<void> initializeOrUpdate({
required int threshold,
required int minimumRequiredImagesExchanged,
@ -44,6 +74,50 @@ class UserDiscoveryService {
return UserDiscoveryVersion.fromBuffer(version);
}
static Future<Uint8List?> shouldRequestNewMessages(
int fromUserId,
List<int> receivedVersion,
) async {
try {
return await FlutterUserDiscovery.shouldRequestNewMessages(
contactId: fromUserId,
version: receivedVersion,
);
} catch (e) {
Log.error(e);
return null;
}
}
static Future<List<Uint8List>?> getNewMessages(
int fromUserId,
List<int> receivedVersion,
) async {
try {
return await FlutterUserDiscovery.getNewMessages(
contactId: fromUserId,
receivedVersion: receivedVersion,
);
} catch (e) {
Log.error(e);
return null;
}
}
static Future<void> handleNewMessages(
int fromUserId,
List<Uint8List> messages,
) async {
try {
return await FlutterUserDiscovery.handleNewMessages(
contactId: fromUserId,
messages: messages,
);
} catch (e) {
Log.error(e);
}
}
static Future<void> disable() async {
await updateUserdata((u) {
u.isUserDiscoveryEnabled = false;

View file

@ -167,17 +167,17 @@ class _SearchUsernameView extends State<AddNewUserView> {
),
),
),
Align(
alignment: Alignment.centerRight,
child: IconButton(
onPressed: () =>
context.push(Routes.settingsPublicProfile),
icon: const FaIcon(FontAwesomeIcons.qrcode),
),
),
],
),
),
const SizedBox(
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(

View file

@ -34,6 +34,11 @@ class _ChatListViewState extends State<ChatListView> {
GlobalKey searchForOtherUsers = GlobalKey();
bool showFeedbackShortcut = false;
int _countContactRequest = 0;
int _countAnnouncedUsers = 0;
late StreamSubscription<int?> _countContactRequestStream;
late StreamSubscription<int?> _countAnnouncedStream;
@override
void initState() {
initAsync();
@ -52,6 +57,24 @@ class _ChatListViewState extends State<ChatListView> {
});
});
_countContactRequestStream = twonlyDB.contactsDao
.watchContactsRequestedCount()
.listen((update) {
if (update != null) {
setState(() {
_countContactRequest = update;
});
}
});
_countContactRequestStream = twonlyDB.userDiscoveryDao
.watchNewAnnouncementsWithDataCount()
.listen((update) {
setState(() {
_countAnnouncedUsers = update;
});
});
WidgetsBinding.instance.addPostFrameCallback((_) async {
final changeLog = await rootBundle.loadString('CHANGELOG.md');
final changeLogHash = (await compute(
@ -80,6 +103,8 @@ class _ChatListViewState extends State<ChatListView> {
@override
void dispose() {
_contactsSub.cancel();
_countContactRequestStream.cancel();
_countAnnouncedStream.cancel();
super.dispose();
}
@ -132,23 +157,38 @@ class _ChatListViewState extends State<ChatListView> {
),
actions: [
const FeedbackIconButton(),
StreamBuilder(
stream: twonlyDB.contactsDao.watchContactsRequestedCount(),
builder: (context, snapshot) {
var count = 0;
if (snapshot.hasData && snapshot.data != null) {
count = snapshot.data!;
}
return NotificationBadge(
count: count.toString(),
child: IconButton(
key: searchForOtherUsers,
icon: const FaIcon(FontAwesomeIcons.userPlus, size: 18),
onPressed: () => context.push(Routes.chatsAddNewUser),
Stack(
children: [
if (_countAnnouncedUsers + _countContactRequest > 0)
Positioned.fill(
child: Center(
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: context.color.primary,
shape: BoxShape.circle,
),
),
),
),
);
},
Center(
child: NotificationBadge(
count: (_countAnnouncedUsers + _countContactRequest)
.toString(),
child: IconButton(
color: (_countAnnouncedUsers + _countContactRequest > 0)
? Colors.black
: null,
key: searchForOtherUsers,
icon: const FaIcon(FontAwesomeIcons.userPlus, size: 18),
onPressed: () => context.push(Routes.chatsAddNewUser),
),
),
),
],
),
IconButton(
onPressed: () async {
await context.push(Routes.settings);

View file

@ -38,7 +38,10 @@ impl FlutterUserDiscovery {
.await?)
}
pub async fn should_request_new_messages(contact_id: i64, version: &[u8]) -> Result<bool> {
pub async fn should_request_new_messages(
contact_id: i64,
version: &[u8],
) -> Result<Option<Vec<u8>>> {
Ok(get_twonly_flutter()?
.user_discovery
.get()
@ -47,15 +50,12 @@ impl FlutterUserDiscovery {
.await?)
}
pub async fn handle_user_discovery_messages(
contact_id: i64,
messages: Vec<Vec<u8>>,
) -> Result<()> {
pub async fn handle_new_messages(contact_id: i64, messages: Vec<Vec<u8>>) -> Result<()> {
Ok(get_twonly_flutter()?
.user_discovery
.get()
.await
.handle_user_discovery_messages(contact_id, messages)
.handle_new_messages(contact_id, messages)
.await?)
}
}

View file

@ -38,7 +38,7 @@ flutter_rust_bridge::frb_generated_boilerplate!(
default_rust_auto_opaque = RustAutoOpaqueMoi,
);
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0";
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 523281685;
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -630534473;
// Section: executor
@ -77,19 +77,19 @@ let api_received_version = <Vec<u8>>::sse_decode(&mut deserializer);deserializer
})().await)
} })
}
fn wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_handle_user_discovery_messages_impl(
fn wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_handle_new_messages_impl(
port_: flutter_rust_bridge::for_generated::MessagePort,
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
rust_vec_len_: i32,
data_len_: i32,
) {
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec,_,_,_>(flutter_rust_bridge::for_generated::TaskInfo{ debug_name: "flutter_user_discovery_handle_user_discovery_messages", port: Some(port_), mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal }, move || {
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec,_,_,_>(flutter_rust_bridge::for_generated::TaskInfo{ debug_name: "flutter_user_discovery_handle_new_messages", port: Some(port_), mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal }, move || {
let message = unsafe { flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(ptr_, rust_vec_len_, data_len_) };
let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message);
let api_contact_id = <i64>::sse_decode(&mut deserializer);
let api_messages = <Vec<Vec<u8>>>::sse_decode(&mut deserializer);deserializer.end(); move |context| async move {
transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>((move || async move {
let output_ok = crate::bridge::wrapper::user_discovery::FlutterUserDiscovery::handle_user_discovery_messages(api_contact_id, api_messages).await?; Ok(output_ok)
let output_ok = crate::bridge::wrapper::user_discovery::FlutterUserDiscovery::handle_new_messages(api_contact_id, api_messages).await?; Ok(output_ok)
})().await)
} })
}
@ -900,7 +900,7 @@ fn pde_ffi_dispatcher_primary_impl(
match func_id {
1 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_get_current_version_impl(port, ptr, rust_vec_len, data_len),
2 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_get_new_messages_impl(port, ptr, rust_vec_len, data_len),
3 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_handle_user_discovery_messages_impl(port, ptr, rust_vec_len, data_len),
3 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_handle_new_messages_impl(port, ptr, rust_vec_len, data_len),
4 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_initialize_or_update_impl(port, ptr, rust_vec_len, data_len),
5 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_should_request_new_messages_impl(port, ptr, rust_vec_len, data_len),
6 => wire__crate__bridge__callbacks__init_flutter_callbacks_impl(port, ptr, rust_vec_len, data_len),

View file

@ -231,7 +231,7 @@ impl<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store,
&self,
contact_id: UserID,
version: &[u8],
) -> Result<bool> {
) -> Result<Option<Vec<u8>>> {
let received_version = UserDiscoveryVersion::decode(version)?;
let stored_version = match self.store.get_contact_version(contact_id).await? {
Some(buf) => UserDiscoveryVersion::decode(buf.as_slice())?,
@ -247,8 +247,13 @@ impl<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store,
stored.promotion = %stored_version.promotion,
"Comparing version numbers"
);
Ok(received_version.announcement > stored_version.announcement
|| received_version.promotion > stored_version.promotion)
if received_version.announcement > stored_version.announcement
|| received_version.promotion > stored_version.promotion
{
Ok(Some(stored_version.encode_to_vec()))
} else {
Ok(None)
}
}
pub(crate) async fn get_contact_version(&self, contact_id: UserID) -> Result<Option<Vec<u8>>> {
@ -257,7 +262,7 @@ impl<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store,
/// Returns the latest version for this discovery.
/// Before calling this function the application must sure that contact_id is qualified to be announced.
pub async fn handle_user_discovery_messages(
pub async fn handle_new_messages(
&self,
contact_id: UserID,
messages: Vec<Vec<u8>>,

View file

@ -38,7 +38,8 @@ async fn assert_new_messages<S: UserDiscoveryStore>(
assert_eq!(
to.1.should_request_new_messages(from.0 as UserID, to_received_version)
.await
.unwrap(),
.unwrap()
.is_some(),
has_new_messages
);
}
@ -53,7 +54,8 @@ async fn request_and_handle_messages<S: UserDiscoveryStore>(
assert_eq!(
to.1.should_request_new_messages(from.0 as UserID, to_received_version)
.await
.unwrap(),
.unwrap()
.is_some(),
true
);
@ -72,7 +74,7 @@ async fn request_and_handle_messages<S: UserDiscoveryStore>(
assert!(new_messages.len() <= messages_count);
to.1.handle_user_discovery_messages(from.0 as UserID, new_messages)
to.1.handle_new_messages(from.0 as UserID, new_messages)
.await
.unwrap();
@ -82,7 +84,8 @@ async fn request_and_handle_messages<S: UserDiscoveryStore>(
&from.1.get_current_version().await.unwrap()
)
.await
.unwrap(),
.unwrap()
.is_some(),
false
);
}

View file

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

File diff suppressed because it is too large Load diff