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.