From 1ba93cce8a2befe81fbc83a122bf3b7181b72cbe Mon Sep 17 00:00:00 2001 From: ReaperOfVeriod Date: Sun, 25 Oct 2026 05:02:51 +0100 Subject: [PATCH] add Guest Star collaboration panel --- src/App.svelte | 56 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/src/App.svelte b/src/App.svelte index 9a14f5e..0776589 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -50,6 +50,11 @@ let moderationReason = ''; let moderationBusy = false; let moderationError = ''; + let collaborationSession: { id: string; guests: Array<{ user_id: string; user_display_name?: string | null; slot_id?: string | null; is_live?: boolean | null }> } | null = null; + let collaborationInvites: Array<{ user_id: string; invited_at: string; status: string }> = []; + let collaborationBusy = false; + let collaborationError = ''; + let showCollaboration = false; let oauthLink = ''; let oauthIdentity: 'streamer' | 'bot' = 'streamer'; let oauthBusy = false; @@ -100,6 +105,7 @@ await loadActivity(); await loadNotifications(); await loadDiagnostics(); + await loadCollaboration(); await loadStreamInfo(); connectRealtime(); if (!mustReplaceCredentials) password = ''; @@ -151,6 +157,9 @@ showNotifications = false; diagnostics = undefined; chatMessages = []; + collaborationSession = null; + collaborationInvites = []; + showCollaboration = false; chatReplyParent = undefined; pinnedMessageIds = []; } @@ -227,6 +236,51 @@ } } + async function loadCollaboration() { + try { + const response = await fetch(`${serverAddress}/api/v1/twitch/collaboration/session`, { headers: { authorization: `Bearer ${sessionToken}` } }); + if (!response.ok) return; + collaborationSession = await response.json(); + if (collaborationSession?.id) { + const invites = await fetch(`${serverAddress}/api/v1/twitch/collaboration/invites?session_id=${encodeURIComponent(collaborationSession.id)}`, { headers: { authorization: `Bearer ${sessionToken}` } }); + if (invites.ok) collaborationInvites = await invites.json(); + } + } catch { + collaborationSession = null; + } + } + + async function createCollaboration() { + collaborationBusy = true; + collaborationError = ''; + try { + const response = await fetch(`${serverAddress}/api/v1/twitch/collaboration/session`, { method: 'POST', headers: { authorization: `Bearer ${sessionToken}` } }); + if (!response.ok) throw new Error('Twitch could not start a Guest Star session.'); + collaborationSession = await response.json(); + await loadCollaboration(); + } catch (error) { + collaborationError = error instanceof Error ? error.message : 'Collaboration session failed.'; + } finally { + collaborationBusy = false; + } + } + + async function endCollaboration() { + if (!collaborationSession) return; + collaborationBusy = true; + collaborationError = ''; + try { + const response = await fetch(`${serverAddress}/api/v1/twitch/collaboration/session/end?session_id=${encodeURIComponent(collaborationSession.id)}`, { method: 'POST', headers: { authorization: `Bearer ${sessionToken}` } }); + if (!response.ok) throw new Error('Twitch could not end the Guest Star session.'); + collaborationSession = null; + collaborationInvites = []; + } catch (error) { + collaborationError = error instanceof Error ? error.message : 'Collaboration session could not end.'; + } finally { + collaborationBusy = false; + } + } + async function markNotificationSeen(notificationId: string) { const response = await fetch(`${serverAddress}/api/v1/admin/notifications/${notificationId}/seen`, { method: 'POST', @@ -512,7 +566,7 @@
STREAM PREVIEW⋮
{#if playerSrc}{:else}
STREAM OFFLINE
CONNECT TWITCH TO BEGIN
{/if}
ACTIVITY FEED⋮
{#if activityEvents.filter((event) => activityFilter === 'all' || event.event_type.startsWith('moderation.')).length}
{#each activityEvents.filter((event) => activityFilter === 'all' || event.event_type.startsWith('moderation.')) as event}
{event.event_type}{new Date(event.occurred_at).toLocaleTimeString()}
{/each}
{:else}
◌
Nothing here yet{activityFilter === 'moderation' ? 'Moderation events will appear here.' : 'Follows, subscriptions, raids, and server events will appear here.'}
{/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}
+
QUICK ACTIONS⋮
{#if showCollaboration}
GUEST STAR SESSION
{#if collaborationSession}
ACTIVE · {collaborationSession.guests.length} GUESTS
{#if collaborationInvites.length}
PENDING INVITES
{#each collaborationInvites as invite}
{invite.user_id}{invite.status}
{/each}{/if}{:else}
No active collaboration.
{/if}{#if collaborationError}
{collaborationError}
{/if}
{/if}{#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}