multiuser

This commit is contained in:
2026-03-12 01:08:16 +11:00
parent 30a7725769
commit 7cf6b4e6ae
12 changed files with 939 additions and 247 deletions

View File

@@ -0,0 +1,184 @@
import React from 'react';
import { Box, Text, Icon, Icons, MenuItem, config } from 'folds';
import { useAtomValue } from 'jotai';
import { sessionsAtom, Session } from '../../state/sessions';
import { setLocalStorageItem } from '../../state/utils/atomWithLocalStorage';
import { getMxIdServer } from '../../utils/matrix';
import { useMatrixClient } from '../../hooks/useMatrixClient';
type AccountSwitcherProps = {
currentUserId?: string;
};
const deleteAllMatrixDatabases = async (): Promise<void> => {
console.log('[AccountSwitcher] Deleting all Matrix IndexedDB databases...');
// List of known Matrix database names/patterns to delete
const dbsToDelete = [
'matrix-js-sdk::matrix-sdk-crypto', // Shared crypto store (causes conflicts)
];
// Try to get all databases if the API is available
try {
if ('databases' in indexedDB) {
const allDbs = await (indexedDB as any).databases();
const matrixDbs = allDbs
.map((db: any) => db.name)
.filter((name: string) =>
name && (
name.startsWith('matrix-js-sdk') ||
name.includes('@@') ||
name.startsWith('sync@@') ||
name.startsWith('crypto@@')
)
);
dbsToDelete.push(...matrixDbs);
console.log('[AccountSwitcher] Found Matrix databases:', matrixDbs);
}
} catch (e) {
console.warn('[AccountSwitcher] Could not enumerate databases:', e);
}
// Delete each database with timeout
const deletePromises = dbsToDelete.map(async (dbName) => {
try {
const deletePromise = new Promise<void>((resolve, reject) => {
const request = indexedDB.deleteDatabase(dbName);
request.onsuccess = () => {
console.log(`[AccountSwitcher] ✅ Deleted database: ${dbName}`);
resolve();
};
request.onerror = () => {
console.warn(`[AccountSwitcher] ⚠️ Failed to delete: ${dbName}`, request.error);
resolve(); // Don't reject, just continue
};
request.onblocked = () => {
console.warn(`[AccountSwitcher] ⚠️ Delete blocked: ${dbName}`);
resolve(); // Don't reject, reload will handle it
};
});
// Add 1-second timeout per database
const timeoutPromise = new Promise<void>((resolve) => {
setTimeout(() => {
console.warn(`[AccountSwitcher] ⏱️ Delete timeout: ${dbName}`);
resolve();
}, 1000);
});
await Promise.race([deletePromise, timeoutPromise]);
} catch (error) {
console.warn(`[AccountSwitcher] Error deleting ${dbName}:`, error);
}
});
await Promise.all(deletePromises);
console.log('[AccountSwitcher] ✅ Database cleanup complete');
};
export function AccountSwitcher({ currentUserId }: AccountSwitcherProps) {
const sessions = useAtomValue(sessionsAtom);
const mx = useMatrixClient();
const handleSwitchAccount = async (session: Session) => {
if (session.userId === currentUserId) return;
console.log('[AccountSwitcher] Switching account from', currentUserId, 'to', session.userId);
// Stop the current client before switching
if (mx) {
console.log('[AccountSwitcher] Stopping current Matrix client...');
try {
mx.stopClient();
console.log('[AccountSwitcher] Matrix client stopped');
} catch (error) {
console.error('[AccountSwitcher] Error stopping client:', error);
}
}
// Set the new current session
setLocalStorageItem('currentSessionUserId', session.userId);
// Delete all Matrix databases to prevent conflicts
await deleteAllMatrixDatabases();
// Small delay to ensure cleanup is complete
await new Promise(resolve => setTimeout(resolve, 100));
// Reload the page to initialize the new session
console.log('[AccountSwitcher] Reloading page...');
window.location.reload();
};
// If no current user, don't show anything
if (!currentUserId) {
return null;
}
// Filter out other sessions (not current)
const otherSessions = sessions.filter(s => s.userId !== currentUserId);
const hasMultipleAccounts = sessions.length > 1;
return (
<Box direction="Column" gap="100">
<Text size="L400" style={{ padding: config.space.S200 }}>
{hasMultipleAccounts ? 'Switch Account' : 'Accounts'}
</Text>
{/* Show current account */}
{currentUserId && (
<MenuItem
key={currentUserId}
size="300"
radii="300"
variant="SurfaceVariant"
disabled
after={<Icon size="100" src={Icons.Check} filled />}
>
<Box gap="200" alignItems="Center">
<Icon size="100" src={Icons.User} />
<Box direction="Column" gap="100">
<Text size="T300" truncate>
{currentUserId}
</Text>
{getMxIdServer(currentUserId) && (
<Text size="T200" priority="300" truncate>
{getMxIdServer(currentUserId)}
</Text>
)}
</Box>
</Box>
</MenuItem>
)}
{/* Show other accounts if any */}
{otherSessions.map((session) => {
const server = getMxIdServer(session.userId);
return (
<MenuItem
key={session.userId}
size="300"
radii="300"
onClick={() => handleSwitchAccount(session)}
variant="Surface"
>
<Box gap="200" alignItems="Center">
<Icon size="100" src={Icons.User} />
<Box direction="Column" gap="100">
<Text size="T300" truncate>
{session.userId}
</Text>
{server && (
<Text size="T200" priority="300" truncate>
{server}
</Text>
)}
</Box>
</Box>
</MenuItem>
);
})}
</Box>
);
}

View File

@@ -0,0 +1 @@
export { AccountSwitcher } from './AccountSwitcher';