From 0bb461498eb83d211ccd360091ed1dad82a3bd79 Mon Sep 17 00:00:00 2001 From: doomtube Date: Tue, 6 Jan 2026 23:56:06 -0500 Subject: [PATCH] Fix: Force pull images in deploy workflow --- backend/src/controllers/AdminController.cpp | 5 +-- frontend/src/lib/chat/chatStore.js | 25 +++++++++++-- frontend/src/lib/chat/chatWebSocket.js | 6 +++- .../lib/components/chat/ChatMessage.svelte | 29 ++++++++++++--- frontend/src/lib/stores/watchSync.js | 12 +++++++ .../src/routes/[realm]/watch/+page.svelte | 36 ++++++++++++------- frontend/src/routes/admin/+page.svelte | 14 +++++++- 7 files changed, 104 insertions(+), 23 deletions(-) diff --git a/backend/src/controllers/AdminController.cpp b/backend/src/controllers/AdminController.cpp index 6895b68..eaa4305 100644 --- a/backend/src/controllers/AdminController.cpp +++ b/backend/src/controllers/AdminController.cpp @@ -1081,7 +1081,7 @@ void AdminController::approveStickerSubmission(const HttpRequestPtr &req, // Insert into stickers table *dbClient << "INSERT INTO stickers (name, file_path) VALUES ($1, $2) RETURNING id" << stickerName << filePath - >> [callback, dbClient, id, reviewerId, stickerName](const Result& insertResult) { + >> [callback, dbClient, id, reviewerId, stickerName, filePath](const Result& insertResult) { if (insertResult.empty()) { callback(jsonError("Failed to create sticker")); return; @@ -1093,7 +1093,7 @@ void AdminController::approveStickerSubmission(const HttpRequestPtr &req, *dbClient << "UPDATE sticker_submissions SET status = 'approved', " "reviewed_by = $1, reviewed_at = CURRENT_TIMESTAMP WHERE id = $2" << reviewerId << id - >> [callback, newStickerId, stickerName](const Result&) { + >> [callback, newStickerId, stickerName, filePath](const Result&) { // Notify chat-service to refresh sticker cache notifyChatServiceStickerUpdate(); @@ -1102,6 +1102,7 @@ void AdminController::approveStickerSubmission(const HttpRequestPtr &req, resp["message"] = "Sticker approved and added"; resp["sticker"]["id"] = static_cast(newStickerId); resp["sticker"]["name"] = stickerName; + resp["sticker"]["filePath"] = filePath; callback(jsonResp(resp)); } >> [callback](const DrogonDbException& e) { diff --git a/frontend/src/lib/chat/chatStore.js b/frontend/src/lib/chat/chatStore.js index d6faa68..d489eb5 100644 --- a/frontend/src/lib/chat/chatStore.js +++ b/frontend/src/lib/chat/chatStore.js @@ -119,8 +119,29 @@ export function clearMessages() { } // Set message history -export function setMessageHistory(messageList) { - messages.set(messageList); +// If merge=true, adds new messages to existing ones (for realm switching in global chat) +// If merge=false (default), replaces all messages (for initial connection) +export function setMessageHistory(messageList, merge = false) { + if (merge) { + messages.update((existing) => { + // Create a Map of existing messages by messageId for O(1) lookup + const existingMap = new Map(existing.map((m) => [m.messageId, m])); + + // Add new messages that don't already exist + for (const msg of messageList) { + if (!existingMap.has(msg.messageId)) { + existingMap.set(msg.messageId, msg); + } + } + + // Convert back to array and sort by timestamp + return Array.from(existingMap.values()).sort( + (a, b) => new Date(a.timestamp) - new Date(b.timestamp) + ); + }); + } else { + messages.set(messageList); + } } // Toggle channel diff --git a/frontend/src/lib/chat/chatWebSocket.js b/frontend/src/lib/chat/chatWebSocket.js index fd71a37..e7c721f 100644 --- a/frontend/src/lib/chat/chatWebSocket.js +++ b/frontend/src/lib/chat/chatWebSocket.js @@ -132,7 +132,11 @@ class ChatWebSocket { break; case 'history': - setMessageHistory(data.messages || []); + // Merge history when switching realms to preserve messages from other realms + // Backend includes realmId only for realm-switch history (from handleJoinRealm) + // Initial connection history (handleNewConnection) has no realmId + const shouldMerge = data.realmId !== undefined; + setMessageHistory(data.messages || [], shouldMerge); break; case 'new_message': diff --git a/frontend/src/lib/components/chat/ChatMessage.svelte b/frontend/src/lib/components/chat/ChatMessage.svelte index 8e5f978..e7e4a5d 100644 --- a/frontend/src/lib/components/chat/ChatMessage.svelte +++ b/frontend/src/lib/components/chat/ChatMessage.svelte @@ -38,19 +38,39 @@ // Self-destruct countdown let countdownSeconds = 0; let countdownInterval = null; + let countdownStartedAt = null; + let initialCountdown = 0; $: hasSelfDestruct = message.selfDestructAt && message.selfDestructAt > 0; function updateCountdown() { - if (!message.selfDestructAt) return; - const now = Date.now(); - const remaining = Math.max(0, Math.ceil((message.selfDestructAt - now) / 1000)); + if (!countdownStartedAt) return; + + // Calculate elapsed time since countdown started (local time only, avoids clock skew) + const elapsedSec = Math.floor((Date.now() - countdownStartedAt) / 1000); + const remaining = Math.max(0, initialCountdown - elapsedSec); + countdownSeconds = remaining; + if (remaining <= 0 && countdownInterval) { clearInterval(countdownInterval); countdownInterval = null; } } + function startCountdown() { + if (!message.selfDestructAt || !message.timestamp) return; + + // Calculate the intended duration from server timestamps (no clock skew between them) + const intendedDurationSec = Math.ceil((message.selfDestructAt - message.timestamp) / 1000); + + // Record when we started counting down locally + countdownStartedAt = Date.now(); + initialCountdown = intendedDurationSec; + countdownSeconds = intendedDurationSec; + + countdownInterval = setInterval(updateCountdown, 1000); + } + function formatCountdown(seconds) { if (seconds >= 60) { const m = Math.floor(seconds / 60); @@ -69,8 +89,7 @@ onMount(async () => { // Set up self-destruct countdown if applicable if (message.selfDestructAt && message.selfDestructAt > 0) { - updateCountdown(); - countdownInterval = setInterval(updateCountdown, 1000); + startCountdown(); } // Ensure stickers are loaded (uses shared store - only fetches once across all components) diff --git a/frontend/src/lib/stores/watchSync.js b/frontend/src/lib/stores/watchSync.js index 24883d7..9ba5eee 100644 --- a/frontend/src/lib/stores/watchSync.js +++ b/frontend/src/lib/stores/watchSync.js @@ -106,6 +106,18 @@ function createWatchSyncStore() { repeatCount: data.repeatCount || 0, isRepeating: true })); + } else if (data.event === 'locked_restart') { + // Locked video loop - restart from beginning + update(state => ({ + ...state, + playbackState: 'playing', + currentTime: 0, + serverTime: data.serverTime || Date.now(), + currentVideo: data.currentVideo !== undefined ? data.currentVideo : state.currentVideo, + leadIn: true, + repeatCount: 0, + isRepeating: true // Video is looping (locked) + })); } else if (data.event === 'skip') { // Skip resets repeat state update(state => ({ diff --git a/frontend/src/routes/[realm]/watch/+page.svelte b/frontend/src/routes/[realm]/watch/+page.svelte index 9cbd4e3..4d20c41 100644 --- a/frontend/src/routes/[realm]/watch/+page.svelte +++ b/frontend/src/routes/[realm]/watch/+page.svelte @@ -24,6 +24,21 @@ $: realmName = $page.params.realm; + // Helper to get auth token for WebSocket connections + async function getAuthToken() { + if (!browser) return null; + try { + const response = await fetch('/api/user/token', { credentials: 'include' }); + if (response.ok) { + const data = await response.json(); + return data.token || null; + } + } catch (e) { + console.error('Failed to get auth token:', e); + } + return null; + } + // Re-check ownership and reconnect WebSocket when auth state changes (login/logout) let lastAuthUserId = undefined; // undefined = not yet initialized $: { @@ -37,11 +52,10 @@ // Auth changed - reconnect WebSocket to get updated permissions lastAuthUserId = currentUserId; checkOwnership(); - const token = browser ? localStorage.getItem('token') : null; watchSync.disconnect(); - setTimeout(() => { + getAuthToken().then(token => { watchSync.connect(realm.id, token); - }, 100); + }); } } } @@ -62,7 +76,7 @@ } // Connect to watch sync WebSocket - const token = localStorage.getItem('token'); + const token = await getAuthToken(); watchSync.connect(realm.id, token); // Load playlist @@ -129,16 +143,14 @@ // Only run this check once after WebSocket is connected and we know we're the owner if (!permissionCheckDone && isOwner && !loading && $auth.user) { // Give the WebSocket a moment to receive welcome message - setTimeout(() => { + setTimeout(async () => { if (isOwner && !$canControl && realm) { console.log('Permission mismatch detected: owner but no control. Reconnecting...'); - const token = browser ? localStorage.getItem('token') : null; - if (token) { - watchSync.disconnect(); - setTimeout(() => { - watchSync.connect(realm.id, token); - }, 100); - } + const token = await getAuthToken(); + watchSync.disconnect(); + setTimeout(() => { + watchSync.connect(realm.id, token); + }, 100); } permissionCheckDone = true; }, 1500); diff --git a/frontend/src/routes/admin/+page.svelte b/frontend/src/routes/admin/+page.svelte index 51b995d..49928a2 100644 --- a/frontend/src/routes/admin/+page.svelte +++ b/frontend/src/routes/admin/+page.svelte @@ -907,8 +907,20 @@ }); if (response.ok) { + const data = await response.json(); message = 'Sticker approved and added'; - await Promise.all([loadStickers(), loadStickerSubmissions()]); + + // Optimistically add the new sticker to the list using response data + if (data.sticker) { + stickers = [...stickers, { + id: data.sticker.id, + name: data.sticker.name, + filePath: data.sticker.filePath + }].sort((a, b) => a.name.localeCompare(b.name)); + } + + // Remove from pending submissions + await loadStickerSubmissions(); } else { const data = await response.json(); error = data.error || 'Failed to approve sticker';