diff --git a/src/App.svelte b/src/App.svelte index a5b9446..0323c80 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -17,7 +17,7 @@ let realtimeState = 'DISCONNECTED'; let realtimeSocket: WebSocket | undefined; let activityEvents: Array<{ event_id: string; event_type: string; occurred_at: string }> = []; - let chatMessages: Array<{ event_id: string; message_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 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 notifications: Array<{ id: string; severity: string; title: string; message: string; seen_at: string | null; created_at: string }> = []; let showNotifications = false; @@ -166,12 +166,12 @@ const event: { event_type?: string } = JSON.parse(message.data); if (event.event_type === 'connection.ready') realtimeState = 'CONNECTED'; else if ('event_id' in event && event.event_type) { - const activity = event as { event_id: string; event_type: string; occurred_at: string; payload?: { message_id?: string; chatter_user_name?: string; message?: { text?: string; fragments?: Array<{ type: string; text: string; emote?: { id?: string } }> } } }; + const activity = event as { event_id: string; event_type: string; occurred_at: string; payload?: { message_id?: string; chatter_user_id?: string; chatter_user_name?: string; message?: { text?: string; fragments?: Array<{ type: string; text: string; emote?: { id?: string } }> } } }; if (!activityEvents.some((item) => item.event_id === activity.event_id)) { activityEvents = [activity, ...activityEvents].slice(0, 100); } if (activity.event_type === 'twitch.channel.chat.message' && activity.payload?.message?.text) { - chatMessages = [{ event_id: activity.event_id, message_id: activity.payload.message_id ?? activity.event_id, user_name: activity.payload.chatter_user_name ?? 'unknown', text: activity.payload.message.text, fragments: (activity.payload.message.fragments ?? []).map((fragment) => ({ type: fragment.type, text: fragment.text, emote_id: fragment.emote?.id })), sent_at: activity.occurred_at }, ...chatMessages].slice(0, 100); + chatMessages = [{ event_id: activity.event_id, message_id: activity.payload.message_id ?? activity.event_id, user_id: activity.payload.chatter_user_id ?? '', user_name: activity.payload.chatter_user_name ?? 'unknown', text: activity.payload.message.text, fragments: (activity.payload.message.fragments ?? []).map((fragment) => ({ type: fragment.type, text: fragment.text, emote_id: fragment.emote?.id })), sent_at: activity.occurred_at }, ...chatMessages].slice(0, 100); } } } catch { @@ -344,6 +344,12 @@ chatReplyParent = { message_id: message.message_id, user_name: message.user_name }; } + function moderateChatUser(userId: string) { + if (!userId) return; + moderationUserId = userId; + showModeration = true; + } + async function applyModeration() { moderationBusy = true; moderationError = ''; @@ -491,7 +497,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}{#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 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 698c1b7..0a75bb6 100644 --- a/src/styles.css +++ b/src/styles.css @@ -253,6 +253,9 @@ button:hover:not(:disabled) { line-height: 1.35; } .chat-user { + border: 0; + padding: 0; + background: transparent; color: var(--ed-accent); font-family: var(--font-data); font-weight: 600;