atagen

malice

public
Star0
Watch0

Commit f604ef6df0c4

tarball

atagen <boss@atagen.co>2026-10-24 10:12 UTC

3 files changed+108-93
web: resolve sender names from the roster and even out timeline strips

On a fresh login macltok hands out senders with no display_name, so rooms showed raw user ids until the members pane happened to load the roster. The roster now loads whenever a sender lacks a name and backs names in rows, replies, pins and system lines. systemLine read displayName from closure, which Svelte doesn't track, so system lines kept the stale id. Banners become flush strips like the pinned strip, the completion lists anchor to the input field, and the sent state is a tick that no longer adds a line under the message.
Modified file: web/src/App.svelte+72 additions−61 deletions
@@ -1240,6 +1240,15 @@
12401240 return out;
12411241 }
12421242
1243+ const rosterTried = new Set<string>();
1244+ $: fillMissingNames($selectedRoomId, items);
1245+ function fillMissingNames(roomId: string | null, list: TimelineItem[]): void {
1246+ if (!roomId || rosterTried.has(roomId) || $membersByRoom[roomId]) return;
1247+ if (!list.some((i) => i.kind === 'event' && !i.event.sender.display_name)) return;
1248+ rosterTried.add(roomId);
1249+ void loadMembers(roomId);
1250+ }
1251+
12431252 function senderAvatarFor(userId: string): string | null {
12441253 return avatarByUser[userId] ?? null;
12451254 }
@@ -1756,7 +1765,7 @@
17561765 {#if selectedRoom}
17571766 <div class="head-title">
17581767 <div class="head-room">
1759- <MxcAvatar mxc={selectedRoom.avatar} label={selectedRoom.name} size={36} shape={selectedRoom.space ? 'rounded' : 'circle'} />
1768+ <MxcAvatar mxc={selectedRoom.avatar} label={selectedRoom.name} size={36} shape="rounded" />
17601769 <div class="head-room-text">
17611770 <h1>{selectedRoom.name}</h1>
17621771 <div class="head-meta">
@@ -1769,10 +1778,12 @@
17691778 {#if selectedRoom.direct}
17701779 <span class="pill">direct</span>
17711780 {/if}
1772- <span class="conn" title={`Sync state: ${$connection}`}>
1773- <span class="dot {connectionClass($connection)}" aria-hidden="true"></span>
1774- {$connection}
1775- </span>
1781+ {#if $connection !== 'online'}
1782+ <span class="conn" title={`Sync state: ${$connection}`}>
1783+ <span class="dot {connectionClass($connection)}" aria-hidden="true"></span>
1784+ {$connection}
1785+ </span>
1786+ {/if}
17761787 </div>
17771788 </div>
17781789 </div>
@@ -2178,7 +2189,7 @@
21782189 <div class="pinned-head">
21792190 <svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path d="M9 3h6l-1 6 4 4v2h-5v6l-1 1-1-1v-6H6v-2l4-4z" fill="currentColor" /></svg>
21802191 <button type="button" class="pinned-latest" title="Jump to pinned message" on:click={() => jumpToPin(pins[0].event_id ?? '')}>
2181- <strong>{senderLabel(pins[0].sender)}</strong>
2192+ <strong>{people[pins[0].sender.user_id]?.name ?? senderLabel(pins[0].sender)}</strong>
21822193 <span>{contentPreview(pins[0].content)}</span>
21832194 </button>
21842195 {#if pins.length > 1}
@@ -2198,7 +2209,7 @@
21982209 {#each pins.slice(1) as pin (pin.event_id)}
21992210 <li>
22002211 <button type="button" on:click={() => jumpToPin(pin.event_id ?? '')}>
2201- <strong>{senderLabel(pin.sender)}</strong>
2212+ <strong>{people[pin.sender.user_id]?.name ?? senderLabel(pin.sender)}</strong>
22022213 <span>{contentPreview(pin.content)}</span>
22032214 </button>
22042215 {#if selectedRoom.can_pin}
@@ -2345,7 +2356,7 @@
23452356 {:else if replyingTo && replyingTo.details.kind === 'Ready'}
23462357 <div class="reply-bar" role="status">
23472358 <span class="reply-bar-label">
2348- Replying to <strong>{senderLabel(replyingTo.details.sender)}</strong>
2359+ Replying to <strong>{people[replyingTo.details.sender.user_id]?.name ?? senderLabel(replyingTo.details.sender)}</strong>
23492360 <span class="reply-bar-text">{contentPreview(replyingTo.details.content)}</span>
23502361 </span>
23512362 <button type="button" class="icon-btn" aria-label="Cancel reply" title="Cancel reply (Esc)" on:click={() => (replyingTo = null)}>
@@ -2378,59 +2389,6 @@
23782389 {/if}
23792390 <form class="composer" class:replying={replyingTo || editing} class:staging={currentStaged.length > 0} on:submit|preventDefault={() => send()}>
23802391 <label class="visually-hidden" for="composer">Message {selectedRoom.name}</label>
2381- {#if commandQuery !== null && commandCandidates.length > 0}
2382- <ul class="mention-finder" role="listbox" aria-label="Commands">
2383- {#each commandCandidates as name, i (name)}
2384- <li
2385- role="option"
2386- aria-selected={i === commandIndex}
2387- class:active={i === commandIndex}
2388- on:pointerdown|preventDefault={() => void pickCommand(name)}
2389- on:pointerenter={() => (commandIndex = i)}
2390- >
2391- <span class="mention-name">/{name}</span>
2392- <span class="mention-id">{COMMAND_USAGE[name]}</span>
2393- </li>
2394- {/each}
2395- </ul>
2396- {/if}
2397- {#if emoteQuery && emoteCandidates.length > 0}
2398- <ul class="mention-finder" role="listbox" aria-label="Emotes">
2399- {#each emoteCandidates as candidate, i (candidate.kind + candidate.shortcode)}
2400- <li
2401- role="option"
2402- aria-selected={i === emoteIndex}
2403- class:active={i === emoteIndex}
2404- on:pointerdown|preventDefault={() => void pickEmote(candidate)}
2405- on:pointerenter={() => (emoteIndex = i)}
2406- >
2407- {#if candidate.kind === 'custom'}
2408- <img class="finder-emote" src={mediaUrl(candidate.mxc) ?? ''} alt="" />
2409- {:else}
2410- <span class="finder-emote"><EmojiText text={candidate.emoji} /></span>
2411- {/if}
2412- <span class="mention-name">:{candidate.shortcode}:</span>
2413- </li>
2414- {/each}
2415- </ul>
2416- {/if}
2417- {#if mentionQuery && mentionCandidates.length > 0}
2418- <ul class="mention-finder" role="listbox" aria-label="Mention someone">
2419- {#each mentionCandidates as person, i (person.user_id)}
2420- <li
2421- role="option"
2422- aria-selected={i === mentionIndex}
2423- class:active={i === mentionIndex}
2424- on:pointerdown|preventDefault={() => void pickMention(person)}
2425- on:pointerenter={() => (mentionIndex = i)}
2426- >
2427- <MxcAvatar mxc={person.avatar} label={person.name} size={22} />
2428- <span class="mention-name">{person.name}</span>
2429- <span class="mention-id">{person.user_id}</span>
2430- </li>
2431- {/each}
2432- </ul>
2433- {/if}
24342392 <input
24352393 type="file"
24362394 multiple
@@ -2490,6 +2448,59 @@
24902448 </div>
24912449 {/if}
24922450 <div class="composer-field">
2451+ {#if commandQuery !== null && commandCandidates.length > 0}
2452+ <ul class="mention-finder" role="listbox" aria-label="Commands">
2453+ {#each commandCandidates as name, i (name)}
2454+ <li
2455+ role="option"
2456+ aria-selected={i === commandIndex}
2457+ class:active={i === commandIndex}
2458+ on:pointerdown|preventDefault={() => void pickCommand(name)}
2459+ on:pointerenter={() => (commandIndex = i)}
2460+ >
2461+ <span class="mention-name">/{name}</span>
2462+ <span class="mention-id">{COMMAND_USAGE[name]}</span>
2463+ </li>
2464+ {/each}
2465+ </ul>
2466+ {/if}
2467+ {#if emoteQuery && emoteCandidates.length > 0}
2468+ <ul class="mention-finder" role="listbox" aria-label="Emotes">
2469+ {#each emoteCandidates as candidate, i (candidate.kind + candidate.shortcode)}
2470+ <li
2471+ role="option"
2472+ aria-selected={i === emoteIndex}
2473+ class:active={i === emoteIndex}
2474+ on:pointerdown|preventDefault={() => void pickEmote(candidate)}
2475+ on:pointerenter={() => (emoteIndex = i)}
2476+ >
2477+ {#if candidate.kind === 'custom'}
2478+ <img class="finder-emote" src={mediaUrl(candidate.mxc) ?? ''} alt="" />
2479+ {:else}
2480+ <span class="finder-emote"><EmojiText text={candidate.emoji} /></span>
2481+ {/if}
2482+ <span class="mention-name">:{candidate.shortcode}:</span>
2483+ </li>
2484+ {/each}
2485+ </ul>
2486+ {/if}
2487+ {#if mentionQuery && mentionCandidates.length > 0}
2488+ <ul class="mention-finder" role="listbox" aria-label="Mention someone">
2489+ {#each mentionCandidates as person, i (person.user_id)}
2490+ <li
2491+ role="option"
2492+ aria-selected={i === mentionIndex}
2493+ class:active={i === mentionIndex}
2494+ on:pointerdown|preventDefault={() => void pickMention(person)}
2495+ on:pointerenter={() => (mentionIndex = i)}
2496+ >
2497+ <MxcAvatar mxc={person.avatar} label={person.name} size={22} />
2498+ <span class="mention-name">{person.name}</span>
2499+ <span class="mention-id">{person.user_id}</span>
2500+ </li>
2501+ {/each}
2502+ </ul>
2503+ {/if}
24932504 <div class="composer-marks" aria-hidden="true" bind:this={marksEl}>{#each composerMarks as part}{#if part.mark}<mark>{part.text}</mark>{:else}{part.text}{/if}{/each}&#8203;</div>
24942505 <textarea
24952506 id="composer"
Modified file: web/src/app.css+13 additions−13 deletions
@@ -247,15 +247,15 @@ input, textarea { font: inherit; }
247247 .rail-add { color: var(--muted); }
248248 .rail-add:hover { color: #fff; }
249249 .sec-banner {
250+ flex: none;
250251 display: flex;
251252 align-items: center;
252253 gap: 10px;
253- margin: 8px 12px 0;
254- padding: 5px 6px 5px 12px;
254+ min-height: 36px;
255+ padding: 0 8px 0 16px;
255256 background: color-mix(in srgb, var(--accent) 14%, transparent);
256- border: 1px solid var(--accent);
257+ border-bottom: 1px solid var(--accent);
257258 color: var(--text);
258- border-radius: var(--r-md);
259259 font-size: 12.5px;
260260 }
261261 .sec-banner.warn { background: var(--warn-bg); border-color: var(--warn-border); color: var(--warn-text); }
@@ -593,12 +593,11 @@ input, textarea { font: inherit; }
593593 padding: 2px 6px;
594594 }
595595 .conn-banner {
596- margin: 10px 16px 0;
597- padding: 7px 12px;
596+ margin: 0;
597+ padding: 9px 16px;
598598 background: var(--warn-bg);
599- border: 1px solid var(--warn-border);
599+ border-bottom: 1px solid var(--warn-border);
600600 color: var(--warn-text);
601- border-radius: var(--r-md);
602601 font-size: 12.5px;
603602 }
604603
@@ -673,9 +672,9 @@ input, textarea { font: inherit; }
673672 .composer .icon-btn[aria-expanded='true'], .composer .icon-btn[aria-pressed='true'] { color: var(--accent); }
674673 .mention-finder {
675674 position: absolute;
676- left: 16px;
677- right: 16px;
678- bottom: calc(100% + 4px);
675+ left: -1px;
676+ width: min(420px, calc(100% + 2px));
677+ bottom: calc(100% + 6px);
679678 z-index: 20;
680679 max-width: 420px;
681680 margin: 0;
@@ -926,7 +925,8 @@ input, textarea { font: inherit; }
926925 font-size: 13px;
927926 animation: fade-in var(--pop);
928927 }
929-.pinned-head { display: flex; align-items: center; gap: 8px; padding: 0 8px 0 14px; min-height: 36px; color: var(--accent); }
928+.pinned-head { display: flex; align-items: center; gap: 10px; padding: 0 8px 0 14px; min-height: 36px; color: var(--accent); }
929+.pinned-head > svg { flex: none; width: 34px; }
930930 .pinned-latest, .pinned-list li > button:first-child {
931931 flex: 1;
932932 min-width: 0;
@@ -961,6 +961,6 @@ input, textarea { font: inherit; }
961961 .pinned-count:hover { color: var(--text); }
962962 .pinned-count svg { transition: transform var(--pop); }
963963 .pinned-strip.open .pinned-count svg { transform: rotate(180deg); }
964-.pinned-list { list-style: none; margin: 0; padding: 0 8px 6px 36px; max-height: 40vh; overflow-y: auto; animation: pop-in var(--pop); }
964+.pinned-list { list-style: none; margin: 0; padding: 0 8px 6px 58px; max-height: 40vh; overflow-y: auto; animation: pop-in var(--pop); }
965965 .pinned-list li { display: flex; align-items: center; gap: 6px; border-top: 1px solid var(--border); }
966966 .pinned-strip .icon-btn { width: 26px; height: 26px; }
Modified file: web/src/lib/MessageRow.svelte+23 additions−19 deletions
@@ -81,7 +81,7 @@
8181 KnockDenied: 'had the request to join denied'
8282 };
8383
84- $: name = senderLabel(event.sender);
84+ $: name = event.sender.display_name ?? people[event.sender.user_id]?.name ?? event.sender.user_id;
8585 $: displayName = event.sender.name_ambiguous ? `${name} (${event.sender.user_id})` : name;
8686 $: readers = event.read_by.map((id) => people[id] ?? { user_id: id, name: id, avatar: null });
8787 $: interactive = event.content.kind === 'Message' || event.content.kind === 'Sticker' || event.content.kind === 'Poll';
@@ -101,10 +101,11 @@
101101 }
102102 }
103103
104+ $: sent = interactive && showSent && event.own && event.send_state.kind === 'Sent' && event.event_id !== null;
105+
104106 function sendLabel(state: SendState): string {
105107 if (state.kind === 'Failed') return state.will_retry ? 'failed — will retry' : 'failed — not delivered';
106108 if (state.kind === 'Sending') return 'sending…';
107- if (showSent && event.own && state.kind === 'Sent' && event.event_id) return '✓ sent';
108109 return '';
109110 }
110111
@@ -180,7 +181,7 @@
180181 }
181182
182183 function actorLabel(userId: string, name: string | null): string {
183- return name ?? userId;
184+ return name ?? people[userId]?.name ?? userId;
184185 }
185186
186187 function replyPreview(content: TimelineContent): string {
@@ -192,12 +193,12 @@
192193 return systemLine(content);
193194 }
194195
195- function systemLine(content: TimelineContent): string {
196+ function systemLine(content: TimelineContent, actor = displayName): string {
196197 switch (content.kind) {
197198 case 'Redacted':
198199 return 'Message deleted';
199200 case 'UnableToDecrypt':
200- return `Message from ${displayName} could not be decrypted`;
201+ return `Message from ${actor} could not be decrypted`;
201202 case 'Membership': {
202203 const who = actorLabel(content.user_id, content.display_name);
203204 return `${who} ${MEMBERSHIP_LABELS[content.change] ?? 'updated their membership'}`;
@@ -217,26 +218,26 @@
217218 }
218219 case 'RoomName':
219220 return content.name
220- ? `${displayName} changed the room name to "${content.name}"`
221- : `${displayName} removed the room name`;
221+ ? `${actor} changed the room name to "${content.name}"`
222+ : `${actor} removed the room name`;
222223 case 'RoomTopic':
223224 return content.topic
224- ? `${displayName} changed the topic to "${content.topic}"`
225- : `${displayName} removed the topic`;
225+ ? `${actor} changed the topic to "${content.topic}"`
226+ : `${actor} removed the topic`;
226227 case 'RoomAvatar':
227- return content.avatar ? `${displayName} changed the room avatar` : `${displayName} removed the room avatar`;
228+ return content.avatar ? `${actor} changed the room avatar` : `${actor} removed the room avatar`;
228229 case 'EncryptionEnabled':
229- return `${displayName} enabled encryption in this room`;
230+ return `${actor} enabled encryption in this room`;
230231 case 'RoomCreated':
231- return `${displayName} created the room`;
232+ return `${actor} created the room`;
232233 case 'RoomUpgraded':
233- return `${displayName} upgraded the room to a new version`;
234+ return `${actor} upgraded the room to a new version`;
234235 case 'OtherState':
235- return `${displayName} ${STATE_LABELS[content.event_type] ?? `sent a state event (${content.event_type})`}`;
236+ return `${actor} ${STATE_LABELS[content.event_type] ?? `sent a state event (${content.event_type})`}`;
236237 case 'Other':
237- return `${displayName} sent an event (${content.event_type})`;
238+ return `${actor} sent an event (${content.event_type})`;
238239 default:
239- return displayName;
240+ return actor;
240241 }
241242 }
242243 </script>
@@ -388,7 +389,7 @@
388389 on:click|stopPropagation={() => onJump?.(target)}
389390 >
390391 {#if event.reply_to.details.kind === 'Ready'}
391- <span class="reply-sender">{senderLabel(event.reply_to.details.sender)}</span>
392+ <span class="reply-sender">{event.reply_to.details.sender.display_name ?? people[event.reply_to.details.sender.user_id]?.name ?? senderLabel(event.reply_to.details.sender)}</span>
392393 <span class="reply-text"><EmojiText text={replyPreview(event.reply_to.details.content)} /></span>
393394 {:else if event.reply_to.details.kind === 'Pending'}
394395 Loading reply…
@@ -421,7 +422,7 @@
421422 {:else if event.content.kind === 'Poll'}
422423 <p class="text">📊 {event.content.question}</p>
423424 {:else}
424- <p class="system-line">{systemLine(event.content)}</p>
425+ <p class="system-line">{systemLine(event.content, displayName)}</p>
425426 {/if}
426427
427428 {#if interactive && event.reactions.length > 0}
@@ -451,6 +452,7 @@
451452 {/each}
452453 </div>
453454 {/if}
455+ {#if sent}<span class="sent-tick" title="Sent" aria-label="Sent">✓</span>{/if}
454456 {#if interactive && sendLabel(event.send_state)}
455457 <div class="msg-foot">
456458 <span class="sendstate" class:failed={event.send_state.kind === 'Failed'}>{sendLabel(event.send_state)}</span>
@@ -474,6 +476,7 @@
474476 .msg:hover .hover-time, .msg.selected .hover-time { opacity: 1; }
475477 .msg-foot { display: flex; align-items: center; gap: 8px; }
476478 .sendstate { font-size: 11px; color: var(--muted); }
479+ .sent-tick { position: absolute; right: 12px; bottom: 4px; font-size: 11px; color: var(--muted); }
477480 .sendstate.failed { color: var(--danger); }
478481 .link-previews { display: flex; flex-direction: column; gap: 6px; margin-top: 6px; max-width: 420px; }
479482 .avatar {
@@ -524,7 +527,8 @@
524527 .text.emote { font-style: italic; color: var(--muted); }
525528 .text.notice { color: var(--muted); }
526529 .system-line { color: var(--muted); font-size: 12.5px; font-style: italic; margin: 1px 0; }
527- .msg.system { opacity: 0.85; }
530+ .msg.system { opacity: 0.85; --msg-gap: 1.5px; }
531+ .msg.system .avatar-spacer { height: 0; padding-top: 0; }
528532 .reactions { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 4px; }
529533 .reaction {
530534 display: inline-flex; align-items: center; gap: 4px;