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 @@
{#if playerSrc}{:else}STREAM OFFLINE
CONNECT TWITCH TO BEGIN
{/if}
{#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}
-
{#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}
+
{#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}
{#if showStreamEditor}{/if}{#if showModeration}{/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;