diff --git a/CHANGELOG.md b/CHANGELOG.md index 96f975ce..6ba97d43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ## 0.1.9 - New: Feature to find friends without a phone number -- New: The verification state is now transferred to the scanned user. +- New: The verification state is now transferred to the scanned user - New: Registration setup to configure the most important configurations - Improved: FAQ is now in the app rather than opening in the browser - Fix: Many smaller issues diff --git a/lib/src/database/daos/contacts.dao.dart b/lib/src/database/daos/contacts.dao.dart index 1212df6f..7c970cc3 100644 --- a/lib/src/database/daos/contacts.dao.dart +++ b/lib/src/database/daos/contacts.dao.dart @@ -135,27 +135,35 @@ class ContactsDao extends DatabaseAccessor with _$ContactsDaoMixin { } Stream> watchContactsAnnouncedViaUserDiscovery() { - return (select(contacts)..where( - (t) => - t.userDiscoveryVersion.isNotNull() & - t.userDiscoveryExcluded.equals(false) & - t.mediaSendCounter.isBiggerOrEqualValue( - userService.currentUser.requiredSendImages, - ), - )) - .watch(); + return (select(contacts)..where((t) { + var expr = t.userDiscoveryVersion.isNotNull() & + t.userDiscoveryExcluded.equals(false) & + t.mediaSendCounter.isBiggerOrEqualValue( + userService.currentUser.requiredSendImages, + ); + + if (userService.currentUser.userDiscoveryRequiresManualApproval) { + expr = expr & t.userDiscoveryManualApproved.equals(true); + } + + return expr; + })).watch(); } Future> getContactsAnnouncedViaUserDiscovery() async { - return (select(contacts)..where( - (t) => - t.userDiscoveryVersion.isNotNull() & - t.userDiscoveryExcluded.equals(false) & - t.mediaSendCounter.isBiggerOrEqualValue( - userService.currentUser.requiredSendImages, - ), - )) - .get(); + return (select(contacts)..where((t) { + var expr = t.userDiscoveryVersion.isNotNull() & + t.userDiscoveryExcluded.equals(false) & + t.mediaSendCounter.isBiggerOrEqualValue( + userService.currentUser.requiredSendImages, + ); + + if (userService.currentUser.userDiscoveryRequiresManualApproval) { + expr = expr & t.userDiscoveryManualApproved.equals(true); + } + + return expr; + })).get(); } Stream> watchAllContacts() { diff --git a/lib/src/database/schemas/twonly_db/drift_schema_v12.json b/lib/src/database/schemas/twonly_db/drift_schema_v12.json index 0c1a96f0..b6e128cf 100644 --- a/lib/src/database/schemas/twonly_db/drift_schema_v12.json +++ b/lib/src/database/schemas/twonly_db/drift_schema_v12.json @@ -197,7 +197,7 @@ "name": "user_discovery_manual_approved", "getter_name": "userDiscoveryManualApproved", "moor_type": "bool", - "nullable": false, + "nullable": true, "customConstraints": null, "defaultConstraints": "CHECK (\"user_discovery_manual_approved\" IN (0, 1))", "dialectAwareDefaultConstraints": { @@ -2557,7 +2557,7 @@ "sql": [ { "dialect": "sqlite", - "sql": "CREATE TABLE IF NOT EXISTS \"contacts\" (\"user_id\" INTEGER NOT NULL, \"username\" TEXT NOT NULL, \"display_name\" TEXT NULL, \"nick_name\" TEXT NULL, \"avatar_svg_compressed\" BLOB NULL, \"sender_profile_counter\" INTEGER NOT NULL DEFAULT 0, \"accepted\" INTEGER NOT NULL DEFAULT 0 CHECK (\"accepted\" IN (0, 1)), \"deleted_by_user\" INTEGER NOT NULL DEFAULT 0 CHECK (\"deleted_by_user\" IN (0, 1)), \"requested\" INTEGER NOT NULL DEFAULT 0 CHECK (\"requested\" IN (0, 1)), \"blocked\" INTEGER NOT NULL DEFAULT 0 CHECK (\"blocked\" IN (0, 1)), \"verified\" INTEGER NOT NULL DEFAULT 0 CHECK (\"verified\" IN (0, 1)), \"account_deleted\" INTEGER NOT NULL DEFAULT 0 CHECK (\"account_deleted\" IN (0, 1)), \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), \"user_discovery_version\" BLOB NULL, \"user_discovery_excluded\" INTEGER NOT NULL DEFAULT 0 CHECK (\"user_discovery_excluded\" IN (0, 1)), \"user_discovery_manual_approved\" INTEGER NOT NULL DEFAULT 0 CHECK (\"user_discovery_manual_approved\" IN (0, 1)), \"media_send_counter\" INTEGER NOT NULL DEFAULT 0, \"media_received_counter\" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (\"user_id\"));" + "sql": "CREATE TABLE IF NOT EXISTS \"contacts\" (\"user_id\" INTEGER NOT NULL, \"username\" TEXT NOT NULL, \"display_name\" TEXT NULL, \"nick_name\" TEXT NULL, \"avatar_svg_compressed\" BLOB NULL, \"sender_profile_counter\" INTEGER NOT NULL DEFAULT 0, \"accepted\" INTEGER NOT NULL DEFAULT 0 CHECK (\"accepted\" IN (0, 1)), \"deleted_by_user\" INTEGER NOT NULL DEFAULT 0 CHECK (\"deleted_by_user\" IN (0, 1)), \"requested\" INTEGER NOT NULL DEFAULT 0 CHECK (\"requested\" IN (0, 1)), \"blocked\" INTEGER NOT NULL DEFAULT 0 CHECK (\"blocked\" IN (0, 1)), \"verified\" INTEGER NOT NULL DEFAULT 0 CHECK (\"verified\" IN (0, 1)), \"account_deleted\" INTEGER NOT NULL DEFAULT 0 CHECK (\"account_deleted\" IN (0, 1)), \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), \"user_discovery_version\" BLOB NULL, \"user_discovery_excluded\" INTEGER NOT NULL DEFAULT 0 CHECK (\"user_discovery_excluded\" IN (0, 1)), \"user_discovery_manual_approved\" INTEGER NULL DEFAULT 0 CHECK (\"user_discovery_manual_approved\" IN (0, 1)), \"media_send_counter\" INTEGER NOT NULL DEFAULT 0, \"media_received_counter\" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (\"user_id\"));" } ] }, diff --git a/lib/src/database/tables/contacts.table.dart b/lib/src/database/tables/contacts.table.dart index e32b7f97..7388d672 100644 --- a/lib/src/database/tables/contacts.table.dart +++ b/lib/src/database/tables/contacts.table.dart @@ -30,7 +30,7 @@ class Contacts extends Table { boolean().withDefault(const Constant(false))(); BoolColumn get userDiscoveryManualApproved => - boolean().withDefault(const Constant(false))(); + boolean().nullable().withDefault(const Constant(false))(); IntColumn get mediaSendCounter => integer().withDefault(const Constant(0))(); IntColumn get mediaReceivedCounter => diff --git a/lib/src/database/twonly.db.g.dart b/lib/src/database/twonly.db.g.dart index 1cc53547..7a2f7f69 100644 --- a/lib/src/database/twonly.db.g.dart +++ b/lib/src/database/twonly.db.g.dart @@ -207,7 +207,7 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> { GeneratedColumn( 'user_discovery_manual_approved', aliasedName, - false, + true, type: DriftSqlType.bool, requiredDuringInsert: false, defaultConstraints: GeneratedColumn.constraintIsAlways( @@ -483,7 +483,7 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> { userDiscoveryManualApproved: attachedDatabase.typeMapping.read( DriftSqlType.bool, data['${effectivePrefix}user_discovery_manual_approved'], - )!, + ), mediaSendCounter: attachedDatabase.typeMapping.read( DriftSqlType.int, data['${effectivePrefix}media_send_counter'], @@ -517,7 +517,7 @@ class Contact extends DataClass implements Insertable { final DateTime createdAt; final Uint8List? userDiscoveryVersion; final bool userDiscoveryExcluded; - final bool userDiscoveryManualApproved; + final bool? userDiscoveryManualApproved; final int mediaSendCounter; final int mediaReceivedCounter; const Contact({ @@ -536,7 +536,7 @@ class Contact extends DataClass implements Insertable { required this.createdAt, this.userDiscoveryVersion, required this.userDiscoveryExcluded, - required this.userDiscoveryManualApproved, + this.userDiscoveryManualApproved, required this.mediaSendCounter, required this.mediaReceivedCounter, }); @@ -566,9 +566,11 @@ class Contact extends DataClass implements Insertable { map['user_discovery_version'] = Variable(userDiscoveryVersion); } map['user_discovery_excluded'] = Variable(userDiscoveryExcluded); - map['user_discovery_manual_approved'] = Variable( - userDiscoveryManualApproved, - ); + if (!nullToAbsent || userDiscoveryManualApproved != null) { + map['user_discovery_manual_approved'] = Variable( + userDiscoveryManualApproved, + ); + } map['media_send_counter'] = Variable(mediaSendCounter); map['media_received_counter'] = Variable(mediaReceivedCounter); return map; @@ -599,7 +601,10 @@ class Contact extends DataClass implements Insertable { ? const Value.absent() : Value(userDiscoveryVersion), userDiscoveryExcluded: Value(userDiscoveryExcluded), - userDiscoveryManualApproved: Value(userDiscoveryManualApproved), + userDiscoveryManualApproved: + userDiscoveryManualApproved == null && nullToAbsent + ? const Value.absent() + : Value(userDiscoveryManualApproved), mediaSendCounter: Value(mediaSendCounter), mediaReceivedCounter: Value(mediaReceivedCounter), ); @@ -634,7 +639,7 @@ class Contact extends DataClass implements Insertable { userDiscoveryExcluded: serializer.fromJson( json['userDiscoveryExcluded'], ), - userDiscoveryManualApproved: serializer.fromJson( + userDiscoveryManualApproved: serializer.fromJson( json['userDiscoveryManualApproved'], ), mediaSendCounter: serializer.fromJson(json['mediaSendCounter']), @@ -664,7 +669,7 @@ class Contact extends DataClass implements Insertable { userDiscoveryVersion, ), 'userDiscoveryExcluded': serializer.toJson(userDiscoveryExcluded), - 'userDiscoveryManualApproved': serializer.toJson( + 'userDiscoveryManualApproved': serializer.toJson( userDiscoveryManualApproved, ), 'mediaSendCounter': serializer.toJson(mediaSendCounter), @@ -688,7 +693,7 @@ class Contact extends DataClass implements Insertable { DateTime? createdAt, Value userDiscoveryVersion = const Value.absent(), bool? userDiscoveryExcluded, - bool? userDiscoveryManualApproved, + Value userDiscoveryManualApproved = const Value.absent(), int? mediaSendCounter, int? mediaReceivedCounter, }) => Contact( @@ -711,8 +716,9 @@ class Contact extends DataClass implements Insertable { ? userDiscoveryVersion.value : this.userDiscoveryVersion, userDiscoveryExcluded: userDiscoveryExcluded ?? this.userDiscoveryExcluded, - userDiscoveryManualApproved: - userDiscoveryManualApproved ?? this.userDiscoveryManualApproved, + userDiscoveryManualApproved: userDiscoveryManualApproved.present + ? userDiscoveryManualApproved.value + : this.userDiscoveryManualApproved, mediaSendCounter: mediaSendCounter ?? this.mediaSendCounter, mediaReceivedCounter: mediaReceivedCounter ?? this.mediaReceivedCounter, ); @@ -852,7 +858,7 @@ class ContactsCompanion extends UpdateCompanion { final Value createdAt; final Value userDiscoveryVersion; final Value userDiscoveryExcluded; - final Value userDiscoveryManualApproved; + final Value userDiscoveryManualApproved; final Value mediaSendCounter; final Value mediaReceivedCounter; const ContactsCompanion({ @@ -959,7 +965,7 @@ class ContactsCompanion extends UpdateCompanion { Value? createdAt, Value? userDiscoveryVersion, Value? userDiscoveryExcluded, - Value? userDiscoveryManualApproved, + Value? userDiscoveryManualApproved, Value? mediaSendCounter, Value? mediaReceivedCounter, }) { @@ -11687,7 +11693,7 @@ typedef $$ContactsTableCreateCompanionBuilder = Value createdAt, Value userDiscoveryVersion, Value userDiscoveryExcluded, - Value userDiscoveryManualApproved, + Value userDiscoveryManualApproved, Value mediaSendCounter, Value mediaReceivedCounter, }); @@ -11708,7 +11714,7 @@ typedef $$ContactsTableUpdateCompanionBuilder = Value createdAt, Value userDiscoveryVersion, Value userDiscoveryExcluded, - Value userDiscoveryManualApproved, + Value userDiscoveryManualApproved, Value mediaSendCounter, Value mediaReceivedCounter, }); @@ -12967,7 +12973,7 @@ class $$ContactsTableTableManager Value createdAt = const Value.absent(), Value userDiscoveryVersion = const Value.absent(), Value userDiscoveryExcluded = const Value.absent(), - Value userDiscoveryManualApproved = const Value.absent(), + Value userDiscoveryManualApproved = const Value.absent(), Value mediaSendCounter = const Value.absent(), Value mediaReceivedCounter = const Value.absent(), }) => ContactsCompanion( @@ -13007,7 +13013,7 @@ class $$ContactsTableTableManager Value createdAt = const Value.absent(), Value userDiscoveryVersion = const Value.absent(), Value userDiscoveryExcluded = const Value.absent(), - Value userDiscoveryManualApproved = const Value.absent(), + Value userDiscoveryManualApproved = const Value.absent(), Value mediaSendCounter = const Value.absent(), Value mediaReceivedCounter = const Value.absent(), }) => ContactsCompanion.insert( diff --git a/lib/src/database/twonly.db.steps.dart b/lib/src/database/twonly.db.steps.dart index 46df63b5..4f8306bc 100644 --- a/lib/src/database/twonly.db.steps.dart +++ b/lib/src/database/twonly.db.steps.dart @@ -6294,10 +6294,10 @@ i1.GeneratedColumn _column_213(String aliasedName) => i1.GeneratedColumn( 'user_discovery_manual_approved', aliasedName, - false, + true, type: i1.DriftSqlType.int, $customConstraints: - 'NOT NULL DEFAULT 0 CHECK (user_discovery_manual_approved IN (0, 1))', + 'NULL DEFAULT 0 CHECK (user_discovery_manual_approved IN (0, 1))', defaultValue: const i1.CustomExpression('0'), ); i1.GeneratedColumn _column_214(String aliasedName) => diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index 510bacff..d36abcd2 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -2971,6 +2971,24 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Stop sharing'** String get userDiscoveryEnabledStopSharing; + + /// No description provided for @userDiscoveryManualApprovalReachedThreshold. + /// + /// In en, this message translates to: + /// **'{username} has reached your threshold and now needs your manual approval to be shared with your friends.'** + String userDiscoveryManualApprovalReachedThreshold(Object username); + + /// No description provided for @userDiscoveryManualApprovalHideContact. + /// + /// In en, this message translates to: + /// **'Hide contact'** + String get userDiscoveryManualApprovalHideContact; + + /// No description provided for @userDiscoveryManualApprovalShareContact. + /// + /// In en, this message translates to: + /// **'Share contact'** + String get userDiscoveryManualApprovalShareContact; } class _AppLocalizationsDelegate diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index c7a4938a..0c3f90b4 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -1672,4 +1672,15 @@ class AppLocalizationsDe extends AppLocalizations { @override String get userDiscoveryEnabledStopSharing => 'Nicht mehr teilen'; + + @override + String userDiscoveryManualApprovalReachedThreshold(Object username) { + return '$username hat deinen Schwellenwert erreicht und benötigt nur noch eine manuelle Zustimmung, um mit deinen Freunden geteilt zu werden.'; + } + + @override + String get userDiscoveryManualApprovalHideContact => 'Kontakt verbergen'; + + @override + String get userDiscoveryManualApprovalShareContact => 'Kontakt teilen'; } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 1d44059e..eca519ff 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -1656,4 +1656,15 @@ class AppLocalizationsEn extends AppLocalizations { @override String get userDiscoveryEnabledStopSharing => 'Stop sharing'; + + @override + String userDiscoveryManualApprovalReachedThreshold(Object username) { + return '$username has reached your threshold and now needs your manual approval to be shared with your friends.'; + } + + @override + String get userDiscoveryManualApprovalHideContact => 'Hide contact'; + + @override + String get userDiscoveryManualApprovalShareContact => 'Share contact'; } diff --git a/lib/src/localization/translations b/lib/src/localization/translations index dc33fa1b..492bef11 160000 --- a/lib/src/localization/translations +++ b/lib/src/localization/translations @@ -1 +1 @@ -Subproject commit dc33fa1bb1e0b9f6d15a0ee36ed37a7f0063f2a0 +Subproject commit 492bef11cf6bd472a71bd1c1b3007d731adea433 diff --git a/lib/src/services/api.service.dart b/lib/src/services/api.service.dart index d7a0e7be..85d7d049 100644 --- a/lib/src/services/api.service.dart +++ b/lib/src/services/api.service.dart @@ -24,6 +24,7 @@ import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart' as server; import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pbserver.dart'; +import 'package:twonly/src/services/api/client2client/user_discovery.c2c.dart'; import 'package:twonly/src/services/api/mediafiles/download.api.dart'; import 'package:twonly/src/services/api/mediafiles/upload.api.dart'; import 'package:twonly/src/services/api/messages.api.dart'; @@ -123,6 +124,7 @@ class ApiService { unawaited(setupNotificationWithUsers()); unawaited(signalHandleNewServerConnection()); resetResyncedUsers(); + resetUserDiscoveryRequestUpdates(); unawaited(fetchGroupStatesForUnjoinedGroups()); unawaited(fetchMissingGroupPublicKey()); unawaited(checkForDeletedUsernames()); diff --git a/lib/src/services/api/client2client/contact.c2c.dart b/lib/src/services/api/client2client/contact.c2c.dart index 6817687f..d074f2eb 100644 --- a/lib/src/services/api/client2client/contact.c2c.dart +++ b/lib/src/services/api/client2client/contact.c2c.dart @@ -185,7 +185,6 @@ Future checkForProfileUpdate( .getSingleOrNull(); if (contact != null) { if (contact.senderProfileCounter < senderProfileCounter) { - Log.info('${contact.senderProfileCounter} < $senderProfileCounter'); await sendCipherText( fromUserId, EncryptedContent( diff --git a/lib/src/services/api/client2client/user_discovery.c2c.dart b/lib/src/services/api/client2client/user_discovery.c2c.dart index 0a081db8..b206109d 100644 --- a/lib/src/services/api/client2client/user_discovery.c2c.dart +++ b/lib/src/services/api/client2client/user_discovery.c2c.dart @@ -8,6 +8,10 @@ import 'package:twonly/src/utils/log.dart'; final _requestedUpdates = {}; +void resetUserDiscoveryRequestUpdates() { + _requestedUpdates.clear(); +} + Future checkForUserDiscoveryChanges( int fromUserId, List receivedVersion, @@ -19,7 +23,7 @@ Future checkForUserDiscoveryChanges( if (currentVersion != null) { if (_requestedUpdates.contains(fromUserId)) { - /// Only request a new version once per app session + // Only request a new version once per app session return; } Log.info('Having old version from contact. Requesting new version.'); @@ -46,12 +50,10 @@ Future handleUserDiscoveryRequest( return; } final contact = await twonlyDB.contactsDao.getContactById(fromUserId); - if (contact == null) return; - if (contact.mediaSendCounter < userService.currentUser.requiredSendImages || - contact.userDiscoveryExcluded) { + if (!UserDiscoveryService.isContactAllowed(contact)) { Log.warn( - 'Got a request to update user discovery, but mediaSendCounter (${contact.mediaSendCounter}) < ${userService.currentUser.requiredSendImages} or user is excluded ${contact.userDiscoveryExcluded}', + 'Got a request to update user discovery, but mediaSendCounter (${contact?.mediaSendCounter}) < ${userService.currentUser.requiredSendImages} or user is excluded ${contact?.userDiscoveryExcluded}', ); return; } diff --git a/lib/src/services/api/messages.api.dart b/lib/src/services/api/messages.api.dart index 9e33cfcd..22e7534a 100644 --- a/lib/src/services/api/messages.api.dart +++ b/lib/src/services/api/messages.api.dart @@ -352,10 +352,7 @@ Future<(Uint8List, Uint8List?)?> sendCipherText( if (userService.currentUser.isUserDiscoveryEnabled && messageId != null) { final contact = await twonlyDB.contactsDao.getContactById(contactId); - if (contact != null && - contact.mediaSendCounter >= - userService.currentUser.requiredSendImages && - !contact.userDiscoveryExcluded) { + if (UserDiscoveryService.isContactAllowed(contact)) { final version = await UserDiscoveryService.getCurrentVersion(); if (version != null) { encryptedContent.senderUserDiscoveryVersion = version; diff --git a/lib/src/services/user_discovery.service.dart b/lib/src/services/user_discovery.service.dart index 297671fd..03535313 100644 --- a/lib/src/services/user_discovery.service.dart +++ b/lib/src/services/user_discovery.service.dart @@ -46,6 +46,31 @@ class UserDiscoveryService { } } + static bool isContactAllowed(Contact? c) { + if (c == null) return false; + final u = userService.currentUser; + // Only accepted users are allowed. + if (!c.accepted || c.blocked) return false; + if (c.mediaSendCounter < u.requiredSendImages) return false; + if (c.userDiscoveryExcluded) return false; + if (u.userDiscoveryRequiresManualApproval && + (c.userDiscoveryManualApproved == null || + !c.userDiscoveryManualApproved!)) { + return false; + } + return true; + } + + static bool shouldRequestManualApproval(Contact c) { + final u = userService.currentUser; + if (!u.isUserDiscoveryEnabled) return false; + if (c.mediaSendCounter < u.requiredSendImages) return false; + if (c.userDiscoveryExcluded) return false; + if (!u.userDiscoveryRequiresManualApproval) return false; + if (c.userDiscoveryManualApproved == true) return false; + return true; + } + static Future initializeOrUpdate({ required int threshold, required bool sharePromotion, diff --git a/lib/src/services/user_study.service.dart b/lib/src/services/user_study.service.dart index b1192098..36d0cce8 100644 --- a/lib/src/services/user_study.service.dart +++ b/lib/src/services/user_study.service.dart @@ -69,6 +69,10 @@ Future handleUserStudyUpload() async { userService.currentUser.requiredSendImages, 'user_discovery_threshold': userService.currentUser.userDiscoveryThreshold, + 'user_discovery_requires_manual_approval': + userService.currentUser.userDiscoveryRequiresManualApproval, + 'user_discovery_share_promotion': + userService.currentUser.userDiscoverySharePromotion, 'user_discovery_count_friends_shared': udFriendsShared.length, diff --git a/lib/src/visual/views/chats/chat_messages_components/message_input.dart b/lib/src/visual/views/chats/chat_messages_components/message_input.dart index 1bb63f47..d00143fa 100644 --- a/lib/src/visual/views/chats/chat_messages_components/message_input.dart +++ b/lib/src/visual/views/chats/chat_messages_components/message_input.dart @@ -18,6 +18,7 @@ import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/views/camera/camera_send_to.view.dart'; import 'package:twonly/src/visual/views/chats/chat_messages_components/bottom_sheets/share_additional.bottom_sheet.dart'; import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/chat_audio_entry.dart'; +import 'package:twonly/src/visual/views/chats/chat_messages_components/user_discovery_manual_approval.comp.dart'; class MessageInput extends StatefulWidget { const MessageInput({ @@ -198,6 +199,7 @@ class _MessageInputState extends State { Widget build(BuildContext context) { return Column( children: [ + UserDiscoveryManualApprovalComp(group: widget.group), Padding( padding: const EdgeInsets.only( bottom: 10, diff --git a/lib/src/visual/views/chats/chat_messages_components/user_discovery_manual_approval.comp.dart b/lib/src/visual/views/chats/chat_messages_components/user_discovery_manual_approval.comp.dart new file mode 100644 index 00000000..46a0f46e --- /dev/null +++ b/lib/src/visual/views/chats/chat_messages_components/user_discovery_manual_approval.comp.dart @@ -0,0 +1,112 @@ +import 'package:drift/drift.dart' show Value; +import 'package:flutter/material.dart'; +import 'package:twonly/locator.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/services/user_discovery.service.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/components/avatar_icon.comp.dart'; + +class UserDiscoveryManualApprovalComp extends StatefulWidget { + const UserDiscoveryManualApprovalComp({required this.group, super.key}); + + final Group group; + + @override + State createState() => + _UserDiscoveryManualApprovalCompState(); +} + +class _UserDiscoveryManualApprovalCompState + extends State { + @override + Widget build(BuildContext context) { + return StreamBuilder>( + stream: twonlyDB.groupsDao.watchGroupMembers(widget.group.groupId), + builder: (context, snapshot) { + if (!snapshot.hasData || snapshot.data!.isEmpty) { + return const SizedBox.shrink(); + } + + final contactsToApprove = snapshot.data! + .map((e) => e.$1) + .where(UserDiscoveryService.shouldRequestManualApproval) + .toList(); + + if (contactsToApprove.isEmpty) { + return const SizedBox.shrink(); + } + + return Column( + children: contactsToApprove.map((contact) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(16), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + AvatarIcon(contactId: contact.userId, fontSize: 18), + const SizedBox(width: 12), + Expanded( + child: Text( + context.lang + .userDiscoveryManualApprovalReachedThreshold( + getContactDisplayName(contact), + ), + style: TextStyle( + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + fontSize: 14, + ), + ), + ), + ], + ), + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () async { + await twonlyDB.contactsDao.updateContact( + contact.userId, + const ContactsCompanion( + userDiscoveryExcluded: Value(true), + ), + ); + }, + child: Text( + context.lang.userDiscoveryManualApprovalHideContact, + ), + ), + FilledButton( + onPressed: () async { + await twonlyDB.contactsDao.updateContact( + contact.userId, + const ContactsCompanion( + userDiscoveryManualApproved: Value(true), + ), + ); + }, + child: Text( + context.lang.userDiscoveryManualApprovalShareContact, + ), + ), + ], + ), + ], + ), + ); + }).toList(), + ); + }, + ); + } +} diff --git a/lib/src/visual/views/contact/add_new_contact_components/open_requests_list.comp.dart b/lib/src/visual/views/contact/add_new_contact_components/open_requests_list.comp.dart index 0d7ecf93..53a3a3c0 100644 --- a/lib/src/visual/views/contact/add_new_contact_components/open_requests_list.comp.dart +++ b/lib/src/visual/views/contact/add_new_contact_components/open_requests_list.comp.dart @@ -6,9 +6,9 @@ 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.api.dart'; -import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/components/avatar_icon.comp.dart'; +import 'package:twonly/src/visual/components/verification_badge.comp.dart'; import 'package:twonly/src/visual/elements/headline.element.dart'; import 'package:twonly/src/visual/themes/light.dart'; import 'package:twonly/src/visual/views/contact/add_new_contact_components/friend_suggestions.comp.dart'; @@ -150,9 +150,6 @@ class OpenRequestsListComp extends StatelessWidget { ), ...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( @@ -165,11 +162,16 @@ class OpenRequestsListComp extends StatelessWidget { break; } } - return ListTile( key: ValueKey(contact.userId), contentPadding: EdgeInsets.zero, - title: Text(substringBy(contact.username, 25)), + title: Row( + children: [ + Text(substringBy(contact.username, 25)), + const SizedBox(width: 3), + VerificationBadgeComp(contact: contact), + ], + ), subtitle: subtitle, leading: AvatarIcon( contactId: contact.userId, diff --git a/lib/src/visual/views/contact/contact.view.dart b/lib/src/visual/views/contact/contact.view.dart index 1a2353c7..99e34e24 100644 --- a/lib/src/visual/views/contact/contact.view.dart +++ b/lib/src/visual/views/contact/contact.view.dart @@ -306,35 +306,58 @@ class _ContactViewState extends State { ], ), if (userService.currentUser.isUserDiscoveryEnabled) - BetterListTile( - icon: FontAwesomeIcons.usersViewfinder, - text: context.lang.userDiscoverySettingsTitle, - subtitle: - !contact.userDiscoveryExcluded && - contact.mediaSendCounter < - userService.currentUser.requiredSendImages - ? Text( - context.lang.contactUserDiscoveryImagesLeft( - userService.currentUser.requiredSendImages - - 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 UserDiscoveryService.changeExclusionForContact( + if (userService.currentUser.userDiscoveryRequiresManualApproval && + contact.userDiscoveryManualApproved != true) + BetterListTile( + icon: FontAwesomeIcons.usersViewfinder, + text: context.lang.userDiscoverySettingsTitle, + subtitle: const Text( + 'Contact was not yet manual approved.', + style: TextStyle(fontSize: 10), + ), + trailing: TextButton( + onPressed: () async { + await twonlyDB.contactsDao.updateContact( contact.userId, - !a, + const ContactsCompanion( + userDiscoveryManualApproved: Value(true), + ), ); }, + child: const Text('Approve'), + ), + ) + else + BetterListTile( + icon: FontAwesomeIcons.usersViewfinder, + text: context.lang.userDiscoverySettingsTitle, + subtitle: + !contact.userDiscoveryExcluded && + contact.mediaSendCounter < + userService.currentUser.requiredSendImages + ? Text( + context.lang.contactUserDiscoveryImagesLeft( + userService.currentUser.requiredSendImages - + 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 UserDiscoveryService.changeExclusionForContact( + contact.userId, + !a, + ); + }, + ), ), ), - ), + BetterListTile( icon: FontAwesomeIcons.flag, text: context.lang.reportUser, diff --git a/rust_dependencies/protocols/src/user_discovery.rs b/rust_dependencies/protocols/src/user_discovery.rs index 47170c3b..a7df228c 100644 --- a/rust_dependencies/protocols/src/user_discovery.rs +++ b/rust_dependencies/protocols/src/user_discovery.rs @@ -566,15 +566,33 @@ impl UserDiscovery Err(UserDiscoveryError::ShamirsSecret(err.to_string())), diff --git a/test/drift/twonly_db/generated/schema_v12.dart b/test/drift/twonly_db/generated/schema_v12.dart index 9a646d65..d314edab 100644 --- a/test/drift/twonly_db/generated/schema_v12.dart +++ b/test/drift/twonly_db/generated/schema_v12.dart @@ -145,17 +145,17 @@ class Contacts extends Table with TableInfo { 'NOT NULL DEFAULT 0 CHECK (user_discovery_excluded IN (0, 1))', defaultValue: const CustomExpression('0'), ); - late final GeneratedColumn - userDiscoveryManualApproved = GeneratedColumn( - 'user_discovery_manual_approved', - aliasedName, - false, - type: DriftSqlType.int, - requiredDuringInsert: false, - $customConstraints: - 'NOT NULL DEFAULT 0 CHECK (user_discovery_manual_approved IN (0, 1))', - defaultValue: const CustomExpression('0'), - ); + late final GeneratedColumn userDiscoveryManualApproved = + GeneratedColumn( + 'user_discovery_manual_approved', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NULL DEFAULT 0 CHECK (user_discovery_manual_approved IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); late final GeneratedColumn mediaSendCounter = GeneratedColumn( 'media_send_counter', aliasedName, @@ -269,7 +269,7 @@ class Contacts extends Table with TableInfo { userDiscoveryManualApproved: attachedDatabase.typeMapping.read( DriftSqlType.int, data['${effectivePrefix}user_discovery_manual_approved'], - )!, + ), mediaSendCounter: attachedDatabase.typeMapping.read( DriftSqlType.int, data['${effectivePrefix}media_send_counter'], @@ -308,7 +308,7 @@ class ContactsData extends DataClass implements Insertable { final int createdAt; final i2.Uint8List? userDiscoveryVersion; final int userDiscoveryExcluded; - final int userDiscoveryManualApproved; + final int? userDiscoveryManualApproved; final int mediaSendCounter; final int mediaReceivedCounter; const ContactsData({ @@ -327,7 +327,7 @@ class ContactsData extends DataClass implements Insertable { required this.createdAt, this.userDiscoveryVersion, required this.userDiscoveryExcluded, - required this.userDiscoveryManualApproved, + this.userDiscoveryManualApproved, required this.mediaSendCounter, required this.mediaReceivedCounter, }); @@ -361,9 +361,11 @@ class ContactsData extends DataClass implements Insertable { ); } map['user_discovery_excluded'] = Variable(userDiscoveryExcluded); - map['user_discovery_manual_approved'] = Variable( - userDiscoveryManualApproved, - ); + if (!nullToAbsent || userDiscoveryManualApproved != null) { + map['user_discovery_manual_approved'] = Variable( + userDiscoveryManualApproved, + ); + } map['media_send_counter'] = Variable(mediaSendCounter); map['media_received_counter'] = Variable(mediaReceivedCounter); return map; @@ -394,7 +396,10 @@ class ContactsData extends DataClass implements Insertable { ? const Value.absent() : Value(userDiscoveryVersion), userDiscoveryExcluded: Value(userDiscoveryExcluded), - userDiscoveryManualApproved: Value(userDiscoveryManualApproved), + userDiscoveryManualApproved: + userDiscoveryManualApproved == null && nullToAbsent + ? const Value.absent() + : Value(userDiscoveryManualApproved), mediaSendCounter: Value(mediaSendCounter), mediaReceivedCounter: Value(mediaReceivedCounter), ); @@ -429,7 +434,7 @@ class ContactsData extends DataClass implements Insertable { userDiscoveryExcluded: serializer.fromJson( json['userDiscoveryExcluded'], ), - userDiscoveryManualApproved: serializer.fromJson( + userDiscoveryManualApproved: serializer.fromJson( json['userDiscoveryManualApproved'], ), mediaSendCounter: serializer.fromJson(json['mediaSendCounter']), @@ -461,7 +466,7 @@ class ContactsData extends DataClass implements Insertable { userDiscoveryVersion, ), 'userDiscoveryExcluded': serializer.toJson(userDiscoveryExcluded), - 'userDiscoveryManualApproved': serializer.toJson( + 'userDiscoveryManualApproved': serializer.toJson( userDiscoveryManualApproved, ), 'mediaSendCounter': serializer.toJson(mediaSendCounter), @@ -485,7 +490,7 @@ class ContactsData extends DataClass implements Insertable { int? createdAt, Value userDiscoveryVersion = const Value.absent(), int? userDiscoveryExcluded, - int? userDiscoveryManualApproved, + Value userDiscoveryManualApproved = const Value.absent(), int? mediaSendCounter, int? mediaReceivedCounter, }) => ContactsData( @@ -508,8 +513,9 @@ class ContactsData extends DataClass implements Insertable { ? userDiscoveryVersion.value : this.userDiscoveryVersion, userDiscoveryExcluded: userDiscoveryExcluded ?? this.userDiscoveryExcluded, - userDiscoveryManualApproved: - userDiscoveryManualApproved ?? this.userDiscoveryManualApproved, + userDiscoveryManualApproved: userDiscoveryManualApproved.present + ? userDiscoveryManualApproved.value + : this.userDiscoveryManualApproved, mediaSendCounter: mediaSendCounter ?? this.mediaSendCounter, mediaReceivedCounter: mediaReceivedCounter ?? this.mediaReceivedCounter, ); @@ -649,7 +655,7 @@ class ContactsCompanion extends UpdateCompanion { final Value createdAt; final Value userDiscoveryVersion; final Value userDiscoveryExcluded; - final Value userDiscoveryManualApproved; + final Value userDiscoveryManualApproved; final Value mediaSendCounter; final Value mediaReceivedCounter; const ContactsCompanion({ @@ -756,7 +762,7 @@ class ContactsCompanion extends UpdateCompanion { Value? createdAt, Value? userDiscoveryVersion, Value? userDiscoveryExcluded, - Value? userDiscoveryManualApproved, + Value? userDiscoveryManualApproved, Value? mediaSendCounter, Value? mediaReceivedCounter, }) {