diff --git a/lib/src/database/daos/messages.dao.dart b/lib/src/database/daos/messages.dao.dart index 7425f5c..d65c947 100644 --- a/lib/src/database/daos/messages.dao.dart +++ b/lib/src/database/daos/messages.dao.dart @@ -191,13 +191,13 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { } Future handleTextEdit( - int contactId, + int? contactId, String messageId, String text, DateTime timestamp, ) async { final msg = await getMessageById(messageId).getSingleOrNull(); - if (msg == null || msg.content == null || msg.senderId == contactId) { + if (msg == null || msg.content == null || msg.senderId != contactId) { return; } await into(messageHistories).insert( @@ -209,11 +209,12 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { ); await (update(messages) ..where( - (t) => t.messageId.equals(messageId) & t.senderId.equals(contactId), + (t) => t.messageId.equals(messageId), )) .write( MessagesCompanion( content: Value(text), + modifiedAt: Value(timestamp), ), ); } diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index 25776e0..c450638 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -174,6 +174,7 @@ "submit": "Abschicken", "close": "Schließen", "cancel": "Abbrechen", + "edit": "Bearbeiten", "ok": "Ok", "now": "Jetzt", "you": "Du", diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index 9f2cc07..039ffea 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -307,6 +307,7 @@ "react": "React", "reply": "Reply", "copy": "Copy", + "edit": "Edit", "delete": "Delete", "info": "Info", "ok": "Ok", diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index b8fd2e1..b65722b 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -1106,6 +1106,12 @@ abstract class AppLocalizations { /// **'Copy'** String get copy; + /// No description provided for @edit. + /// + /// In en, this message translates to: + /// **'Edit'** + String get edit; + /// No description provided for @delete. /// /// 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 9aa1c2c..4273b75 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -560,6 +560,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get copy => 'Kopieren'; + @override + String get edit => 'Bearbeiten'; + @override String get delete => 'Löschen'; diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 0394caa..f58b9ed 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -555,6 +555,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get copy => 'Copy'; + @override + String get edit => 'Edit'; + @override String get delete => 'Delete'; diff --git a/lib/src/views/chats/chat_messages_components/chat_list_entry.dart b/lib/src/views/chats/chat_messages_components/chat_list_entry.dart index 322c049..4061a4a 100644 --- a/lib/src/views/chats/chat_messages_components/chat_list_entry.dart +++ b/lib/src/views/chats/chat_messages_components/chat_list_entry.dart @@ -23,6 +23,7 @@ class ChatListEntry extends StatefulWidget { required this.nextMessage, required this.onResponseTriggered, required this.scrollToMessage, + this.disableContextMenu = false, super.key, }); final Message? prevMessage; @@ -32,6 +33,7 @@ class ChatListEntry extends StatefulWidget { final List galleryItems; final void Function(String) scrollToMessage; final void Function() onResponseTriggered; + final bool disableContextMenu; @override State createState() => _ChatListEntryState(); @@ -96,81 +98,84 @@ class _ChatListEntryState extends State { reactions.where((t) => seen.add(t.emoji)).toList().length; if (reactionsForWidth > 4) reactionsForWidth = 4; - return Align( - alignment: right ? Alignment.centerRight : Alignment.centerLeft, - child: Padding( - padding: padding, - child: MessageContextMenu( + Widget child = Column( + mainAxisAlignment: + right ? MainAxisAlignment.end : MainAxisAlignment.start, + crossAxisAlignment: + right ? CrossAxisAlignment.end : CrossAxisAlignment.start, + children: [ + MessageActions( message: widget.message, onResponseTriggered: widget.onResponseTriggered, - child: Column( - mainAxisAlignment: - right ? MainAxisAlignment.end : MainAxisAlignment.start, - crossAxisAlignment: - right ? CrossAxisAlignment.end : CrossAxisAlignment.start, + child: Stack( + // overflow: Overflow.visible, + // clipBehavior: Clip.none, + alignment: right ? Alignment.centerRight : Alignment.centerLeft, children: [ - MessageActions( - message: widget.message, - onResponseTriggered: widget.onResponseTriggered, - child: Stack( - // overflow: Overflow.visible, - // clipBehavior: Clip.none, - alignment: - right ? Alignment.centerRight : Alignment.centerLeft, + if (widget.message.isDeletedFromSender) + ChatTextEntry( + message: widget.message, + nextMessage: widget.nextMessage, + borderRadius: borderRadius, + minWidth: reactionsForWidth * 43, + ) + else + Column( children: [ - if (widget.message.isDeletedFromSender) - ChatTextEntry( - message: widget.message, - nextMessage: widget.nextMessage, - borderRadius: borderRadius, - minWidth: reactionsForWidth * 43, - ) - else - Column( - children: [ - ResponseContainer( - msg: widget.message, - group: widget.group, - mediaService: mediaService, - borderRadius: borderRadius, - scrollToMessage: widget.scrollToMessage, - child: (widget.message.type == MessageType.text) - ? ChatTextEntry( - message: widget.message, - nextMessage: widget.nextMessage, - borderRadius: borderRadius, - minWidth: reactionsForWidth * 43, - ) - : (mediaService == null) - ? null - : ChatMediaEntry( - message: widget.message, - group: widget.group, - mediaService: mediaService!, - galleryItems: widget.galleryItems, - ), - ), - if (reactionsForWidth > 0) - const SizedBox(height: 20, width: 10), - ], - ), - if (!widget.message.isDeletedFromSender) - Positioned( - bottom: -20, - left: 5, - right: 5, - child: ReactionRow( - message: widget.message, - reactions: reactions, - ), - ), + ResponseContainer( + msg: widget.message, + group: widget.group, + mediaService: mediaService, + borderRadius: borderRadius, + scrollToMessage: widget.scrollToMessage, + child: (widget.message.type == MessageType.text) + ? ChatTextEntry( + message: widget.message, + nextMessage: widget.nextMessage, + borderRadius: borderRadius, + minWidth: reactionsForWidth * 43, + ) + : (mediaService == null) + ? null + : ChatMediaEntry( + message: widget.message, + group: widget.group, + mediaService: mediaService!, + galleryItems: widget.galleryItems, + ), + ), + if (reactionsForWidth > 0) + const SizedBox(height: 20, width: 10), ], ), - ), + if (!widget.message.isDeletedFromSender) + Positioned( + bottom: -20, + left: 5, + right: 5, + child: ReactionRow( + message: widget.message, + reactions: reactions, + ), + ), ], ), ), - ), + ], + ); + + if (!widget.disableContextMenu) { + child = MessageContextMenu( + message: widget.message, + group: widget.group, + onResponseTriggered: widget.onResponseTriggered, + child: child, + ); + } + + return Align( + alignment: right ? Alignment.centerRight : Alignment.centerLeft, + child: Padding(padding: padding, child: child), ); } } diff --git a/lib/src/views/chats/chat_messages_components/chat_text_entry.dart b/lib/src/views/chats/chat_messages_components/chat_text_entry.dart index 1369ac3..26c4c2f 100644 --- a/lib/src/views/chats/chat_messages_components/chat_text_entry.dart +++ b/lib/src/views/chats/chat_messages_components/chat_text_entry.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:intl/intl.dart' hide TextDirection; import 'package:twonly/src/database/tables/messages.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; @@ -89,12 +90,28 @@ class ChatTextEntry extends StatelessWidget { alignment: AlignmentGeometry.centerRight, child: Padding( padding: const EdgeInsets.only(left: 6), - child: Text( - friendlyTime(context, message.createdAt), - style: TextStyle( - fontSize: 10, - color: Colors.white.withAlpha(150), - ), + child: Row( + children: [ + if (message.modifiedAt != null) + Padding( + padding: const EdgeInsets.only(right: 5), + child: SizedBox( + height: 10, + child: FaIcon( + FontAwesomeIcons.pencil, + color: Colors.white.withAlpha(150), + size: 10, + ), + ), + ), + Text( + friendlyTime(context, message.createdAt), + style: TextStyle( + fontSize: 10, + color: Colors.white.withAlpha(150), + ), + ), + ], ), ), ), diff --git a/lib/src/views/chats/chat_messages_components/message_context_menu.dart b/lib/src/views/chats/chat_messages_components/message_context_menu.dart index 1fab50d..54db4e4 100644 --- a/lib/src/views/chats/chat_messages_components/message_context_menu.dart +++ b/lib/src/views/chats/chat_messages_components/message_context_menu.dart @@ -1,10 +1,12 @@ // ignore_for_file: inference_failure_on_function_invocation +import 'package:fixnum/fixnum.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:pie_menu/pie_menu.dart'; import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/tables/messages.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart' as pb; @@ -12,15 +14,18 @@ import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/camera/image_editor/data/layer.dart'; import 'package:twonly/src/views/camera/image_editor/modules/all_emojis.dart'; +import 'package:twonly/src/views/chats/message_info.view.dart'; import 'package:twonly/src/views/components/alert_dialog.dart'; class MessageContextMenu extends StatelessWidget { const MessageContextMenu({ required this.message, + required this.group, required this.child, required this.onResponseTriggered, super.key, }); + final Group group; final Widget child; final Message message; final VoidCallback onResponseTriggered; @@ -35,40 +40,52 @@ class MessageContextMenu extends StatelessWidget { } }, actions: [ - PieAction( - tooltip: Text(context.lang.react), - onSelect: () async { - final layer = await showModalBottomSheet( - context: context, - backgroundColor: Colors.black, - builder: (BuildContext context) { - return const Emojis(); - }, - ) as EmojiLayerData?; - if (layer == null) return; + if (!message.isDeletedFromSender) + PieAction( + tooltip: Text(context.lang.react), + onSelect: () async { + final layer = await showModalBottomSheet( + context: context, + backgroundColor: Colors.black, + builder: (BuildContext context) { + return const Emojis(); + }, + ) as EmojiLayerData?; + if (layer == null) return; - await twonlyDB.reactionsDao - .updateMyReaction(message.messageId, layer.text); + await twonlyDB.reactionsDao + .updateMyReaction(message.messageId, layer.text); - await sendCipherTextToGroup( - message.groupId, - pb.EncryptedContent( - reaction: pb.EncryptedContent_Reaction( - targetMessageId: message.messageId, - emoji: layer.text, - remove: false, + await sendCipherTextToGroup( + message.groupId, + pb.EncryptedContent( + reaction: pb.EncryptedContent_Reaction( + targetMessageId: message.messageId, + emoji: layer.text, + remove: false, + ), ), - ), - null, - ); - }, - child: const FaIcon(FontAwesomeIcons.faceLaugh), - ), - PieAction( - tooltip: Text(context.lang.reply), - onSelect: onResponseTriggered, - child: const FaIcon(FontAwesomeIcons.reply), - ), + null, + ); + }, + child: const FaIcon(FontAwesomeIcons.faceLaugh), + ), + if (!message.isDeletedFromSender) + PieAction( + tooltip: Text(context.lang.reply), + onSelect: onResponseTriggered, + child: const FaIcon(FontAwesomeIcons.reply), + ), + if (!message.isDeletedFromSender && + message.senderId == null && + message.type == MessageType.text) + PieAction( + tooltip: Text(context.lang.edit), + onSelect: () async { + await editTextMessage(context, message); + }, + child: const FaIcon(FontAwesomeIcons.pencil), + ), if (message.content != null) PieAction( tooltip: Text(context.lang.copy), @@ -115,13 +132,107 @@ class MessageContextMenu extends StatelessWidget { }, child: const FaIcon(FontAwesomeIcons.trash), ), - // PieAction( - // tooltip: Text(context.lang.info), - // onSelect: () {}, - // child: const FaIcon(FontAwesomeIcons.circleInfo), - // ), + if (!message.isDeletedFromSender) + PieAction( + tooltip: Text(context.lang.info), + onSelect: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return MessageInfoView( + message: message, + group: group, + ); + }, + ), + ); + }, + child: const FaIcon(FontAwesomeIcons.circleInfo), + ), ], child: child, ); } } + +Future editTextMessage(BuildContext context, Message message) async { + var newText = message.content; + final controller = TextEditingController(text: message.content); + await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + content: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only(top: 8), + child: TextField( + controller: controller, + autofocus: true, + keyboardType: TextInputType.multiline, + maxLines: 4, + minLines: 1, + onChanged: (value) => setState(() { + newText = value; + }), + decoration: const InputDecoration( + border: OutlineInputBorder(), + ), + textCapitalization: TextCapitalization.characters, + ), + ), + ], + ), + ); + }, + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text(context.lang.cancel), + ), + TextButton( + onPressed: () async { + if (newText != null && + newText != message.content && + newText != '') { + final timestamp = DateTime.now(); + + await twonlyDB.messagesDao.handleTextEdit( + null, + message.messageId, + newText!, + timestamp, + ); + await sendCipherTextToGroup( + message.groupId, + pb.EncryptedContent( + messageUpdate: pb.EncryptedContent_MessageUpdate( + type: pb.EncryptedContent_MessageUpdate_Type.EDIT_TEXT, + senderMessageId: message.messageId, + text: newText, + timestamp: Int64( + timestamp.millisecondsSinceEpoch, + ), + ), + ), + null, + ); + } + if (!context.mounted) return; + Navigator.of(context).pop(); + }, + child: Text(context.lang.ok), + ), + ], + ); + }, + ); +} diff --git a/lib/src/views/chats/message_info.view.dart b/lib/src/views/chats/message_info.view.dart new file mode 100644 index 0000000..3d198ec --- /dev/null +++ b/lib/src/views/chats/message_info.view.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/chats/chat_messages_components/chat_list_entry.dart'; + +class MessageInfoView extends StatefulWidget { + const MessageInfoView({ + required this.message, + required this.group, + super.key, + }); + + final Message message; + final Group group; + + @override + State createState() => _MessageInfoViewState(); +} + +class _MessageInfoViewState extends State { + @override + void initState() { + initAsync(); + super.initState(); + } + + Future initAsync() async { + // watch message edit history + // watch message actions + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text(''), + ), + body: Padding( + padding: const EdgeInsets.all(8), + child: ListView( + children: [ + Padding( + padding: const EdgeInsets.all(8), + child: ChatListEntry( + group: widget.group, + galleryItems: const [], + prevMessage: null, + message: widget.message, + disableContextMenu: true, + nextMessage: null, + onResponseTriggered: () {}, + scrollToMessage: (_) {}, + ), + ), + Row( + children: [ + const Text('Versendet'), + const SizedBox(width: 13), + Text(formatDateTime(context, widget.message.createdAt)), + ], + ), + // Row( + // children: [ + // Text("Empfangen"), + // SizedBox(width: 13), + // Text(formatDateTime(context, widget.message.ackByUser)), + // ], + // ) + const SizedBox(height: 10), + const Divider(), + const SizedBox(height: 10), + const Text('Zugestelt an'), + ], + ), + ), + ); + } +}