Commit 445bf199a9cf
tarball17 files changed+94-76
@@ -1866,13 +1866,13 @@
18661866
<div class="head-room">18671867
<MxcAvatar mxc={selectedRoom.avatar} label={selectedRoom.name} size={36} shape="rounded" />18681868
<div class="head-room-text">1869
- <h1>{selectedRoom.name}</h1>1869
+ <h1><EmojiText text={selectedRoom.name} /></h1>18701870
<div class="head-meta">18711871
{#if selectedRoom.space}18721872
<span class="pill">space</span>18731873
{/if}18741874
{#if selectedRoom.encrypted}1875
- <span class="pill" title="End-to-end encrypted">🔒 encrypted</span>1875
+ <span class="pill" title="End-to-end encrypted"><EmojiText text="🔒 encrypted" /></span>18761876
{/if}18771877
{#if selectedRoom.direct}18781878
<span class="pill">direct</span>@@ -1894,7 +1894,7 @@
18941894
<MxcAvatar mxc={selectedSpace.avatar} label={selectedSpace.name} size={36} shape="rounded" />18951895
{/if}18961896
<div class="head-room-text">1897
- <h1>{selectedSpace?.name ?? 'Home'}</h1>1897
+ <h1><EmojiText text={selectedSpace?.name ?? 'Home'} /></h1>18981898
</div>18991899
</div>19001900
</div>@@ -2065,7 +2065,7 @@
20652065
{@const parentId = spaceParents.get(selectedSpace.room_id) ?? ''}20662066
<button type="button" class="room-row up-row" on:click={() => void selectSpace(parentId)}>20672067
<span aria-hidden="true">↑</span>2068
- <span class="room-name">{$rooms.find((r) => r.room_id === parentId)?.name ?? 'Parent space'}</span>2068
+ <span class="room-name"><EmojiText text={$rooms.find((r) => r.room_id === parentId)?.name ?? 'Parent space'} /></span>20692069
</button>20702070
{/if}20712071
{#if roomsLoading && $rooms.length === 0}@@ -2080,7 +2080,7 @@
20802080
{#if selectedSpaceId && spaceChildrenLoading}20812081
<p class="side-note">Loading channels…</p>20822082
{:else if selectedSpaceId && spaceChildrenError}2083
- <p class="side-note" role="alert">{spaceChildrenError}</p>2083
+ <p class="side-note" role="alert"><EmojiText text={spaceChildrenError} /></p>20842084
<div class="side-cta">20852085
<button type="button" on:click={() => selectedSpaceId && void selectSpace(selectedSpaceId)}>Retry</button>20862086
<button type="button" on:click={() => void selectHome()}>Back to Home</button>@@ -2104,7 +2104,7 @@
21042104
on:contextmenu|preventDefault={(ev) => openRoomMenu(ev, room)}21052105
>21062106
<MxcAvatar mxc={room.avatar} label={room.name} size={26} shape="rounded" />2107
- <span class="room-name">{room.name}</span>2107
+ <span class="room-name"><EmojiText text={room.name} /></span>21082108
<span21092109
class="invite-join"21102110
role="button"@@ -2140,7 +2140,7 @@
21402140
on:contextmenu|preventDefault={(ev) => openRoomMenu(ev, room)}21412141
>21422142
<MxcAvatar mxc={room.avatar} label={room.name} size={26} />2143
- <span class="room-name">{room.name}</span>2143
+ <span class="room-name"><EmojiText text={room.name} /></span>21442144
{#if roomUnread(room) > 0}21452145
<span21462146
class="badge"@@ -2171,7 +2171,7 @@
21712171
on:contextmenu|preventDefault={(ev) => openRoomMenu(ev, room)}21722172
>21732173
<MxcAvatar mxc={room.avatar} label={room.name} size={26} shape="rounded" />2174
- <span class="room-name">{room.name}</span>2174
+ <span class="room-name"><EmojiText text={room.name} /></span>21752175
{#if roomUnread(room) > 0}21762176
<span21772177
class="badge"@@ -2202,7 +2202,7 @@
22022202
on:contextmenu|preventDefault={(ev) => openRoomMenu(ev, room)}22032203
>22042204
<MxcAvatar mxc={room.avatar} label={room.name} size={26} shape="rounded" />2205
- <span class="room-name">{room.name}</span>2205
+ <span class="room-name"><EmojiText text={room.name} /></span>22062206
</button>22072207
<PinButton pinned={room.favourite} label={room.name} onToggle={() => void togglePin(room)} />22082208
</li>@@ -2211,7 +2211,7 @@
22112211
{/if}22122212
22132213
{#if query && visibleRooms.length === 0}2214
- <p class="side-note">No rooms match “{search.trim()}”.</p>2214
+ <p class="side-note">No rooms match “<EmojiText text={search.trim()} />”.</p>22152215
{/if}22162216
{/if}22172217
{/if}@@ -2227,7 +2227,7 @@
22272227
<span class="me-avatar" aria-hidden="true">{$sessionUser.slice(1, 2).toUpperCase()}</span>22282228
{/if}22292229
<span class="me-names">2230
- <span class="me-name">{profile?.display_name || $sessionUser}</span>2230
+ <span class="me-name"><EmojiText text={profile?.display_name || $sessionUser} /></span>22312231
{#if profile?.display_name && profile.display_name !== $sessionUser}<span class="me-id">{$sessionUser}</span>{/if}22322232
</span>22332233
</button>@@ -2252,7 +2252,7 @@
22522252
<div class="banners" aria-live="polite">22532253
{#each $banners as banner (banner.id)}22542254
<div class="banner" role="alert">2255
- <span>{banner.message}</span>2255
+ <span><EmojiText text={banner.message} /></span>22562256
<button22572257
type="button"22582258
aria-label="Dismiss error"@@ -2270,7 +2270,7 @@
22702270
<div class="empty" in:fade={{ duration: motionDuration(180) }}>22712271
<div class="empty-mark" aria-hidden="true">◈</div>22722272
{#if selectedSpace}2273
- <h1>{selectedSpace.name}</h1>2273
+ <h1><EmojiText text={selectedSpace.name} /></h1>22742274
{:else}22752275
<h1>Welcome to Malice</h1>22762276
{/if}@@ -2289,8 +2289,8 @@
22892289
<div class="pinned-head">22902290
<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>22912291
<button type="button" class="pinned-latest" title="Jump to pinned message" on:click={() => jumpToPin(pins[0].event_id ?? '')}>2292
- <strong>{people[pins[0].sender.user_id]?.name ?? senderLabel(pins[0].sender)}</strong>2293
- <span>{contentPreview(pins[0].content)}</span>2292
+ <strong><EmojiText text={people[pins[0].sender.user_id]?.name ?? senderLabel(pins[0].sender)} /></strong>2293
+ <span><EmojiText text={contentPreview(pins[0].content)} /></span>22942294
</button>22952295
{#if pins.length > 1}22962296
<button type="button" class="pinned-count" aria-expanded={pinsExpanded} on:click={() => (pinsExpanded = !pinsExpanded)}>@@ -2309,8 +2309,8 @@
23092309
{#each pins.slice(1) as pin (pin.event_id)}23102310
<li>23112311
<button type="button" on:click={() => jumpToPin(pin.event_id ?? '')}>2312
- <strong>{people[pin.sender.user_id]?.name ?? senderLabel(pin.sender)}</strong>2313
- <span>{contentPreview(pin.content)}</span>2312
+ <strong><EmojiText text={people[pin.sender.user_id]?.name ?? senderLabel(pin.sender)} /></strong>2313
+ <span><EmojiText text={contentPreview(pin.content)} /></span>23142314
</button>23152315
{#if selectedRoom.can_pin}23162316
<button type="button" class="icon-btn" aria-label="Unpin" title="Unpin" on:click={() => setPinned(pin.event_id ?? '', false)}>@@ -2341,10 +2341,10 @@
23412341
<span>Verification started</span>23422342
<button type="button" on:click={() => (dialog = 'security')}>Open verification</button>23432343
{:else if verificationBanner.kind === 'Failed'}2344
- <span>Verification failed: {verificationBanner.message}</span>2344
+ <span>Verification failed: <EmojiText text={verificationBanner.message} /></span>23452345
<button type="button" on:click={() => (dialog = 'security')}>Try again</button>23462346
{:else if verificationBanner.kind === 'Cancelled'}2347
- <span>Verification cancelled: {verificationBanner.reason}</span>2347
+ <span>Verification cancelled: <EmojiText text={verificationBanner.reason} /></span>23482348
<button type="button" on:click={() => (dialog = 'security')}>Open security</button>23492349
{:else}23502350
<span>Verification in progress</span>@@ -2353,16 +2353,16 @@
23532353
</div>23542354
{/if}23552355
{#if $selectedRoomId && openFailed[$selectedRoomId]}2356
- <p class="conn-banner error" role="alert">{openFailed[$selectedRoomId]}</p>2356
+ <p class="conn-banner error" role="alert"><EmojiText text={openFailed[$selectedRoomId]} /></p>23572357
{/if}23582358
23592359
{#if pinPreview}23602360
<div class="pin-preview" role="dialog" aria-label="Pinned message preview">23612361
<div class="pin-preview-head">2362
- <strong>{people[pinPreview.sender.user_id]?.name ?? senderLabel(pinPreview.sender)}</strong>2362
+ <strong><EmojiText text={people[pinPreview.sender.user_id]?.name ?? senderLabel(pinPreview.sender)} /></strong>23632363
<button type="button" class="icon-btn" aria-label="Close pinned message preview" on:click={() => (pinPreview = null)}>✕</button>23642364
</div>2365
- <p>{contentPreview(pinPreview.content)}</p>2365
+ <p><EmojiText text={contentPreview(pinPreview.content)} /></p>23662366
<button type="button" class="primary-btn" disabled={pinJumpBusy} on:click={() => void jumpToPin(pinPreview?.event_id ?? '')}>23672367
{pinJumpBusy ? 'Loading message…' : 'Jump to message'}23682368
</button>@@ -2448,7 +2448,7 @@
24482448
/>24492449
{#if p.status === 'failed'}24502450
<div class="pending-actions">2451
- <span class="pending-error" role="alert">{p.error}</span>2451
+ <span class="pending-error" role="alert"><EmojiText text={p.error ?? ''} /></span>24522452
<button type="button" class="ghost-btn" on:click={() => retryPending(p.id)}>Retry</button>24532453
<button type="button" class="ghost-btn danger" on:click={() => discardPending(p.id)}>Discard</button>24542454
</div>@@ -2462,7 +2462,7 @@
24622462
<div class="reply-bar" role="status">24632463
<span class="reply-bar-label">24642464
Editing message2465
- <span class="reply-bar-text">{editing.original}</span>2465
+ <span class="reply-bar-text"><EmojiText text={editing.original} /></span>24662466
</span>24672467
<button type="button" class="icon-btn" aria-label="Cancel edit" title="Cancel edit (Esc)" on:click={cancelEdit}>24682468
<span aria-hidden="true">✕</span>@@ -2471,8 +2471,8 @@
24712471
{:else if replyingTo && replyingTo.details.kind === 'Ready'}24722472
<div class="reply-bar" role="status">24732473
<span class="reply-bar-label">2474
- Replying to <strong>{people[replyingTo.details.sender.user_id]?.name ?? senderLabel(replyingTo.details.sender)}</strong>2475
- <span class="reply-bar-text">{contentPreview(replyingTo.details.content)}</span>2474
+ Replying to <strong><EmojiText text={people[replyingTo.details.sender.user_id]?.name ?? senderLabel(replyingTo.details.sender)} /></strong>2475
+ <span class="reply-bar-text"><EmojiText text={contentPreview(replyingTo.details.content)} /></span>24762476
</span>24772477
<button type="button" class="icon-btn" aria-label="Cancel reply" title="Cancel reply (Esc)" on:click={() => (replyingTo = null)}>24782478
<span aria-hidden="true">✕</span>@@ -2487,7 +2487,7 @@
24872487
{#if attachment.previewUrl}24882488
<img src={attachment.previewUrl} alt={attachment.file.name || 'Pasted image'} />24892489
{:else}2490
- <span class="staged-name">{attachment.file.name || 'attachment'}</span>2490
+ <span class="staged-name"><EmojiText text={attachment.file.name || 'attachment'} /></span>24912491
{/if}24922492
<button24932493
type="button"@@ -2610,7 +2610,7 @@
26102610
on:pointerenter={() => (mentionIndex = i)}26112611
>26122612
<MxcAvatar mxc={person.avatar} label={person.name} size={22} />2613
- <span class="mention-name">{person.name}</span>2613
+ <span class="mention-name"><EmojiText text={person.name} /></span>26142614
<span class="mention-id">{person.user_id}</span>26152615
</li>26162616
{/each}@@ -2652,7 +2652,7 @@
26522652
<div class="join-bar">26532653
<p>You are not a member of this room.</p>26542654
<button type="button" class="primary-btn" disabled={joinBusy} on:click={() => void join(selectedRoom.room_id)}>2655
- {joinBusy ? 'Joining…' : `Join ${selectedRoom.name}`}2655
+ <EmojiText text={joinBusy ? 'Joining…' : `Join ${selectedRoom.name}`} />26562656
</button>26572657
</div>26582658
{/if}@@ -2699,7 +2699,7 @@
26992699
>27002700
<MxcAvatar mxc={member.avatar} label={member.display_name || member.name || member.user_id} seed={member.user_id} size={28} />27012701
<span class="member-names">2702
- <span class="member-name">{member.display_name || member.name}</span>2702
+ <span class="member-name"><EmojiText text={member.display_name || member.name} /></span>27032703
<span class="member-id">{member.user_id}</span>27042704
</span>27052705
</button>@@ -10,6 +10,7 @@
1010
1111
<script lang="ts">1212
import { onMount, tick } from 'svelte';13
+ import EmojiText from './EmojiText.svelte';1314
1415
export let items: MenuItem[];1516
export let x: number;@@ -54,7 +55,7 @@
5455
{#each items as item}5556
{#if item.divider}<hr />{/if}5657
<button type="button" role="menuitem" class:danger={item.danger} disabled={item.disabled} on:click={() => pick(item)}>57
- {item.label}58
+ <EmojiText text={item.label} />5859
</button>5960
{/each}6061
</div>@@ -1,6 +1,7 @@
11
<script lang="ts">22
import { tick } from 'svelte';33
import { lightbox } from './lightbox';4
+ import EmojiText from './EmojiText.svelte';45
56
let closeBtn: HTMLButtonElement | null = null;67
let copied = false;@@ -46,7 +47,7 @@
4647
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->4748
<div class="lightbox" role="dialog" tabindex="-1" aria-modal="true" aria-label={$lightbox.filename} on:click|self={close}>4849
<div class="lightbox-bar">49
- <span class="lightbox-name">{$lightbox.filename}</span>50
+ <span class="lightbox-name"><EmojiText text={$lightbox.filename} /></span>5051
<button5152
type="button"5253
class="ghost-btn"@@ -1,5 +1,6 @@
11
<script lang="ts">22
import { messageMediaUrl } from './api';3
+ import EmojiText from './EmojiText.svelte';34
import type { LinkPreview } from './types';45
import type { UrlPreview } from './urls';56
@@ -27,15 +28,15 @@
2728
<a class="link-card" class:rich class:banner href={link.href} target="_blank" rel="noopener noreferrer">2829
{#if rich && preview}2930
<span class="body">30
- <span class="link-host">{preview.site_name ?? link.hostname}</span>31
- {#if preview.title}<span class="link-title">{preview.title}</span>{/if}32
- {#if preview.description}<span class="link-desc">{preview.description}</span>{/if}31
+ <span class="link-host"><EmojiText text={preview.site_name ?? link.hostname} /></span>32
+ {#if preview.title}<span class="link-title"><EmojiText text={preview.title} /></span>{/if}33
+ {#if preview.description}<span class="link-desc"><EmojiText text={preview.description} /></span>{/if}3334
</span>3435
{:else}3536
<span class="body">36
- <span class="link-host">{link.hostname}</span>37
- {#if link.path}<span class="link-path">{link.path}</span>{/if}38
- <span class="link-url">{link.display}</span>37
+ <span class="link-host"><EmojiText text={link.hostname} /></span>38
+ {#if link.path}<span class="link-path"><EmojiText text={link.path} /></span>{/if}39
+ <span class="link-url"><EmojiText text={link.display} /></span>3940
</span>4041
{/if}4142
{#if imageSrc}@@ -1,4 +1,5 @@
11
<script lang="ts">2
+ import EmojiText from './EmojiText.svelte';23
import { createEventDispatcher } from 'svelte';34
import { login } from './api';45
import type { SoftLogout } from './api';@@ -56,7 +57,7 @@
5657
<div class="login-brand" aria-hidden="true">M</div>5758
<h1>Malice</h1>5859
{#if error}59
- <p class="login-error" role="alert">{error}</p>60
+ <p class="login-error" role="alert"><EmojiText text={error} /></p>6061
{/if}6162
{#if locked}6263
<div class="login-notice" role="status">@@ -3,6 +3,8 @@
33
import type { MediaAttachment } from './types';44
import { messageMediaUrl } from './api';55
import { lightbox } from './lightbox';6
+ import Emoji from './Emoji.svelte';7
+ import EmojiText from './EmojiText.svelte';68
79
export let attachment: MediaAttachment;810
@@ -98,9 +100,9 @@
98100
{#if attachment.kind !== 'Sticker'}99101
<figcaption class="media-caption">100102
{#if attachment.caption && attachment.caption !== attachment.filename}101
- <span>{attachment.caption}</span>103
+ <span><EmojiText text={attachment.caption} /></span>102104
{:else}103
- <span>{attachment.filename}</span>105
+ <span><EmojiText text={attachment.filename} /></span>104106
{/if}105107
{#if sizeLabel()}<span class="media-size"> · {sizeLabel()}</span>{/if}106108
</figcaption>@@ -108,8 +110,8 @@
108110
</figure>109111
{:else if previewable && fullFailed}110112
<div class="media-file">111
- <span class="file-icon" aria-hidden="true">🖼</span>112
- <a href={fullUrl} target="_blank" rel="noopener noreferrer">{attachment.caption || attachment.filename}</a>113
+ <span class="file-icon" aria-hidden="true"><Emoji emoji="🖼️" size={18} /></span>114
+ <a href={fullUrl} target="_blank" rel="noopener noreferrer"><EmojiText text={attachment.caption || attachment.filename} /></a>113115
<span class="media-size">{retryTimer !== null ? 'Retrying preview…' : 'Preview unavailable'}</span>114116
<button type="button" class="ghost-btn" on:click={() => retryPreview(true)}>Retry</button>115117
</div>@@ -121,7 +123,7 @@
121123
Your browser cannot play this video.122124
</video>123125
<div class="media-caption">124
- <a href={fullUrl} target="_blank" rel="noopener noreferrer">{attachment.caption || attachment.filename}</a>126
+ <a href={fullUrl} target="_blank" rel="noopener noreferrer"><EmojiText text={attachment.caption || attachment.filename} /></a>125127
{#if durationLabel()}<span class="media-size"> · {durationLabel()}</span>{/if}126128
{#if sizeLabel()}<span class="media-size"> · {sizeLabel()}</span>{/if}127129
</div>@@ -131,16 +133,16 @@
131133
<!-- svelte-ignore a11y-media-has-caption -->132134
<audio class="media-audio" controls preload="metadata" src={fullUrl}></audio>133135
<div class="media-caption">134
- <span>{attachment.caption || attachment.filename}</span>136
+ <span><EmojiText text={attachment.caption || attachment.filename} /></span>135137
{#if durationLabel()}<span class="media-size"> · {durationLabel()}</span>{/if}136138
{#if sizeLabel()}<span class="media-size"> · {sizeLabel()}</span>{/if}137139
</div>138140
</div>139141
{:else}140142
<div class="media-file">141
- <span class="file-icon" aria-hidden="true">📎</span>143
+ <span class="file-icon" aria-hidden="true"><Emoji emoji="📎" size={18} /></span>142144
<a href={fullUrl} target="_blank" rel="noopener noreferrer" download={attachment.filename}>143
- {attachment.caption || attachment.filename}145
+ <EmojiText text={attachment.caption || attachment.filename} />144146
</a>145147
{#if sizeLabel()}<span class="media-size">{sizeLabel()}</span>{/if}146148
</div>@@ -350,7 +350,7 @@
350350
<MxcAvatar mxc={senderAvatar} label={name} size={34} />351351
{:else}352352
<span class="avatar" style={`background:hsl(${senderHue(event.sender.user_id)} 35% 32%)`}>353
- {initialsOf(name)}353
+ <span><EmojiText text={initialsOf(name)} /></span>354354
</span>355355
{/if}356356
</button>@@ -369,7 +369,7 @@
369369
on:click|stopPropagation={(ev) => openUser(ev, false)}370370
on:contextmenu|preventDefault|stopPropagation={(ev) => openUser(ev, true)}371371
>372
- {displayName}372
+ <EmojiText text={displayName} />373373
</button>374374
<time class="time" datetime={new Date(event.timestamp_ms).toISOString()}>375375
{formatDay(event.timestamp_ms)} {formatClock(event.timestamp_ms)}@@ -389,7 +389,7 @@
389389
on:click|stopPropagation={() => onJump?.(target)}390390
>391391
{#if event.reply_to.details.kind === 'Ready'}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>392
+ <span class="reply-sender"><EmojiText text={event.reply_to.details.sender.display_name ?? people[event.reply_to.details.sender.user_id]?.name ?? senderLabel(event.reply_to.details.sender)} /></span>393393
<span class="reply-text"><EmojiText text={replyPreview(event.reply_to.details.content)} /></span>394394
{:else if event.reply_to.details.kind === 'Pending'}395395
Loading reply…@@ -402,7 +402,7 @@
402402
{#if event.content.kind === 'Message'}403403
{@const m = event.content.message}404404
{#if stripped.text}405
- <div class="text" class:emote={m.kind === 'Emote'} class:notice={m.kind === 'Notice' || m.kind === 'ServerNotice'}>{#if m.kind === 'Emote'}* {displayName} {/if}<MessageText text={stripped.text} html={m.formatted_body} jumbo={m.kind === 'Text'} {mentions} {self} nameOf={(id) => people[id]?.name ?? null} />{#if m.edited && !showSender}<span class="edited" title="Edited">(edited)</span>{/if}</div>405
+ <div class="text" class:emote={m.kind === 'Emote'} class:notice={m.kind === 'Notice' || m.kind === 'ServerNotice'}>{#if m.kind === 'Emote'}* <EmojiText text={displayName} /> {/if}<MessageText text={stripped.text} html={m.formatted_body} jumbo={m.kind === 'Text'} {mentions} {self} nameOf={(id) => people[id]?.name ?? null} />{#if m.edited && !showSender}<span class="edited" title="Edited">(edited)</span>{/if}</div>406406
{/if}407407
{#if m.media}408408
<MediaAttachmentView attachment={m.media} />@@ -419,9 +419,9 @@
419419
<MediaAttachmentView attachment={event.content.media} />420420
{:else if event.content.body}<p class="text"><EmojiText text={event.content.body} /></p>{/if}421421
{:else if event.content.kind === 'Poll'}422
- <p class="text">📊 {event.content.question}</p>422
+ <p class="text"><EmojiText text={`📊 ${event.content.question}`} /></p>423423
{:else}424
- <p class="system-line">{systemLine(event.content, displayName)}</p>424
+ <p class="system-line"><EmojiText text={systemLine(event.content, displayName)} /></p>425425
{/if}426426
427427
{#if interactive && event.reactions.length > 0}@@ -19,7 +19,7 @@
1919
$: single = segments.length === 1 && pieces[0].length === 1;2020
</script>2121
22
-{#if rich}<div class="rich"><RichNodes nodes={rich} {mentions} {self} {nameOf} /></div>{:else}{#each segments as s, i}{#if s.kind === 'inline'}<code class="inline">{s.text}</code>{:else if s.kind === 'block'}<pre class="block"><code>{s.text}</code></pre>{:else}{#each pieces[i] as p}{#if p.kind === 'mention'}<span class="mention" class:me={p.user_id === self} title={p.user_id}><EmojiText text={p.text === p.user_id ? (nameOf(p.user_id) ?? p.text) : p.text} /></span>{:else if p.kind === 'link'}<a class="link" href={p.href} target="_blank" rel="noopener noreferrer">{p.text}</a>{:else}<EmojiText text={p.text} jumbo={jumbo && single} />{/if}{/each}{/if}{/each}{/if}22
+{#if rich}<div class="rich"><RichNodes nodes={rich} {mentions} {self} {nameOf} /></div>{:else}{#each segments as s, i}{#if s.kind === 'inline'}<code class="inline">{s.text}</code>{:else if s.kind === 'block'}<pre class="block"><code>{s.text}</code></pre>{:else}{#each pieces[i] as p}{#if p.kind === 'mention'}<span class="mention" class:me={p.user_id === self} title={p.user_id}><EmojiText text={p.text === p.user_id ? (nameOf(p.user_id) ?? p.text) : p.text} /></span>{:else if p.kind === 'link'}<a class="link" href={p.href} target="_blank" rel="noopener noreferrer"><EmojiText text={p.text} /></a>{:else}<EmojiText text={p.text} jumbo={jumbo && single} />{/if}{/each}{/if}{/each}{/if}2323
2424
<style>2525
code, .rich :global(code) {@@ -1,6 +1,7 @@
11
<script lang="ts">22
import { mediaUrl } from './api';33
import { initialsOf, senderHue } from './format';4
+ import EmojiText from './EmojiText.svelte';45
56
export let mxc: string | null;67
export let label: string;@@ -33,7 +34,7 @@
3334
/>3435
{:else}3536
<span class="mxc-fallback" class:circle={shape === 'circle'} aria-hidden="true" style={`width:${px};height:${px};font-size:${fontPx};background:hsl(${hue} 32% 30%);`}>36
- {initialsOf(label || '#')}37
+ <span><EmojiText text={initialsOf(label || '#')} /></span>3738
</span>3839
{/if}3940
@@ -2,6 +2,7 @@
22
import { createEventDispatcher } from 'svelte';33
import ContextMenu, { type MenuItem } from './ContextMenu.svelte';44
import MxcAvatar from './MxcAvatar.svelte';5
+ import EmojiText from './EmojiText.svelte';56
import { messageMediaUrl, runCommand, uploadMedia } from './api';67
import {78
allPacks,@@ -210,14 +211,14 @@
210211
<nav class="pack-list" aria-label="Packs">211212
<button type="button" class="pack-item" class:active={selectedId === 'user'} on:click={() => (selectedId = 'user')}>212213
<MxcAvatar mxc={userPack?.avatar ?? null} label="Your pack" size={28} shape="rounded" />213
- <span>{userPack?.display_name ?? 'Your pack'}</span>214
+ <span><EmojiText text={userPack?.display_name ?? 'Your pack'} /></span>214215
</button>215216
{#if roomPacks.length}216217
<h4>Room packs</h4>217218
{#each roomPacks as pack (pack.id)}218219
<button type="button" class="pack-item" class:active={selectedId === pack.id} on:click={() => (selectedId = pack.id)}>219220
<MxcAvatar mxc={pack.avatar} label={packLabel(pack, roomName)} size={28} shape="rounded" />220
- <span>{packLabel(pack, roomName)}</span>221
+ <span><EmojiText text={packLabel(pack, roomName)} /></span>221222
</button>222223
{/each}223224
{/if}@@ -226,7 +227,7 @@
226227
{#each otherPacks as pack (pack.id)}227228
<button type="button" class="pack-item" class:active={selectedId === pack.id} on:click={() => (selectedId = pack.id)}>228229
<MxcAvatar mxc={pack.avatar} label={packLabel(pack, roomName)} size={28} shape="rounded" />229
- <span>{packLabel(pack, roomName)}</span>230
+ <span><EmojiText text={packLabel(pack, roomName)} /></span>230231
</button>231232
{/each}232233
{/if}@@ -306,7 +307,7 @@
306307
{:else if selected}307308
<div class="meta">308309
<MxcAvatar mxc={selected.avatar} label={packLabel(selected, roomName)} size={48} shape="rounded" />309
- <strong class="name">{packLabel(selected, roomName)}</strong>310
+ <strong class="name"><EmojiText text={packLabel(selected, roomName)} /></strong>310311
<span class="spacer"></span>311312
<button type="button" class="primary-btn" on:click={(ev) => selected && clonePackMenu(selected, ev)}>Clone pack</button>312313
</div>@@ -1,4 +1,5 @@
11
<script lang="ts">2
+ import EmojiText from './EmojiText.svelte';23
import { onMount } from 'svelte';34
import { mediaUrl, messageMediaUrl } from './api';45
import Emoji from './Emoji.svelte';@@ -128,7 +129,7 @@
128129
<div class="scroll" aria-busy={groups === null && !loadFailed}>129130
{#each filteredPacks as { pack, images } (pack.id)}130131
<section class="group" aria-label={packName(pack)}>131
- <h3>{packName(pack)}</h3>132
+ <h3><EmojiText text={packName(pack)} /></h3>132133
<div class="grid">133134
{#each images as image (image.reaction_key + image.shortcode)}134135
<button@@ -176,7 +177,7 @@
176177
{/each}177178
{/if}178179
{#if nothingFound}179
- <p class="status" role="status">No emoji match "{query.trim()}".</p>180
+ <p class="status" role="status">No emoji match "<EmojiText text={query.trim()} />".</p>180181
{/if}181182
</div>182183
@@ -11,6 +11,7 @@
1111
export let self: string | null = null;1212
export let nameOf: (userId: string) => string | null = () => null;1313
export let plain = false;14
+ export let verbatim = false;1415
</script>1516
16
-{#each nodes as n}{#if n.kind === 'text'}{#if plain}{n.text}{:else}{#each segmentInline(n.text, mentions) as p}{#if p.kind === 'mention'}<span class="mention" class:me={p.user_id === self} title={p.user_id}><EmojiText text={p.text === p.user_id ? (nameOf(p.user_id) ?? p.text) : p.text} /></span>{:else if p.kind === 'link'}<a class="link" href={p.href} target="_blank" rel="noopener noreferrer">{p.text}</a>{:else}<EmojiText text={p.text} />{/if}{/each}{/if}{:else if n.kind === 'mention'}<span class="mention" class:me={n.user_id === self} title={n.user_id}><RichNodes nodes={n.children} plain /></span>{:else if n.kind === 'spoiler'}<span class="spoiler" role="button" tabindex="0"><RichNodes nodes={n.children} {mentions} {self} {nameOf} {plain} /></span>{:else if n.kind === 'emote'}<img class="emote" src={mediaUrl(n.mxc) ?? ''} alt={n.alt} title={n.alt} />{:else if n.tag === 'br'}<br />{:else if n.tag === 'hr'}<hr />{:else if n.tag === 'a'}<a class="link" href={n.attrs['href']} target="_blank" rel="noopener noreferrer"><RichNodes nodes={n.children} plain /></a>{:else}<svelte:element this={n.tag} {...n.attrs}><RichNodes nodes={n.children} {mentions} {self} {nameOf} plain={plain || n.tag === 'code' || n.tag === 'pre'} /></svelte:element>{/if}{/each}17
+{#each nodes as n}{#if n.kind === 'text'}{#if verbatim}{n.text}{:else if plain}<EmojiText text={n.text} />{:else}{#each segmentInline(n.text, mentions) as p}{#if p.kind === 'mention'}<span class="mention" class:me={p.user_id === self} title={p.user_id}><EmojiText text={p.text === p.user_id ? (nameOf(p.user_id) ?? p.text) : p.text} /></span>{:else if p.kind === 'link'}<a class="link" href={p.href} target="_blank" rel="noopener noreferrer"><EmojiText text={p.text} /></a>{:else}<EmojiText text={p.text} />{/if}{/each}{/if}{:else if n.kind === 'mention'}<span class="mention" class:me={n.user_id === self} title={n.user_id}><RichNodes nodes={n.children} plain {verbatim} /></span>{:else if n.kind === 'spoiler'}<span class="spoiler" role="button" tabindex="0"><RichNodes nodes={n.children} {mentions} {self} {nameOf} {plain} {verbatim} /></span>{:else if n.kind === 'emote'}<img class="emote" src={mediaUrl(n.mxc) ?? ''} alt={n.alt} title={n.alt} />{:else if n.tag === 'br'}<br />{:else if n.tag === 'hr'}<hr />{:else if n.tag === 'a'}<a class="link" href={n.attrs['href']} target="_blank" rel="noopener noreferrer"><RichNodes nodes={n.children} plain {verbatim} /></a>{:else}<svelte:element this={n.tag} {...n.attrs}><RichNodes nodes={n.children} {mentions} {self} {nameOf} plain={plain || n.tag === 'code' || n.tag === 'pre'} verbatim={verbatim || n.tag === 'code' || n.tag === 'pre'} /></svelte:element>{/if}{/each}@@ -7,6 +7,8 @@
77
type VerifyAction88
} from './api';99
import { pushBanner } from './store';10
+ import Emoji from './Emoji.svelte';11
+ import EmojiText from './EmojiText.svelte';1012
import type { EncryptionState, VerificationState } from './types';1113
1214
const dispatch = createEventDispatcher<{ close: void }>();@@ -151,7 +153,7 @@
151153
{:else if verificationState.kind === 'Comparison'}152154
<ol class="emoji-grid" aria-label="Verification emojis">153155
{#each verificationState.emojis as item (item.name + item.emoji)}154
- <li><span class="emoji" aria-hidden="true">{item.emoji}</span><span class="emoji-name">{item.name}</span></li>156
+ <li><span class="emoji" aria-hidden="true"><Emoji emoji={item.emoji} size={24} /></span><span class="emoji-name"><EmojiText text={item.name} /></span></li>155157
{/each}156158
</ol>157159
<p class="sec-line decimals" aria-label="Verification numbers">@@ -173,12 +175,12 @@
173175
<button type="button" class="ghost-btn" on:click={start} disabled={busy !== null}>Verify again</button>174176
</div>175177
{:else if verificationState.kind === 'Cancelled'}176
- <p class="sec-line muted">Cancelled: {verificationState.reason}</p>178
+ <p class="sec-line muted">Cancelled: <EmojiText text={verificationState.reason} /></p>177179
<div class="row">178180
<button type="button" class="ghost-btn" on:click={start} disabled={busy !== null}>Start again</button>179181
</div>180182
{:else if verificationState.kind === 'Failed'}181
- <p class="sec-line muted">Failed: {verificationState.message}</p>183
+ <p class="sec-line muted">Failed: <EmojiText text={verificationState.message} /></p>182184
<div class="row">183185
<button type="button" class="ghost-btn" on:click={start} disabled={busy !== null}>Try again</button>184186
</div>@@ -2,6 +2,7 @@
22
import { onMount } from 'svelte';33
import { messageMediaUrl } from './api';44
import MxcAvatar from './MxcAvatar.svelte';5
+ import EmojiText from './EmojiText.svelte';56
import { packLabel, stickersOf } from './packs';67
import type { ImagePack, PackImage } from './types';78
@@ -77,7 +78,7 @@
7778
{#each shown as g (g.pack.id)}7879
<section class="group" aria-label={packLabel(g.pack, roomName)}>7980
<div class="group-head">80
- <h3>{packLabel(g.pack, roomName)}</h3>81
+ <h3><EmojiText text={packLabel(g.pack, roomName)} /></h3>8182
{#if !g.pack.editable}8283
<button type="button" class="clone-pack" on:click={(ev) => onClonePack(g.pack, ev.clientX, ev.clientY)}>Clone pack</button>8384
{/if}@@ -104,7 +105,7 @@
104105
<button type="button" class="primary-btn" on:click={onManage}>Add a pack</button>105106
</div>106107
{:else}107
- <p class="status">No stickers match "{query.trim()}"</p>108
+ <p class="status">No stickers match "<EmojiText text={query.trim()} />"</p>108109
{/if}109110
{/each}110111
</div>@@ -1,5 +1,6 @@
11
<script lang="ts">22
import { onDestroy } from 'svelte';3
+ import EmojiText from './EmojiText.svelte';34
import { get } from 'svelte/store';45
import { THEMES, accent, customThemes, theme, themePreview, type ThemeId } from './appearance';56
import {@@ -140,14 +141,14 @@
140141
{#each THEMES as item (item.id)}141142
<button type="button" role="radio" aria-checked={$theme === item.id} class="theme-opt" class:active={$theme === item.id}142143
data-swatch={item.id} on:click={() => chooseTheme(item.id)}>143
- <span class="swatch" aria-hidden="true"></span>{item.label}144
+ <span class="swatch" aria-hidden="true"></span><EmojiText text={item.label} />144145
</button>145146
{/each}146147
{#each $customThemes as item (item.id)}147148
<button type="button" role="radio" aria-checked={$theme === item.id} class="theme-opt" class:active={$theme === item.id}148149
on:click={() => chooseTheme(item.id)}>149150
<span class="swatch" style:background={item.colours.bg} style:border-color={item.colours.accent} aria-hidden="true"></span>150
- <span class="theme-name">{item.name}</span>151
+ <span class="theme-name"><EmojiText text={item.name} /></span>151152
</button>152153
{/each}153154
</div>@@ -206,7 +207,7 @@
206207
<input type="range" min="0" max="100" step="1" bind:value={draft.nameLightness} />207208
</label>208209
<div class="theme-preview" style={themeStyle(draft)} aria-label="Theme preview">209
- <div class="preview-heading">{draft.name || 'Your theme'}<span class="preview-muted">just now</span></div>210
+ <div class="preview-heading"><span><EmojiText text={draft.name || 'Your theme'} /></span><span class="preview-muted">just now</span></div>210211
<p>A little room to make your own. <span class="preview-link">A shared link</span></p>211212
<div class="preview-status"><span class="preview-ok">Connected</span><span class="preview-warn">Warning</span><span class="preview-error">Error</span></div>212213
<div class="preview-composer"><span class="preview-muted">Write a message…</span><span class="primary-btn" aria-hidden="true">Send</span></div>@@ -220,8 +221,8 @@
220221
</div>221222
</div>222223
{/if}223
- {#if error}<p class="theme-error" role="alert">{error}</p>{/if}224
- {#if notice}<p class="theme-notice" role="status">{notice}</p>{/if}224
+ {#if error}<p class="theme-error" role="alert"><EmojiText text={error} /></p>{/if}225
+ {#if notice}<p class="theme-notice" role="status"><EmojiText text={notice} /></p>{/if}225226
</section>226227
227228
<style>@@ -8,6 +8,7 @@
88
<script lang="ts">99
import { onMount, tick } from 'svelte';1010
import MxcAvatar from './MxcAvatar.svelte';11
+ import EmojiText from './EmojiText.svelte';1112
import { allowed, isMuted, levelOf, roleName, type PowerLevels } from './permissions';1213
import type { MemberSummary, Person } from './types';1314
@@ -83,7 +84,7 @@
8384
<div class="identity">8485
<MxcAvatar mxc={person.avatar} label={person.name} seed={person.user_id} size={56} />8586
<div class="names">86
- <strong class="name">{person.name}</strong>87
+ <strong class="name"><EmojiText text={person.name} /></strong>8788
<button type="button" class="user-id" title="Copy user ID" on:click={() => void copyId()}>8889
{copied ? 'Copied' : person.user_id}8990
</button>@@ -103,7 +104,7 @@
103104
<input type="text" placeholder="Reason" bind:value={reason} autofocus />104105
<div class="row">105106
<button type="button" class="ghost-btn" on:click={() => (confirm = null)}>Cancel</button>106
- <button type="submit" class="ghost-btn danger">{confirm === 'kick' ? 'Kick' : 'Ban'} {person.name}</button>107
+ <button type="submit" class="ghost-btn danger">{confirm === 'kick' ? 'Kick' : 'Ban'} <EmojiText text={person.name} /></button>107108
</div>108109
</form>109110
{:else}@@ -26,10 +26,13 @@ export function formatDay(timestampMs: number): string {
2626
});2727
}2828
29
+const initialsSegmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' });30
+2931
export function initialsOf(name: string): string {3032
const clean = name.replace(/^[@#!]/, '').split(/[:\s._-]+/).filter(Boolean);31
- const first = clean[0]?.[0] ?? '#';32
- const second = clean.length > 1 ? (clean[clean.length - 1][0] ?? '') : '';33
+ const initial = (part: string) => initialsSegmenter.segment(part)[Symbol.iterator]().next().value?.segment ?? '';34
+ const first = initial(clean[0] ?? '') || '#';35
+ const second = clean.length > 1 ? initial(clean[clean.length - 1]) : '';3336
return (first + second).toUpperCase();3437
}