allow editing in/out attachments from messages

This commit is contained in:
Henry Hiles 2026-11-02 17:14:06 -05:00
commit e398043a49
Signed by: Henry-Hiles
SSH key fingerprint: SHA256:VKQUdS31Q90KvX7EkKMHMBpUspcmItAh86a+v7PGiIs
4 changed files with 71 additions and 20 deletions

View file

@ -29,6 +29,8 @@ class AttachmentController(final String roomId)
state = (filename, content); state = (filename, content);
} }
void set((String, MessageContent?)? attachment) => state = attachment;
static final provider = NotifierProvider.family static final provider = NotifierProvider.family
.autoDispose<AttachmentController, (String, MessageContent?)?, String>( .autoDispose<AttachmentController, (String, MessageContent?)?, String>(
AttachmentController.new, AttachmentController.new,

View file

@ -8,6 +8,7 @@ import "package:fluttertagger/fluttertagger.dart";
import "package:nexus/controllers/attachment.dart"; import "package:nexus/controllers/attachment.dart";
import "package:nexus/controllers/client.dart"; import "package:nexus/controllers/client.dart";
import "package:nexus/controllers/rooms.dart"; import "package:nexus/controllers/rooms.dart";
import "package:nexus/helpers/extensions/is_media.dart";
import "package:nexus/helpers/extensions/send_failed.dart"; import "package:nexus/helpers/extensions/send_failed.dart";
import "package:nexus/models/content/content.dart"; import "package:nexus/models/content/content.dart";
import "package:nexus/models/content/image_pack.dart"; import "package:nexus/models/content/image_pack.dart";
@ -231,22 +232,24 @@ class RoomChatController(final ChatParams info)
required RelationType relationType, required RelationType relationType,
int? relatedEventRowId, int? relatedEventRowId,
}) async { }) async {
Content? baseContent; final events = ref.read(RoomsController.provider)[info.roomId]?.events;
final relatedEvent = relatedEventRowId == null final relatedEvent = relatedEventRowId == null
? null ? null
: ref.read( : events?[relatedEventRowId];
RoomsController.provider.select(
(rooms) => rooms[info.roomId]?.events[relatedEventRowId],
),
);
if (relationType == .edit && relatedEventRowId != null) { final attachmentProvider = AttachmentController.provider(info.roomId);
baseContent = relatedEvent?.content; final attachment = ref.read(attachmentProvider)?.$2;
} else { ref.invalidate(attachmentProvider);
final provider = AttachmentController.provider(info.roomId);
baseContent = ref.read(provider)?.$2; // Edits of media use the attachment, which starts as the current media
ref.invalidate(provider); final latestContent = relatedEvent == null
} ? null
: (events?[relatedEvent.lastEditRowId] ?? relatedEvent).content;
final baseContent =
attachment ??
(relationType == .edit && latestContent?.isMedia == false
? latestContent
: null);
// Replaces each formatted tag, like "@id#name#", with its ID // Replaces each formatted tag, like "@id#name#", with its ID
final taggedMessage = tags.fold( final taggedMessage = tags.fold(

View file

@ -0,0 +1,12 @@
import "package:nexus/models/content/content.dart";
import "package:nexus/models/content/message.dart";
extension IsMedia on Content {
bool get isMedia => switch (this) {
ImageMessageContent() ||
VideoMessageContent() ||
AudioMessageContent() ||
FileMessageContent() => true,
_ => false,
};
}

View file

@ -3,6 +3,8 @@ import "dart:io";
import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:nexus/controllers/rooms.dart"; import "package:nexus/controllers/rooms.dart";
import "package:nexus/widgets/emoji_picker.dart"; import "package:nexus/widgets/emoji_picker.dart";
import "package:nexus/helpers/extensions/get_filename.dart";
import "package:nexus/helpers/extensions/is_media.dart";
import "package:nexus/helpers/extensions/show_adaptive_sheet.dart"; import "package:nexus/helpers/extensions/show_adaptive_sheet.dart";
import "package:nexus/widgets/adaptive_menu_button.dart"; import "package:nexus/widgets/adaptive_menu_button.dart";
import "package:material_ui/material_ui.dart"; import "package:material_ui/material_ui.dart";
@ -111,6 +113,8 @@ class const Composer(
); );
final textBefore = useState<String?>(null); final textBefore = useState<String?>(null);
// Wrapped, since no attachment is a draft too
final attachmentBefore = useState<((String, MessageContent?)?,)?>(null);
/// The sticker to send, which replaces the text field until it is sent or /// The sticker to send, which replaces the text field until it is sent or
/// removed, as stickers can't have a caption /// removed, as stickers can't have a caption
@ -132,16 +136,24 @@ class const Composer(
final isEditing = relationType == .edit && relatedEvent != null; final isEditing = relationType == .edit && relatedEvent != null;
useEffect(() { useEffect(() {
final attachmentProvider = AttachmentController.provider(roomId);
if (relationType == .edit && relatedEvent != null) { if (relationType == .edit && relatedEvent != null) {
textBefore.value ??= controller.text; textBefore.value ??= controller.text;
attachmentBefore.value ??= (ref.read(attachmentProvider),);
sticker.value = null; sticker.value = null;
poll.value = null; poll.value = null;
location.value = null; location.value = null;
final content = relatedEvent.content;
final text = final text =
relatedEvent.localContent?.editSource ?? relatedEvent.localContent?.editSource ??
switch (relatedEvent.content) { switch (content) {
TextMessageContent(:final body) => body, TextMessageContent(:final body) ||
ImageMessageContent(:final body) ||
VideoMessageContent(:final body) ||
AudioMessageContent(:final body) ||
FileMessageContent(:final body) => body,
_ => "", _ => "",
}; };
@ -149,12 +161,34 @@ class const Composer(
text: text, text: text,
selection: .collapsed(offset: text.length), selection: .collapsed(offset: text.length),
); );
} else if (textBefore.value case final draft?) {
controller.value = .new( Future.microtask(
text: draft, () => ref
selection: .collapsed(offset: draft.length), .read(attachmentProvider.notifier)
.set(
content is MessageContent && content.isMedia
? (
content.filename ?? content.body ?? "Attachment",
content,
)
: null,
),
); );
textBefore.value = null; } else {
if (textBefore.value case final draft?) {
controller.value = .new(
text: draft,
selection: .collapsed(offset: draft.length),
);
textBefore.value = null;
}
if (attachmentBefore.value case (final draft,)) {
Future.microtask(
() => ref.read(attachmentProvider.notifier).set(draft),
);
attachmentBefore.value = null;
}
} }
return null; return null;