diff --git a/src/App.svelte b/src/App.svelte index 7bec005..a7c1ee4 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -36,6 +36,8 @@ let categorySearchTimer: number | undefined; let chatIdentity: 'streamer' | 'bot' = 'streamer'; let chatMessage = ''; + let chatEmoteOptions: Array<{ name: string; url: string; provider: string }> = []; + let chatEmoteTimer: number | undefined; let chatBusy = false; let chatError = ''; let showModeration = false; @@ -313,6 +315,31 @@ } } + function searchChatEmotes(value: string) { + const match = value.match(/(^|\s):([A-Za-z0-9_]{1,64})$/); + chatEmoteOptions = []; + if (chatEmoteTimer) window.clearTimeout(chatEmoteTimer); + if (!match) return; + const query = match[2]; + chatEmoteTimer = window.setTimeout(async () => { + try { + const response = await fetch(`${serverAddress}/api/v1/twitch/chat/emotes?query=${encodeURIComponent(query)}`, { + headers: { authorization: `Bearer ${sessionToken}` }, + }); + if (response.ok) chatEmoteOptions = await response.json(); + } catch { + chatEmoteOptions = []; + } + }, 200); + } + + function selectChatEmote(name: string) { + const match = chatMessage.match(/(^|\s):[A-Za-z0-9_]{1,64}$/); + if (!match) return; + chatMessage = `${chatMessage.slice(0, chatMessage.length - match[0].length)}${match[1]}${name} `; + chatEmoteOptions = []; + } + function replyToChatMessage(message: { message_id: string; user_name: string }) { chatReplyParent = { message_id: message.message_id, user_name: message.user_name }; } @@ -464,7 +491,7 @@
STREAM PREVIEW⋮
{#if playerSrc}{:else}
STREAM OFFLINE
CONNECT TWITCH TO BEGIN
{/if}
ACTIVITY FEED⋮
{#if activityEvents.length}
{#each activityEvents as event}
{event.event_type}{new Date(event.occurred_at).toLocaleTimeString()}
{/each}
{:else}
◌
Nothing here yetFollows, subscriptions, raids, and server events will appear here.
{/if}
-
MY CHAT⋮
{#if chatMessages.length}
{#each chatMessages as message}
{message.user_name}{message.text}
{/each}
{:else}
⌁
Chat is waitingConnect a streamer or bot account to begin.
{/if}
{#if chatReplyParent}
REPLYING TO @{chatReplyParent.user_name}
{/if}
event.key === 'Enter' && sendChatMessage()} />
{#if chatError}
{chatError}
{/if}
+
MY CHAT⋮
{#if chatMessages.length}
{#each chatMessages as message}
{message.user_name}{message.text}
{/each}
{:else}
⌁
Chat is waitingConnect a streamer or bot account to begin.
{/if}
{#if chatReplyParent}
REPLYING TO @{chatReplyParent.user_name}
{/if}
{#if chatEmoteOptions.length}
{#each chatEmoteOptions as emote}{/each}
{/if}
searchChatEmotes(chatMessage)} onkeydown={(event) => event.key === 'Enter' && sendChatMessage()} />
{#if chatError}
{chatError}
{/if}
QUICK ACTIONS⋮
{#if showStreamEditor}
searchCategories(event.currentTarget.value)} autocomplete="off" placeholder="Search Twitch categories" />{#if categoryOptions.length}
{#each categoryOptions as category}{/each}
{/if}{#if streamError}
{streamError}
{/if}
{/if}{#if showModeration}
{#if moderationError}
{moderationError}
{/if}
{/if}
{/if} diff --git a/src/styles.css b/src/styles.css index 805d0f8..202e293 100644 --- a/src/styles.css +++ b/src/styles.css @@ -278,6 +278,30 @@ button:hover:not(:disabled) { color: var(--ed-accent); font: 10px var(--font-data); } +.chat-emote-picker { + display: flex; + max-height: 180px; + flex-direction: column; + overflow: auto; + margin-bottom: 6px; + border: 1px solid var(--ui-line); + background: var(--ed-bg); +} +.chat-emote-option { + display: grid; + grid-template-columns: 28px 1fr auto; + align-items: center; + gap: 8px; + width: 100%; + border: 0; + border-bottom: 1px solid var(--ui-divider); + text-align: left; +} +.chat-emote-option img { + width: 28px; + height: 28px; + object-fit: contain; +} .workspace { display: grid; grid-template-columns: 214px 1fr;