From 28ea654e1c5c0ff7d07cf7d89664ccb8983d5f1b Mon Sep 17 00:00:00 2001 From: Narasimha-sc <166327228+Narasimha-sc@users.noreply.github.com> Date: Tue, 26 May 2026 15:09:47 +0000 Subject: [PATCH] android, desktop, ios: drop stale mention map entry when @name is edited in place The compose-state mentions map was only pruned when fewer @name tokens were parsed from the text than were in the map. Editing an inserted @Name token in place (without re-picking from the picker) keeps the count at 1 while the parsed name no longer matches the stored key, so the stale entry was sent and the core rejected it with SEInvalidMention. Pruning now triggers whenever any map key is absent from the parsed mention names; the modified token is then sent as an unresolved formatted @ token. --- .../Views/Chat/Group/GroupMentions.swift | 2 +- .../common/views/chat/group/GroupMentions.kt | 2 +- .../2026-05-26-fix-invalid-mention-on-edit.md | 49 +++++++++++++++++++ 3 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 plans/2026-05-26-fix-invalid-mention-on-edit.md diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMentions.swift b/apps/ios/Shared/Views/Chat/Group/GroupMentions.swift index cdbed7fe30..2862e1099a 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMentions.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMentions.swift @@ -173,7 +173,7 @@ struct GroupMentionsView: View { let usedMentions: Set = Set(parsedMsg.compactMap { ft in if case let .mention(name) = ft.format { name } else { nil } }) - if usedMentions.count < composeState.mentions.count { + if composeState.mentions.contains(where: { !usedMentions.contains($0.key) }) { composeState = composeState.copy(mentions: composeState.mentions.filter({ usedMentions.contains($0.key) })) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMentions.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMentions.kt index aa737a02d3..dbb9ace70a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMentions.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMentions.kt @@ -318,7 +318,7 @@ private fun removeUnusedMentions(composeState: MutableState, parse } }.toSet() - if (usedMentions.size < composeState.value.mentions.size) { + if (composeState.value.mentions.keys.any { it !in usedMentions }) { composeState.value = composeState.value.copy( mentions = composeState.value.mentions.filterKeys { it in usedMentions } ) diff --git a/plans/2026-05-26-fix-invalid-mention-on-edit.md b/plans/2026-05-26-fix-invalid-mention-on-edit.md new file mode 100644 index 0000000000..04e7b1e50b --- /dev/null +++ b/plans/2026-05-26-fix-invalid-mention-on-edit.md @@ -0,0 +1,49 @@ +# Fix: "error store invalidMention" when an inserted @mention is edited in place + +## Symptom + +Rare "Error sending message: error store invalidMention" when sending a group +message that contains an @mention. + +## Root cause + +`removeUnusedMentions` in both `GroupMentions.kt` and `GroupMentions.swift` +prunes the compose-state `mentions` map only when the number of parsed +`@name` tokens is **strictly less** than the number of entries in the map: + +```kotlin +if (usedMentions.size < composeState.value.mentions.size) { ... } +``` + +When the user picks a member from the picker, the client inserts `@Name` into +the message text and records `mentions[Name] = memberId`. If the user then +**edits the inserted token in place** (e.g. fixes a typo, deletes one +character) without re-picking from the picker, the parser sees one mention +with the new name while the map still contains the old name. Both have size 1, +so the guard does not fire and the stale entry is sent to the core. + +The core's `getCIMentions` (`Internal.hs:267-271`) requires every key in the +client-supplied mentions map to also appear as a parsed `@name` token in the +message text, and throws `SEInvalidMention` otherwise. + +## Fix + +Prune whenever any map key is missing from the set of parsed mention names, +regardless of count. After pruning, the edited `@name` token becomes a plain +formatted mention with no member binding — exactly the desired behaviour for a +token the user has modified away from the originally picked member. + +- `apps/multiplatform/.../GroupMentions.kt:321` +- `apps/ios/.../GroupMentions.swift:176` + +Both clients had the identical bug; both are patched. + +## Manual reproduction + +1. In a group, type `@` and pick a member from the picker. Text becomes + `@Name ` and the mentions map gets `{Name → memberId}`. +2. Place the cursor inside the inserted name and add or delete one character + (`@Nam`, `@Names`, etc.) — do not use the picker. +3. Tap send. Before the fix: send fails with `error store invalidMention`. + After the fix: message is sent, the modified token appears as a plain + formatted `@` token with no member link.