atagen

malice

public
Star0
Watch0

Commit 2021154f9e16

tarball

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

4 files changed+52-7
emoji: render twemoji in the composer and reaction tooltips
Modified file: web/src/App.svelte+25 additions−5 deletions
@@ -71,6 +71,7 @@
7171 import type { PendingMessage } from './lib/pending';
7272 import { insideOpenFence } from './lib/markdown';
7373 import { TimelineViewport } from './lib/timeline-viewport';
74+ import ComposerText from './lib/ComposerText.svelte';
7475 import { markMentions } from './lib/composer-marks';
7576 import { mentionQueryAt, mentionsFromHtml, rankPeople, resolveMentions } from './lib/mentions';
7677 import type { Mention, MentionQuery } from './lib/mentions';
@@ -123,6 +124,7 @@
123124 let timelineViewport: TimelineViewport | null = null;
124125 let composerEl: HTMLTextAreaElement | null = null;
125126 let marksEl: HTMLDivElement | null = null;
127+ let composerComposing = false;
126128 $: composerMarks = markMentions(
127129 composer,
128130 ($selectedRoomId && pickedMentions[$selectedRoomId]) || [],
@@ -614,6 +616,22 @@
614616 timelineViewport?.releaseScroll();
615617 }
616618
619+ function syncComposer(node: HTMLTextAreaElement) {
620+ const sync = () => {
621+ if (!marksEl) return;
622+ marksEl.style.width = `${node.clientWidth}px`;
623+ marksEl.scrollTop = node.scrollTop;
624+ marksEl.scrollLeft = node.scrollLeft;
625+ };
626+ const observer = new ResizeObserver(sync);
627+ observer.observe(node);
628+ node.addEventListener('scroll', sync);
629+ return { destroy: () => {
630+ observer.disconnect();
631+ node.removeEventListener('scroll', sync);
632+ } };
633+ }
634+
617635 function onTimelineKey(e: KeyboardEvent): void {
618636 if (['ArrowUp', 'ArrowDown', 'PageUp', 'PageDown', 'Home', 'End', ' '].includes(e.key)) noteUserScroll();
619637 }
@@ -1158,7 +1176,7 @@
11581176 }
11591177
11601178 function onComposerKey(ev: KeyboardEvent): void {
1161- if (ev.isComposing) return;
1179+ if (ev.isComposing || composerComposing) return;
11621180 if (emoteQuery && emoteCandidates.length > 0) {
11631181 if (ev.key === 'ArrowDown' || ev.key === 'ArrowUp') {
11641182 ev.preventDefault();
@@ -2544,7 +2562,7 @@
25442562 />
25452563 </div>
25462564 {/if}
2547- <div class="composer-field">
2565+ <div class="composer-field" class:composing={composerComposing}>
25482566 {#if commandQuery !== null && commandCandidates.length > 0}
25492567 <ul class="mention-finder" role="listbox" aria-label="Commands">
25502568 {#each commandCandidates as name, i (name)}
@@ -2598,22 +2616,24 @@
25982616 {/each}
25992617 </ul>
26002618 {/if}
2601- <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>
2619+ <div class="composer-marks" aria-hidden="true" bind:this={marksEl}>{#each composerMarks as part}{#if part.mark}<mark><ComposerText text={part.text} /></mark>{:else}<ComposerText text={part.text} />{/if}{/each}&#8203;</div>
26022620 <textarea
26032621 id="composer"
26042622 bind:this={composerEl}
26052623 bind:value={composer}
2624+ use:syncComposer
26062625 rows="1"
26072626 placeholder={`Message ${selectedRoom.name}`}
26082627 aria-label={`Message ${selectedRoom.name}. Enter to send, Shift Enter for a new line.`}
26092628 on:keydown={onComposerKey}
26102629 on:input={() => void onComposerInput()}
2630+ on:compositionstart={() => (composerComposing = true)}
2631+ on:compositionend={() => (composerComposing = false)}
26112632 on:click={updateMentionQuery}
26122633 on:focus={ensureEmoji}
26132634 on:keyup={(e) => (e.key.startsWith('Arrow') && e.key !== 'ArrowUp' && e.key !== 'ArrowDown') || e.key === 'Home' || e.key === 'End' ? updateMentionQuery() : undefined}
2614- on:blur={() => ((mentionQuery = null), (commandQuery = null), (emoteQuery = null))}
2635+ on:blur={() => ((mentionQuery = null), (commandQuery = null), (emoteQuery = null), (composerComposing = false))}
26152636 on:paste={onComposerPaste}
2616- on:scroll={() => marksEl && composerEl && (marksEl.scrollTop = composerEl.scrollTop)}
26172637 disabled={!($selectedRoomId && openReady[$selectedRoomId])}
26182638 ></textarea>
26192639 </div>
Modified file: web/src/app.css+10 additions
@@ -828,6 +828,7 @@ input, textarea { font: inherit; }
828828 }
829829 .composer-marks {
830830 pointer-events: none;
831+ z-index: 1;
831832 color: var(--text);
832833 overflow: hidden;
833834 }
@@ -837,6 +838,15 @@ input, textarea { font: inherit; }
837838 border-radius: var(--r-sm);
838839 box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 26%, transparent);
839840 }
841+.composer-field > textarea::placeholder { color: var(--muted); }
842+.composer-field > textarea::selection { color: transparent; background: var(--selection-bg); }
843+.composer-field.composing > textarea { color: var(--text); }
844+.composer-field.composing > .composer-marks { visibility: hidden; }
845+@media (forced-colors: active) {
846+ .composer-field > textarea { color: CanvasText; }
847+ .composer-field > textarea::selection { color: HighlightText; background: Highlight; }
848+ .composer-field > .composer-marks { visibility: hidden; }
849+}
840850 .join-bar {
841851 display: flex;
842852 align-items: center;
Added file: web/src/lib/ComposerText.svelte+15 additions
@@ -0,0 +1,15 @@
1+<script lang="ts">
2+ import { segmentEmoji, twemojiUrl } from './twemoji';
3+
4+ export let text: string;
5+
6+ let failed = new Set<string>();
7+ $: segments = segmentEmoji(text);
8+</script>
9+
10+{#each segments as segment}{#if segment.emoji && !failed.has(segment.text)}<span class="composer-emoji">{segment.text}<img src={twemojiUrl(segment.text)} alt="" draggable="false" on:error={() => (failed = new Set([...failed, segment.text]))} /></span>{:else}{segment.text}{/if}{/each}
11+
12+<style>
13+ .composer-emoji { position: relative; color: transparent; }
14+ .composer-emoji img { position: absolute; left: 0; top: 50%; transform: translateY(-50%); width: 100%; height: 1.15em; object-fit: contain; }
15+</style>
Modified file: web/src/lib/MessageRow.svelte+2 additions−2 deletions
@@ -444,8 +444,8 @@
444444 {/if}
445445 <span class="count">{r.count}</span>
446446 <span class="reactors" role="tooltip" id={`${itemId}-reactors-${r.key}`}>
447- <span class="reactors-key">{r.shortcode ? `:${r.shortcode}:` : r.key.startsWith('mxc://') ? 'custom emote' : r.key}</span>
448- {reactorNames(r.senders)}
447+ <span class="reactors-key"><EmojiText text={r.shortcode ? `:${r.shortcode}:` : r.key.startsWith('mxc://') ? 'custom emote' : r.key} /></span>
448+ <EmojiText text={reactorNames(r.senders)} />
449449 </span>
450450 </button>
451451 {/each}