mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-08-18 15:23:13 +00:00
fix(extension): preserve target and chat visibility state
This commit is contained in:
+11
-43
@@ -701,38 +701,6 @@ function clearCurrentContentTarget() {
|
||||
currentTargetHasVideo = false;
|
||||
}
|
||||
|
||||
function clearTargetTabForIdle(expectedTabId = null, expectedGeneration = null) {
|
||||
if (expectedTabId !== null && normalizeTabId(currentTabId) !== normalizeTabId(expectedTabId)) {
|
||||
return false;
|
||||
}
|
||||
if (expectedGeneration !== null && targetActivationGeneration !== expectedGeneration) {
|
||||
return false;
|
||||
}
|
||||
|
||||
completeForceSyncBeforeTargetChange(null);
|
||||
invalidateTargetActivations();
|
||||
clearPendingTarget().catch(() => {});
|
||||
if (currentTabId) deactivateTargetTab(currentTabId).catch(() => {});
|
||||
currentTabId = null;
|
||||
currentTabTitle = null;
|
||||
clearCurrentContentTarget();
|
||||
lastContentHeartbeatAt = null;
|
||||
if (currentRoom) {
|
||||
roomIdleSince = Date.now();
|
||||
}
|
||||
chrome.storage.session.set({
|
||||
currentTabId,
|
||||
currentTabTitle,
|
||||
currentTargetFrameId,
|
||||
currentTargetDocumentId,
|
||||
currentTargetHasVideo,
|
||||
roomIdleSince,
|
||||
lastContentHeartbeatAt
|
||||
}).catch(() => {});
|
||||
updateBadgeStatus();
|
||||
return true;
|
||||
}
|
||||
|
||||
async function leaveRoomAfterIdleGrace(reason) {
|
||||
if (!currentRoom) return;
|
||||
connectIntent = false;
|
||||
@@ -2717,10 +2685,15 @@ async function activateTargetTab(tabId, tabTitle, {
|
||||
}
|
||||
return { status: 'superseded' };
|
||||
}
|
||||
if (previousTabId === selectedTabId
|
||||
&& expectedCurrentTabId === selectedTabId
|
||||
&& isMediaTargetNavigationError(error)) {
|
||||
addLog('Media document changed during refresh; keeping the previous target until navigation completes', 'warn');
|
||||
const isCurrentTargetRefresh = previousTabId === selectedTabId
|
||||
&& expectedCurrentTabId === selectedTabId;
|
||||
if (isCurrentTargetRefresh) {
|
||||
addLog(
|
||||
isMediaTargetNavigationError(error)
|
||||
? 'Media document changed during refresh; keeping the previous target until navigation completes'
|
||||
: `Media target refresh failed (${error.message}); keeping the selected target for recovery`,
|
||||
'warn'
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
currentTabId = null;
|
||||
@@ -3087,16 +3060,12 @@ async function _routeToContentInternal(tabId, action, payload, actionTimestamp,
|
||||
return;
|
||||
}
|
||||
if (retries >= 3) {
|
||||
addLog(`Content Script not responding in tab ${tabId} after ${retries} retries`, 'warn');
|
||||
clearTargetTabForIdle(tabId, targetGeneration);
|
||||
addLog(`Content Script not responding in tab ${tabId} after ${retries} retries; keeping the selected target for recovery`, 'warn');
|
||||
return;
|
||||
}
|
||||
|
||||
const message = String(error?.message || '');
|
||||
if (message.includes('Receiving end does not exist')
|
||||
|| message.includes('Extension context invalidated')
|
||||
|| message.includes('No document with id')
|
||||
|| message.includes('No document with ID')) {
|
||||
if (isMissingContentReceiverError(error) || message.includes('Extension context invalidated')) {
|
||||
try {
|
||||
const response = await refreshCurrentMediaTarget(tabId);
|
||||
if (response?.status !== 'ok' && response?.status !== 'activation_in_progress') return;
|
||||
@@ -3116,7 +3085,6 @@ async function _routeToContentInternal(tabId, action, payload, actionTimestamp,
|
||||
}
|
||||
|
||||
addLog(`Content Script not responding in tab ${tabId}`, 'warn');
|
||||
clearTargetTabForIdle(tabId, targetGeneration);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -173,6 +173,16 @@ describe('chat overlay contract', () => {
|
||||
expect(overlaySource).toContain("if (destroyed || area !== 'local') return");
|
||||
});
|
||||
|
||||
it('persists the last manual open/closed state across context refreshes', () => {
|
||||
expect(overlaySource).toContain('const openStateKey = `chatOverlayOpen:${location.origin}`');
|
||||
expect(overlaySource).toContain('let lastUserOpenState = null');
|
||||
expect(overlaySource).toContain('function setOpened(next, persistPreference = true)');
|
||||
expect(overlaySource).toContain('setLocalStorage({ [openStateKey]: opened })');
|
||||
expect(overlaySource).toContain('setOpened(false, false)');
|
||||
expect(overlaySource).toContain('setOpened(lastUserOpenState ?? (chatStartMode === \'open\'), false)');
|
||||
expect(overlaySource).toContain('typeof data[openStateKey] === \'boolean\'');
|
||||
});
|
||||
|
||||
it('keeps chat hidden by default without discarding the room chat key', () => {
|
||||
expect(popupSource).toContain('localData.chatEnabled === true');
|
||||
expect(backgroundSource).toContain('chatEnabled: data.chatEnabled === true');
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
large: Object.freeze({ width: 440, height: 640 })
|
||||
});
|
||||
const storageKey = `chatOverlayLayout:${location.origin}`;
|
||||
const openStateKey = `chatOverlayOpen:${location.origin}`;
|
||||
const systemTheme = window.matchMedia('(prefers-color-scheme: light)');
|
||||
let context = null;
|
||||
let opened = false;
|
||||
@@ -57,6 +58,7 @@
|
||||
let chatSize = 'standard';
|
||||
let chatStartMode = 'bubble';
|
||||
let chatReactionDisplay = 'chat';
|
||||
let lastUserOpenState = null;
|
||||
let themeMode = 'system';
|
||||
let themePalette = 'eucalyptus';
|
||||
let pageDockTarget = null;
|
||||
@@ -576,8 +578,12 @@
|
||||
if (persistPreference) setLocalStorage({ chatSize });
|
||||
}
|
||||
|
||||
function setOpened(next) {
|
||||
function setOpened(next, persistPreference = true) {
|
||||
opened = !!next && !!context?.enabled;
|
||||
if (persistPreference) {
|
||||
lastUserOpenState = opened;
|
||||
setLocalStorage({ [openStateKey]: opened });
|
||||
}
|
||||
panel.classList.toggle('open', opened);
|
||||
launcher.style.display = opened ? 'none' : '';
|
||||
if (opened) {
|
||||
@@ -604,10 +610,10 @@
|
||||
launcher.setAttribute('aria-disabled', String(!context?.enabled));
|
||||
if (!optedIn) startStateApplied = false;
|
||||
if (!context?.enabled) {
|
||||
setOpened(false);
|
||||
setOpened(false, false);
|
||||
} else if (preferencesLoaded && !startStateApplied) {
|
||||
startStateApplied = true;
|
||||
setOpened(chatStartMode === 'open');
|
||||
setOpened(lastUserOpenState ?? (chatStartMode === 'open'), false);
|
||||
}
|
||||
applyStrings();
|
||||
applyLayout();
|
||||
@@ -967,7 +973,7 @@
|
||||
systemTheme.addEventListener('change', handleSystemTheme);
|
||||
chrome.storage.onChanged.addListener(handleStorage);
|
||||
chrome.runtime.onMessage.addListener(handleRuntime);
|
||||
chrome.storage.local.get([storageKey, 'themeMode', 'themePalette', 'chatPosition', 'chatSize', 'chatStartMode', 'chatReactionDisplay'], data => {
|
||||
chrome.storage.local.get([storageKey, openStateKey, 'themeMode', 'themePalette', 'chatPosition', 'chatSize', 'chatStartMode', 'chatReactionDisplay'], data => {
|
||||
if (destroyed) return;
|
||||
const storedLayout = data[storageKey];
|
||||
if (storedLayout && typeof storedLayout === 'object') {
|
||||
@@ -978,6 +984,7 @@
|
||||
chatPosition = normalizePosition(data.chatPosition);
|
||||
chatSize = normalizeSize(data.chatSize);
|
||||
chatStartMode = data.chatStartMode === 'open' ? 'open' : 'bubble';
|
||||
lastUserOpenState = typeof data[openStateKey] === 'boolean' ? data[openStateKey] : null;
|
||||
chatReactionDisplay = data.chatReactionDisplay === 'video' ? 'video' : 'chat';
|
||||
layout.mode = chatPosition;
|
||||
if (layout.mode === 'detached') layout.detachedInitialized = true;
|
||||
|
||||
@@ -77,6 +77,22 @@ describe('target tab lifecycle', () => {
|
||||
expect(backgroundSource).toContain("activation?.status === 'activation_in_progress'");
|
||||
});
|
||||
|
||||
it('does not discard a selected tab when its media frame refresh is transiently unavailable', () => {
|
||||
const refreshFailureGuard = backgroundSource.slice(
|
||||
backgroundSource.indexOf('const isCurrentTargetRefresh'),
|
||||
backgroundSource.indexOf('currentTabId = null', backgroundSource.indexOf('const isCurrentTargetRefresh'))
|
||||
);
|
||||
expect(refreshFailureGuard).toContain('keeping the selected target for recovery');
|
||||
expect(refreshFailureGuard).not.toContain('currentTabId = null');
|
||||
|
||||
const routeSource = backgroundSource.slice(
|
||||
backgroundSource.indexOf('async function _routeToContentInternal'),
|
||||
backgroundSource.indexOf('// --- Keep-Alive Mechanism ---')
|
||||
);
|
||||
expect(routeSource).toContain('keeping the selected target for recovery');
|
||||
expect(routeSource).not.toContain('clearTargetTabForIdle(tabId, targetGeneration)');
|
||||
});
|
||||
|
||||
it('serializes content commands and coalesces target refreshes', () => {
|
||||
expect(backgroundSource).toContain('contentCommandQueue.catch(() => {}).then(deliver)');
|
||||
expect(backgroundSource).toContain('if (mediaTargetRefreshTask && mediaTargetRefreshTabId === selectedTabId)');
|
||||
|
||||
Reference in New Issue
Block a user