diff --git a/lib/src/database/daos/messages.dao.dart b/lib/src/database/daos/messages.dao.dart index d65c947..3189e66 100644 --- a/lib/src/database/daos/messages.dao.dart +++ b/lib/src/database/daos/messages.dao.dart @@ -68,6 +68,24 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { .watch(); } + // Stream> watchMembersByGroupId(String groupId) { + // return (select(groupMembers)..where((t) => t.groupId.equals(groupId))) + // .watch(); + // } + + Stream> watchMembersByGroupId(String groupId) { + final query = (select(groupMembers).join([ + leftOuterJoin( + contacts, + contacts.userId.equalsExp(groupMembers.contactId), + ), + ]) + ..where(groupMembers.groupId.equals(groupId))); + return query + .map((row) => (row.readTable(groupMembers), row.readTable(contacts))) + .watch(); + } + Stream> watchMessageActionChanges(String messageId) { return (select(messageActions)..where((t) => t.messageId.equals(messageId))) .watch(); @@ -410,6 +428,26 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { return (select(messages)..where((t) => t.mediaId.equals(mediaId))).get(); } + Stream> watchMessageActions(String messageId) { + final query = (select(messageActions).join([ + leftOuterJoin( + contacts, + contacts.userId.equalsExp(messageActions.contactId), + ), + ]) + ..where(messageActions.messageId.equals(messageId))); + return query + .map((row) => (row.readTable(messageActions), row.readTable(contacts))) + .watch(); + } + + Stream> watchMessageHistory(String messageId) { + return (select(messageHistories) + ..where((t) => t.messageId.equals(messageId)) + ..orderBy([(t) => OrderingTerm.desc(t.createdAt)])) + .watch(); + } + // Future> getMessagesByMediaUploadId(int mediaUploadId) async { // return (select(messages) // ..where((t) => t.mediaUploadId.equals(mediaUploadId))) diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index aa12dcc..b337e7f 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -345,5 +345,11 @@ "newDeviceRegistered": "Du hast dich auf einem anderen Gerät angemeldet. Daher wurdest du hier abgemeldet.", "tabToRemoveEmoji": "Tippen um zu entfernen", "quotedMessageWasDeleted": "Die zitierte Nachricht wurde gelöscht.", - "messageWasDeleted": "Nachricht wurde gelöscht." + "messageWasDeleted": "Nachricht wurde gelöscht.", + "sent": "Versendet", + "sentTo": "Zugestellt an", + "received": "Empfangen", + "opened": "Geöffnet", + "waitingForInternet": "Warten auf Internet", + "editHistory": "Bearbeitungshistorie" } \ No newline at end of file diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index 6c8654d..3b91d4d 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -501,5 +501,11 @@ "newDeviceRegistered": "You have logged in on another device. You have therefore been logged out here.", "tabToRemoveEmoji": "Tab to remove", "quotedMessageWasDeleted": "The quoted message has been deleted.", - "messageWasDeleted": "Message has been deleted." + "messageWasDeleted": "Message has been deleted.", + "sent": "Delivered", + "sentTo": "Delivered to", + "received": "Received", + "opened": "Opened", + "waitingForInternet": "Waiting for internet", + "editHistory": "Edit history" } \ No newline at end of file diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index 4163bcf..fac03be 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -2113,6 +2113,42 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Message has been deleted.'** String get messageWasDeleted; + + /// No description provided for @sent. + /// + /// In en, this message translates to: + /// **'Delivered'** + String get sent; + + /// No description provided for @sentTo. + /// + /// In en, this message translates to: + /// **'Delivered to'** + String get sentTo; + + /// No description provided for @received. + /// + /// In en, this message translates to: + /// **'Received'** + String get received; + + /// No description provided for @opened. + /// + /// In en, this message translates to: + /// **'Opened'** + String get opened; + + /// No description provided for @waitingForInternet. + /// + /// In en, this message translates to: + /// **'Waiting for internet'** + String get waitingForInternet; + + /// No description provided for @editHistory. + /// + /// In en, this message translates to: + /// **'Edit history'** + String get editHistory; } class _AppLocalizationsDelegate diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 5e94470..653f33f 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -1122,4 +1122,22 @@ class AppLocalizationsDe extends AppLocalizations { @override String get messageWasDeleted => 'Nachricht wurde gelöscht.'; + + @override + String get sent => 'Versendet'; + + @override + String get sentTo => 'Zugestellt an'; + + @override + String get received => 'Empfangen'; + + @override + String get opened => 'Geöffnet'; + + @override + String get waitingForInternet => 'Warten auf Internet'; + + @override + String get editHistory => 'Bearbeitungshistorie'; } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 479cd81..e797023 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -1115,4 +1115,22 @@ class AppLocalizationsEn extends AppLocalizations { @override String get messageWasDeleted => 'Message has been deleted.'; + + @override + String get sent => 'Delivered'; + + @override + String get sentTo => 'Delivered to'; + + @override + String get received => 'Received'; + + @override + String get opened => 'Opened'; + + @override + String get waitingForInternet => 'Waiting for internet'; + + @override + String get editHistory => 'Edit history'; } diff --git a/lib/src/services/api/messages.dart b/lib/src/services/api/messages.dart index 31cc9ee..aa8a7ac 100644 --- a/lib/src/services/api/messages.dart +++ b/lib/src/services/api/messages.dart @@ -251,19 +251,22 @@ Future notifyContactAboutOpeningMessage( } Log.info('Opened messages: $messageOtherIds'); + final actionAt = DateTime.now(); + await sendCipherText( contactId, pb.EncryptedContent( messageUpdate: pb.EncryptedContent_MessageUpdate( type: pb.EncryptedContent_MessageUpdate_Type.OPENED, multipleTargetMessageIds: messageOtherIds, + timestamp: Int64(actionAt.millisecondsSinceEpoch), ), ), ); for (final messageId in messageOtherIds) { await twonlyDB.messagesDao.updateMessageId( messageId, - MessagesCompanion(openedAt: Value(DateTime.now())), + MessagesCompanion(openedAt: Value(actionAt)), ); } await updateLastMessageId(contactId, biggestMessageId); diff --git a/lib/src/services/api/server_messages/text_message.server_messages.dart b/lib/src/services/api/server_messages/text_message.server_messages.dart index c20d803..dfe6260 100644 --- a/lib/src/services/api/server_messages/text_message.server_messages.dart +++ b/lib/src/services/api/server_messages/text_message.server_messages.dart @@ -26,6 +26,7 @@ Future handleTextMessage( textMessage.hasQuoteMessageId() ? textMessage.quoteMessageId : null, ), createdAt: Value(fromTimestamp(textMessage.timestamp)), + ackByServer: Value(DateTime.now()), ), ); if (message != null) { diff --git a/lib/src/utils/misc.dart b/lib/src/utils/misc.dart index bd34ec1..b2adc93 100644 --- a/lib/src/utils/misc.dart +++ b/lib/src/utils/misc.dart @@ -348,3 +348,27 @@ String getUUIDforDirectChat(int a, int b) { ]; return parts.join('-'); } + +String friendlyDateTime( + BuildContext context, + DateTime dt, { + bool includeSeconds = false, + Locale? locale, +}) { + // Build date part + final datePart = + DateFormat.yMd(Localizations.localeOf(context).toString()).format(dt); + + final use24Hour = MediaQuery.of(context).alwaysUse24HourFormat; + + var timePart = ''; + if (use24Hour) { + timePart = + DateFormat.jm(Localizations.localeOf(context).toString()).format(dt); + } else { + timePart = + DateFormat.Hm(Localizations.localeOf(context).toString()).format(dt); + } + + return '$timePart $datePart'; +} diff --git a/lib/src/views/chats/chat_messages_components/bottom_sheets/message_history.bottom_sheet.dart b/lib/src/views/chats/chat_messages_components/bottom_sheets/message_history.bottom_sheet.dart new file mode 100644 index 0000000..7569305 --- /dev/null +++ b/lib/src/views/chats/chat_messages_components/bottom_sheets/message_history.bottom_sheet.dart @@ -0,0 +1,80 @@ +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 MessageHistoryView extends StatelessWidget { + const MessageHistoryView({ + required this.message, + required this.group, + required this.changes, + super.key, + }); + + final Message message; + final Group group; + final List changes; + + @override + Widget build(BuildContext context) { + final json = message.toJson(); + json['createdAt'] = message.modifiedAt; + final currentMessage = Message.fromJson(json); + return SingleChildScrollView( + child: Container( + padding: EdgeInsets.zero, + height: 450, + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(32), + topRight: Radius.circular(32), + ), + color: context.color.surface, + boxShadow: const [ + BoxShadow( + blurRadius: 10.9, + color: Color.fromRGBO(0, 0, 0, 0.1), + ), + ], + ), + child: Column( + children: [ + Container( + margin: const EdgeInsets.all(30), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(32), + color: Colors.grey, + ), + height: 3, + width: 60, + ), + Expanded( + child: ListView( + children: [ + ChatListEntry( + group: group, + message: currentMessage, + hideReactions: true, + ), + ...changes.map( + (change) { + final json = message.toJson(); + json['content'] = change.content; + json['createdAt'] = change.createdAt; + final msgChanged = Message.fromJson(json); + return ChatListEntry( + group: group, + message: msgChanged, + hideReactions: true, + ); + }, + ), + ], + ), + ), + ], + ), + ), + ); + } +} 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 4061a4a..858a3cc 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 @@ -17,23 +17,23 @@ import 'package:twonly/src/views/chats/chat_messages_components/response_contain class ChatListEntry extends StatefulWidget { const ChatListEntry({ required this.group, - required this.galleryItems, - required this.prevMessage, required this.message, - required this.nextMessage, - required this.onResponseTriggered, - required this.scrollToMessage, - this.disableContextMenu = false, + this.galleryItems = const [], + this.scrollToMessage, + this.onResponseTriggered, + this.prevMessage, + this.nextMessage, + this.hideReactions = false, super.key, }); final Message? prevMessage; final Message? nextMessage; final Message message; final Group group; + final bool hideReactions; final List galleryItems; - final void Function(String) scrollToMessage; - final void Function() onResponseTriggered; - final bool disableContextMenu; + final void Function(String)? scrollToMessage; + final void Function()? onResponseTriggered; @override State createState() => _ChatListEntryState(); @@ -98,77 +98,70 @@ class _ChatListEntryState extends State { reactions.where((t) => seen.add(t.emoji)).toList().length; if (reactionsForWidth > 4) reactionsForWidth = 4; - Widget child = Column( - mainAxisAlignment: - right ? MainAxisAlignment.end : MainAxisAlignment.start, - crossAxisAlignment: - right ? CrossAxisAlignment.end : CrossAxisAlignment.start, + Widget 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 && !widget.hideReactions) + Positioned( + bottom: -20, + left: 5, + right: 5, + child: ReactionRow( + message: widget.message, + reactions: reactions, + ), + ), ], ); - if (!widget.disableContextMenu) { + if (widget.onResponseTriggered != null) { + child = MessageActions( + message: widget.message, + onResponseTriggered: widget.onResponseTriggered!, + child: child, + ); + child = MessageContextMenu( message: widget.message, group: widget.group, - onResponseTriggered: widget.onResponseTriggered, + onResponseTriggered: widget.onResponseTriggered!, child: child, ); } diff --git a/lib/src/views/chats/chat_messages_components/chat_reaction_row.dart b/lib/src/views/chats/chat_messages_components/chat_reaction_row.dart index be40794..b0d6991 100644 --- a/lib/src/views/chats/chat_messages_components/chat_reaction_row.dart +++ b/lib/src/views/chats/chat_messages_components/chat_reaction_row.dart @@ -2,6 +2,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.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/bottom_sheets/all_reactions.bottom_sheet.dart'; import 'package:twonly/src/views/components/animate_icon.dart'; @@ -26,7 +27,6 @@ class ReactionRow extends StatelessWidget { ); }, ); - // if (layer == null) return; } @override @@ -99,7 +99,9 @@ class ReactionRow extends StatelessWidget { decoration: BoxDecoration( border: Border.all(), borderRadius: BorderRadius.circular(12), - color: const Color.fromARGB(255, 74, 74, 74), + color: isDarkMode(context) + ? const Color.fromARGB(255, 74, 74, 74) + : const Color.fromARGB(255, 197, 197, 197), ), child: Row( children: [ @@ -107,8 +109,16 @@ class ReactionRow extends StatelessWidget { if (entry.$2 > 1) SizedBox( height: 19, + width: 13, child: Text( entry.$2.toString(), + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 15, + color: Colors.black, + decoration: TextDecoration.none, + fontWeight: FontWeight.normal, + ), ), ), ], 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 233d884..121fa94 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 @@ -26,10 +26,6 @@ class ChatTextEntry extends StatelessWidget { Widget build(BuildContext context) { var text = message.content ?? ''; - if (message.isDeletedFromSender) { - text = context.lang.messageWasDeleted; - } - if (EmojiAnimation.supported(text)) { return Container( constraints: const BoxConstraints( @@ -61,6 +57,11 @@ class ChatTextEntry extends StatelessWidget { expanded = true; } + if (message.isDeletedFromSender) { + text = context.lang.messageWasDeleted; + color = Colors.grey; + } + return Container( constraints: BoxConstraints( maxWidth: MediaQuery.of(context).size.width * 0.8, @@ -109,6 +110,8 @@ class ChatTextEntry extends StatelessWidget { style: TextStyle( fontSize: 10, color: Colors.white.withAlpha(150), + decoration: TextDecoration.none, + fontWeight: FontWeight.normal, ), ), ], diff --git a/lib/src/views/chats/chat_messages_components/response_container.dart b/lib/src/views/chats/chat_messages_components/response_container.dart index 3db2601..404bb40 100644 --- a/lib/src/views/chats/chat_messages_components/response_container.dart +++ b/lib/src/views/chats/chat_messages_components/response_container.dart @@ -13,9 +13,9 @@ class ResponseContainer extends StatefulWidget { required this.msg, required this.group, required this.child, - required this.scrollToMessage, required this.mediaService, required this.borderRadius, + this.scrollToMessage, super.key, }); @@ -24,7 +24,7 @@ class ResponseContainer extends StatefulWidget { final Group group; final MediaFileService? mediaService; final BorderRadius borderRadius; - final void Function(String) scrollToMessage; + final void Function(String)? scrollToMessage; @override State createState() => _ResponseContainerState(); @@ -65,7 +65,9 @@ class _ResponseContainerState extends State { return widget.child!; } return GestureDetector( - onTap: () => widget.scrollToMessage(widget.msg.quotesMessageId!), + onTap: widget.scrollToMessage == null + ? null + : () => widget.scrollToMessage!(widget.msg.quotesMessageId!), child: Container( constraints: BoxConstraints( maxWidth: MediaQuery.of(context).size.width * 0.8, diff --git a/lib/src/views/chats/message_info.view.dart b/lib/src/views/chats/message_info.view.dart index 3d198ec..36bb78f 100644 --- a/lib/src/views/chats/message_info.view.dart +++ b/lib/src/views/chats/message_info.view.dart @@ -1,7 +1,16 @@ +import 'dart:async'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/database/tables/messages.table.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/bottom_sheets/message_history.bottom_sheet.dart'; import 'package:twonly/src/views/chats/chat_messages_components/chat_list_entry.dart'; +import 'package:twonly/src/views/components/avatar_icon.component.dart'; +import 'package:twonly/src/views/components/better_list_title.dart'; class MessageInfoView extends StatefulWidget { const MessageInfoView({ @@ -18,22 +27,132 @@ class MessageInfoView extends StatefulWidget { } class _MessageInfoViewState extends State { + StreamSubscription>? actionsStream; + StreamSubscription>? historyStream; + StreamSubscription>? groupMemberStream; + + List<(MessageAction, Contact)> messageActions = []; + List messageHistory = []; + List<(GroupMember, Contact)> groupMembers = []; + @override void initState() { initAsync(); super.initState(); } - Future initAsync() async { - // watch message edit history - // watch message actions - } - @override void dispose() { + actionsStream?.cancel(); + historyStream?.cancel(); + groupMemberStream?.cancel(); super.dispose(); } + Future initAsync() async { + final streamActions = + twonlyDB.messagesDao.watchMessageActions(widget.message.messageId); + actionsStream = streamActions.listen((update) { + setState(() { + messageActions = update; + }); + }); + + final streamGroup = + twonlyDB.messagesDao.watchMembersByGroupId(widget.message.groupId); + groupMemberStream = streamGroup.listen((update) { + setState(() { + groupMembers = update; + }); + }); + + final streamHistory = + twonlyDB.messagesDao.watchMessageHistory(widget.message.messageId); + historyStream = streamHistory.listen((update) { + setState(() { + messageHistory = update; + }); + }); + } + + List getReceivedColumns(BuildContext context) { + if (widget.message.senderId != null) return []; + + final columns = [ + const SizedBox(height: 10), + const Divider(), + const SizedBox(height: 20), + Text(context.lang.sentTo), + const SizedBox(height: 10), + ]; + + for (final groupMember in groupMembers) { + final ackByServer = messageActions.firstWhereOrNull( + (t) => + t.$1.type == MessageActionType.ackByServerAt && + t.$2.userId == groupMember.$2.userId, + ); + final ackByUser = messageActions.firstWhereOrNull( + (t) => + t.$1.type == MessageActionType.ackByUserAt && + t.$2.userId == groupMember.$2.userId, + ); + final openedByUser = messageActions.firstWhereOrNull( + (t) => + t.$1.type == MessageActionType.openedAt && + t.$2.userId == groupMember.$2.userId, + ); + + var actionTypeText = context.lang.waitingForInternet; + var actionAt = widget.message.createdAt; + if (ackByServer != null) { + actionTypeText = context.lang.sent; + actionAt = ackByServer.$1.actionAt; + } + if (ackByUser != null) { + actionTypeText = context.lang.received; + actionAt = ackByUser.$1.actionAt; + } + if (openedByUser != null) { + actionTypeText = context.lang.opened; + actionAt = openedByUser.$1.actionAt; + } + + columns.add( + Row( + children: [ + AvatarIcon( + contact: groupMember.$2, + fontSize: 15, + ), + const SizedBox(width: 6), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + getContactDisplayName(groupMember.$2), + style: const TextStyle(fontSize: 17), + ), + ], + ), + ), + Column( + children: [ + Text( + friendlyDateTime(context, actionAt), + style: const TextStyle(fontSize: 12), + ), + Text(actionTypeText), + ], + ), + ], + ), + ); + } + return columns; + } + @override Widget build(BuildContext context) { return Scaffold( @@ -41,40 +160,47 @@ class _MessageInfoViewState extends State { title: const Text(''), ), body: Padding( - padding: const EdgeInsets.all(8), + padding: const EdgeInsets.symmetric(horizontal: 24), 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: (_) {}, + const SizedBox(height: 20), + ChatListEntry( + group: widget.group, + message: widget.message, + ), + Text( + '${context.lang.sent}: ${friendlyDateTime(context, widget.message.createdAt)}', + ), + if (widget.message.senderId != null && + widget.message.ackByServer != null) + Text( + '${context.lang.received}: ${friendlyDateTime(context, widget.message.ackByServer!)}', ), - ), - 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'), + if (messageHistory.isNotEmpty) ...[ + const SizedBox(height: 10), + const Divider(), + const SizedBox(height: 10), + BetterListTile( + icon: FontAwesomeIcons.pencil, + padding: EdgeInsets.zero, + text: context.lang.editHistory, + onTap: () async { + // ignore: inference_failure_on_function_invocation + await showModalBottomSheet( + context: context, + backgroundColor: Colors.black, + builder: (BuildContext context) { + return MessageHistoryView( + message: widget.message, + changes: messageHistory, + group: widget.group, + ); + }, + ); + }, + ), + ], + ...getReceivedColumns(context), ], ), ), diff --git a/lib/src/views/components/better_list_title.dart b/lib/src/views/components/better_list_title.dart index b7ad83c..c83eca2 100644 --- a/lib/src/views/components/better_list_title.dart +++ b/lib/src/views/components/better_list_title.dart @@ -10,6 +10,7 @@ class BetterListTile extends StatelessWidget { this.color, this.subtitle, this.iconSize = 20, + this.padding, }); final IconData icon; final String text; @@ -17,15 +18,18 @@ class BetterListTile extends StatelessWidget { final Color? color; final VoidCallback onTap; final double iconSize; + final EdgeInsets? padding; @override Widget build(BuildContext context) { return ListTile( leading: Padding( - padding: const EdgeInsets.only( - right: 10, - left: 19, - ), + padding: (padding == null) + ? const EdgeInsets.only( + right: 10, + left: 19, + ) + : padding!, child: FaIcon( icon, size: iconSize, diff --git a/lib/src/views/components/better_text.dart b/lib/src/views/components/better_text.dart index d5f27b2..97399a2 100644 --- a/lib/src/views/components/better_text.dart +++ b/lib/src/views/components/better_text.dart @@ -68,6 +68,8 @@ class BetterText extends StatelessWidget { style: const TextStyle( color: Colors.white, fontSize: 17, + decoration: TextDecoration.none, + fontWeight: FontWeight.normal, ), ); }