implement manual approval and fix bug

This commit is contained in:
otsmr 2026-04-29 12:11:07 +02:00
parent f0741bfdc1
commit c54265495c
22 changed files with 356 additions and 110 deletions

View file

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

View file

@ -135,27 +135,35 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
}
Stream<List<Contact>> watchContactsAnnouncedViaUserDiscovery() {
return (select(contacts)..where(
(t) =>
t.userDiscoveryVersion.isNotNull() &
return (select(contacts)..where((t) {
var expr = t.userDiscoveryVersion.isNotNull() &
t.userDiscoveryExcluded.equals(false) &
t.mediaSendCounter.isBiggerOrEqualValue(
userService.currentUser.requiredSendImages,
),
))
.watch();
);
if (userService.currentUser.userDiscoveryRequiresManualApproval) {
expr = expr & t.userDiscoveryManualApproved.equals(true);
}
return expr;
})).watch();
}
Future<List<Contact>> getContactsAnnouncedViaUserDiscovery() async {
return (select(contacts)..where(
(t) =>
t.userDiscoveryVersion.isNotNull() &
return (select(contacts)..where((t) {
var expr = t.userDiscoveryVersion.isNotNull() &
t.userDiscoveryExcluded.equals(false) &
t.mediaSendCounter.isBiggerOrEqualValue(
userService.currentUser.requiredSendImages,
),
))
.get();
);
if (userService.currentUser.userDiscoveryRequiresManualApproval) {
expr = expr & t.userDiscoveryManualApproved.equals(true);
}
return expr;
})).get();
}
Stream<List<Contact>> watchAllContacts() {

View file

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

View file

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

View file

@ -207,7 +207,7 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> {
GeneratedColumn<bool>(
'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<Contact> {
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<Contact> {
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<Contact> {
map['user_discovery_version'] = Variable<Uint8List>(userDiscoveryVersion);
}
map['user_discovery_excluded'] = Variable<bool>(userDiscoveryExcluded);
if (!nullToAbsent || userDiscoveryManualApproved != null) {
map['user_discovery_manual_approved'] = Variable<bool>(
userDiscoveryManualApproved,
);
}
map['media_send_counter'] = Variable<int>(mediaSendCounter);
map['media_received_counter'] = Variable<int>(mediaReceivedCounter);
return map;
@ -599,7 +601,10 @@ class Contact extends DataClass implements Insertable<Contact> {
? 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<Contact> {
userDiscoveryExcluded: serializer.fromJson<bool>(
json['userDiscoveryExcluded'],
),
userDiscoveryManualApproved: serializer.fromJson<bool>(
userDiscoveryManualApproved: serializer.fromJson<bool?>(
json['userDiscoveryManualApproved'],
),
mediaSendCounter: serializer.fromJson<int>(json['mediaSendCounter']),
@ -664,7 +669,7 @@ class Contact extends DataClass implements Insertable<Contact> {
userDiscoveryVersion,
),
'userDiscoveryExcluded': serializer.toJson<bool>(userDiscoveryExcluded),
'userDiscoveryManualApproved': serializer.toJson<bool>(
'userDiscoveryManualApproved': serializer.toJson<bool?>(
userDiscoveryManualApproved,
),
'mediaSendCounter': serializer.toJson<int>(mediaSendCounter),
@ -688,7 +693,7 @@ class Contact extends DataClass implements Insertable<Contact> {
DateTime? createdAt,
Value<Uint8List?> userDiscoveryVersion = const Value.absent(),
bool? userDiscoveryExcluded,
bool? userDiscoveryManualApproved,
Value<bool?> userDiscoveryManualApproved = const Value.absent(),
int? mediaSendCounter,
int? mediaReceivedCounter,
}) => Contact(
@ -711,8 +716,9 @@ class Contact extends DataClass implements Insertable<Contact> {
? 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<Contact> {
final Value<DateTime> createdAt;
final Value<Uint8List?> userDiscoveryVersion;
final Value<bool> userDiscoveryExcluded;
final Value<bool> userDiscoveryManualApproved;
final Value<bool?> userDiscoveryManualApproved;
final Value<int> mediaSendCounter;
final Value<int> mediaReceivedCounter;
const ContactsCompanion({
@ -959,7 +965,7 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
Value<DateTime>? createdAt,
Value<Uint8List?>? userDiscoveryVersion,
Value<bool>? userDiscoveryExcluded,
Value<bool>? userDiscoveryManualApproved,
Value<bool?>? userDiscoveryManualApproved,
Value<int>? mediaSendCounter,
Value<int>? mediaReceivedCounter,
}) {
@ -11687,7 +11693,7 @@ typedef $$ContactsTableCreateCompanionBuilder =
Value<DateTime> createdAt,
Value<Uint8List?> userDiscoveryVersion,
Value<bool> userDiscoveryExcluded,
Value<bool> userDiscoveryManualApproved,
Value<bool?> userDiscoveryManualApproved,
Value<int> mediaSendCounter,
Value<int> mediaReceivedCounter,
});
@ -11708,7 +11714,7 @@ typedef $$ContactsTableUpdateCompanionBuilder =
Value<DateTime> createdAt,
Value<Uint8List?> userDiscoveryVersion,
Value<bool> userDiscoveryExcluded,
Value<bool> userDiscoveryManualApproved,
Value<bool?> userDiscoveryManualApproved,
Value<int> mediaSendCounter,
Value<int> mediaReceivedCounter,
});
@ -12967,7 +12973,7 @@ class $$ContactsTableTableManager
Value<DateTime> createdAt = const Value.absent(),
Value<Uint8List?> userDiscoveryVersion = const Value.absent(),
Value<bool> userDiscoveryExcluded = const Value.absent(),
Value<bool> userDiscoveryManualApproved = const Value.absent(),
Value<bool?> userDiscoveryManualApproved = const Value.absent(),
Value<int> mediaSendCounter = const Value.absent(),
Value<int> mediaReceivedCounter = const Value.absent(),
}) => ContactsCompanion(
@ -13007,7 +13013,7 @@ class $$ContactsTableTableManager
Value<DateTime> createdAt = const Value.absent(),
Value<Uint8List?> userDiscoveryVersion = const Value.absent(),
Value<bool> userDiscoveryExcluded = const Value.absent(),
Value<bool> userDiscoveryManualApproved = const Value.absent(),
Value<bool?> userDiscoveryManualApproved = const Value.absent(),
Value<int> mediaSendCounter = const Value.absent(),
Value<int> mediaReceivedCounter = const Value.absent(),
}) => ContactsCompanion.insert(

View file

@ -6294,10 +6294,10 @@ i1.GeneratedColumn<int> _column_213(String aliasedName) =>
i1.GeneratedColumn<int>(
'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<int> _column_214(String aliasedName) =>

View file

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

View file

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

View file

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

@ -1 +1 @@
Subproject commit dc33fa1bb1e0b9f6d15a0ee36ed37a7f0063f2a0
Subproject commit 492bef11cf6bd472a71bd1c1b3007d731adea433

View file

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

View file

@ -185,7 +185,6 @@ Future<int?> checkForProfileUpdate(
.getSingleOrNull();
if (contact != null) {
if (contact.senderProfileCounter < senderProfileCounter) {
Log.info('${contact.senderProfileCounter} < $senderProfileCounter');
await sendCipherText(
fromUserId,
EncryptedContent(

View file

@ -8,6 +8,10 @@ import 'package:twonly/src/utils/log.dart';
final _requestedUpdates = <int>{};
void resetUserDiscoveryRequestUpdates() {
_requestedUpdates.clear();
}
Future<void> checkForUserDiscoveryChanges(
int fromUserId,
List<int> receivedVersion,
@ -19,7 +23,7 @@ Future<void> 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<void> 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;
}

View file

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

View file

@ -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<void> initializeOrUpdate({
required int threshold,
required bool sharePromotion,

View file

@ -69,6 +69,10 @@ Future<void> 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,

View file

@ -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<MessageInput> {
Widget build(BuildContext context) {
return Column(
children: [
UserDiscoveryManualApprovalComp(group: widget.group),
Padding(
padding: const EdgeInsets.only(
bottom: 10,

View file

@ -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<UserDiscoveryManualApprovalComp> createState() =>
_UserDiscoveryManualApprovalCompState();
}
class _UserDiscoveryManualApprovalCompState
extends State<UserDiscoveryManualApprovalComp> {
@override
Widget build(BuildContext context) {
return StreamBuilder<List<(Contact, GroupMember)>>(
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(),
);
},
);
}
}

View file

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

View file

@ -306,6 +306,28 @@ class _ContactViewState extends State<ContactView> {
],
),
if (userService.currentUser.isUserDiscoveryEnabled)
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,
const ContactsCompanion(
userDiscoveryManualApproved: Value(true),
),
);
},
child: const Text('Approve'),
),
)
else
BetterListTile(
icon: FontAwesomeIcons.usersViewfinder,
text: context.lang.userDiscoverySettingsTitle,
@ -335,6 +357,7 @@ class _ContactViewState extends State<ContactView> {
),
),
),
BetterListTile(
icon: FontAwesomeIcons.flag,
text: context.lang.reportUser,

View file

@ -566,15 +566,33 @@ impl<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store,
contact_id,
announced_user.user_id
);
// User is known, so add him to thr users relations
self.store
.push_new_user_relation(
contact_id,
announced_user,
announced_user.clone(),
public_key_verified_timestamp,
)
.await?;
// As we no now the public_id from the user, all promotions up to this point are also known, so add these to the relations database as well
let promotions = self
.store
.get_other_promotions_by_public_id(uda.public_id)
.await?;
for promotion in promotions {
self.store
.push_new_user_relation(
promotion.from_contact_id,
announced_user,
promotion.public_key_verified_timestamp,
)
.await?;
return Ok(());
}
Ok(())
}
Err(err) => Err(UserDiscoveryError::ShamirsSecret(err.to_string())),

View file

@ -145,15 +145,15 @@ class Contacts extends Table with TableInfo<Contacts, ContactsData> {
'NOT NULL DEFAULT 0 CHECK (user_discovery_excluded IN (0, 1))',
defaultValue: const CustomExpression('0'),
);
late final GeneratedColumn<int>
userDiscoveryManualApproved = GeneratedColumn<int>(
late final GeneratedColumn<int> userDiscoveryManualApproved =
GeneratedColumn<int>(
'user_discovery_manual_approved',
aliasedName,
false,
true,
type: DriftSqlType.int,
requiredDuringInsert: false,
$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 CustomExpression('0'),
);
late final GeneratedColumn<int> mediaSendCounter = GeneratedColumn<int>(
@ -269,7 +269,7 @@ class Contacts extends Table with TableInfo<Contacts, ContactsData> {
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<ContactsData> {
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<ContactsData> {
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<ContactsData> {
);
}
map['user_discovery_excluded'] = Variable<int>(userDiscoveryExcluded);
if (!nullToAbsent || userDiscoveryManualApproved != null) {
map['user_discovery_manual_approved'] = Variable<int>(
userDiscoveryManualApproved,
);
}
map['media_send_counter'] = Variable<int>(mediaSendCounter);
map['media_received_counter'] = Variable<int>(mediaReceivedCounter);
return map;
@ -394,7 +396,10 @@ class ContactsData extends DataClass implements Insertable<ContactsData> {
? 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<ContactsData> {
userDiscoveryExcluded: serializer.fromJson<int>(
json['userDiscoveryExcluded'],
),
userDiscoveryManualApproved: serializer.fromJson<int>(
userDiscoveryManualApproved: serializer.fromJson<int?>(
json['userDiscoveryManualApproved'],
),
mediaSendCounter: serializer.fromJson<int>(json['mediaSendCounter']),
@ -461,7 +466,7 @@ class ContactsData extends DataClass implements Insertable<ContactsData> {
userDiscoveryVersion,
),
'userDiscoveryExcluded': serializer.toJson<int>(userDiscoveryExcluded),
'userDiscoveryManualApproved': serializer.toJson<int>(
'userDiscoveryManualApproved': serializer.toJson<int?>(
userDiscoveryManualApproved,
),
'mediaSendCounter': serializer.toJson<int>(mediaSendCounter),
@ -485,7 +490,7 @@ class ContactsData extends DataClass implements Insertable<ContactsData> {
int? createdAt,
Value<i2.Uint8List?> userDiscoveryVersion = const Value.absent(),
int? userDiscoveryExcluded,
int? userDiscoveryManualApproved,
Value<int?> userDiscoveryManualApproved = const Value.absent(),
int? mediaSendCounter,
int? mediaReceivedCounter,
}) => ContactsData(
@ -508,8 +513,9 @@ class ContactsData extends DataClass implements Insertable<ContactsData> {
? 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<ContactsData> {
final Value<int> createdAt;
final Value<i2.Uint8List?> userDiscoveryVersion;
final Value<int> userDiscoveryExcluded;
final Value<int> userDiscoveryManualApproved;
final Value<int?> userDiscoveryManualApproved;
final Value<int> mediaSendCounter;
final Value<int> mediaReceivedCounter;
const ContactsCompanion({
@ -756,7 +762,7 @@ class ContactsCompanion extends UpdateCompanion<ContactsData> {
Value<int>? createdAt,
Value<i2.Uint8List?>? userDiscoveryVersion,
Value<int>? userDiscoveryExcluded,
Value<int>? userDiscoveryManualApproved,
Value<int?>? userDiscoveryManualApproved,
Value<int>? mediaSendCounter,
Value<int>? mediaReceivedCounter,
}) {