feat: integrate Matrix chat (Option 2 - credentials stored in Supabase)
- Add matrix-js-sdk, marked, highlight.js, twemoji, @tanstack/svelte-virtual deps - Copy Matrix core layer: /matrix/, /stores/matrix.ts, /cache/, /services/ - Copy Matrix components: matrix/, message/, chat-layout/, chat-settings/ - Copy UI components: EmojiPicker, Twemoji, ImagePreviewModal, VirtualList - Copy utils: emojiData, twemoji, twemojiGlobal - Replace lucide-svelte with Material Symbols in SyncRecoveryBanner - Extend Avatar with xs size and status indicator prop - Fix ui.ts store conflict: re-export toasts from toast.svelte.ts - Add migration 020_matrix_credentials for storing Matrix tokens per user/org - Add /api/matrix-credentials endpoint (GET/POST/DELETE) - Create [orgSlug]/chat page with Matrix login form + full chat UI - Add Chat to sidebar navigation
This commit is contained in:
88
src/lib/components/matrix/EmojiAutocomplete.svelte
Normal file
88
src/lib/components/matrix/EmojiAutocomplete.svelte
Normal file
@@ -0,0 +1,88 @@
|
||||
<script lang="ts">
|
||||
import Twemoji from '$lib/components/ui/Twemoji.svelte';
|
||||
import { searchEmojis, type EmojiItem } from '$lib/utils/emojiData';
|
||||
|
||||
interface Props {
|
||||
query: string;
|
||||
onSelect: (emoji: string) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
let {
|
||||
query,
|
||||
onSelect,
|
||||
onClose,
|
||||
}: Props = $props();
|
||||
|
||||
let selectedIndex = $state(0);
|
||||
|
||||
// Filter emojis based on query
|
||||
const filteredEmojis = $derived(
|
||||
searchEmojis(query).slice(0, 10)
|
||||
);
|
||||
|
||||
// Reset selection when query changes
|
||||
$effect(() => {
|
||||
query;
|
||||
selectedIndex = 0;
|
||||
});
|
||||
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
if (filteredEmojis.length === 0) return;
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
selectedIndex = (selectedIndex + 1) % filteredEmojis.length;
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
selectedIndex = (selectedIndex - 1 + filteredEmojis.length) % filteredEmojis.length;
|
||||
break;
|
||||
case 'Enter':
|
||||
case 'Tab':
|
||||
e.preventDefault();
|
||||
if (filteredEmojis[selectedIndex]) {
|
||||
onSelect(filteredEmojis[selectedIndex].emoji);
|
||||
}
|
||||
break;
|
||||
case 'Escape':
|
||||
e.preventDefault();
|
||||
onClose();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Expose keyboard handler for parent to call
|
||||
export { handleKeyDown };
|
||||
</script>
|
||||
|
||||
{#if filteredEmojis.length > 0}
|
||||
<div
|
||||
class="absolute z-50 bg-dark border border-light/10 rounded-lg shadow-xl overflow-hidden max-h-64 overflow-y-auto"
|
||||
style="bottom: 100%; left: 0; margin-bottom: 8px; min-width: 280px;"
|
||||
>
|
||||
<div class="p-2 text-xs text-light/50 border-b border-light/10">
|
||||
Emojis matching :{query}
|
||||
</div>
|
||||
{#each filteredEmojis as emoji, i}
|
||||
<button
|
||||
class="w-full flex items-center gap-3 px-3 py-2 text-left transition-colors {i === selectedIndex ? 'bg-primary/20' : 'hover:bg-light/5'}"
|
||||
onclick={() => onSelect(emoji.emoji)}
|
||||
onmouseenter={() => selectedIndex = i}
|
||||
>
|
||||
<div class="w-6 h-6 flex items-center justify-center">
|
||||
<Twemoji emoji={emoji.emoji} size={20} />
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-light">:{emoji.names[0]}:</p>
|
||||
{#if emoji.names.length > 1}
|
||||
<p class="text-xs text-light/40 truncate">
|
||||
Also: {emoji.names.slice(1, 4).map(n => `:${n}:`).join(' ')}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
Reference in New Issue
Block a user