starting with message info page

This commit is contained in:
otsmr 2025-10-27 01:11:18 +01:00
parent 8f8f2cabe0
commit 4f68d22e07
10 changed files with 342 additions and 111 deletions

View file

@ -191,13 +191,13 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
}
Future<void> 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<TwonlyDB> 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),
),
);
}

View file

@ -174,6 +174,7 @@
"submit": "Abschicken",
"close": "Schließen",
"cancel": "Abbrechen",
"edit": "Bearbeiten",
"ok": "Ok",
"now": "Jetzt",
"you": "Du",

View file

@ -307,6 +307,7 @@
"react": "React",
"reply": "Reply",
"copy": "Copy",
"edit": "Edit",
"delete": "Delete",
"info": "Info",
"ok": "Ok",

View file

@ -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:

View file

@ -560,6 +560,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get copy => 'Kopieren';
@override
String get edit => 'Bearbeiten';
@override
String get delete => 'Löschen';

View file

@ -555,6 +555,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get copy => 'Copy';
@override
String get edit => 'Edit';
@override
String get delete => 'Delete';

View file

@ -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<MemoryItem> galleryItems;
final void Function(String) scrollToMessage;
final void Function() onResponseTriggered;
final bool disableContextMenu;
@override
State<ChatListEntry> createState() => _ChatListEntryState();
@ -96,81 +98,84 @@ class _ChatListEntryState extends State<ChatListEntry> {
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),
);
}
}

View file

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

View file

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

View file

@ -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<MessageInfoView> createState() => _MessageInfoViewState();
}
class _MessageInfoViewState extends State<MessageInfoView> {
@override
void initState() {
initAsync();
super.initState();
}
Future<void> 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'),
],
),
),
);
}
}