Commit f604ef6df0c4
tarball3 files changed+108-93
@@ -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
- <li2385
- 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
- <li2401
- 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
- <li2421
- 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
<input24352393
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
+ <li2455
+ 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
+ <li2471
+ 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
+ <li2491
+ 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}​</div>24942505
<textarea24952506
id="composer"@@ -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; }@@ -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.name220
- ? `${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.topic224
- ? `${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;