eleven / messenger Public

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>
Paul Campbell 24124cec8c0b6d121044964451a82ee4f0b37aa2 parent 3542a7e
7 files changed, +45 −14
  • changelog/2026-07-11-mentions-show-your-names.md +4 −0
  • ios/LChat/Views/ChatView.swift +3 −1
  • ios/LChat/Views/NativeChatScreen.swift +3 −1
  • ios/LChatCore/Sources/LChatCore/ComposerTypeahead.swift +11 −4
  • ios/LChatCore/Sources/LChatCore/MemberProfileSheet.swift +1 −2
  • ios/LChatCore/Sources/LChatCore/TypeaheadStrip.swift +7 −3
  • web/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) => {