Commit 2021154f9e16
tarball4 files changed+52-7
@@ -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}​</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}​</div>26022620
<textarea26032621
id="composer"26042622
bind:this={composerEl}26052623
bind:value={composer}2624
+ use:syncComposer26062625
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>@@ -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;@@ -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>@@ -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}