diff --git a/src/app/components/AnimatedOutlet.css.ts b/src/app/components/AnimatedOutlet.css.ts
new file mode 100644
index 0000000..fba6579
--- /dev/null
+++ b/src/app/components/AnimatedOutlet.css.ts
@@ -0,0 +1,21 @@
+import { style } from '@vanilla-extract/css';
+
+export const RoutePane = style({
+ flex: 1,
+ minWidth: 0,
+ minHeight: 0,
+ overflow: 'hidden',
+ display: 'flex',
+ flexDirection: 'column',
+});
+
+/** Full-bleed overlay for the press→navigate gap only. */
+export const PendingOverlay = style({
+ position: 'absolute',
+ inset: 0,
+ zIndex: 2,
+ display: 'flex',
+ minWidth: 0,
+ minHeight: 0,
+ pointerEvents: 'none',
+});
diff --git a/src/app/components/AnimatedOutlet.tsx b/src/app/components/AnimatedOutlet.tsx
index 83e56ff..fc9af0a 100644
--- a/src/app/components/AnimatedOutlet.tsx
+++ b/src/app/components/AnimatedOutlet.tsx
@@ -1,27 +1,61 @@
-import React from 'react';
-import { Outlet, useLocation } from 'react-router-dom';
+import React, { useEffect } from 'react';
+import { Outlet } from 'react-router-dom';
+import { useAtom, useAtomValue } from 'jotai';
+import { pendingRoomNavigationAtom } from '../state/pendingRoomNavigation';
+import { useMatrixClient } from '../hooks/useMatrixClient';
+import { IsDirectRoomProvider, RoomProvider } from '../hooks/useRoom';
+import { mDirectAtom } from '../state/mDirectList';
+import { useSelectedRoom } from '../hooks/router/useSelectedRoom';
+import { RoomFrame } from '../features/room/Room';
+import * as css from './AnimatedOutlet.css';
+
+/** Chrome-only shell for the press→navigate gap (no timeline — avoids scroll fights). */
+function PendingRoomShell({ roomId }: { roomId: string }) {
+ const mx = useMatrixClient();
+ const mDirects = useAtomValue(mDirectAtom);
+ const room = mx.getRoom(roomId);
+
+ if (!room) return null;
-/**
- * Wrapper for Outlet that adds route-based animation
- * Forces remount on route change by using location as key
- */
-export function AnimatedOutlet() {
- const location = useLocation();
-
return (
-
-
+
+
+
+
+
+
+
+ );
+}
+
+/** Optimistic room navigation: brief pending chrome until the route settles. */
+export function AnimatedOutlet() {
+ const selectedRoomId = useSelectedRoom();
+ const [pendingRoomId, setPendingRoomId] = useAtom(pendingRoomNavigationAtom);
+
+ // Safety: if navigation never lands, don't leave pending forever.
+ useEffect(() => {
+ if (!pendingRoomId) return undefined;
+ const t = window.setTimeout(() => setPendingRoomId(null), 4000);
+ return () => window.clearTimeout(t);
+ }, [pendingRoomId, setPendingRoomId]);
+
+ // Only cover the gap before the target room route is active.
+ const showOverlay =
+ typeof pendingRoomId === 'string' &&
+ pendingRoomId.length > 0 &&
+ pendingRoomId !== selectedRoomId;
+
+ return (
+
+ {showOverlay &&
}
+ {/*
+ Do NOT key this by pathname — room switches share CachedRooms keep-alive.
+ Remounting here wiped the cache and made warm rooms feel as slow as cold ones.
+ */}
+
+
+
);
}
diff --git a/src/app/components/message/MsgTypeRenderers.tsx b/src/app/components/message/MsgTypeRenderers.tsx
index 7f29d36..6ade4a4 100644
--- a/src/app/components/message/MsgTypeRenderers.tsx
+++ b/src/app/components/message/MsgTypeRenderers.tsx
@@ -27,8 +27,8 @@ import {
MATRIX_SPOILER_REASON_PROPERTY_NAME,
} from '../../../types/matrix/common';
import { FALLBACK_MIMETYPE, getBlobSafeMimeType } from '../../utils/mimeTypes';
-import { parseGeoUri, scaleYDimension, fitMediaSize } from '../../utils/common';
-import { getMediaDimensions } from '../../state/mediaDimensionCache';
+import { parseGeoUri, scaleYDimension } from '../../utils/common';
+import { resolveAttachmentBoxSize } from '../../state/mediaDimensionCache';
import { Attachment, AttachmentBox, AttachmentContent, AttachmentHeader } from './attachment';
import { FileHeader, FileDownloadButton } from './FileHeader';
@@ -206,18 +206,11 @@ type MImageProps = {
outlined?: boolean;
};
-const ATTACHMENT_MAX = 400;
-
const resolveMediaBoxSize = (
mxcUrl: string,
w?: number,
h?: number
-): { width: number; height: number } => {
- const cached = (!w || !h) ? getMediaDimensions(mxcUrl) : undefined;
- const naturalW = w && w > 0 ? w : cached?.w;
- const naturalH = h && h > 0 ? h : cached?.h;
- return fitMediaSize(naturalW ?? 0, naturalH ?? 0, ATTACHMENT_MAX, ATTACHMENT_MAX);
-};
+): { width: number; height: number } => resolveAttachmentBoxSize(mxcUrl, w, h);
export function MImage({ content, renderImageContent, outlined }: MImageProps) {
const imgInfo = content?.info;
diff --git a/src/app/components/message/content/ImageContent.tsx b/src/app/components/message/content/ImageContent.tsx
index 1891867..0f3520f 100644
--- a/src/app/components/message/content/ImageContent.tsx
+++ b/src/app/components/message/content/ImageContent.tsx
@@ -169,7 +169,7 @@ export const ImageContent = as<'div', ImageContentProps>(
setShowPlaceholder(true);
return undefined;
}
- const timer = window.setTimeout(() => setShowPlaceholder(false), 300);
+ const timer = window.setTimeout(() => setShowPlaceholder(false), 540);
return () => window.clearTimeout(timer);
}, [load]);
diff --git a/src/app/components/message/content/VideoContent.tsx b/src/app/components/message/content/VideoContent.tsx
index 9fcf31a..ca615a6 100644
--- a/src/app/components/message/content/VideoContent.tsx
+++ b/src/app/components/message/content/VideoContent.tsx
@@ -134,7 +134,7 @@ export const VideoContent = as<'div', VideoContentProps>(
setShowPlaceholder(true);
return undefined;
}
- const timer = window.setTimeout(() => setShowPlaceholder(false), 300);
+ const timer = window.setTimeout(() => setShowPlaceholder(false), 540);
return () => window.clearTimeout(timer);
}, [load]);
diff --git a/src/app/components/message/content/style.css.ts b/src/app/components/message/content/style.css.ts
index f777f9b..481ef65 100644
--- a/src/app/components/message/content/style.css.ts
+++ b/src/app/components/message/content/style.css.ts
@@ -78,6 +78,15 @@ export const MediaSkeleton = style([
},
]);
+const blurhashFadeIn = keyframes({
+ from: {
+ opacity: 0,
+ },
+ to: {
+ opacity: 1,
+ },
+});
+
/** Soft blurhash layer that fills the reserved media box. */
export const BlurhashPlaceholder = style([
DefaultReset,
@@ -90,6 +99,8 @@ export const BlurhashPlaceholder = style([
// Soften the decode so it reads as color blobs; slight scale hides blur edges.
filter: 'blur(14px)',
transform: 'scale(1.06)',
+ opacity: 0,
+ animation: `${blurhashFadeIn} 560ms cubic-bezier(0.22, 1, 0.36, 1) forwards`,
},
]);
@@ -97,7 +108,7 @@ export const BlurhashPlaceholder = style([
export const MediaFadeIn = style({
zIndex: 1,
opacity: 0,
- transition: 'opacity 280ms ease-out',
+ transition: 'opacity 520ms cubic-bezier(0.22, 1, 0.36, 1)',
});
export const MediaFadeInLoaded = style({
diff --git a/src/app/components/page/Page.tsx b/src/app/components/page/Page.tsx
index 29b0e26..0252012 100644
--- a/src/app/components/page/Page.tsx
+++ b/src/app/components/page/Page.tsx
@@ -1,12 +1,17 @@
-import React, { ComponentProps, MutableRefObject, ReactNode } from 'react';
+import React, { ComponentProps, MutableRefObject, ReactNode, Suspense, lazy } from 'react';
import { Box, Header, Line, Scroll, Text, as } from 'folds';
import classNames from 'classnames';
import { ContainerColor } from '../../styles/ContainerColor.css';
import * as css from './style.css';
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
-import { SidebarDockedCallPanel } from '../../features/call/SidebarDockedCallPanel';
import { useShowCompactMasterView } from '../../hooks/useCompactNav';
+const SidebarDockedCallPanel = lazy(() =>
+ import('../../features/call/SidebarDockedCallPanel').then((module) => ({
+ default: module.SidebarDockedCallPanel,
+ }))
+);
+
type PageRootProps = {
nav: ReactNode;
children: ReactNode;
@@ -46,7 +51,9 @@ export function PageNav({
>
{children}
-
+
+
+
);
diff --git a/src/app/features/call/callMemberUtils.ts b/src/app/features/call/callMemberUtils.ts
new file mode 100644
index 0000000..fb970c5
--- /dev/null
+++ b/src/app/features/call/callMemberUtils.ts
@@ -0,0 +1,60 @@
+import { MatrixEvent, Room } from 'matrix-js-sdk';
+
+/** The Matrix state event type for call membership (MSC3401) */
+export const CALL_MEMBER_EVENT_TYPE = 'org.matrix.msc3401.call.member';
+
+/** Represents an active call member in a room */
+export interface CallMember {
+ userId: string;
+ deviceId: string;
+ sessionId: string;
+ expiresTs: number;
+}
+
+/**
+ * Extracts active call members from a room's state events
+ */
+export function getActiveCallMembers(room: Room): CallMember[] {
+ const members: CallMember[] = [];
+ const now = Date.now();
+
+ try {
+ const stateEvents = room.currentState.getStateEvents(CALL_MEMBER_EVENT_TYPE);
+
+ stateEvents.forEach((event) => {
+ const content = event.getContent();
+ const userId = event.getStateKey();
+
+ if (!userId || !content['m.calls']) return;
+
+ const calls = content['m.calls'];
+ if (!Array.isArray(calls)) return;
+
+ calls.forEach((call) => {
+ const devices = call['m.devices'];
+ if (!Array.isArray(devices)) return;
+
+ devices.forEach((device) => {
+ const expiresTs = device.expires_ts || 0;
+
+ if (expiresTs > now) {
+ members.push({
+ userId,
+ deviceId: device.device_id,
+ sessionId: device.session_id,
+ expiresTs,
+ });
+ }
+ });
+ });
+ });
+ } catch {
+ // Ignore malformed call member events
+ }
+
+ return members;
+}
+
+export function isCallMemberEvent(event: MatrixEvent): boolean {
+ return event.getType() === CALL_MEMBER_EVENT_TYPE;
+}
diff --git a/src/app/features/call/useCall.ts b/src/app/features/call/useCall.ts
index 6b0417d..914be50 100644
--- a/src/app/features/call/useCall.ts
+++ b/src/app/features/call/useCall.ts
@@ -230,62 +230,76 @@ export function useCallService(): CallContextValue {
/**
* Hook to check if a call is active in a specific room
*/
+/** Persists across keep-alive remounts so room switches don't re-hit well-known. */
+const roomCallSupportCache = new Map
();
+
export function useRoomCall(roomId: string) {
const { activeCall, startCall, endCall, callSupported: globalCallSupported, callSupportLoading: globalCallSupportLoading, callService } = useCall();
const mx = useMatrixClient();
- const [roomCallSupported, setRoomCallSupported] = useState(false);
- const [roomCallSupportLoading, setRoomCallSupportLoading] = useState(true);
+ const [roomCallSupported, setRoomCallSupported] = useState(
+ () => roomCallSupportCache.get(roomId) ?? false
+ );
+ const [roomCallSupportLoading, setRoomCallSupportLoading] = useState(
+ () => !roomCallSupportCache.has(roomId)
+ );
// Check if calls are supported for this specific room by checking all priority homeservers
useEffect(() => {
+ const cached = roomCallSupportCache.get(roomId);
+ if (cached !== undefined) {
+ setRoomCallSupported(cached);
+ setRoomCallSupportLoading(false);
+ return undefined;
+ }
+
+ let cancelled = false;
+
const checkRoomCallSupport = async () => {
setRoomCallSupportLoading(true);
-
+
const room = mx.getRoom(roomId);
if (!room) {
- setRoomCallSupported(false);
- setRoomCallSupportLoading(false);
+ if (!cancelled) {
+ setRoomCallSupported(false);
+ setRoomCallSupportLoading(false);
+ }
return;
}
- // Get homeserver priority list for this room
const homeserverPriority = getLiveKitHomeserverPriority(
room,
mx.getHomeserverUrl(),
mx.getSafeUserId()
);
- console.log(`Checking call support for room ${roomId}, homeserver priority:`, homeserverPriority);
-
- // Check each homeserver in priority order
for (const homeserver of homeserverPriority) {
try {
- console.log(`Fetching well-known from ${homeserver}...`);
const wellKnown = await fetchWellKnownWithRTC(homeserver);
- console.log(`Well-known response from ${homeserver}:`, wellKnown);
-
const livekitFocus = getLiveKitFocus(wellKnown);
- console.log(`LiveKit focus from ${homeserver}:`, livekitFocus);
-
if (livekitFocus) {
- console.log(`Call support found for room ${roomId} on homeserver: ${homeserver}`);
- setRoomCallSupported(true);
- setRoomCallSupportLoading(false);
+ roomCallSupportCache.set(roomId, true);
+ if (!cancelled) {
+ setRoomCallSupported(true);
+ setRoomCallSupportLoading(false);
+ }
return;
}
- } catch (error) {
- console.warn(`Failed to check call support on ${homeserver}:`, error);
+ } catch {
// Continue to next homeserver
}
}
- // No homeserver supports calls
- console.log(`No call support found for room ${roomId}`);
- setRoomCallSupported(false);
- setRoomCallSupportLoading(false);
+ roomCallSupportCache.set(roomId, false);
+ if (!cancelled) {
+ setRoomCallSupported(false);
+ setRoomCallSupportLoading(false);
+ }
};
checkRoomCallSupport();
+ return () => {
+ cancelled = true;
+ };
}, [roomId, mx]);
const isInCall = activeCall?.roomId === roomId;
diff --git a/src/app/features/call/useRoomCallMembers.ts b/src/app/features/call/useRoomCallMembers.ts
index b6ffde3..a39813a 100644
--- a/src/app/features/call/useRoomCallMembers.ts
+++ b/src/app/features/call/useRoomCallMembers.ts
@@ -1,115 +1,12 @@
-/* eslint-disable no-console */
-import { useCallback, useEffect, useState } from 'react';
-import { MatrixEvent, Room, RoomStateEvent } from 'matrix-js-sdk';
-import { useMatrixClient } from '../../hooks/useMatrixClient';
+import { useRoomCallMembersFromRegistry } from '../../state/activeCallRoomsRegistry';
-/** The Matrix state event type for call membership (MSC3401) */
-const CALL_MEMBER_EVENT_TYPE = 'org.matrix.msc3401.call.member';
-
-/** Represents an active call member in a room */
-export interface CallMember {
- userId: string;
- deviceId: string;
- sessionId: string;
- expiresTs: number;
-}
+export type { CallMember } from './callMemberUtils';
+export { getActiveCallMembers, CALL_MEMBER_EVENT_TYPE } from './callMemberUtils';
/**
- * Extracts active call members from a room's state events
- * @param room - The Matrix room to check
- * @returns Array of active call members
+ * Hook to get the list of users currently in a call in a room.
+ * Backed by a shared registry — one state listener for all rooms.
*/
-export function getActiveCallMembers(room: Room): CallMember[] {
- const members: CallMember[] = [];
- const now = Date.now();
-
- try {
- const stateEvents = room.currentState.getStateEvents(CALL_MEMBER_EVENT_TYPE);
-
- stateEvents.forEach((event) => {
- const content = event.getContent();
- const userId = event.getStateKey();
-
- if (!userId || !content['m.calls']) return;
-
- const calls = content['m.calls'];
- if (!Array.isArray(calls)) return;
-
- calls.forEach((call) => {
- const devices = call['m.devices'];
- if (!Array.isArray(devices)) return;
-
- devices.forEach((device) => {
- const expiresTs = device.expires_ts || 0;
-
- // Check if the call membership is still valid (not expired)
- if (expiresTs > now) {
- members.push({
- userId,
- deviceId: device.device_id,
- sessionId: device.session_id,
- expiresTs,
- });
- }
- });
- });
- });
- } catch (e) {
- console.error('Error reading call member events:', e);
- }
-
- return members;
-}
-
-/**
- * Hook to get the list of users currently in a call in a room
- * @param roomId - The Matrix room ID to monitor
- * @returns Object with call members and whether a call is active
- */
-export function useRoomCallMembers(roomId: string) {
- const mx = useMatrixClient();
- const [callMembers, setCallMembers] = useState([]);
-
- const updateCallMembers = useCallback(() => {
- const room = mx.getRoom(roomId);
- if (!room) {
- setCallMembers([]);
- return;
- }
-
- const members = getActiveCallMembers(room);
- setCallMembers(members);
- }, [mx, roomId]);
-
- useEffect(() => {
- updateCallMembers();
-
- // Listen for state event changes
- const handleStateEvent = (event: MatrixEvent) => {
- if (event.getRoomId() === roomId && event.getType() === CALL_MEMBER_EVENT_TYPE) {
- updateCallMembers();
- }
- };
-
- mx.on(RoomStateEvent.Events, handleStateEvent);
-
- // Also refresh periodically to handle expired memberships
- const interval = setInterval(updateCallMembers, 30000);
-
- return () => {
- mx.off(RoomStateEvent.Events, handleStateEvent);
- clearInterval(interval);
- };
- }, [mx, roomId, updateCallMembers]);
-
- const isCallActive = callMembers.length > 0;
- const myUserId = mx.getUserId();
- const othersInCall = callMembers.filter((m) => m.userId !== myUserId);
-
- return {
- callMembers,
- isCallActive,
- othersInCall,
- callMemberCount: callMembers.length,
- };
+export function useRoomCallMembers(roomId: string, enabled = true) {
+ return useRoomCallMembersFromRegistry(roomId, enabled);
}
diff --git a/src/app/features/call/useRtcConfig.ts b/src/app/features/call/useRtcConfig.ts
index 84d74ce..8d2b636 100644
--- a/src/app/features/call/useRtcConfig.ts
+++ b/src/app/features/call/useRtcConfig.ts
@@ -10,9 +10,7 @@ import { getActiveCallMembers } from './useRoomCallMembers';
* Get room server from room ID (format: !roomid:server.com)
*/
function getRoomServer(roomId: string): string | undefined {
- const server = getMxIdServer(roomId);
- console.log(`[getRoomServer] Room ID: ${roomId} -> Server: ${server}`);
- return server;
+ return getMxIdServer(roomId);
}
/**
@@ -31,31 +29,24 @@ export function getLiveKitHomeserverPriority(
const servers: string[] = [];
const memberCount = room.getMembers().length;
const isDM = memberCount <= 2;
-
- console.log(`[getLiveKitHomeserverPriority] Room: ${room.roomId}`);
- console.log(`[getLiveKitHomeserverPriority] Member count: ${memberCount}, isDM: ${isDM}`);
- console.log(`[getLiveKitHomeserverPriority] User homeserver: ${userHomeserver}`);
-
+
// Check for active call members
const activeCallMembers = getActiveCallMembers(room);
const hasActiveCall = activeCallMembers.length > 0;
- console.log(`[getLiveKitHomeserverPriority] Active call members: ${activeCallMembers.length}, hasActiveCall: ${hasActiveCall}`);
-
if (hasActiveCall) {
// Joining existing call - prioritize room's homeserver (where call is hosted)
const roomServer = getRoomServer(room.roomId);
- console.log(`[getLiveKitHomeserverPriority] Room server (active call): ${roomServer}`);
if (roomServer) {
servers.push(`https://${roomServer}`);
}
-
+
// Add user's homeserver as fallback
const userDomain = userHomeserver.replace('https://', '').replace('http://', '');
if (!roomServer || roomServer !== userDomain) {
servers.push(userHomeserver);
}
-
+
// For DMs, also try other member's homeserver
if (isDM) {
const otherUserId = guessDmRoomUserId(room, myUserId);
@@ -66,42 +57,31 @@ export function getLiveKitHomeserverPriority(
}
}
}
- } else {
- // Starting new call - use original priority logic
- if (isDM) {
- console.log(`[getLiveKitHomeserverPriority] DM path - user homeserver first`);
- // For DMs: user's homeserver first, then other member's homeserver
- servers.push(userHomeserver);
-
- const otherUserId = guessDmRoomUserId(room, myUserId);
- console.log(`[getLiveKitHomeserverPriority] Other user ID: ${otherUserId}`);
- if (otherUserId && otherUserId !== myUserId) {
- const otherServer = getMxIdServer(otherUserId);
- console.log(`[getLiveKitHomeserverPriority] Other user server: ${otherServer}`);
- const userDomain = userHomeserver.replace('https://', '').replace('http://', '');
- if (otherServer && otherServer !== userDomain) {
- servers.push(`https://${otherServer}`);
- }
- }
- } else {
- console.log(`[getLiveKitHomeserverPriority] Channel path - room homeserver first`);
- // For channels: room's homeserver first, then user's homeserver
- const roomServer = getRoomServer(room.roomId);
- console.log(`[getLiveKitHomeserverPriority] Room server from ID: ${roomServer}`);
- if (roomServer) {
- servers.push(`https://${roomServer}`);
- }
-
- // Fallback to user's homeserver if different
+ } else if (isDM) {
+ // For DMs: user's homeserver first, then other member's homeserver
+ servers.push(userHomeserver);
+
+ const otherUserId = guessDmRoomUserId(room, myUserId);
+ if (otherUserId && otherUserId !== myUserId) {
+ const otherServer = getMxIdServer(otherUserId);
const userDomain = userHomeserver.replace('https://', '').replace('http://', '');
- console.log(`[getLiveKitHomeserverPriority] User domain: ${userDomain}, room server: ${roomServer}`);
- if (!roomServer || roomServer !== userDomain) {
- servers.push(userHomeserver);
+ if (otherServer && otherServer !== userDomain) {
+ servers.push(`https://${otherServer}`);
}
}
+ } else {
+ // For channels: room's homeserver first, then user's homeserver
+ const roomServer = getRoomServer(room.roomId);
+ if (roomServer) {
+ servers.push(`https://${roomServer}`);
+ }
+
+ const userDomain = userHomeserver.replace('https://', '').replace('http://', '');
+ if (!roomServer || roomServer !== userDomain) {
+ servers.push(userHomeserver);
+ }
}
- console.log(`[getLiveKitHomeserverPriority] Final server list:`, servers);
return servers;
}
@@ -112,46 +92,46 @@ export function getLiveKitHomeserverPriority(
*/
export function getLiveKitFocus(wellKnown: WellKnownWithRTC): LiveKitFocus | undefined {
const rtcFoci = wellKnown['org.matrix.msc4143.rtc_foci'];
- console.log(`[getLiveKitFocus] rtcFoci from well-known:`, rtcFoci);
-
+
if (!rtcFoci || !Array.isArray(rtcFoci)) {
- console.log(`[getLiveKitFocus] No rtc_foci found or not an array`);
return undefined;
}
- const livekitFocus = rtcFoci.find((focus): focus is LiveKitFocus => focus.type === 'livekit');
- console.log(`[getLiveKitFocus] Found LiveKit focus:`, livekitFocus);
- return livekitFocus;
+ return rtcFoci.find((focus): focus is LiveKitFocus => focus.type === 'livekit');
}
/**
- * Fetches the well-known client configuration from a homeserver
- * @param baseUrl - The homeserver base URL
- * @returns The well-known response with RTC configuration
+ * Fetches the well-known client configuration from a homeserver.
+ * Results are cached — room switches must not re-hit the network.
*/
+const wellKnownCache = new Map>();
+
export async function fetchWellKnownWithRTC(baseUrl: string): Promise {
- // In development mode (localhost), use CORS proxy to avoid CORS issues
+ const cacheKey = baseUrl.replace(/\/$/, '');
+ const cached = wellKnownCache.get(cacheKey);
+ if (cached) return cached;
+
const isDev = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
-
+
let wellKnownUrl: string;
if (isDev) {
- // Extract hostname from baseUrl for proxy
- const hostname = baseUrl.replace('https://', '').replace('http://', '').replace(/\/$/, '');
+ const hostname = cacheKey.replace('https://', '').replace('http://', '');
wellKnownUrl = `/_matrix-cors-proxy/${hostname}/.well-known/matrix/client`;
} else {
- wellKnownUrl = `${baseUrl}/.well-known/matrix/client`;
+ wellKnownUrl = `${cacheKey}/.well-known/matrix/client`;
}
- console.log(`[fetchWellKnownWithRTC] Fetching from: ${wellKnownUrl}`);
+ const fetchPromise = (async () => {
+ const response = await fetch(wellKnownUrl);
+ if (!response.ok) {
+ wellKnownCache.delete(cacheKey);
+ throw new Error(`Failed to fetch well-known: ${response.status}`);
+ }
+ return (await response.json()) as WellKnownWithRTC;
+ })();
- const response = await fetch(wellKnownUrl);
- if (!response.ok) {
- throw new Error(`Failed to fetch well-known: ${response.status}`);
- }
-
- const data = await response.json();
- console.log(`[fetchWellKnownWithRTC] Response data:`, data);
- return data;
+ wellKnownCache.set(cacheKey, fetchPromise);
+ return fetchPromise;
}
/**
diff --git a/src/app/features/forum/ForumAwareSpaceLayout.tsx b/src/app/features/forum/ForumAwareSpaceLayout.tsx
index 30750a9..650d9d2 100644
--- a/src/app/features/forum/ForumAwareSpaceLayout.tsx
+++ b/src/app/features/forum/ForumAwareSpaceLayout.tsx
@@ -1,9 +1,9 @@
import React, { useMemo } from 'react';
import { PageRoot } from '../../components/page';
-import { AnimatedOutlet } from '../../components/AnimatedOutlet';
import { MobileFriendlyPageNav } from '../../pages/MobileFriendly';
import { SPACE_PATH } from '../../pages/paths';
import { Space } from '../../pages/client/space/Space';
+import { SpaceRoomLayout } from '../../pages/client/space/SpaceRoomLayout';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useSelectedRoom } from '../../hooks/router/useSelectedRoom';
import { useSpace } from '../../hooks/useSpace';
@@ -11,7 +11,7 @@ import { isForumContainer, shouldShowForumLobby } from '../../utils/room';
import { ForumBoardProvider } from './ForumBoardContext';
/**
- * Space sidebar + outlet. Forum spaces render ForumFeedSidebar in Space.tsx.
+ * Space sidebar + room keep-alive host. Forum spaces render ForumFeedSidebar in Space.tsx.
*/
export function ForumAwareSpaceLayout() {
const mx = useMatrixClient();
@@ -33,7 +33,7 @@ export function ForumAwareSpaceLayout() {
}
>
-
+
);
diff --git a/src/app/features/lobby/Lobby.tsx b/src/app/features/lobby/Lobby.tsx
index d8de5f1..3027315 100644
--- a/src/app/features/lobby/Lobby.tsx
+++ b/src/app/features/lobby/Lobby.tsx
@@ -60,6 +60,10 @@ import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
import { getRoomPermissionsAPI } from '../../hooks/useRoomPermissions';
import { getRoomCreatorsForRoomId } from '../../hooks/useRoomCreators';
import { joiningProgressAtom } from '../../state/joiningProgress';
+import {
+ getCachedHierarchyRooms,
+ hierarchyRoomsToMap,
+} from '../../state/spaceHierarchyCache';
const useCanDropLobbyItem = (
space: Room,
@@ -166,7 +170,6 @@ function SpaceCardLobby() {
const isForum = shouldShowForumLobby(space);
const spacePowerLevels = usePowerLevels(space);
const lex = useMemo(() => new ASCIILexicalTable(' '.charCodeAt(0), '~'.charCodeAt(0), 6), []);
- const members = useRoomMembers(mx, space.roomId);
const joiningProgress = useAtomValue(joiningProgressAtom);
const currentJoiningProgress = joiningProgress.get(space.roomId);
@@ -174,7 +177,8 @@ function SpaceCardLobby() {
const heroSectionRef = useRef(null);
const [heroSectionHeight, setHeroSectionHeight] = useState();
const [spaceRooms, setSpaceRooms] = useAtom(spaceRoomsAtom);
- const [isDrawer] = useSetting(settingsAtom, 'isPeopleDrawer');
+ const [isDrawer] = useSetting(settingsAtom, 'peopleDrawerInRooms');
+ const members = useRoomMembers(mx, space.roomId, isDrawer);
const screenSize = useScreenSizeContext();
const [onTop, setOnTop] = useState(true);
const [closedCategories, setClosedCategories] = useAtom(useClosedLobbyCategoriesAtom());
@@ -190,7 +194,10 @@ function SpaceCardLobby() {
return new Set(sideSpaces);
}, [sidebarItems]);
- const [spacesItems, setSpacesItem] = useState
{hideActivity ?