Fix: Force pull images in deploy workflow
All checks were successful
Build and Push / build-all (push) Successful in 7m30s
All checks were successful
Build and Push / build-all (push) Successful in 7m30s
This commit is contained in:
parent
7f56f19e94
commit
0bb461498e
7 changed files with 104 additions and 23 deletions
|
|
@ -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<Json::Int64>(newStickerId);
|
||||
resp["sticker"]["name"] = stickerName;
|
||||
resp["sticker"]["filePath"] = filePath;
|
||||
callback(jsonResp(resp));
|
||||
}
|
||||
>> [callback](const DrogonDbException& e) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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':
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 => ({
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue