diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index bf320c8..c1352ee 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -65,6 +65,7 @@ "chatListViewSendFirstTwonly": "Sende dein erstes twonly!", "chatListDetailInput": "Nachricht eingeben", "userDeletedAccount": "Der Nutzer hat sein Konto gelöscht.", + "contextMenuUserProfile": "Userprofil", "contextMenuVerifyUser": "Verifizieren", "@contextMenuVerifyUser": {}, "contextMenuArchiveUser": "Archivieren", diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index 4100040..88167ac 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -116,6 +116,7 @@ "chatListDetailInput": "Type a message", "@chatListDetailInput": {}, "userDeletedAccount": "The user has deleted its account.", + "contextMenuUserProfile": "User profile", "contextMenuVerifyUser": "Verify", "@contextMenuVerifyUser": {}, "contextMenuArchiveUser": "Archive", diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index 8685b2e..f6ec53e 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -452,6 +452,12 @@ abstract class AppLocalizations { /// **'The user has deleted its account.'** String get userDeletedAccount; + /// No description provided for @contextMenuUserProfile. + /// + /// In en, this message translates to: + /// **'User profile'** + String get contextMenuUserProfile; + /// No description provided for @contextMenuVerifyUser. /// /// In en, this message translates to: diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 2b3ed4e..d55e9b4 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -206,6 +206,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get userDeletedAccount => 'Der Nutzer hat sein Konto gelöscht.'; + @override + String get contextMenuUserProfile => 'Userprofil'; + @override String get contextMenuVerifyUser => 'Verifizieren'; diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index c6c322e..46fa612 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -204,6 +204,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get userDeletedAccount => 'The user has deleted its account.'; + @override + String get contextMenuUserProfile => 'User profile'; + @override String get contextMenuVerifyUser => 'Verify'; diff --git a/lib/src/services/api/media_send.dart b/lib/src/services/api/media_send.dart index 9b46eac..e5abd52 100644 --- a/lib/src/services/api/media_send.dart +++ b/lib/src/services/api/media_send.dart @@ -94,9 +94,11 @@ Future checkForFailedUploads() async { mediaUploadIds.add(message.mediaUploadId!); } } - Log.error( - "Got ${messages.length} messages (${mediaUploadIds.length} media upload files) that are not correctly uploaded. Trying from scratch again.", - ); + if (messages.isNotEmpty) { + 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 } diff --git a/lib/src/services/api/server_messages.dart b/lib/src/services/api/server_messages.dart index b1f5690..bbc994e 100644 --- a/lib/src/services/api/server_messages.dart +++ b/lib/src/services/api/server_messages.dart @@ -54,7 +54,6 @@ Future handleServerMessage(server.ServerToClient msg) async { Future handleNewMessage(int fromUserId, Uint8List body) async { MessageJson? message = await signalDecryptMessage(fromUserId, body); if (message == null) { - Log.info("Got invalid cipher text from $fromUserId. Deleting it."); // Message is not valid, so server can delete it var ok = client.Response_Ok()..none = true; return client.Response()..ok = ok; diff --git a/lib/src/services/signal/encryption.signal.dart b/lib/src/services/signal/encryption.signal.dart index 5982e66..a5238f0 100644 --- a/lib/src/services/signal/encryption.signal.dart +++ b/lib/src/services/signal/encryption.signal.dart @@ -116,6 +116,10 @@ Future signalDecryptMessage(int source, Uint8List msg) async { } return MessageJson.fromJson( 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) { Log.error(e.toString()); return null; diff --git a/lib/src/views/components/user_context_menu.dart b/lib/src/views/components/user_context_menu.dart index 4ef3cfc..d581c84 100644 --- a/lib/src/views/components/user_context_menu.dart +++ b/lib/src/views/components/user_context_menu.dart @@ -6,14 +6,18 @@ import 'package:twonly/globals.dart'; import 'package:twonly/src/database/twonly_database.dart'; import 'package:twonly/src/utils/misc.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'; class UserContextMenu extends StatefulWidget { final Widget child; final Contact contact; - const UserContextMenu( - {super.key, required this.contact, required this.child}); + const UserContextMenu({ + super.key, + required this.contact, + required this.child, + }); @override State createState() => _UserContextMenuState(); @@ -96,6 +100,67 @@ class _UserContextMenuState extends State { } } +class UserContextMenuBlocked extends StatefulWidget { + final Widget child; + final Contact contact; + + const UserContextMenuBlocked({ + super.key, + required this.contact, + required this.child, + }); + + @override + State createState() => _UserContextMenuBlocked(); +} + +class _UserContextMenuBlocked extends State { + @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) { return PieTheme( brightness: Theme.of(context).brightness, diff --git a/lib/src/views/settings/privacy_view_block.users.dart b/lib/src/views/settings/privacy_view_block.users.dart index 688fde9..e493c64 100644 --- a/lib/src/views/settings/privacy_view_block.users.dart +++ b/lib/src/views/settings/privacy_view_block.users.dart @@ -1,10 +1,12 @@ import 'package:drift/drift.dart' hide Column; import 'package:flutter/material.dart'; +import 'package:pie_menu/pie_menu.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/views/components/initialsavatar.dart'; import 'package:twonly/src/database/daos/contacts_dao.dart'; import 'package:twonly/src/database/twonly_database.dart'; import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/components/user_context_menu.dart'; class PrivacyViewBlockUsers extends StatefulWidget { const PrivacyViewBlockUsers({super.key}); @@ -35,49 +37,52 @@ class _PrivacyViewBlockUsers extends State { appBar: AppBar( title: Text(context.lang.settingsPrivacyBlockUsers), ), - body: Padding( - padding: EdgeInsets.only(bottom: 20, left: 10, top: 20, right: 10), - child: Column( - children: [ - Padding( - padding: EdgeInsets.symmetric(horizontal: 10), - child: TextField( - onChanged: (value) => setState(() { - filter = value; - }), - decoration: getInputDecoration( - context, - context.lang.searchUsernameInput, + body: PieCanvas( + theme: getPieCanvasTheme(context), + child: Padding( + padding: EdgeInsets.only(bottom: 20, left: 10, top: 20, right: 10), + child: Column( + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 10), + child: TextField( + onChanged: (value) => setState(() { + filter = value; + }), + decoration: getInputDecoration( + context, + context.lang.searchUsernameInput, + ), ), ), - ), - const SizedBox(height: 20), - Text( - context.lang.settingsPrivacyBlockUsersDesc, - 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: 20), + Text( + context.lang.settingsPrivacyBlockUsersDesc, + 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), + ); + }, + ), + ) + ], + ), ), ), ); @@ -106,20 +111,23 @@ class UserList extends StatelessWidget { itemCount: users.length, itemBuilder: (BuildContext context, int i) { Contact user = users[i]; - return ListTile( - title: Row(children: [ - Text(getContactDisplayName(user)), - ]), - leading: ContactAvatar(contact: user, fontSize: 15), - trailing: Checkbox( - value: user.blocked, - onChanged: (bool? value) { - block(context, user.userId, value); + return UserContextMenuBlocked( + contact: user, + child: ListTile( + title: Row(children: [ + Text(getContactDisplayName(user)), + ]), + leading: ContactAvatar(contact: user, fontSize: 15), + trailing: Checkbox( + 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); - }, ); }, );