fix newlines, wip edits

This commit is contained in:
Henry Hiles 2025-12-27 23:42:30 -05:00
commit 9663995114
No known key found for this signature in database
6 changed files with 79 additions and 33 deletions

View file

@ -8,6 +8,7 @@ import "package:nexus/controllers/events_controller.dart";
import "package:nexus/helpers/extensions/event_to_message.dart"; import "package:nexus/helpers/extensions/event_to_message.dart";
import "package:nexus/helpers/extensions/list_to_messages.dart"; import "package:nexus/helpers/extensions/list_to_messages.dart";
import "package:fluttertagger/fluttertagger.dart" as tagger; import "package:fluttertagger/fluttertagger.dart" as tagger;
import "package:nexus/models/relation_type.dart";
class RoomChatController extends AsyncNotifier<ChatController> { class RoomChatController extends AsyncNotifier<ChatController> {
final Room room; final Room room;
@ -37,7 +38,10 @@ class RoomChatController extends AsyncNotifier<ChatController> {
(element) => element.id == event.relationshipEventId, (element) => element.id == event.relationshipEventId,
); );
if (oldMessage == null || message == null) return; if (oldMessage == null || message == null) return;
return await updateMessage(oldMessage, message); return await updateMessage(
oldMessage,
message.copyWith(id: oldMessage.id),
);
} }
if (message != null) { if (message != null) {
return await insertMessage(message); return await insertMessage(message);
@ -97,7 +101,8 @@ class RoomChatController extends AsyncNotifier<ChatController> {
Future<void> send( Future<void> send(
String message, { String message, {
required Iterable<tagger.Tag> tags, required Iterable<tagger.Tag> tags,
Message? replyTo, required RelationType relationType,
Message? relation,
}) async { }) async {
var taggedMessage = message; var taggedMessage = message;
@ -113,7 +118,10 @@ class RoomChatController extends AsyncNotifier<ChatController> {
await room.sendTextEvent( await room.sendTextEvent(
taggedMessage, taggedMessage,
inReplyTo: replyTo == null ? null : await room.getEventById(replyTo.id), editEventId: relationType == RelationType.edit ? relation?.id : null,
inReplyTo: (relationType == RelationType.reply && relation != null)
? await room.getEventById(relation.id)
: null,
); );
} }

View file

@ -25,8 +25,10 @@ extension EventToMessage on Event {
newContent?["formatted_body"] ?? newContent?["formatted_body"] ??
newContent?["body"] ?? newContent?["body"] ??
event.content["formatted_body"] ?? event.content["formatted_body"] ??
event.content["body"], event.content["body"] ??
"",
"reply": await replyEvent?.toMessage(mustBeText: true), "reply": await replyEvent?.toMessage(mustBeText: true),
"body": newContent?["body"] ?? event.content["body"],
"eventType": event.type, "eventType": event.type,
"avatarUrl": sender.avatarUrl.toString(), "avatarUrl": sender.avatarUrl.toString(),
"displayName": sender.displayName ?? sender.id, "displayName": sender.displayName ?? sender.id,
@ -69,7 +71,7 @@ extension EventToMessage on Event {
return switch (type) { return switch (type) {
EventTypes.Encrypted => asText.copyWith( EventTypes.Encrypted => asText.copyWith(
text: "Unable to decrypt message.", text: "Unable to decrypt message.",
metadata: {"formatted": "Unable to decrypt message.", ...metadata}, metadata: {...metadata, "formatted": "Unable to decrypt message."},
), ),
(EventTypes.Sticker || EventTypes.Message) => switch (messageType) { (EventTypes.Sticker || EventTypes.Message) => switch (messageType) {
(MessageTypes.Sticker || MessageTypes.Image) => Message.image( (MessageTypes.Sticker || MessageTypes.Image) => Message.image(

View file

@ -0,0 +1 @@
enum RelationType { edit, reply }

View file

@ -7,15 +7,18 @@ import "package:fluttertagger/fluttertagger.dart";
import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:matrix/matrix.dart"; import "package:matrix/matrix.dart";
import "package:nexus/controllers/room_chat_controller.dart"; import "package:nexus/controllers/room_chat_controller.dart";
import "package:nexus/models/relation_type.dart";
import "package:nexus/widgets/chat_page/mention_overlay.dart"; import "package:nexus/widgets/chat_page/mention_overlay.dart";
import "package:nexus/widgets/chat_page/reply_preview.dart"; import "package:nexus/widgets/chat_page/relation_preview.dart";
class ChatBox extends HookConsumerWidget { class ChatBox extends HookConsumerWidget {
final Message? replyToMessage; final Message? relatedMessage;
final RelationType relationType;
final VoidCallback onDismiss; final VoidCallback onDismiss;
final Room room; final Room room;
const ChatBox({ const ChatBox({
required this.replyToMessage, required this.relatedMessage,
required this.relationType,
required this.onDismiss, required this.onDismiss,
required this.room, required this.room,
super.key, super.key,
@ -28,14 +31,25 @@ class ChatBox extends HookConsumerWidget {
final triggerCharacter = useState(""); final triggerCharacter = useState("");
final query = useState(""); final query = useState("");
if (relationType == RelationType.edit &&
relatedMessage is TextMessage &&
controller.value.text.isEmpty) {
final text = (relatedMessage as TextMessage).text;
controller.value.text = relatedMessage?.replyToMessageId == null
? text
: text.split("\n\n").sublist(1).join("\n\n");
}
void send() { void send() {
ref ref
.watch(RoomChatController.provider(room).notifier) .watch(RoomChatController.provider(room).notifier)
.send( .send(
controller.value.formattedText, controller.value.formattedText,
replyTo: replyToMessage, relation: relatedMessage,
relationType: relationType,
tags: controller.value.tags, tags: controller.value.tags,
); );
onDismiss();
controller.value.text = ""; controller.value.text = "";
} }
@ -71,8 +85,9 @@ class ChatBox extends HookConsumerWidget {
borderRadius: BorderRadius.all(Radius.circular(12)), borderRadius: BorderRadius.all(Radius.circular(12)),
child: Column( child: Column(
children: [ children: [
ReplyPreview( RelationPreview(
replyToMessage: replyToMessage, relatedMessage: relatedMessage,
relationType: relationType,
onDismiss: onDismiss, onDismiss: onDismiss,
room: room, room: room,
), ),

View file

@ -4,14 +4,17 @@ import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:matrix/matrix.dart"; import "package:matrix/matrix.dart";
import "package:nexus/controllers/avatar_controller.dart"; import "package:nexus/controllers/avatar_controller.dart";
import "package:nexus/helpers/extensions/get_headers.dart"; import "package:nexus/helpers/extensions/get_headers.dart";
import "package:nexus/models/relation_type.dart";
import "package:nexus/widgets/avatar_or_hash.dart"; import "package:nexus/widgets/avatar_or_hash.dart";
class ReplyPreview extends ConsumerWidget { class RelationPreview extends ConsumerWidget {
final Message? replyToMessage; final Message? relatedMessage;
final RelationType relationType;
final VoidCallback onDismiss; final VoidCallback onDismiss;
final Room room; final Room room;
const ReplyPreview({ const RelationPreview({
required this.replyToMessage, required this.relatedMessage,
required this.relationType,
required this.onDismiss, required this.onDismiss,
required this.room, required this.room,
super.key, super.key,
@ -19,9 +22,9 @@ class ReplyPreview extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
if (relatedMessage == null) return SizedBox.shrink();
final theme = Theme.of(context); final theme = Theme.of(context);
if (replyToMessage == null) return SizedBox.shrink();
return Container( return Container(
color: theme.colorScheme.surfaceContainerHigh, color: theme.colorScheme.surfaceContainerHigh,
padding: EdgeInsets.symmetric(horizontal: 8), padding: EdgeInsets.symmetric(horizontal: 8),
@ -29,34 +32,40 @@ class ReplyPreview extends ConsumerWidget {
spacing: 8, spacing: 8,
children: [ children: [
SizedBox(width: 4), SizedBox(width: 4),
if (relationType == RelationType.edit)
Text(
"Editing message:",
style: TextStyle(fontWeight: FontWeight.bold),
),
AvatarOrHash( AvatarOrHash(
ref ref
.watch( .watch(
AvatarController.provider( AvatarController.provider(
replyToMessage!.metadata!["avatarUrl"], relatedMessage!.metadata!["avatarUrl"],
), ),
) )
.whenOrNull(data: (data) => data), .whenOrNull(data: (data) => data),
replyToMessage!.metadata!["displayName"].toString(), relatedMessage!.metadata!["displayName"].toString(),
headers: room.client.headers, headers: room.client.headers,
height: 16, height: 16,
), ),
Text( Text(
replyToMessage!.metadata?["displayName"] ?? relatedMessage!.metadata?["displayName"] ??
replyToMessage!.authorId, relatedMessage!.authorId,
style: theme.textTheme.labelMedium?.copyWith( style: theme.textTheme.labelMedium?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
Expanded( Expanded(
child: (replyToMessage is TextMessage) child: Text(
? Text( (relatedMessage is TextMessage)
(replyToMessage as TextMessage).text, ? (relatedMessage as TextMessage).text
overflow: TextOverflow.ellipsis, : relatedMessage?.metadata?["body"] ??
style: theme.textTheme.labelMedium, relatedMessage?.metadata?["eventType"],
maxLines: 1, overflow: TextOverflow.ellipsis,
) style: theme.textTheme.labelMedium,
: SizedBox(), maxLines: 1,
),
), ),
IconButton( IconButton(
onPressed: onDismiss, onPressed: onDismiss,

View file

@ -13,6 +13,7 @@ import "package:nexus/controllers/room_chat_controller.dart";
import "package:nexus/helpers/extensions/better_when.dart"; import "package:nexus/helpers/extensions/better_when.dart";
import "package:nexus/helpers/extensions/get_headers.dart"; import "package:nexus/helpers/extensions/get_headers.dart";
import "package:nexus/helpers/extensions/show_context_menu.dart"; import "package:nexus/helpers/extensions/show_context_menu.dart";
import "package:nexus/models/relation_type.dart";
import "package:nexus/widgets/chat_page/chat_box.dart"; import "package:nexus/widgets/chat_page/chat_box.dart";
import "package:nexus/widgets/chat_page/html/html.dart"; import "package:nexus/widgets/chat_page/html/html.dart";
import "package:nexus/widgets/chat_page/member_list.dart"; import "package:nexus/widgets/chat_page/member_list.dart";
@ -34,6 +35,7 @@ class RoomChat extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final replyToMessage = useState<Message?>(null); final replyToMessage = useState<Message?>(null);
final memberListOpened = useState<bool>(showMembersByDefault); final memberListOpened = useState<bool>(showMembersByDefault);
final relationType = useState(RelationType.reply);
final theme = Theme.of(context); final theme = Theme.of(context);
final danger = theme.colorScheme.error; final danger = theme.colorScheme.error;
@ -56,7 +58,10 @@ class RoomChat extends HookConsumerWidget {
List<PopupMenuEntry> getMessageOptions(Message message) => [ List<PopupMenuEntry> getMessageOptions(Message message) => [
PopupMenuItem( PopupMenuItem(
onTap: () => replyToMessage.value = message, onTap: () {
replyToMessage.value = message;
relationType.value = RelationType.reply;
},
child: ListTile( child: ListTile(
leading: Icon(Icons.reply), leading: Icon(Icons.reply),
title: Text("Reply"), title: Text("Reply"),
@ -64,7 +69,10 @@ class RoomChat extends HookConsumerWidget {
), ),
if (message.authorId == room.roomData.client.userID) if (message.authorId == room.roomData.client.userID)
PopupMenuItem( PopupMenuItem(
onTap: () {}, onTap: () {
replyToMessage.value = message;
relationType.value = RelationType.edit;
},
child: ListTile( child: ListTile(
leading: Icon(Icons.edit), leading: Icon(Icons.edit),
title: Text("Edit"), title: Text("Edit"),
@ -217,7 +225,8 @@ class RoomChat extends HookConsumerWidget {
bottomPadding: 72, bottomPadding: 72,
), ),
composerBuilder: (_) => ChatBox( composerBuilder: (_) => ChatBox(
replyToMessage: replyToMessage.value, relationType: relationType.value,
relatedMessage: replyToMessage.value,
onDismiss: () => onDismiss: () =>
replyToMessage.value = null, replyToMessage.value = null,
room: room.roomData, room: room.roomData,
@ -231,7 +240,8 @@ class RoomChat extends HookConsumerWidget {
MessageGroupStatus? groupStatus, MessageGroupStatus? groupStatus,
}) => FlyerChatTextMessage( }) => FlyerChatTextMessage(
customWidget: Html( customWidget: Html(
message.metadata?["formatted"] (message.metadata?["formatted"]
as String)
.replaceAllMapped( .replaceAllMapped(
RegExp( RegExp(
regexLink, regexLink,
@ -239,7 +249,8 @@ class RoomChat extends HookConsumerWidget {
), ),
(m) => (m) =>
"<a href=\"${m.group(0)!}\">${m.group(0)!}</a>", "<a href=\"${m.group(0)!}\">${m.group(0)!}</a>",
) + )
.replaceAll("\n", "<br/>") +
((message.editedAt != null) ((message.editedAt != null)
? "<sub edited>(edited)</sub>" ? "<sub edited>(edited)</sub>"
: ""), : ""),