diff --git a/src/App.svelte b/src/App.svelte index 0323c80..69a359a 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -19,6 +19,7 @@ let activityEvents: Array<{ event_id: string; event_type: string; occurred_at: string }> = []; let chatMessages: Array<{ event_id: string; message_id: string; user_id: string; user_name: string; text: string; fragments: Array<{ type: string; text: string; emote_id?: string }>; sent_at: string; reply_parent_message_id?: string }> = []; let chatReplyParent: { message_id: string; user_name: string } | undefined; + let pinnedMessageIds: string[] = []; let notifications: Array<{ id: string; severity: string; title: string; message: string; seen_at: string | null; created_at: string }> = []; let showNotifications = false; let showStreamEditor = false; @@ -150,6 +151,7 @@ diagnostics = undefined; chatMessages = []; chatReplyParent = undefined; + pinnedMessageIds = []; } function connectRealtime() { @@ -350,6 +352,17 @@ showModeration = true; } + function togglePinnedMessage(messageId: string) { + pinnedMessageIds = pinnedMessageIds.includes(messageId) ? pinnedMessageIds.filter((id) => id !== messageId) : [messageId, ...pinnedMessageIds]; + } + + function deleteChatMessage(message: { message_id: string; user_id: string }) { + moderationAction = 'delete_message'; + moderationUserId = message.user_id; + moderationMessageId = message.message_id; + showModeration = true; + } + async function applyModeration() { moderationBusy = true; moderationError = ''; @@ -497,7 +510,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}
{#if message.fragments.length}{#each message.fragments as fragment}{#if fragment.type === 'emote' && fragment.emote_id}{fragment.text}{:else}{fragment.text}{/if}{/each}{:else}{message.text}{/if}
{/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}
+
MY CHAT⋮
{#if pinnedMessageIds.length}
PINNED{#each chatMessages.filter((message) => pinnedMessageIds.includes(message.message_id)) as message}
{message.user_name}{message.text}
{/each}
{/if}{#if chatMessages.length}
{#each chatMessages as message}
{#if message.fragments.length}{#each message.fragments as fragment}{#if fragment.type === 'emote' && fragment.emote_id}{fragment.text}{:else}{fragment.text}{/if}{/each}{:else}{message.text}{/if}
{/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 0a75bb6..6e27bac 100644 --- a/src/styles.css +++ b/src/styles.css @@ -278,6 +278,37 @@ button:hover:not(:disabled) { color: var(--ed-dim); font: 9px var(--font-data); } +.chat-actions { + display: flex; + margin-left: auto; + gap: 3px; + opacity: 0; +} +.chat-message:hover .chat-actions, +.chat-message:focus-within .chat-actions { + opacity: 1; +} +.chat-reply.danger { + color: var(--ed-danger); +} +.message-pinned { + background: var(--ui-surface); +} +.pinned-messages { + padding: 7px 10px; + border-bottom: 1px solid var(--ui-divider); + background: var(--ui-surface); +} +.pinned-message { + display: flex; + gap: 7px; + margin-top: 5px; + overflow: hidden; + color: var(--ed-fg); + font-size: 11px; + text-overflow: ellipsis; + white-space: nowrap; +} .chat-reply-context { display: flex; align-items: center;