eleven / ocho Public

web: member-list rows open the profile card; Message moves onto the card (#648)

The contacts-app convention — and the missing group path to per-viewer
names (#577): the card is where you rename someone and answer their
rename notice, but it was only reachable from a DM header. Tapping a
member now shows the card (no more accidental DMs from a stray tap);
Message on the card opens/creates the DM (shared openOrCreateDM, also
deduping the mention-menu copy). Hidden inside their own DM.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell 03b49d9f2289428c225a1b3ed073125de15bcc98 parent 099e52a
2 files changed, +43 −26
  • changelog/2026-07-11-tap-a-member-see-their-card.md +4 −0
  • web/static/shell.js +39 −26
diff --git a/changelog/2026-07-11-tap-a-member-see-their-card.md b/changelog/2026-07-11-tap-a-member-see-their-card.md
new file mode 100644
index 0000000..b5418e5
--- /dev/null
+++ b/changelog/2026-07-11-tap-a-member-see-their-card.md
@@ -0,0 +1,4 @@
+🤖 Tapping someone in a group's member list now shows their profile card —
+their photo, what they're about, and the place to choose what you call them —
+instead of instantly starting a chat. A Message button on the card starts the
+chat when that's what you wanted.
diff --git a/web/static/shell.js b/web/static/shell.js
index 14349c5..bfd9816 100644
--- a/web/static/shell.js
+++ b/web/static/shell.js
@@ -6177,6 +6177,21 @@ async function editGroup() {
input.focus();
}
+// Open the DM with someone, creating it on first use — the one "Message them"
+// behaviour behind the profile card, the member sheet, and the mention menu.
+async function openOrCreateDM(uid) {
+ const dm = existingDM(uid);
+ if (dm) return openThread(dm.id); // already have a DM — reopen it, don't start a second
+ try {
+ const id = await createThread([uid], { group: false });
+ await loadThreads();
+ renderThreadList();
+ openThread(id);
+ } catch (e) {
+ console.error(e);
+ }
+}
+
function showMembers() {
const t = threads.find((x) => x.id === activeId);
if (!t) return;
@@ -6194,7 +6209,9 @@ function showMembers() {
const row = document.createElement("div");
row.className = "list-row" + (me ? " static" : "");
- // The identity area (avatar + name); tapping a member opens/reopens a DM.
+ // The identity area (avatar + name); tapping a member opens their profile
+ // card (the contacts-app convention) — Message lives on the card, so a
+ // stray tap no longer starts a DM you didn't want.
const main = document.createElement(me ? "div" : "button");
main.className = "member-main";
main.appendChild(avatarEl(name, () => avatarFor(uid)));
@@ -6228,21 +6245,9 @@ function showMembers() {
}
main.appendChild(idcol);
if (!me)
- main.onclick = async () => {
+ main.onclick = () => {
backdrop.remove();
- const dm = existingDM(uid);
- if (dm) {
- openThread(dm.id); // already have a DM — reopen it, don't start a second
- return;
- }
- try {
- const id = await createThread([uid], { group: false });
- await loadThreads();
- renderThreadList();
- openThread(id);
- } catch (e) {
- console.error(e);
- }
+ viewProfile(uid);
};
row.appendChild(main);
@@ -6375,6 +6380,23 @@ function viewProfile(uid) {
p.textContent = blurb;
card.appendChild(p);
}
+
+ // Message them — the card's action when it was reached from somewhere other
+ // than their DM (the member sheet, a mention). Inside their DM it's noise.
+ const dm = existingDM(uid);
+ if (uid !== myUserId && !(dm && dm.id === activeId)) {
+ const row = document.createElement("div");
+ row.className = "row";
+ row.style.cssText = "justify-content:center;margin-top:14px";
+ const msg = document.createElement("button");
+ msg.textContent = `Message ${displayNameFor(nicknames[uid], e.label) || "them"}`;
+ msg.onclick = () => {
+ backdrop.remove();
+ openOrCreateDM(uid);
+ };
+ row.appendChild(msg);
+ card.appendChild(row);
+ }
}
// A DM partner renamed themself while I hold my own name for them (#577): a
@@ -8268,18 +8290,9 @@ function openMentionMenu(entry, anchor) {
const msg = document.createElement("button");
msg.className = "mention-menu-item";
msg.textContent = `Message ${entry.label}`;
- msg.onclick = async () => {
+ msg.onclick = () => {
pop.remove();
- const dm = existingDM(entry.user_id);
- if (dm) return openThread(dm.id);
- try {
- const id = await createThread([entry.user_id], { group: false });
- await loadThreads();
- renderThreadList();
- openThread(id);
- } catch (e) {
- console.error(e);
- }
+ openOrCreateDM(entry.user_id);
};
pop.appendChild(msg);
document.body.appendChild(pop);