delete contact requested users and improve logging

This commit is contained in:
otsmr 2025-06-03 13:41:00 +02:00
parent b8aeb0e6ed
commit 036c4cab77
10 changed files with 150 additions and 58 deletions

View file

@ -65,6 +65,7 @@
"chatListViewSendFirstTwonly": "Sende dein erstes twonly!", "chatListViewSendFirstTwonly": "Sende dein erstes twonly!",
"chatListDetailInput": "Nachricht eingeben", "chatListDetailInput": "Nachricht eingeben",
"userDeletedAccount": "Der Nutzer hat sein Konto gelöscht.", "userDeletedAccount": "Der Nutzer hat sein Konto gelöscht.",
"contextMenuUserProfile": "Userprofil",
"contextMenuVerifyUser": "Verifizieren", "contextMenuVerifyUser": "Verifizieren",
"@contextMenuVerifyUser": {}, "@contextMenuVerifyUser": {},
"contextMenuArchiveUser": "Archivieren", "contextMenuArchiveUser": "Archivieren",

View file

@ -116,6 +116,7 @@
"chatListDetailInput": "Type a message", "chatListDetailInput": "Type a message",
"@chatListDetailInput": {}, "@chatListDetailInput": {},
"userDeletedAccount": "The user has deleted its account.", "userDeletedAccount": "The user has deleted its account.",
"contextMenuUserProfile": "User profile",
"contextMenuVerifyUser": "Verify", "contextMenuVerifyUser": "Verify",
"@contextMenuVerifyUser": {}, "@contextMenuVerifyUser": {},
"contextMenuArchiveUser": "Archive", "contextMenuArchiveUser": "Archive",

View file

@ -452,6 +452,12 @@ abstract class AppLocalizations {
/// **'The user has deleted its account.'** /// **'The user has deleted its account.'**
String get userDeletedAccount; String get userDeletedAccount;
/// No description provided for @contextMenuUserProfile.
///
/// In en, this message translates to:
/// **'User profile'**
String get contextMenuUserProfile;
/// No description provided for @contextMenuVerifyUser. /// No description provided for @contextMenuVerifyUser.
/// ///
/// In en, this message translates to: /// In en, this message translates to:

View file

@ -206,6 +206,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get userDeletedAccount => 'Der Nutzer hat sein Konto gelöscht.'; String get userDeletedAccount => 'Der Nutzer hat sein Konto gelöscht.';
@override
String get contextMenuUserProfile => 'Userprofil';
@override @override
String get contextMenuVerifyUser => 'Verifizieren'; String get contextMenuVerifyUser => 'Verifizieren';

View file

@ -204,6 +204,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get userDeletedAccount => 'The user has deleted its account.'; String get userDeletedAccount => 'The user has deleted its account.';
@override
String get contextMenuUserProfile => 'User profile';
@override @override
String get contextMenuVerifyUser => 'Verify'; String get contextMenuVerifyUser => 'Verify';

View file

@ -94,9 +94,11 @@ Future<bool> checkForFailedUploads() async {
mediaUploadIds.add(message.mediaUploadId!); mediaUploadIds.add(message.mediaUploadId!);
} }
} }
Log.error( if (messages.isNotEmpty) {
"Got ${messages.length} messages (${mediaUploadIds.length} media upload files) that are not correctly uploaded. Trying from scratch again.", Log.error(
); "Got ${messages.length} messages (${mediaUploadIds.length} media upload files) that are not correctly uploaded. Trying from scratch again.",
);
}
return mediaUploadIds.isNotEmpty; // return true if there are affected return mediaUploadIds.isNotEmpty; // return true if there are affected
} }

View file

@ -54,7 +54,6 @@ Future handleServerMessage(server.ServerToClient msg) async {
Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async { Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
MessageJson? message = await signalDecryptMessage(fromUserId, body); MessageJson? message = await signalDecryptMessage(fromUserId, body);
if (message == null) { if (message == null) {
Log.info("Got invalid cipher text from $fromUserId. Deleting it.");
// Message is not valid, so server can delete it // Message is not valid, so server can delete it
var ok = client.Response_Ok()..none = true; var ok = client.Response_Ok()..none = true;
return client.Response()..ok = ok; return client.Response()..ok = ok;

View file

@ -116,6 +116,10 @@ Future<MessageJson?> signalDecryptMessage(int source, Uint8List msg) async {
} }
return MessageJson.fromJson( return MessageJson.fromJson(
jsonDecode(utf8.decode(gzip.decode(plaintext)))); jsonDecode(utf8.decode(gzip.decode(plaintext))));
} on InvalidKeyIdException catch (_) {
return null; // got the same message again
} on DuplicateMessageException catch (_) {
return null; // to the same message again
} catch (e) { } catch (e) {
Log.error(e.toString()); Log.error(e.toString());
return null; return null;

View file

@ -6,14 +6,18 @@ import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly_database.dart'; import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/chats/chat_messages.view.dart'; import 'package:twonly/src/views/chats/chat_messages.view.dart';
import 'package:twonly/src/views/contact/contact.view.dart';
import 'package:twonly/src/views/contact/contact_verify.view.dart'; import 'package:twonly/src/views/contact/contact_verify.view.dart';
class UserContextMenu extends StatefulWidget { class UserContextMenu extends StatefulWidget {
final Widget child; final Widget child;
final Contact contact; final Contact contact;
const UserContextMenu( const UserContextMenu({
{super.key, required this.contact, required this.child}); super.key,
required this.contact,
required this.child,
});
@override @override
State<UserContextMenu> createState() => _UserContextMenuState(); State<UserContextMenu> createState() => _UserContextMenuState();
@ -96,6 +100,67 @@ class _UserContextMenuState extends State<UserContextMenu> {
} }
} }
class UserContextMenuBlocked extends StatefulWidget {
final Widget child;
final Contact contact;
const UserContextMenuBlocked({
super.key,
required this.contact,
required this.child,
});
@override
State<UserContextMenuBlocked> createState() => _UserContextMenuBlocked();
}
class _UserContextMenuBlocked extends State<UserContextMenuBlocked> {
@override
Widget build(BuildContext context) {
return PieMenu(
onPressed: () => (),
actions: [
if (!widget.contact.archived)
PieAction(
tooltip: Text(context.lang.contextMenuArchiveUser),
onSelect: () async {
final update = ContactsCompanion(archived: Value(true));
if (context.mounted) {
await twonlyDB.contactsDao
.updateContact(widget.contact.userId, update);
}
},
child: FaIcon(FontAwesomeIcons.boxArchive),
),
if (widget.contact.archived)
PieAction(
tooltip: Text(context.lang.contextMenuUndoArchiveUser),
onSelect: () async {
final update = ContactsCompanion(archived: Value(false));
if (context.mounted) {
await twonlyDB.contactsDao
.updateContact(widget.contact.userId, update);
}
},
child: FaIcon(FontAwesomeIcons.boxOpen),
),
PieAction(
tooltip: Text(context.lang.contextMenuUserProfile),
onSelect: () {
Navigator.push(context, MaterialPageRoute(
builder: (context) {
return ContactView(widget.contact.userId);
},
));
},
child: const FaIcon(FontAwesomeIcons.user),
),
],
child: widget.child,
);
}
}
PieTheme getPieCanvasTheme(BuildContext context) { PieTheme getPieCanvasTheme(BuildContext context) {
return PieTheme( return PieTheme(
brightness: Theme.of(context).brightness, brightness: Theme.of(context).brightness,

View file

@ -1,10 +1,12 @@
import 'package:drift/drift.dart' hide Column; import 'package:drift/drift.dart' hide Column;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pie_menu/pie_menu.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/views/components/initialsavatar.dart'; import 'package:twonly/src/views/components/initialsavatar.dart';
import 'package:twonly/src/database/daos/contacts_dao.dart'; import 'package:twonly/src/database/daos/contacts_dao.dart';
import 'package:twonly/src/database/twonly_database.dart'; import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/components/user_context_menu.dart';
class PrivacyViewBlockUsers extends StatefulWidget { class PrivacyViewBlockUsers extends StatefulWidget {
const PrivacyViewBlockUsers({super.key}); const PrivacyViewBlockUsers({super.key});
@ -35,49 +37,52 @@ class _PrivacyViewBlockUsers extends State<PrivacyViewBlockUsers> {
appBar: AppBar( appBar: AppBar(
title: Text(context.lang.settingsPrivacyBlockUsers), title: Text(context.lang.settingsPrivacyBlockUsers),
), ),
body: Padding( body: PieCanvas(
padding: EdgeInsets.only(bottom: 20, left: 10, top: 20, right: 10), theme: getPieCanvasTheme(context),
child: Column( child: Padding(
children: [ padding: EdgeInsets.only(bottom: 20, left: 10, top: 20, right: 10),
Padding( child: Column(
padding: EdgeInsets.symmetric(horizontal: 10), children: [
child: TextField( Padding(
onChanged: (value) => setState(() { padding: EdgeInsets.symmetric(horizontal: 10),
filter = value; child: TextField(
}), onChanged: (value) => setState(() {
decoration: getInputDecoration( filter = value;
context, }),
context.lang.searchUsernameInput, decoration: getInputDecoration(
context,
context.lang.searchUsernameInput,
),
), ),
), ),
), const SizedBox(height: 20),
const SizedBox(height: 20), Text(
Text( context.lang.settingsPrivacyBlockUsersDesc,
context.lang.settingsPrivacyBlockUsersDesc, textAlign: TextAlign.center,
textAlign: TextAlign.center,
),
const SizedBox(height: 30),
Expanded(
child: StreamBuilder(
stream: allUsers,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Container();
}
final filteredContacts = snapshot.data!.where((contact) {
return getContactDisplayName(contact)
.toLowerCase()
.contains(filter.toLowerCase());
}).toList();
return UserList(
List.from(filteredContacts),
);
},
), ),
) const SizedBox(height: 30),
], Expanded(
child: StreamBuilder(
stream: allUsers,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Container();
}
final filteredContacts = snapshot.data!.where((contact) {
return getContactDisplayName(contact)
.toLowerCase()
.contains(filter.toLowerCase());
}).toList();
return UserList(
List.from(filteredContacts),
);
},
),
)
],
),
), ),
), ),
); );
@ -106,20 +111,23 @@ class UserList extends StatelessWidget {
itemCount: users.length, itemCount: users.length,
itemBuilder: (BuildContext context, int i) { itemBuilder: (BuildContext context, int i) {
Contact user = users[i]; Contact user = users[i];
return ListTile( return UserContextMenuBlocked(
title: Row(children: [ contact: user,
Text(getContactDisplayName(user)), child: ListTile(
]), title: Row(children: [
leading: ContactAvatar(contact: user, fontSize: 15), Text(getContactDisplayName(user)),
trailing: Checkbox( ]),
value: user.blocked, leading: ContactAvatar(contact: user, fontSize: 15),
onChanged: (bool? value) { trailing: Checkbox(
block(context, user.userId, value); value: user.blocked,
onChanged: (bool? value) {
block(context, user.userId, value);
},
),
onTap: () {
block(context, user.userId, !user.blocked);
}, },
), ),
onTap: () {
block(context, user.userId, !user.blocked);
},
); );
}, },
); );