From e63946c7167a72e49a8e847dc8c6461b3fb6f1d9 Mon Sep 17 00:00:00 2001 From: Erwan Leboucher Date: Sun, 19 Jul 2026 18:02:05 +0200 Subject: [PATCH 1/7] fix: disable sidebar drag-and-drop on mobile and tighten drawer swipe deadzone --- src/app/components/page/MobileNavDrawer.tsx | 7 ++++- src/app/pages/client/sidebar/SpaceTabs.tsx | 33 +++++++++++++-------- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/app/components/page/MobileNavDrawer.tsx b/src/app/components/page/MobileNavDrawer.tsx index ddddf84c73..96f4647d88 100644 --- a/src/app/components/page/MobileNavDrawer.tsx +++ b/src/app/components/page/MobileNavDrawer.tsx @@ -269,7 +269,7 @@ export function MobileNavDrawer({ nav, rail, bottomNav, children }: MobileNavDra return; } - if (mx > DIRECTION_DEADZONE || !canOpenRoom) { + if (mx > DIRECTION_DEADZONE) { if (draggingRef.current) { draggingRef.current = false; settle(0); @@ -277,6 +277,10 @@ export function MobileNavDrawer({ nav, rail, bottomNav, children }: MobileNavDra cancel(); return; } + if (!canOpenRoom && mx < -DIRECTION_DEADZONE) { + cancel(); + return; + } if (active) { if (first) { x.stop(); @@ -307,6 +311,7 @@ export function MobileNavDrawer({ nav, rail, bottomNav, children }: MobileNavDra { axis: 'x', filterTaps: true, + tapsThreshold: DIRECTION_DEADZONE, pointer: { capture: false }, from: () => [readX(), 0], } diff --git a/src/app/pages/client/sidebar/SpaceTabs.tsx b/src/app/pages/client/sidebar/SpaceTabs.tsx index a9ee1cbc05..a3d3115d66 100644 --- a/src/app/pages/client/sidebar/SpaceTabs.tsx +++ b/src/app/pages/client/sidebar/SpaceTabs.tsx @@ -344,11 +344,13 @@ const useDraggableItem = ( item: SidebarDraggable, targetRef: RefObject, onDragging: (item?: SidebarDraggable) => void, - dragHandleRef?: RefObject + dragHandleRef?: RefObject, + enabled: boolean = true ): boolean => { const [dragging, setDragging] = useState(false); useEffect(() => { + if (!enabled) return undefined; const target = targetRef.current; const dragHandle = dragHandleRef?.current ?? undefined; @@ -367,18 +369,20 @@ const useDraggableItem = ( }, }) : undefined; - }, [targetRef, dragHandleRef, item, onDragging]); + }, [targetRef, dragHandleRef, item, onDragging, enabled]); return dragging; }; const useDropTarget = ( item: SidebarDraggable, - targetRef: RefObject + targetRef: RefObject, + enabled: boolean = true ): Instruction | undefined => { const [dropState, setDropState] = useState(); useEffect(() => { + if (!enabled) return undefined; const target = targetRef.current; if (!target) return undefined; @@ -415,7 +419,7 @@ const useDropTarget = ( onDragLeave: () => setDropState(undefined), onDrop: () => setDropState(undefined), }); - }, [item, targetRef]); + }, [item, targetRef, enabled]); return dropState; }; @@ -423,11 +427,13 @@ const useDropTarget = ( function useDropTargetInstruction( item: SidebarDraggable, targetRef: RefObject, - instructionType: T + instructionType: T, + enabled: boolean = true ): T | undefined { const [dropState, setDropState] = useState(); useEffect(() => { + if (!enabled) return undefined; const target = targetRef.current; if (!target) return undefined; @@ -448,7 +454,7 @@ function useDropTargetInstruction( onDragLeave: () => setDropState(undefined), onDrop: () => setDropState(undefined), }); - }, [item, targetRef, instructionType]); + }, [item, targetRef, instructionType, enabled]); return dropState; } @@ -532,6 +538,7 @@ function SpaceTab({ disabled, onUnpin, }: Readonly) { + const isMobile = useScreenSizeContext() === ScreenSize.Mobile; const targetRef = useRef(null); const spaceDraggable: SidebarDraggable = useMemo( @@ -545,8 +552,8 @@ function SpaceTab({ [folder, space] ); - useDraggableItem(spaceDraggable, targetRef, onDragging); - const dropState = useDropTarget(spaceDraggable, targetRef); + useDraggableItem(spaceDraggable, targetRef, onDragging, undefined, !isMobile); + const dropState = useDropTarget(spaceDraggable, targetRef, !isMobile); const dropType = dropState?.type; const [menuAnchor, setMenuAnchor] = useState(); @@ -641,13 +648,14 @@ function OpenedSpaceFolder({ onFolderContextMenu, children, }: Readonly) { + const isMobile = useScreenSizeContext() === ScreenSize.Mobile; const aboveTargetRef = useRef(null); const belowTargetRef = useRef(null); const spaceDraggable: SidebarDraggable = useMemo(() => ({ folder, open: true }), [folder]); - const orderAbove = useDropTargetInstruction(spaceDraggable, aboveTargetRef, 'reorder-above'); - const orderBelow = useDropTargetInstruction(spaceDraggable, belowTargetRef, 'reorder-below'); + const orderAbove = useDropTargetInstruction(spaceDraggable, aboveTargetRef, 'reorder-above', !isMobile); + const orderBelow = useDropTargetInstruction(spaceDraggable, belowTargetRef, 'reorder-below', !isMobile); return ( ) { const mx = useMatrixClient(); + const isMobile = useScreenSizeContext() === ScreenSize.Mobile; const handlerRef = useRef(null); const spaceDraggable: FolderDraggable = useMemo(() => ({ folder }), [folder]); - useDraggableItem(spaceDraggable, handlerRef, onDragging); - const dropState = useDropTarget(spaceDraggable, handlerRef); + useDraggableItem(spaceDraggable, handlerRef, onDragging, undefined, !isMobile); + const dropState = useDropTarget(spaceDraggable, handlerRef, !isMobile); const dropType = dropState?.type; const tooltipName = folderDefaultDisplayName(mx, folder); From 2dcce00a988945616e73790f4473760087bdd430 Mon Sep 17 00:00:00 2001 From: Erwan Leboucher Date: Sun, 19 Jul 2026 20:13:12 +0200 Subject: [PATCH 2/7] perf: lazy-load auth, settings, inbox and explore route subtrees --- src/app/pages/Router.tsx | 128 ++++++++++++++++++++++++++++++++------- 1 file changed, 105 insertions(+), 23 deletions(-) diff --git a/src/app/pages/Router.tsx b/src/app/pages/Router.tsx index 4686929ca8..1114d842e2 100644 --- a/src/app/pages/Router.tsx +++ b/src/app/pages/Router.tsx @@ -1,3 +1,4 @@ +import { lazy, Suspense } from 'react'; import { Outlet, Route, @@ -10,16 +11,12 @@ import * as Sentry from '@sentry/react'; import type { ClientConfig } from '$hooks/useClientConfig'; import { ErrorPage } from '$components/DefaultErrorPage'; -import { SettingsRoute } from '$features/settings'; -import { SettingsShallowRouteRenderer } from '$features/settings/SettingsShallowRouteRenderer'; import { Room } from '$features/room'; import { Lobby } from '$features/lobby'; import { PageRoot } from '$components/page'; import { ScreenSize } from '$hooks/useScreenSize'; import { ReceiveSelfDeviceVerification } from '$components/DeviceVerification'; import { AutoRestoreBackupOnVerification } from '$components/BackupRestore'; -import { RoomSettingsRenderer } from '$features/room-settings'; -import { SpaceSettingsRenderer } from '$features/space-settings'; import { UserRoomProfileRenderer } from '$components/UserRoomProfileRenderer'; import { CreateRoomModalRenderer } from '$features/create-room'; import { CreateSpaceModalRenderer } from '$features/create-space'; @@ -31,7 +28,7 @@ import { NotificationJumper } from '$hooks/useNotificationJumper'; import { SearchModalRenderer } from '$features/navigate'; import { GlobalKeyboardShortcuts } from '$components/GlobalKeyboardShortcuts'; import { CallEmbedProvider } from '$components/CallEmbedProvider'; -import { AuthLayout, Login, Register, ResetPassword } from './auth'; +import { SplashScreen } from '$components/splash-screen'; import { DIRECT_PATH, EXPLORE_PATH, @@ -71,8 +68,39 @@ import { HandleNotificationClick, ClientNonUIFeatures } from './client/ClientNon import { Home, HomeRouteRoomProvider, HomeSearch } from './client/home'; import { Direct, DirectCreate, DirectRouteRoomProvider } from './client/direct'; import { RouteSpaceProvider, Space, SpaceRouteRoomProvider, SpaceSearch } from './client/space'; -import { Explore, FeaturedRooms, PublicRooms } from './client/explore'; -import { Notifications, Inbox, Invites, Bookmarks } from './client/inbox'; +// Lazy-loaded: auth subtree, settings, inbox/bookmarks, explore +const AuthLayout = lazy(() => import('./auth').then((m) => ({ default: m.AuthLayout }))); +const Login = lazy(() => import('./auth').then((m) => ({ default: m.Login }))); +const Register = lazy(() => import('./auth').then((m) => ({ default: m.Register }))); +const ResetPassword = lazy(() => import('./auth').then((m) => ({ default: m.ResetPassword }))); + +const SettingsRoute = lazy(() => import('$features/settings').then((m) => ({ default: m.SettingsRoute }))); +const SettingsShallowRouteRenderer = lazy(() => + import('$features/settings/SettingsShallowRouteRenderer').then((m) => ({ + default: m.SettingsShallowRouteRenderer, + })) +); +const RoomSettingsRenderer = lazy(() => + import('$features/room-settings').then((m) => ({ default: m.RoomSettingsRenderer })) +); +const SpaceSettingsRenderer = lazy(() => + import('$features/space-settings').then((m) => ({ default: m.SpaceSettingsRenderer })) +); + +const Notifications = lazy(() => + import('./client/inbox').then((m) => ({ default: m.Notifications })) +); +const Inbox = lazy(() => import('./client/inbox').then((m) => ({ default: m.Inbox }))); +const Invites = lazy(() => import('./client/inbox').then((m) => ({ default: m.Invites }))); +const Bookmarks = lazy(() => import('./client/inbox').then((m) => ({ default: m.Bookmarks }))); + +const Explore = lazy(() => import('./client/explore').then((m) => ({ default: m.Explore }))); +const FeaturedRooms = lazy(() => + import('./client/explore').then((m) => ({ default: m.FeaturedRooms })) +); +const PublicRooms = lazy(() => + import('./client/explore').then((m) => ({ default: m.PublicRooms })) +); import { setAfterLoginRedirectPath } from './afterLoginRedirectPath'; import { WelcomePage } from './client/WelcomePage'; import { SidebarNav } from './client/SidebarNav'; @@ -148,11 +176,13 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize) )} beforeCapture={(scope) => scope.setTag('section', 'auth')} > - <> - - - - + {null}}> + <> + + + + + } > @@ -215,9 +245,15 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize) - - - + + + + + + + + + {/* Screen reader live region — populated by announce() in utils/announce.ts */}
} nav={ - + {null}}> + + } > @@ -368,20 +406,43 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize) element={} /> )} - } /> - } /> + {null}}> + + + } + /> + {null}}> + + + } + /> } /> } /> } /> - } /> + {null}}> + + + } + /> - + {null}}> + + } > @@ -396,9 +457,30 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize) element={} /> )} - } /> - } /> - } /> + {null}}> + + + } + /> + {null}}> + + + } + /> + {null}}> + + + } + /> } /> From eeb07b490e4170aad57017dd8f61f32166f445f9 Mon Sep 17 00:00:00 2001 From: Erwan Leboucher Date: Sun, 19 Jul 2026 20:13:12 +0200 Subject: [PATCH 3/7] perf: preload config.json and active locale from index.html --- index.html | 19 +++++++++++++++ src/app/components/ClientConfigLoader.tsx | 10 ++++++++ src/app/i18n.ts | 11 +++++++++ src/app/utils/preload.ts | 29 +++++++++++++++++++++++ 4 files changed, 69 insertions(+) create mode 100644 src/app/utils/preload.ts diff --git a/index.html b/index.html index 3a9c594781..e63ff02db5 100644 --- a/index.html +++ b/index.html @@ -143,6 +143,25 @@ +
diff --git a/src/app/components/ClientConfigLoader.tsx b/src/app/components/ClientConfigLoader.tsx index b601773701..96fc93630c 100644 --- a/src/app/components/ClientConfigLoader.tsx +++ b/src/app/components/ClientConfigLoader.tsx @@ -2,6 +2,7 @@ import type { ReactNode } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react'; import { Box, Button, Dialog, Text, color, config } from 'folds'; import type { ClientConfig } from '$hooks/useClientConfig'; +import { takePreloadedConfig } from '$utils/preload'; import { trimTrailingSlash } from '$utils/common'; import { fetch } from '$utils/fetch'; import { SplashScreen } from '$components/splash-screen'; @@ -14,6 +15,15 @@ export const FALLBACK_CLIENT_CONFIG: ClientConfig = { }; export const getClientConfig = async (): Promise => { + const preloaded = takePreloadedConfig(); + if (preloaded !== undefined) { + const data = await preloaded.catch(() => undefined); + if (data && typeof data === 'object' && !Array.isArray(data)) { + return data as ClientConfig; + } + // Preload missed or invalid — fall through to fresh fetch. + } + const url = `${trimTrailingSlash(import.meta.env.BASE_URL)}/config.json`; const response = await fetch(url, { method: 'GET' }); if (!response.ok) { diff --git a/src/app/i18n.ts b/src/app/i18n.ts index 72a4ba3dd2..49d8202d16 100644 --- a/src/app/i18n.ts +++ b/src/app/i18n.ts @@ -4,6 +4,12 @@ import type { HttpBackendOptions } from 'i18next-http-backend'; import Backend from 'i18next-http-backend'; import { initReactI18next } from 'react-i18next'; import { trimTrailingSlash } from './utils/common'; +import { takePreloadedLocale } from './utils/preload'; + +const langFromUrl = (url: string): string | undefined => { + const match = url.match(/\/public\/locales\/([^/]+)\.json/); + return match?.[1]; +}; i18n // i18next-http-backend @@ -26,6 +32,11 @@ i18n load: 'languageOnly', backend: { loadPath: `${trimTrailingSlash(import.meta.env.BASE_URL)}/public/locales/{{lng}}.json`, + alternateFetch: (url: string) => { + const lng = langFromUrl(url); + if (!lng) return undefined; + return takePreloadedLocale(lng) ?? undefined; + }, }, }); diff --git a/src/app/utils/preload.ts b/src/app/utils/preload.ts new file mode 100644 index 0000000000..e041d6c2a8 --- /dev/null +++ b/src/app/utils/preload.ts @@ -0,0 +1,29 @@ +// Shape stashed by the inline preload script in index.html. +type SablePreload = { + config?: Promise; + locale?: { lng: string; promise: Promise }; +}; + +const win = window as Window & { __SABLE_PRELOAD?: SablePreload }; + +/** One-shot: returns the preloaded config.json fetch promise and nulls the slot. */ +export const takePreloadedConfig = (): Promise | undefined => { + const preload = win.__SABLE_PRELOAD; + if (preload?.config !== undefined) { + const promise = preload.config; + delete preload.config; + return promise; + } + return undefined; +}; + +/** One-shot: returns the preloaded locale fetch promise for the given language, nulls the slot. */ +export const takePreloadedLocale = (lng: string): Promise | undefined => { + const preload = win.__SABLE_PRELOAD; + if (preload?.locale !== undefined && preload.locale.lng === lng) { + const promise = preload.locale.promise; + delete preload.locale; + return promise; + } + return undefined; +}; From 1f33d97289b0dedba356e897c528abd8fb18143d Mon Sep 17 00:00:00 2001 From: Erwan Leboucher Date: Sun, 19 Jul 2026 20:13:12 +0200 Subject: [PATCH 4/7] perf: cache server /versions to skip a blocking fetch on startup --- src/client/initMatrix.ts | 7 ++- src/client/versionsCache.ts | 91 +++++++++++++++++++++++++++++++++++++ src/types/matrix-sdk.ts | 2 + 3 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 src/client/versionsCache.ts diff --git a/src/client/initMatrix.ts b/src/client/initMatrix.ts index 2687be11c0..3f33cea53d 100644 --- a/src/client/initMatrix.ts +++ b/src/client/initMatrix.ts @@ -29,6 +29,7 @@ import { scopeEphemeralExtensions, SlidingSyncManager } from './slidingSync'; import { PresenceSyncManager } from './presenceSync'; import { SlidingSyncSidebarCache } from './slidingSyncSidebarCache'; import { hydrateRoomMember } from './roomMemberHydration'; +import { primeVersionsFromCache, revalidateVersionsCache, clearCachedVersions } from './versionsCache'; const log = createLogger('initMatrix'); const debugLog = createDebugLogger('initMatrix'); @@ -274,7 +275,10 @@ const initializeClient = async ( } const { mx, indexedDBStore } = builtClient; - void mx.getVersions().catch(() => undefined); + void primeVersionsFromCache(mx, session.baseUrl, session.userId).then((primed) => { + if (primed) void revalidateVersionsCache(mx, session.baseUrl, session.userId); + else void mx.getVersions().catch(() => undefined); + }); const syncStorePromise = measureStartupPhase('sync_store', () => indexedDBStore.startup()); const cryptoPromise = measureStartupPhase('rust_crypto', () => @@ -597,6 +601,7 @@ export const logoutClient = async (mx: MatrixClient, session?: Session) => { if (session) { SlidingSyncSidebarCache.clear(session.userId); + clearCachedVersions(session.baseUrl, session.userId); const storeName: SessionStoreName = getSessionStoreName(session); await mx.clearStores({ cryptoDatabasePrefix: storeName.rustCryptoPrefix }); await deleteDatabase(storeName.sync); diff --git a/src/client/versionsCache.ts b/src/client/versionsCache.ts new file mode 100644 index 0000000000..ccdc0d7313 --- /dev/null +++ b/src/client/versionsCache.ts @@ -0,0 +1,91 @@ +import type { MatrixClient } from '$types/matrix-sdk'; +import { buildFeatureSupportMap, Method } from '$types/matrix-sdk'; + +const TTL_MS = 24 * 60 * 60 * 1000; // 24 hours +const keyFor = (baseUrl: string, userId: string): string => + `sable.versionsCache.${baseUrl}|${userId}`; + +type CachedVersions = { + versions: unknown; + unstable_features: Record | undefined; + fetchedAt: number; +}; + +const readCache = (baseUrl: string, userId: string): CachedVersions | undefined => { + try { + const raw = localStorage.getItem(keyFor(baseUrl, userId)); + if (!raw) return undefined; + const parsed = JSON.parse(raw) as CachedVersions; + if (typeof parsed.fetchedAt !== 'number') return undefined; + if (Date.now() - parsed.fetchedAt > TTL_MS) return undefined; + return parsed; + } catch { + return undefined; + } +}; + +const writeCache = (baseUrl: string, userId: string, data: { versions: unknown; unstable_features: Record | undefined }): void => { + try { + const entry: CachedVersions = { ...data, fetchedAt: Date.now() }; + localStorage.setItem(keyFor(baseUrl, userId), JSON.stringify(entry)); + } catch { + // localStorage full or disabled — non-fatal, skip caching. + } +}; + +/** + * Seed mx.serverVersionsPromise AND mx.canSupport from a cached /versions payload. + * Seeding the promise alone leaves canSupport empty → semantic drift in + * redaction/relation checks. Both must be set before the first sync request. + */ +export const primeVersionsFromCache = async ( + mx: MatrixClient, + baseUrl: string, + userId: string +): Promise => { + const cached = readCache(baseUrl, userId); + if (!cached) return false; + const serverVersions = { versions: cached.versions, unstable_features: cached.unstable_features ?? {} }; + (mx as unknown as { serverVersionsPromise: Promise | undefined }).serverVersionsPromise = + Promise.resolve(serverVersions); + (mx as unknown as { canSupport: Map }).canSupport = + await buildFeatureSupportMap(serverVersions as Parameters[0]); + return true; +}; + +/** + * Fire-and-forget revalidation of the /versions cache. Updates both the cache + * and the in-memory promise/canSupport so the client stays current. + */ +export const revalidateVersionsCache = async ( + mx: MatrixClient, + baseUrl: string, + userId: string +): Promise => { + try { + const resp = await mx.http.authedRequest( + Method.Get, + '/_matrix/client/versions', + undefined, + undefined, + { prefix: '' } + ); + const data = resp as { versions: unknown; unstable_features?: Record }; + writeCache(baseUrl, userId, { versions: data.versions, unstable_features: data.unstable_features }); + (mx as unknown as { serverVersionsPromise: Promise | undefined }).serverVersionsPromise = + Promise.resolve(data); + (mx as unknown as { canSupport: Map }).canSupport = + await buildFeatureSupportMap(data as Parameters[0]); + } catch { + // Revalidation failed — cached values remain in effect. Non-fatal. + } +}; + +/** Clear the cached versions for a session (used on logout). */ +export const clearCachedVersions = (baseUrl: string, userId: string): void => { + try { + localStorage.removeItem(keyFor(baseUrl, userId)); + } catch { + // non-fatal + } +}; diff --git a/src/types/matrix-sdk.ts b/src/types/matrix-sdk.ts index 12f41e0712..561941238b 100644 --- a/src/types/matrix-sdk.ts +++ b/src/types/matrix-sdk.ts @@ -45,6 +45,8 @@ export * from 'matrix-js-sdk/lib/@types/read_receipts'; export * from 'matrix-js-sdk/lib/@types/membership'; export * from 'matrix-js-sdk/lib/@types/registration'; +export * from 'matrix-js-sdk/lib/feature'; + export * from 'matrix-js-sdk/lib/oidc'; // oidc-client-ts is a transitive dep (not resolvable here), so derive its type from the SDK. From ed2e40f9a7146b81e12289be63668bb2326db3ba Mon Sep 17 00:00:00 2001 From: Erwan Leboucher Date: Sun, 19 Jul 2026 20:13:12 +0200 Subject: [PATCH 5/7] perf: drop manual vendor chunks for pdfjs, element-call and prism --- .changeset/perf-cold-start-bundle.md | 5 +++++ vite.config.ts | 3 --- 2 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 .changeset/perf-cold-start-bundle.md diff --git a/.changeset/perf-cold-start-bundle.md b/.changeset/perf-cold-start-bundle.md new file mode 100644 index 0000000000..7b6bcaa3e4 --- /dev/null +++ b/.changeset/perf-cold-start-bundle.md @@ -0,0 +1,5 @@ +--- +default: patch +--- + +Speed up cold start by keeping the PDF viewer out of the initial bundle (it now loads on demand), cutting the amount of JavaScript fetched and parsed on launch. The desktop and mobile windows also paint the app's background color immediately, so startup no longer flashes a blank black screen before the first frame. diff --git a/vite.config.ts b/vite.config.ts index f1b590379b..05576534e4 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -295,10 +295,7 @@ export default defineConfig(({ command }) => ({ plugins: [inject({ Buffer: ['buffer', 'Buffer'] }) as PluginOption], output: { manualChunks: (id) => { - if (id.includes('pdfjs-dist')) return 'pdf'; - if (id.includes('@sableclient/sable-call-embedded')) return 'element-call'; if (id.includes('@matrix-org') || id.includes('matrix-js-sdk')) return 'matrix'; - if (id.includes('react-prism') || id.includes('prism')) return 'prism'; return undefined; }, }, From ef10ad4243b793b67f99914357d046ae5d86f514 Mon Sep 17 00:00:00 2001 From: Erwan Leboucher Date: Sun, 19 Jul 2026 20:13:12 +0200 Subject: [PATCH 6/7] feat(tauri): show a themed window at first paint with a show() failsafe --- index.html | 4 +-- src-tauri/src/lib.rs | 22 ++++++++++++- .../tauri/TauriFrontendReady.test.tsx | 11 ++++++- .../components/tauri/TauriFrontendReady.tsx | 31 ++++++++++--------- src/app/pages/Router.tsx | 4 ++- src/app/pages/client/sidebar/SpaceTabs.tsx | 14 +++++++-- src/client/initMatrix.ts | 6 +++- src/client/versionsCache.ts | 21 ++++++++++--- 8 files changed, 85 insertions(+), 28 deletions(-) diff --git a/index.html b/index.html index e63ff02db5..a30ee45e73 100644 --- a/index.html +++ b/index.html @@ -146,9 +146,7 @@