import React, { useEffect, useState } from 'react'; import { Avatar, Box, MenuItem, Text, config } from 'folds'; import { Icon, Icons } from '../icons'; import { useAtomValue } from 'jotai'; import { sessionsAtom, Session } from '../../state/sessions'; import { setLocalStorageItem } from '../../state/utils/atomWithLocalStorage'; import { getMxIdServer, mxcUrlToHttp } from '../../utils/matrix'; import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useUserProfile } from '../../hooks/useUserProfile'; import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; import { UserAvatar } from '../user-avatar'; type AccountSwitcherProps = { currentUserId?: string; }; /** * Loads and displays an avatar for a session that is not the active client. * Fetches the profile directly via the homeserver REST API using stored credentials. */ function OtherSessionAvatar({ session }: { session: Session }) { const [src, setSrc] = useState(); useEffect(() => { const load = async () => { try { const res = await fetch( `${session.baseUrl}/_matrix/client/v3/profile/${encodeURIComponent(session.userId)}`, { headers: { Authorization: `Bearer ${session.accessToken}` } } ); if (!res.ok) return; const data = await res.json(); if (data.avatar_url) { const match = (data.avatar_url as string).match(/^mxc:\/\/([^/]+)\/(.+)$/); if (match) { setSrc( `${session.baseUrl}/_matrix/media/v3/thumbnail/${match[1]}/${match[2]}?width=32&height=32&method=crop` ); } } } catch { // ignore — avatar just won't show } }; load(); }, [session.userId, session.baseUrl, session.accessToken]); return ( } /> ); } const deleteAllMatrixDatabases = async (): Promise => { 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((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((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 useAuthentication = useMediaAuthentication(); const currentProfile = useUserProfile(currentUserId ?? ''); const currentAvatarUrl = currentProfile.avatarUrl ? mxcUrlToHttp(mx, currentProfile.avatarUrl, useAuthentication, 32, 32, 'crop') ?? undefined : undefined; 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 ( {hasMultipleAccounts ? 'Switch Account' : 'Accounts'} {/* Show current account */} {currentUserId && ( } > } /> {currentUserId} {getMxIdServer(currentUserId) && ( {getMxIdServer(currentUserId)} )} )} {/* Show other accounts if any */} {otherSessions.map((session) => { const server = getMxIdServer(session.userId); return ( handleSwitchAccount(session)} variant="Surface" style={{ paddingInline: config.space.S200 }} > {session.userId} {server && ( {server} )} ); })} ); }