web+ios+mac: mention typeahead shows the name you use; rename button reads 'Change the name you see' (#682)
Rows display MY name for the person (#577) with their published @name alongside — that's what actually inserts, since every member must resolve it. TypeaheadItem.mention gains a display value (name stays the published insert source); the web builder resolves through nicknames and carries goesBy for the muted hint. One label for the rename entry point on all platforms: 'Change the name you see'.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
7 files changed,
+45
−14
changelog/2026-07-11-mentions-show-your-names.md+4 −0ios/LChat/Views/ChatView.swift+3 −1ios/LChat/Views/NativeChatScreen.swift+3 −1ios/LChatCore/Sources/LChatCore/ComposerTypeahead.swift+11 −4ios/LChatCore/Sources/LChatCore/MemberProfileSheet.swift+1 −2ios/LChatCore/Sources/LChatCore/TypeaheadStrip.swift+7 −3web/static/shell.js+16 −3
diff --git a/changelog/2026-07-11-mentions-show-your-names.md b/changelog/2026-07-11-mentions-show-your-names.md| new file mode 100644 |
| index 0000000..c90676c |
| --- /dev/null |
| +++ b/changelog/2026-07-11-mentions-show-your-names.md |
| @@ -0,0 +1,4 @@ |
| +🤖 The @ mention menu now shows the names you use for people. If you call |
| +someone "Dermo", typing @der lists them as Dermo — with their chosen name |
| +alongside, since that's what appears in the message for everyone. Renaming |
| +someone also reads more plainly: the button is now "Change the name you see". |
diff --git a/ios/LChat/Views/ChatView.swift b/ios/LChat/Views/ChatView.swift| index 7cbfb4b..098582d 100644 |
| --- a/ios/LChat/Views/ChatView.swift |
| +++ b/ios/LChat/Views/ChatView.swift |
| @@ -402,7 +402,9 @@ struct ChatView: View { |
| switch item { |
| case .command(let name, _): return name |
| case .emoji(let glyph, let code): return "\(glyph) :\(code):" |
| - case .mention(let name, _): return "@" + name |
| + // The chip shows the name I use for them (#577); the pick still |
| + // inserts the published @name (item.insert). |
| + case .mention(_, _, let display): return "@" + display |
| } |
| } |
diff --git a/ios/LChat/Views/NativeChatScreen.swift b/ios/LChat/Views/NativeChatScreen.swift| index bc0490a..34b035d 100644 |
| --- a/ios/LChat/Views/NativeChatScreen.swift |
| +++ b/ios/LChat/Views/NativeChatScreen.swift |
| @@ -1421,7 +1421,9 @@ struct NativeChatScreen: View { |
| switch item { |
| case .command(let name, _): return name |
| case .emoji(let glyph, let code): return "\(glyph) :\(code):" |
| - case .mention(let name, _): return "@" + name |
| + // The chip shows the name I use for them (#577); the pick still |
| + // inserts the published @name (item.insert). |
| + case .mention(_, _, let display): return "@" + display |
| } |
| } |
diff --git a/ios/LChatCore/Sources/LChatCore/ComposerTypeahead.swift b/ios/LChatCore/Sources/LChatCore/ComposerTypeahead.swift| index 009bcf5..627cb5a 100644 |
| --- a/ios/LChatCore/Sources/LChatCore/ComposerTypeahead.swift |
| +++ b/ios/LChatCore/Sources/LChatCore/ComposerTypeahead.swift |
| @@ -55,14 +55,17 @@ public func composerToken(_ text: String, caret caretIn: Int? = nil) -> Composer |
| public enum TypeaheadItem: Equatable, Identifiable { |
| case command(name: String, desc: String) |
| case emoji(glyph: String, code: String) |
| - case mention(name: String, userId: String) |
| + // `name` is the PUBLISHED name — what the mention inserts, since every |
| + // member must resolve it; `display` is the name THIS member shows for the |
| + // person (per-viewer names, #577) — what the row renders. |
| + case mention(name: String, userId: String, display: String) |
| /// What a pick splices into the draft, replacing token.start…caret. |
| public var insert: String { |
| switch self { |
| case .command(let name, _): return name + " " |
| case .emoji(let glyph, _): return glyph |
| - case .mention(let name, _): return "@" + name + " " |
| + case .mention(let name, _, _): return "@" + name + " " |
| } |
| } |
| @@ -70,7 +73,7 @@ public enum TypeaheadItem: Equatable, Identifiable { |
| switch self { |
| case .command(let name, _): return "c|" + name |
| case .emoji(_, let code): return "e|" + code |
| - case .mention(_, let userId): return "m|" + userId |
| + case .mention(_, let userId, _): return "m|" + userId |
| } |
| } |
| } |
| @@ -130,7 +133,11 @@ public func typeaheadItems(_ token: ComposerToken, roster: [RosterEntry], |
| && (q.isEmpty || $0.label.lowercased().contains(q) |
| || (displayNames[$0.user_id]?.lowercased().contains(q) ?? false)) } |
| .prefix(8) |
| - .map { .mention(name: $0.label, userId: $0.user_id) } |
| + .map { e -> TypeaheadItem in |
| + let shown = displayNames[e.user_id] ?? "" |
| + return .mention(name: e.label, userId: e.user_id, |
| + display: shown.isEmpty ? e.label : shown) |
| + } |
| } |
| } |
diff --git a/ios/LChatCore/Sources/LChatCore/MemberProfileSheet.swift b/ios/LChatCore/Sources/LChatCore/MemberProfileSheet.swift| index 795402c..fb2847c 100644 |
| --- a/ios/LChatCore/Sources/LChatCore/MemberProfileSheet.swift |
| +++ b/ios/LChatCore/Sources/LChatCore/MemberProfileSheet.swift |
| @@ -85,8 +85,7 @@ public struct MemberProfileSheet<AvatarView: View>: View { |
| .foregroundStyle(.secondary) |
| } |
| - Button(store.nicknames[uid]?.name.isEmpty == false |
| - ? "Change what you call them" : "Call them something else") { |
| + Button("Change the name you see") { |
| draft = shownName |
| editing = true |
| draftFocused = true |
diff --git a/ios/LChatCore/Sources/LChatCore/TypeaheadStrip.swift b/ios/LChatCore/Sources/LChatCore/TypeaheadStrip.swift| index b9d836e..18c3ef4 100644 |
| --- a/ios/LChatCore/Sources/LChatCore/TypeaheadStrip.swift |
| +++ b/ios/LChatCore/Sources/LChatCore/TypeaheadStrip.swift |
| @@ -67,10 +67,14 @@ public struct TypeaheadStrip: View { |
| .frame(width: 24) |
| Text(":" + code + ":").font(.system(size: 15)).foregroundStyle(.secondary) |
| } |
| - case .mention(let name, let userId): |
| + case .mention(let name, let userId, let display): |
| HStack(spacing: 8) { |
| - MentionAvatar(name: name, source: mentionAvatar?(userId)) |
| - Text(name).font(.system(size: 15, weight: .medium)) |
| + MentionAvatar(name: display, source: mentionAvatar?(userId)) |
| + Text(display).font(.system(size: 15, weight: .medium)) |
| + // I call them something else — say whose @name will insert. |
| + if display != name { |
| + Text(name).font(.system(size: 13)).foregroundStyle(.secondary) |
| + } |
| } |
| } |
| } |
diff --git a/web/static/shell.js b/web/static/shell.js| index 4a6d1e0..ef5bee0 100644 |
| --- a/web/static/shell.js |
| +++ b/web/static/shell.js |
| @@ -6612,7 +6612,7 @@ function viewProfile(uid) { |
| // Per-viewer names (#577), people only: when I call them something of my |
| // own, their published name shows beneath; a fresh rename of theirs offers |
| - // the keep/adopt choice; and "Call them something else" edits my name for |
| + // the keep/adopt choice; and "Change the name you see" edits my name for |
| // them (clearing it follows their published name again). |
| if (!e.is_agent && uid !== myUserId && e.public_key) { |
| const nickArea = document.createElement("div"); |
| @@ -6644,7 +6644,7 @@ function viewProfile(uid) { |
| } |
| const edit = document.createElement("button"); |
| edit.className = "linkish"; |
| - edit.textContent = nick && nick.name ? "Change what you call them" : "Call them something else"; |
| + edit.textContent = "Change the name you see"; |
| edit.onclick = () => { |
| nickArea.textContent = ""; |
| const input = document.createElement("input"); |
| @@ -8580,7 +8580,13 @@ function typeaheadItems(tok) { |
| // rosterMatches: typing the name YOU call someone finds them too — but |
| // what inserts is always the published @name every member can resolve. |
| .filter((e) => e.user_id !== myUserId && e.label && rosterMatches(e, q)) |
| - .map((e) => ({ name: e.label, userId: e.user_id, insert: "@" + e.label + " " })), |
| + .map((e) => { |
| + // Rows show the name I use for them (#577); the insert stays the |
| + // published @name every member can resolve — goesBy says so when |
| + // the two differ. |
| + const shown = displayNameFor(nicknames[e.user_id], e.label); |
| + return { name: shown, goesBy: shown === e.label ? "" : e.label, userId: e.user_id, insert: "@" + e.label + " " }; |
| + }), |
| ...appMatches, |
| ].slice(0, 8); |
| } |
| @@ -8621,6 +8627,13 @@ function renderTypeahead() { |
| n.className = "ta-name"; |
| n.textContent = it.name; |
| row.appendChild(n); |
| + // I call them something else — say whose @name will insert. |
| + if (it.goesBy) { |
| + const d = document.createElement("span"); |
| + d.className = "ta-desc"; |
| + d.textContent = "@" + it.goesBy; |
| + row.appendChild(d); |
| + } |
| } |
| // pointerdown fires before the textarea blurs, so the tap isn't lost. |
| row.addEventListener("pointerdown", (e) => { |