A name is not a person, so show what is

The client half of IRC learning who is in a room. A member now carries the
hostmask and services account the network gave, and whether the network calls
them a program.

Hovering a name says what is actually known: the account they are identified
as, and the mask. Both absent where the server never said, which is most
networks for most people - so the tooltip stays the nick it always was rather
than growing empty labels.

The bot badge is kept quiet on purpose. It sits beside every one of a bot's
lines, and a badge that loud at that frequency reads as an alert rather than a
fact.

Part of #148, #152, #154.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-10 16:11:59 -04:00
co-authored by Claude Opus 5
parent 899ac140bb
commit 4fd3bc693c
4 changed files with 53 additions and 2 deletions
+1 -1
Submodule nobilis updated: e10fe489f0...de0ad864dc
+9
View File
@@ -4625,6 +4625,15 @@ video.lightbox-media {
color: var(--primary);
}
/* Not a rank and not a warning - a fact about what is behind the name. Kept
quiet: it belongs beside every one of a bot's lines, and a badge that loud
at that frequency would read as an alert. */
.sender-badge.bot {
background: var(--surface-container-high);
color: var(--surface-variant-text);
letter-spacing: 0.04em;
}
/* The one badge that is a warning rather than a title: this message came
from a device nobody has vouched for. Coloured like an error and left
uncoloured nowhere else, because a doubt about who sent something is the
+25 -1
View File
@@ -35,6 +35,22 @@ export type ModerationAction = 'kick' | 'ban' | 'mute' | 'op' | 'deop' | 'voice'
* person reads the same way in both places. An unfamiliar word keeps its own
* first letters rather than being dropped.
*/
/**
* What hovering a member says about them.
*
* Everything the network has actually told us, and nothing it has not. On IRC
* that is now a hostmask and a services account where the server offered them
* — the two facts that say which *person* a nick belongs to — and on every
* other service it stays what it was.
*/
function memberTitle(member: Member): string {
const lines = [member.nick]
if (member.account) lines.push(`identified as ${member.account}`)
if (member.host) lines.push(member.host)
if (lines.length === 1 && member.userId) return member.userId
return lines.join('\n')
}
function shortBadge(prefix: string): string {
const short: Record<string, string> = {
broadcaster: 'HOST',
@@ -540,7 +556,7 @@ function MemberRow({
className={classes('nick-row', member.away && 'away', blocked && 'blocked')}
onContextMenu={open}
onClick={onMention}
title={member.userId || member.nick}
title={memberTitle(member)}
>
{/* IRC's rank is one character and sits in a fixed slot before the
name. Kick's is a word - "moderator", "subscriber" - which
@@ -558,6 +574,14 @@ function MemberRow({
{shortBadge(member.prefix)}
</span>
)}
{/* The network's own word for it, not a guess from the name. Drawn
after the nick like any other badge, and only where the network
actually said so. */}
{member.bot && (
<span className="sender-badge small bot" title="The network says this one is a program">
BOT
</span>
)}
{blocked && <Icon name="block" size={12} />}
</button>
{menu && <ContextMenu x={menu.x} y={menu.y} entries={entries} onClose={close} />}
+18
View File
@@ -471,6 +471,24 @@ export interface Member {
prefix?: string
away?: boolean
userId?: string
/**
* `user@host`, where the network says. IRC only, and absent until somebody
* has told us — a WHO on join, `userhost-in-names`, or a line from them.
*
* Carried because it is what identifies a *person* rather than a name: a
* nick is theirs only while they hold it, and an ignore or a ban keyed on
* one is undone the moment they change it.
*/
host?: string
/**
* The services account they are signed in to, where the network says.
*
* Distinct from the nick: anybody can take a free nick, and only the
* account holder can be identified to it.
*/
account?: string
/** The network's own word that this one is a program, not a person. */
bot?: boolean
/** The roles this person holds here, by name. Discord only. */
roles?: string[]
powerLevel?: number