Every window shares one server settings document, and every PUT returns the merged whole, so one window's activeProjectId write was adopted by the other on its next unrelated settings save — its sidebar then auto-selected a session in that project and wrote the pointer back, converging both windows onto one session. settings-synced now carries an adoptWorkspace flag: only bootstrap-grade syncs (startup, runtime switch) may adopt the shared workspace pointers; reconcile responses keep the window's own active project while it exists. Notification clicks and session deep links also stopped broadcasting the session switch to every window.
284 lines
7.6 KiB
TypeScript
284 lines
7.6 KiB
TypeScript
import { create } from 'zustand';
|
|
|
|
import { fetchDesktopInstalledApps, isDesktopLocalOriginActive, isDesktopShell, type DesktopSettings, type InstalledDesktopAppInfo } from '@/lib/desktop';
|
|
import { OPEN_IN_APPS, DEFAULT_OPEN_IN_APP_ID, OPEN_IN_ALWAYS_AVAILABLE_APP_IDS, getOpenInAppById, getPlatformOpenInApp, type OpenInApp } from '@/lib/openInApps';
|
|
import { type SettingsSyncedDetail, updateDesktopSettings } from '@/lib/persistence';
|
|
|
|
export type OpenInAppOption = OpenInApp & {
|
|
iconDataUrl?: string;
|
|
};
|
|
|
|
type OpenInAppsState = {
|
|
selectedAppId: string;
|
|
availableApps: OpenInAppOption[];
|
|
hasLoadedApps: boolean;
|
|
isCacheStale: boolean;
|
|
isScanning: boolean;
|
|
initialize: () => void;
|
|
loadInstalledApps: (force?: boolean) => Promise<void>;
|
|
selectApp: (appId: string) => Promise<void>;
|
|
};
|
|
|
|
const getAlwaysAvailableApps = (): OpenInAppOption[] => {
|
|
return OPEN_IN_APPS
|
|
.filter((app) => OPEN_IN_ALWAYS_AVAILABLE_APP_IDS.has(app.id))
|
|
.map((app) => ({ ...getPlatformOpenInApp(app) }));
|
|
};
|
|
|
|
const getStoredAppId = (): string => {
|
|
if (typeof window === 'undefined') {
|
|
return DEFAULT_OPEN_IN_APP_ID;
|
|
}
|
|
|
|
const stored = window.localStorage.getItem('openInAppId');
|
|
if (stored && getOpenInAppById(stored)) {
|
|
return stored;
|
|
}
|
|
|
|
return DEFAULT_OPEN_IN_APP_ID;
|
|
};
|
|
|
|
let initialized = false;
|
|
let loading = false;
|
|
let keepScanning = false;
|
|
let retryAttempt = 0;
|
|
let retryTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
|
|
const clearRetryTimeout = () => {
|
|
if (retryTimeout) {
|
|
clearTimeout(retryTimeout);
|
|
retryTimeout = null;
|
|
}
|
|
};
|
|
|
|
export const useOpenInAppsStore = create<OpenInAppsState>()((set, get) => ({
|
|
selectedAppId: getStoredAppId(),
|
|
availableApps: getAlwaysAvailableApps(),
|
|
hasLoadedApps: false,
|
|
isCacheStale: false,
|
|
isScanning: false,
|
|
|
|
initialize: () => {
|
|
if (initialized || typeof window === 'undefined') {
|
|
return;
|
|
}
|
|
initialized = true;
|
|
|
|
const applyInstalledApps = (installed: InstalledDesktopAppInfo[], hasLoadedApps = true) => {
|
|
if (!Array.isArray(installed) || installed.length === 0) {
|
|
set({ availableApps: getAlwaysAvailableApps(), hasLoadedApps });
|
|
return;
|
|
}
|
|
|
|
const allowed = new Set(installed.map((app) => app.name));
|
|
const iconMap = new Map(installed.map((app) => [app.name, app.iconDataUrl ?? undefined]));
|
|
|
|
const filtered = OPEN_IN_APPS.filter(
|
|
(app) => allowed.has(app.appName) || OPEN_IN_ALWAYS_AVAILABLE_APP_IDS.has(app.id)
|
|
);
|
|
|
|
const withIcons = filtered.map((app) => ({
|
|
...getPlatformOpenInApp(app),
|
|
iconDataUrl: iconMap.get(app.appName),
|
|
}));
|
|
|
|
set({ availableApps: withIcons, hasLoadedApps: true });
|
|
};
|
|
|
|
const loadInstalledApps = async (force?: boolean) => {
|
|
if (!isDesktopShell() || !isDesktopLocalOriginActive()) {
|
|
return;
|
|
}
|
|
|
|
if (loading) {
|
|
return;
|
|
}
|
|
|
|
const state = get();
|
|
if (state.hasLoadedApps && !force) {
|
|
return;
|
|
}
|
|
|
|
const appNames = OPEN_IN_APPS.map((app) => app.appName);
|
|
clearRetryTimeout();
|
|
|
|
if (force) {
|
|
set({ hasLoadedApps: false });
|
|
}
|
|
|
|
loading = true;
|
|
keepScanning = false;
|
|
set({ isScanning: true });
|
|
|
|
try {
|
|
const {
|
|
apps: installed,
|
|
success,
|
|
hasCache,
|
|
isCacheStale,
|
|
} = await fetchDesktopInstalledApps(appNames, force);
|
|
|
|
const shouldRetryEmptyScan = success && !hasCache && installed.length === 0 && retryAttempt < 3;
|
|
|
|
set({ isCacheStale: hasCache ? isCacheStale : false });
|
|
applyInstalledApps(installed, success ? !shouldRetryEmptyScan : false);
|
|
|
|
if (success) {
|
|
if (shouldRetryEmptyScan) {
|
|
const delays = [800, 1600, 3200];
|
|
const delay = delays[retryAttempt] ?? 3200;
|
|
retryAttempt += 1;
|
|
keepScanning = true;
|
|
retryTimeout = setTimeout(() => {
|
|
void loadInstalledApps();
|
|
}, delay);
|
|
return;
|
|
}
|
|
|
|
retryAttempt = 0;
|
|
keepScanning = false;
|
|
return;
|
|
}
|
|
|
|
if (retryAttempt < 3) {
|
|
const delays = [1000, 3000, 7000];
|
|
const delay = delays[retryAttempt] ?? 7000;
|
|
retryAttempt += 1;
|
|
keepScanning = true;
|
|
retryTimeout = setTimeout(() => {
|
|
void loadInstalledApps();
|
|
}, delay);
|
|
}
|
|
} finally {
|
|
loading = false;
|
|
if (!keepScanning) {
|
|
set({ isScanning: false });
|
|
}
|
|
}
|
|
};
|
|
|
|
void loadInstalledApps();
|
|
|
|
const settingsHandler = (event: Event) => {
|
|
const detail = (event as CustomEvent<SettingsSyncedDetail>).detail?.settings;
|
|
const nextId = detail
|
|
&& typeof detail.openInAppId === 'string'
|
|
&& detail.openInAppId.length > 0
|
|
&& getOpenInAppById(detail.openInAppId)
|
|
? detail.openInAppId
|
|
: null;
|
|
|
|
if (!nextId) {
|
|
return;
|
|
}
|
|
|
|
window.localStorage.setItem('openInAppId', nextId);
|
|
set({ selectedAppId: nextId });
|
|
};
|
|
|
|
const appReadyHandler = () => {
|
|
void loadInstalledApps();
|
|
};
|
|
|
|
const updateHandler = (event: Event) => {
|
|
const detail = (event as CustomEvent<InstalledDesktopAppInfo[]>).detail;
|
|
if (!Array.isArray(detail)) {
|
|
return;
|
|
}
|
|
|
|
retryAttempt = 3;
|
|
keepScanning = false;
|
|
set({ isScanning: false, isCacheStale: false });
|
|
applyInstalledApps(detail);
|
|
};
|
|
|
|
window.addEventListener('openchamber:settings-synced', settingsHandler);
|
|
window.addEventListener('openchamber:app-ready', appReadyHandler);
|
|
window.addEventListener('openchamber:installed-apps-updated', updateHandler);
|
|
|
|
const appReady = (window as unknown as { __openchamberAppReady?: boolean }).__openchamberAppReady;
|
|
if (appReady) {
|
|
void loadInstalledApps();
|
|
}
|
|
|
|
window.setTimeout(() => {
|
|
if (!get().hasLoadedApps) {
|
|
void loadInstalledApps();
|
|
}
|
|
}, 5000);
|
|
},
|
|
|
|
loadInstalledApps: async (force?: boolean) => {
|
|
if (!initialized) {
|
|
get().initialize();
|
|
}
|
|
|
|
if (!isDesktopShell() || !isDesktopLocalOriginActive()) {
|
|
return;
|
|
}
|
|
|
|
if (loading) {
|
|
return;
|
|
}
|
|
|
|
const state = get();
|
|
if (state.hasLoadedApps && !force) {
|
|
return;
|
|
}
|
|
|
|
const appNames = OPEN_IN_APPS.map((app) => app.appName);
|
|
clearRetryTimeout();
|
|
|
|
if (force) {
|
|
set({ hasLoadedApps: false });
|
|
}
|
|
|
|
loading = true;
|
|
keepScanning = false;
|
|
set({ isScanning: true });
|
|
|
|
try {
|
|
const {
|
|
apps: installed,
|
|
hasCache,
|
|
isCacheStale,
|
|
} = await fetchDesktopInstalledApps(appNames, force);
|
|
|
|
const allowed = new Set(installed.map((app) => app.name));
|
|
const iconMap = new Map(installed.map((app) => [app.name, app.iconDataUrl ?? undefined]));
|
|
const filtered = OPEN_IN_APPS.filter(
|
|
(app) => allowed.has(app.appName) || OPEN_IN_ALWAYS_AVAILABLE_APP_IDS.has(app.id)
|
|
);
|
|
const withIcons = filtered.map((app) => ({
|
|
...app,
|
|
iconDataUrl: iconMap.get(app.appName),
|
|
}));
|
|
|
|
set({
|
|
availableApps: withIcons.length > 0 ? withIcons : getAlwaysAvailableApps(),
|
|
hasLoadedApps: withIcons.length > 0,
|
|
isCacheStale: hasCache ? isCacheStale : false,
|
|
});
|
|
} finally {
|
|
loading = false;
|
|
if (!keepScanning) {
|
|
set({ isScanning: false });
|
|
}
|
|
}
|
|
},
|
|
|
|
selectApp: async (appId: string) => {
|
|
if (!getOpenInAppById(appId)) {
|
|
return;
|
|
}
|
|
|
|
set({ selectedAppId: appId });
|
|
|
|
if (typeof window !== 'undefined') {
|
|
window.localStorage.setItem('openInAppId', appId);
|
|
}
|
|
|
|
await updateDesktopSettings({ openInAppId: appId });
|
|
},
|
|
}));
|