keep message drafts on room switches

this is for you courge
This commit is contained in:
Henry Hiles 2026-11-05 20:44:13 -05:00
commit e899bb3929
Signed by: Henry-Hiles
SSH key fingerprint: SHA256:VKQUdS31Q90KvX7EkKMHMBpUspcmItAh86a+v7PGiIs
4 changed files with 357 additions and 194 deletions

View file

@ -7,7 +7,9 @@ import "package:nexus/models/content/message.dart";
import "package:nexus/models/composer_params.dart";
import "package:path/path.dart";
class AttachmentController(final ComposerParams composer)
typedef AttachmentParams = ({ComposerParams composer, int? editingRowId});
class AttachmentController(final AttachmentParams params)
extends Notifier<(String, MessageContent?)?> {
@override
Null build() => null;
@ -19,7 +21,7 @@ class AttachmentController(final ComposerParams composer)
final isEncrypted = ref.read(
RoomsController.provider.select(
(value) =>
value[composer.roomId]
value[params.composer.roomId]
?.state[EventType.encryption.type]
?.isNotEmpty ==
true,
@ -35,10 +37,10 @@ class AttachmentController(final ComposerParams composer)
void set((String, MessageContent?)? attachment) => state = attachment;
static final provider = NotifierProvider.family
.autoDispose<
static final provider =
NotifierProvider.family<
AttachmentController,
(String, MessageContent?)?,
ComposerParams
AttachmentParams
>(AttachmentController.new);
}

View file

@ -95,10 +95,14 @@ class BotCommandDraftController(final ComposerParams composer)
}
}
void set(BotCommandDraft? draft) => state = draft;
void clear() => state = null;
static final provider = NotifierProvider.autoDispose
.family<BotCommandDraftController, BotCommandDraft?, ComposerParams>(
BotCommandDraftController.new,
);
static final provider =
NotifierProvider.family<
BotCommandDraftController,
BotCommandDraft?,
ComposerParams
>(BotCommandDraftController.new);
}

View file

@ -0,0 +1,93 @@
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/attachment.dart";
import "package:nexus/controllers/bot_command_draft.dart";
import "package:nexus/controllers/rooms.dart";
import "package:nexus/helpers/extensions/get_filename.dart";
import "package:nexus/helpers/extensions/is_media.dart";
import "package:nexus/helpers/extensions/with_latest_edit.dart";
import "package:nexus/models/composer_draft.dart";
import "package:nexus/models/composer_params.dart";
import "package:nexus/models/content/message.dart";
import "package:nexus/models/relation_type.dart";
class ComposerDraftController(final ComposerParams composer)
extends Notifier<ComposerDraft> {
@override
ComposerDraft build() => const .new();
BotCommandDraftController get _botCommand =>
ref.read(BotCommandDraftController.provider(composer).notifier);
void update(ComposerDraft Function(ComposerDraft draft) update) =>
state = update(state);
void relate(int rowId, RelationType type) {
if (type == .reply) {
_restoreBeforeEdit();
state = state.copyWith(relatedEvent: rowId, relationType: .reply);
return;
}
// Keeps the changes made so far
if (state.isEditing && state.relatedEvent == rowId) return;
final event = ref
.read(RoomsController.provider)[composer.roomId]
?.withLatestEdit(rowId);
final content = event?.content;
final text =
event?.localContent?.editSource ??
switch (content) {
TextMessageContent(:final body) ||
ImageMessageContent(:final body) ||
VideoMessageContent(:final body) ||
AudioMessageContent(:final body) ||
FileMessageContent(:final body) => body,
_ => "",
};
state = ComposerDraft(
text: text,
relatedEvent: rowId,
relationType: .edit,
beforeEdit:
state.beforeEdit ??
(
draft: state,
botCommand: ref.read(BotCommandDraftController.provider(composer)),
),
);
ref
.read(
AttachmentController.provider((
composer: composer,
editingRowId: rowId,
)).notifier,
)
.set(
content is MessageContent && content.isMedia
? (content.filename ?? content.body ?? "Attachment", content)
: null,
);
_botCommand.clear();
}
void dismiss() {
if (state.isEditing) return _restoreBeforeEdit();
state = state.copyWith(relatedEvent: null);
}
void _restoreBeforeEdit() {
if (state.beforeEdit case (:final draft, :final botCommand)) {
state = draft;
_botCommand.set(botCommand);
}
}
static final provider =
NotifierProvider.family<
ComposerDraftController,
ComposerDraft,
ComposerParams
>(ComposerDraftController.new);
}

View file

@ -404,7 +404,10 @@ class RoomChatController(final ChatParams info)
roomId: info.roomId,
threadRoot: info.threadRoot,
);
final attachmentProvider = AttachmentController.provider(composer);
final attachmentProvider = AttachmentController.provider((
composer: composer,
editingRowId: relationType == .edit ? relatedEventRowId : null,
));
final attachment = ref.read(attachmentProvider)?.$2;
ref.invalidate(attachmentProvider);
@ -418,11 +421,13 @@ class RoomChatController(final ChatParams info)
? 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(
text,
(message, tag) => message.replaceAllMapped(
RegExp(r"@+(" + RegExp.escape(tag.id) + r")(#[^#]*#)?"),
RegExp(
"${RegExp.escape(tag.triggerCharacter)}(${RegExp.escape(tag.id)})(#[^#]*#)?",
),
(match) => match.group(1)!,
),
);