display message history

This commit is contained in:
otsmr 2025-10-27 16:40:40 +01:00
parent 6bb18a5bd0
commit 3d5fc3e807
17 changed files with 492 additions and 122 deletions

View file

@ -68,6 +68,24 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
.watch(); .watch();
} }
// Stream<List<GroupMember>> watchMembersByGroupId(String groupId) {
// return (select(groupMembers)..where((t) => t.groupId.equals(groupId)))
// .watch();
// }
Stream<List<(GroupMember, Contact)>> 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<List<MessageAction>> watchMessageActionChanges(String messageId) { Stream<List<MessageAction>> watchMessageActionChanges(String messageId) {
return (select(messageActions)..where((t) => t.messageId.equals(messageId))) return (select(messageActions)..where((t) => t.messageId.equals(messageId)))
.watch(); .watch();
@ -410,6 +428,26 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
return (select(messages)..where((t) => t.mediaId.equals(mediaId))).get(); return (select(messages)..where((t) => t.mediaId.equals(mediaId))).get();
} }
Stream<List<(MessageAction, Contact)>> 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<List<MessageHistory>> watchMessageHistory(String messageId) {
return (select(messageHistories)
..where((t) => t.messageId.equals(messageId))
..orderBy([(t) => OrderingTerm.desc(t.createdAt)]))
.watch();
}
// Future<List<Message>> getMessagesByMediaUploadId(int mediaUploadId) async { // Future<List<Message>> getMessagesByMediaUploadId(int mediaUploadId) async {
// return (select(messages) // return (select(messages)
// ..where((t) => t.mediaUploadId.equals(mediaUploadId))) // ..where((t) => t.mediaUploadId.equals(mediaUploadId)))

View file

@ -345,5 +345,11 @@
"newDeviceRegistered": "Du hast dich auf einem anderen Gerät angemeldet. Daher wurdest du hier abgemeldet.", "newDeviceRegistered": "Du hast dich auf einem anderen Gerät angemeldet. Daher wurdest du hier abgemeldet.",
"tabToRemoveEmoji": "Tippen um zu entfernen", "tabToRemoveEmoji": "Tippen um zu entfernen",
"quotedMessageWasDeleted": "Die zitierte Nachricht wurde gelöscht.", "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"
} }

View file

@ -501,5 +501,11 @@
"newDeviceRegistered": "You have logged in on another device. You have therefore been logged out here.", "newDeviceRegistered": "You have logged in on another device. You have therefore been logged out here.",
"tabToRemoveEmoji": "Tab to remove", "tabToRemoveEmoji": "Tab to remove",
"quotedMessageWasDeleted": "The quoted message has been deleted.", "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"
} }

View file

@ -2113,6 +2113,42 @@ abstract class AppLocalizations {
/// In en, this message translates to: /// In en, this message translates to:
/// **'Message has been deleted.'** /// **'Message has been deleted.'**
String get messageWasDeleted; 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 class _AppLocalizationsDelegate

View file

@ -1122,4 +1122,22 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get messageWasDeleted => 'Nachricht wurde gelöscht.'; 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';
} }

View file

@ -1115,4 +1115,22 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get messageWasDeleted => 'Message has been deleted.'; 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';
} }

View file

@ -251,19 +251,22 @@ Future<void> notifyContactAboutOpeningMessage(
} }
Log.info('Opened messages: $messageOtherIds'); Log.info('Opened messages: $messageOtherIds');
final actionAt = DateTime.now();
await sendCipherText( await sendCipherText(
contactId, contactId,
pb.EncryptedContent( pb.EncryptedContent(
messageUpdate: pb.EncryptedContent_MessageUpdate( messageUpdate: pb.EncryptedContent_MessageUpdate(
type: pb.EncryptedContent_MessageUpdate_Type.OPENED, type: pb.EncryptedContent_MessageUpdate_Type.OPENED,
multipleTargetMessageIds: messageOtherIds, multipleTargetMessageIds: messageOtherIds,
timestamp: Int64(actionAt.millisecondsSinceEpoch),
), ),
), ),
); );
for (final messageId in messageOtherIds) { for (final messageId in messageOtherIds) {
await twonlyDB.messagesDao.updateMessageId( await twonlyDB.messagesDao.updateMessageId(
messageId, messageId,
MessagesCompanion(openedAt: Value(DateTime.now())), MessagesCompanion(openedAt: Value(actionAt)),
); );
} }
await updateLastMessageId(contactId, biggestMessageId); await updateLastMessageId(contactId, biggestMessageId);

View file

@ -26,6 +26,7 @@ Future<void> handleTextMessage(
textMessage.hasQuoteMessageId() ? textMessage.quoteMessageId : null, textMessage.hasQuoteMessageId() ? textMessage.quoteMessageId : null,
), ),
createdAt: Value(fromTimestamp(textMessage.timestamp)), createdAt: Value(fromTimestamp(textMessage.timestamp)),
ackByServer: Value(DateTime.now()),
), ),
); );
if (message != null) { if (message != null) {

View file

@ -348,3 +348,27 @@ String getUUIDforDirectChat(int a, int b) {
]; ];
return parts.join('-'); 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';
}

View file

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

View file

@ -17,23 +17,23 @@ import 'package:twonly/src/views/chats/chat_messages_components/response_contain
class ChatListEntry extends StatefulWidget { class ChatListEntry extends StatefulWidget {
const ChatListEntry({ const ChatListEntry({
required this.group, required this.group,
required this.galleryItems,
required this.prevMessage,
required this.message, required this.message,
required this.nextMessage, this.galleryItems = const [],
required this.onResponseTriggered, this.scrollToMessage,
required this.scrollToMessage, this.onResponseTriggered,
this.disableContextMenu = false, this.prevMessage,
this.nextMessage,
this.hideReactions = false,
super.key, super.key,
}); });
final Message? prevMessage; final Message? prevMessage;
final Message? nextMessage; final Message? nextMessage;
final Message message; final Message message;
final Group group; final Group group;
final bool hideReactions;
final List<MemoryItem> galleryItems; final List<MemoryItem> galleryItems;
final void Function(String) scrollToMessage; final void Function(String)? scrollToMessage;
final void Function() onResponseTriggered; final void Function()? onResponseTriggered;
final bool disableContextMenu;
@override @override
State<ChatListEntry> createState() => _ChatListEntryState(); State<ChatListEntry> createState() => _ChatListEntryState();
@ -98,77 +98,70 @@ class _ChatListEntryState extends State<ChatListEntry> {
reactions.where((t) => seen.add(t.emoji)).toList().length; reactions.where((t) => seen.add(t.emoji)).toList().length;
if (reactionsForWidth > 4) reactionsForWidth = 4; if (reactionsForWidth > 4) reactionsForWidth = 4;
Widget child = Column( Widget child = Stack(
mainAxisAlignment: // overflow: Overflow.visible,
right ? MainAxisAlignment.end : MainAxisAlignment.start, // clipBehavior: Clip.none,
crossAxisAlignment: alignment: right ? Alignment.centerRight : Alignment.centerLeft,
right ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: [ children: [
MessageActions( if (widget.message.isDeletedFromSender)
message: widget.message, ChatTextEntry(
onResponseTriggered: widget.onResponseTriggered, message: widget.message,
child: Stack( nextMessage: widget.nextMessage,
// overflow: Overflow.visible, borderRadius: borderRadius,
// clipBehavior: Clip.none, minWidth: reactionsForWidth * 43,
alignment: right ? Alignment.centerRight : Alignment.centerLeft, )
else
Column(
children: [ children: [
if (widget.message.isDeletedFromSender) ResponseContainer(
ChatTextEntry( msg: widget.message,
message: widget.message, group: widget.group,
nextMessage: widget.nextMessage, mediaService: mediaService,
borderRadius: borderRadius, borderRadius: borderRadius,
minWidth: reactionsForWidth * 43, scrollToMessage: widget.scrollToMessage,
) child: (widget.message.type == MessageType.text)
else ? ChatTextEntry(
Column( message: widget.message,
children: [ nextMessage: widget.nextMessage,
ResponseContainer( borderRadius: borderRadius,
msg: widget.message, minWidth: reactionsForWidth * 43,
group: widget.group, )
mediaService: mediaService, : (mediaService == null)
borderRadius: borderRadius, ? null
scrollToMessage: widget.scrollToMessage, : ChatMediaEntry(
child: (widget.message.type == MessageType.text) message: widget.message,
? ChatTextEntry( group: widget.group,
message: widget.message, mediaService: mediaService!,
nextMessage: widget.nextMessage, galleryItems: widget.galleryItems,
borderRadius: borderRadius, ),
minWidth: reactionsForWidth * 43, ),
) if (reactionsForWidth > 0) const SizedBox(height: 20, width: 10),
: (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.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( child = MessageContextMenu(
message: widget.message, message: widget.message,
group: widget.group, group: widget.group,
onResponseTriggered: widget.onResponseTriggered, onResponseTriggered: widget.onResponseTriggered!,
child: child, child: child,
); );
} }

View file

@ -2,6 +2,7 @@ import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/src/database/twonly.db.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/chats/chat_messages_components/bottom_sheets/all_reactions.bottom_sheet.dart';
import 'package:twonly/src/views/components/animate_icon.dart'; import 'package:twonly/src/views/components/animate_icon.dart';
@ -26,7 +27,6 @@ class ReactionRow extends StatelessWidget {
); );
}, },
); );
// if (layer == null) return;
} }
@override @override
@ -99,7 +99,9 @@ class ReactionRow extends StatelessWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all(), border: Border.all(),
borderRadius: BorderRadius.circular(12), 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( child: Row(
children: [ children: [
@ -107,8 +109,16 @@ class ReactionRow extends StatelessWidget {
if (entry.$2 > 1) if (entry.$2 > 1)
SizedBox( SizedBox(
height: 19, height: 19,
width: 13,
child: Text( child: Text(
entry.$2.toString(), entry.$2.toString(),
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 15,
color: Colors.black,
decoration: TextDecoration.none,
fontWeight: FontWeight.normal,
),
), ),
), ),
], ],

View file

@ -26,10 +26,6 @@ class ChatTextEntry extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
var text = message.content ?? ''; var text = message.content ?? '';
if (message.isDeletedFromSender) {
text = context.lang.messageWasDeleted;
}
if (EmojiAnimation.supported(text)) { if (EmojiAnimation.supported(text)) {
return Container( return Container(
constraints: const BoxConstraints( constraints: const BoxConstraints(
@ -61,6 +57,11 @@ class ChatTextEntry extends StatelessWidget {
expanded = true; expanded = true;
} }
if (message.isDeletedFromSender) {
text = context.lang.messageWasDeleted;
color = Colors.grey;
}
return Container( return Container(
constraints: BoxConstraints( constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.8, maxWidth: MediaQuery.of(context).size.width * 0.8,
@ -109,6 +110,8 @@ class ChatTextEntry extends StatelessWidget {
style: TextStyle( style: TextStyle(
fontSize: 10, fontSize: 10,
color: Colors.white.withAlpha(150), color: Colors.white.withAlpha(150),
decoration: TextDecoration.none,
fontWeight: FontWeight.normal,
), ),
), ),
], ],

View file

@ -13,9 +13,9 @@ class ResponseContainer extends StatefulWidget {
required this.msg, required this.msg,
required this.group, required this.group,
required this.child, required this.child,
required this.scrollToMessage,
required this.mediaService, required this.mediaService,
required this.borderRadius, required this.borderRadius,
this.scrollToMessage,
super.key, super.key,
}); });
@ -24,7 +24,7 @@ class ResponseContainer extends StatefulWidget {
final Group group; final Group group;
final MediaFileService? mediaService; final MediaFileService? mediaService;
final BorderRadius borderRadius; final BorderRadius borderRadius;
final void Function(String) scrollToMessage; final void Function(String)? scrollToMessage;
@override @override
State<ResponseContainer> createState() => _ResponseContainerState(); State<ResponseContainer> createState() => _ResponseContainerState();
@ -65,7 +65,9 @@ class _ResponseContainerState extends State<ResponseContainer> {
return widget.child!; return widget.child!;
} }
return GestureDetector( return GestureDetector(
onTap: () => widget.scrollToMessage(widget.msg.quotesMessageId!), onTap: widget.scrollToMessage == null
? null
: () => widget.scrollToMessage!(widget.msg.quotesMessageId!),
child: Container( child: Container(
constraints: BoxConstraints( constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.8, maxWidth: MediaQuery.of(context).size.width * 0.8,

View file

@ -1,7 +1,16 @@
import 'dart:async';
import 'package:collection/collection.dart';
import 'package:flutter/material.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/database/twonly.db.dart';
import 'package:twonly/src/utils/misc.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/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 { class MessageInfoView extends StatefulWidget {
const MessageInfoView({ const MessageInfoView({
@ -18,22 +27,132 @@ class MessageInfoView extends StatefulWidget {
} }
class _MessageInfoViewState extends State<MessageInfoView> { class _MessageInfoViewState extends State<MessageInfoView> {
StreamSubscription<List<(MessageAction, Contact)>>? actionsStream;
StreamSubscription<List<MessageHistory>>? historyStream;
StreamSubscription<List<(GroupMember, Contact)>>? groupMemberStream;
List<(MessageAction, Contact)> messageActions = [];
List<MessageHistory> messageHistory = [];
List<(GroupMember, Contact)> groupMembers = [];
@override @override
void initState() { void initState() {
initAsync(); initAsync();
super.initState(); super.initState();
} }
Future<void> initAsync() async {
// watch message edit history
// watch message actions
}
@override @override
void dispose() { void dispose() {
actionsStream?.cancel();
historyStream?.cancel();
groupMemberStream?.cancel();
super.dispose(); super.dispose();
} }
Future<void> 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<Widget> getReceivedColumns(BuildContext context) {
if (widget.message.senderId != null) return [];
final columns = <Widget>[
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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@ -41,40 +160,47 @@ class _MessageInfoViewState extends State<MessageInfoView> {
title: const Text(''), title: const Text(''),
), ),
body: Padding( body: Padding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.symmetric(horizontal: 24),
child: ListView( child: ListView(
children: [ children: [
Padding( const SizedBox(height: 20),
padding: const EdgeInsets.all(8), ChatListEntry(
child: ChatListEntry( group: widget.group,
group: widget.group, message: widget.message,
galleryItems: const [], ),
prevMessage: null, Text(
message: widget.message, '${context.lang.sent}: ${friendlyDateTime(context, widget.message.createdAt)}',
disableContextMenu: true, ),
nextMessage: null, if (widget.message.senderId != null &&
onResponseTriggered: () {}, widget.message.ackByServer != null)
scrollToMessage: (_) {}, Text(
'${context.lang.received}: ${friendlyDateTime(context, widget.message.ackByServer!)}',
), ),
), if (messageHistory.isNotEmpty) ...[
Row( const SizedBox(height: 10),
children: [ const Divider(),
const Text('Versendet'), const SizedBox(height: 10),
const SizedBox(width: 13), BetterListTile(
Text(formatDateTime(context, widget.message.createdAt)), icon: FontAwesomeIcons.pencil,
], padding: EdgeInsets.zero,
), text: context.lang.editHistory,
// Row( onTap: () async {
// children: [ // ignore: inference_failure_on_function_invocation
// Text("Empfangen"), await showModalBottomSheet(
// SizedBox(width: 13), context: context,
// Text(formatDateTime(context, widget.message.ackByUser)), backgroundColor: Colors.black,
// ], builder: (BuildContext context) {
// ) return MessageHistoryView(
const SizedBox(height: 10), message: widget.message,
const Divider(), changes: messageHistory,
const SizedBox(height: 10), group: widget.group,
const Text('Zugestelt an'), );
},
);
},
),
],
...getReceivedColumns(context),
], ],
), ),
), ),

View file

@ -10,6 +10,7 @@ class BetterListTile extends StatelessWidget {
this.color, this.color,
this.subtitle, this.subtitle,
this.iconSize = 20, this.iconSize = 20,
this.padding,
}); });
final IconData icon; final IconData icon;
final String text; final String text;
@ -17,15 +18,18 @@ class BetterListTile extends StatelessWidget {
final Color? color; final Color? color;
final VoidCallback onTap; final VoidCallback onTap;
final double iconSize; final double iconSize;
final EdgeInsets? padding;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListTile( return ListTile(
leading: Padding( leading: Padding(
padding: const EdgeInsets.only( padding: (padding == null)
right: 10, ? const EdgeInsets.only(
left: 19, right: 10,
), left: 19,
)
: padding!,
child: FaIcon( child: FaIcon(
icon, icon,
size: iconSize, size: iconSize,

View file

@ -68,6 +68,8 @@ class BetterText extends StatelessWidget {
style: const TextStyle( style: const TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 17, fontSize: 17,
decoration: TextDecoration.none,
fontWeight: FontWeight.normal,
), ),
); );
} }