atagen

malice

public
Star0
Watch0

Commit 445bf199a9cf

tarball

atagen <boss@atagen.co>2026-10-25 02:59 UTC

17 files changed+94-76
emoji: use twemoji across visible app text
Modified file: web/src/App.svelte+30 additions−30 deletions
@@ -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 <span
21092109 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 <span
21462146 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 <span
21772177 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 <button
22572257 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 message
2465- <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 <button
24932493 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>
Modified file: web/src/lib/ContextMenu.svelte+2 additions−1 deletions
@@ -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>
Modified file: web/src/lib/ImageLightbox.svelte+2 additions−1 deletions
@@ -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 <button
5152 type="button"
5253 class="ghost-btn"
Modified file: web/src/lib/LinkCard.svelte+7 additions−6 deletions
@@ -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}
Modified file: web/src/lib/LoginPanel.svelte+2 additions−1 deletions
@@ -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">
Modified file: web/src/lib/MediaAttachmentView.svelte+10 additions−8 deletions
@@ -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>
Modified file: web/src/lib/MessageRow.svelte+6 additions−6 deletions
@@ -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}
Modified file: web/src/lib/MessageText.svelte+1 additions−1 deletions
@@ -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) {
Modified file: web/src/lib/MxcAvatar.svelte+2 additions−1 deletions
@@ -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
Modified file: web/src/lib/PackManager.svelte+5 additions−4 deletions
@@ -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>
Modified file: web/src/lib/ReactionPicker.svelte+3 additions−2 deletions
@@ -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
Modified file: web/src/lib/RichNodes.svelte+2 additions−1 deletions
@@ -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}
Modified file: web/src/lib/SecurityPanel.svelte+5 additions−3 deletions
@@ -7,6 +7,8 @@
77 type VerifyAction
88 } 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>
Modified file: web/src/lib/StickerPicker.svelte+3 additions−2 deletions
@@ -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>
Modified file: web/src/lib/ThemeSettings.svelte+6 additions−5 deletions
@@ -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>
Modified file: web/src/lib/UserCard.svelte+3 additions−2 deletions
@@ -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}
Modified file: web/src/lib/format.ts+5 additions−2 deletions
@@ -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 }