Badges belong to the byline, not to the message

They were siblings of the name, and the name is a block in the comfy and
bubble layouts - so the badges dropped to the next line and sat in with the
message text, reading as part of what was said.

Name and badges are one element now, so a layout that gives the name its
own line takes the badges with it. The compact log keeps the byline inline,
where the name has always run into the text.

The member panel is also named for what it actually is on Kick: there is no
viewer list to ask for, so it lists who has spoken recently and says so.
Kick's badges are words rather than sigils, mapped onto the same ranks the
list already groups by - one grouping for every protocol rather than each
growing its own.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-02 01:04:31 -04:00
co-authored by Claude Opus 5
parent b5d2b53c0c
commit b175861fa3
4 changed files with 46 additions and 9 deletions
+1 -1
Submodule nobilis updated: 0cd38dfabd...f78d18c85c
+15 -4
View File
@@ -470,9 +470,12 @@
/* A name above the bubble, not inside it: it labels who is speaking, and
inside it would read as part of what they said. */
.message-row.bubbles .message-from {
.message-row.bubbles .message-byline {
display: block;
margin: 0 0 2px;
}
.message-row.bubbles .message-from {
font-size: 12px;
}
@@ -568,12 +571,20 @@
word-break: break-word;
}
.message-from {
font-weight: 600;
/* The name and whatever sits with it. One element so a layout that puts the
name on its own line takes the badges with it - as siblings they dropped to
the next line and read as part of the message. */
.message-byline {
margin-right: var(--spacing-s);
}
.message-row.comfy .message-from {
.message-from {
font-weight: 600;
}
/* Comfy and bubble layouts give the byline its own line; the compact log
keeps it inline, where the name has always run into the text. */
.message-row.comfy .message-byline {
display: block;
margin-right: 0;
}
+6 -2
View File
@@ -367,8 +367,12 @@ export function MessageRow({
Badges beside it, because on Kick who is talking is half the
message: a moderator, a two-year subscriber and a stranger read
as three different things. */}
{/* Name and badges in one element, because the name is a block in
comfy and bubble layouts - badges as siblings of it fell to the
next line and sat in with the message text. They belong to the
byline, so they live in it. */}
{!grouped && !isSystem && !own && (
<>
<span className="message-byline">
<span className="message-from" style={{ color: message.senderColor || nickColor(message.from) }}>
{message.isAction ? `* ${message.from}` : message.from}
</span>
@@ -381,7 +385,7 @@ export function MessageRow({
{badgeLabel(badge)}
</span>
))}
</>
</span>
)}
{/* Said in words as well as in styling. Italics and a tinted edge
+24 -2
View File
@@ -28,6 +28,15 @@ import type { Member } from '../../../shared/wire'
*/
export type ModerationAction = 'kick' | 'ban' | 'mute' | 'op' | 'deop' | 'voice'
/**
* Why the list says what it says, for the one service where it needs saying.
*/
function rosterNote(service: string | undefined): string | undefined {
return service === 'kick'
? 'Kick has no viewer list — this is everybody who has spoken here recently'
: undefined
}
/**
* What the local user may do in an IRC channel, read off their own prefix.
*
@@ -95,7 +104,15 @@ export function NickList(): JSX.Element {
// Guard the empty prefix explicitly: indexOf('') is 0, which is the
// owner slot, so every unranked member - all of Sneedchat, and any
// ordinary IRC user - was being filed under Owners.
const rank = (m: Member): number => (m.prefix ? '~&@%+'.indexOf(m.prefix) : -1)
//
// Kick says the same thing in words rather than symbols, since its badges
// have names and no agreed sigils. Mapped onto the same ranks so one
// grouping serves both rather than each protocol growing its own.
const rank = (m: Member): number => {
if (!m.prefix) return -1
const named: Record<string, number> = { broadcaster: 0, moderator: 2, vip: 3, og: 4, founder: 4, subscriber: 4 }
return m.prefix in named ? named[m.prefix] : '~&@%+'.indexOf(m.prefix)
}
return [
{ label: 'Owners', members: filtered.filter((m) => rank(m) === 0).sort(byName) },
{ label: 'Moderators', members: filtered.filter((m) => rank(m) > 0 && rank(m) < 4).sort(byName) },
@@ -121,7 +138,12 @@ export function NickList(): JSX.Element {
}}
/>
) : (
<span className="small muted ellipsis">{members.length} members</span>
// Named for what the list actually is. Kick has no viewer list to
// ask for - it is built from who has spoken - and calling that
// "members" would claim to know something nobody here can know.
<span className="small muted ellipsis" title={rosterNote(account?.service)}>
{members.length} {account?.service === 'kick' ? 'recently talking' : 'members'}
</span>
)}
<IconButton
name={searchActive ? 'close' : 'search'}