1
0
Fork 0
forked from mirror/cinny

Compare commits

...

6 commits

Author SHA1 Message Date
Ajay Bura
d70b75e7f3 fix netlify olm file rewrite 2024-01-31 10:05:19 +05:30
Ajay Bura
c2b4e18008 load client in router layout path 2024-01-30 09:44:59 +05:30
Ajay Bura
08b0bdb431 fix olm wasm relative path in dev server 2024-01-29 13:55:12 +05:30
Ajay Bura
5722311306 add media config context 2024-01-28 15:49:41 +05:30
Ajay Bura
3c8e244a00 add capabilities context and loader 2024-01-28 15:21:45 +05:30
Ajay Bura
e4e6601a6b remove spec version loader deps on discovery info 2024-01-28 15:21:18 +05:30
19 changed files with 310 additions and 105 deletions

View file

@ -9,7 +9,7 @@
status = 200
[[redirects]]
from = "/olm.wasm"
from = "*/olm.wasm"
to = "/olm.wasm"
status = 200

View file

@ -0,0 +1,36 @@
import { ReactNode, useCallback, useEffect } from 'react';
import { Capabilities } from 'matrix-js-sdk';
import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback';
import { useMatrixClient } from '../hooks/useMatrixClient';
import { MediaConfig } from '../hooks/useMediaConfig';
import { promiseFulfilledResult } from '../utils/common';
type CapabilitiesAndMediaConfigLoaderProps = {
children: (capabilities?: Capabilities, mediaConfig?: MediaConfig) => ReactNode;
};
export function CapabilitiesAndMediaConfigLoader({
children,
}: CapabilitiesAndMediaConfigLoaderProps) {
const mx = useMatrixClient();
const [state, load] = useAsyncCallback<
[Capabilities | undefined, MediaConfig | undefined],
unknown,
[]
>(
useCallback(async () => {
const result = await Promise.allSettled([mx.getCapabilities(true), mx.getMediaConfig()]);
const capabilities = promiseFulfilledResult(result[0]);
const mediaConfig = promiseFulfilledResult(result[1]);
return [capabilities, mediaConfig];
}, [mx])
);
useEffect(() => {
load();
}, [load]);
const [capabilities, mediaConfig] =
state.status === AsyncStatus.Success ? state.data : [undefined, undefined];
return children(capabilities, mediaConfig);
}

View file

@ -0,0 +1,19 @@
import { ReactNode, useCallback, useEffect } from 'react';
import { Capabilities } from 'matrix-js-sdk';
import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback';
import { useMatrixClient } from '../hooks/useMatrixClient';
type CapabilitiesLoaderProps = {
children: (capabilities: Capabilities | undefined) => ReactNode;
};
export function CapabilitiesLoader({ children }: CapabilitiesLoaderProps) {
const mx = useMatrixClient();
const [state, load] = useAsyncCallback(useCallback(() => mx.getCapabilities(true), [mx]));
useEffect(() => {
load();
}, [load]);
return children(state.status === AsyncStatus.Success ? state.data : undefined);
}

View file

@ -0,0 +1,19 @@
import { ReactNode, useCallback, useEffect } from 'react';
import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback';
import { useMatrixClient } from '../hooks/useMatrixClient';
import { MediaConfig } from '../hooks/useMediaConfig';
type MediaConfigLoaderProps = {
children: (mediaConfig: MediaConfig | undefined) => ReactNode;
};
export function MediaConfigLoader({ children }: MediaConfigLoaderProps) {
const mx = useMatrixClient();
const [state, load] = useAsyncCallback(useCallback(() => mx.getMediaConfig(), [mx]));
useEffect(() => {
load();
}, [load]);
return children(state.status === AsyncStatus.Success ? state.data : undefined);
}

View file

@ -1,20 +1,25 @@
import { ReactNode, useCallback, useEffect } from 'react';
import { ReactNode, useCallback, useEffect, useState } from 'react';
import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback';
import { SpecVersions, specVersions } from '../cs-api';
import { useAutoDiscoveryInfo } from '../hooks/useAutoDiscoveryInfo';
type SpecVersionsLoaderProps = {
baseUrl: string;
fallback?: () => ReactNode;
error?: (err: unknown) => ReactNode;
error?: (err: unknown, retry: () => void, ignore: () => void) => ReactNode;
children: (versions: SpecVersions) => ReactNode;
};
export function SpecVersionsLoader({ fallback, error, children }: SpecVersionsLoaderProps) {
const autoDiscoveryInfo = useAutoDiscoveryInfo();
const baseUrl = autoDiscoveryInfo['m.homeserver'].base_url;
export function SpecVersionsLoader({
baseUrl,
fallback,
error,
children,
}: SpecVersionsLoaderProps) {
const [state, load] = useAsyncCallback(
useCallback(() => specVersions(fetch, baseUrl), [baseUrl])
);
const [ignoreError, setIgnoreError] = useState(false);
const ignoreCallback = useCallback(() => setIgnoreError(true), []);
useEffect(() => {
load();
@ -24,9 +29,15 @@ export function SpecVersionsLoader({ fallback, error, children }: SpecVersionsLo
return fallback?.();
}
if (state.status === AsyncStatus.Error) {
return error?.(state.error);
if (!ignoreError && state.status === AsyncStatus.Error) {
return error?.(state.error, load, ignoreCallback);
}
return children(state.data);
return children(
state.status === AsyncStatus.Success
? state.data
: {
versions: [],
}
);
}

View file

@ -104,7 +104,7 @@ export const specVersions = async (
request: typeof fetch,
baseUrl: string
): Promise<SpecVersions> => {
const res = await request(`${baseUrl}/_matrix/client/versions`);
const res = await request(`${trimTrailingSlash(baseUrl)}/_matrix/client/versions`);
const data = (await res.json()) as unknown;

View file

@ -0,0 +1,12 @@
import { Capabilities } from 'matrix-js-sdk';
import { createContext, useContext } from 'react';
const CapabilitiesContext = createContext<Capabilities | null>(null);
export const CapabilitiesProvider = CapabilitiesContext.Provider;
export function useCapabilities(): Capabilities {
const capabilities = useContext(CapabilitiesContext);
if (!capabilities) throw new Error('Capabilities are not provided!');
return capabilities;
}

View file

@ -0,0 +1,16 @@
import { createContext, useContext } from 'react';
export interface MediaConfig {
[key: string]: unknown;
'm.upload.size'?: number;
}
const MediaConfigContext = createContext<MediaConfig | null>(null);
export const MediaConfigProvider = MediaConfigContext.Provider;
export function useMediaConfig(): MediaConfig {
const mediaConfig = useContext(MediaConfigContext);
if (!mediaConfig) throw new Error('Media configs are not provided!');
return mediaConfig;
}

View file

@ -1,6 +1,7 @@
import React from 'react';
import { Provider as JotaiProvider } from 'jotai';
import {
Outlet,
Route,
RouterProvider,
createBrowserRouter,
@ -12,12 +13,27 @@ import {
import { ClientConfigLoader } from '../components/ClientConfigLoader';
import { ClientConfig, ClientConfigProvider } from '../hooks/useClientConfig';
import { AuthLayout, Login, Register, ResetPassword, authLayoutLoader } from './auth';
import { LOGIN_PATH, REGISTER_PATH, RESET_PASSWORD_PATH, ROOT_PATH } from './paths';
import {
DIRECT_PATH,
EXPLORE_PATH,
HOME_PATH,
LOGIN_PATH,
NOTIFICATIONS_PATH,
REGISTER_PATH,
RESET_PASSWORD_PATH,
ROOT_PATH,
SPACE_PATH,
_CREATE_PATH,
_LOBBY_PATH,
_ROOM_PATH,
_SEARCH_PATH,
} from './paths';
import { isAuthenticated } from '../../client/state/auth';
import Client from '../templates/client/Client';
import { getLoginPath } from './pathUtils';
import { ConfigConfigError, ConfigConfigLoading } from './ConfigConfig';
import { FeatureCheck } from './FeatureCheck';
import Client from '../templates/client/Client';
import { ClientLayout } from './client';
const createRouter = (clientConfig: ClientConfig) => {
const { hashRouter } = clientConfig;
@ -27,7 +43,7 @@ const createRouter = (clientConfig: ClientConfig) => {
<Route
path={ROOT_PATH}
loader={() => {
if (isAuthenticated()) return redirect('/home');
if (isAuthenticated()) return redirect(HOME_PATH);
return redirect(getLoginPath());
}}
/>
@ -42,11 +58,26 @@ const createRouter = (clientConfig: ClientConfig) => {
if (!isAuthenticated()) return redirect(getLoginPath());
return null;
}}
element={<ClientLayout />}
>
<Route path="/home" element={<Client />} />
<Route path="/direct" element={<p>direct</p>} />
<Route path="/:spaceIdOrAlias" element={<p>:spaceIdOrAlias</p>} />
<Route path="/explore" element={<p>explore</p>} />
<Route path={HOME_PATH} element={<Outlet />}>
<Route index element={<Client />} />
<Route path={_SEARCH_PATH} element={<p>search</p>} />
<Route path={_ROOM_PATH} element={<p>room</p>} />
</Route>
<Route path={DIRECT_PATH} element={<Outlet />}>
<Route index element={<p>index</p>} />
<Route path={_CREATE_PATH} element={<p>create</p>} />
<Route path={_ROOM_PATH} element={<p>room</p>} />
</Route>
<Route path={NOTIFICATIONS_PATH} element={<p>notifications</p>} />
<Route path={SPACE_PATH} element={<Outlet />}>
<Route index element={<p>index</p>} />
<Route path={_LOBBY_PATH} element={<p>lobby</p>} />
<Route path={_SEARCH_PATH} element={<p>search</p>} />
<Route path={_ROOM_PATH} element={<p>room</p>} />
</Route>
<Route path={EXPLORE_PATH} element={<p>explore</p>} />
</Route>
<Route path="/*" element={<p>Page not found</p>} />
</Route>

View file

@ -175,6 +175,7 @@ export function AuthLayout() {
<AuthServerProvider value={discoveryState.data.serverName}>
<AutoDiscoveryInfoProvider value={autoDiscoveryInfo}>
<SpecVersionsLoader
baseUrl={autoDiscoveryInfo['m.homeserver'].base_url}
fallback={() => (
<AuthLayoutLoading
message={`Connecting to ${autoDiscoveryInfo['m.homeserver'].base_url}`}

View file

@ -0,0 +1,56 @@
import { Box, Spinner, Text } from 'folds';
import React, { useEffect, useState } from 'react';
import { Outlet } from 'react-router-dom';
import initMatrix from '../../../client/initMatrix';
import { initHotkeys } from '../../../client/event/hotkeys';
import { initRoomListListener } from '../../../client/event/roomList';
import { getSecret } from '../../../client/state/auth';
import { SplashScreen } from '../../components/splash-screen';
import { CapabilitiesAndMediaConfigLoader } from '../../components/CapabilitiesAndMediaConfigLoader';
import { CapabilitiesProvider } from '../../hooks/useCapabilities';
import { MediaConfigProvider } from '../../hooks/useMediaConfig';
import { MatrixClientProvider } from '../../hooks/useMatrixClient';
import { SpecVersions } from './SpecVersions';
export function ClientLayout() {
const [loading, setLoading] = useState(true);
const { baseUrl } = getSecret();
useEffect(() => {
const handleStart = () => {
initHotkeys();
initRoomListListener(initMatrix.roomList);
setLoading(false);
};
initMatrix.once('init_loading_finished', handleStart);
if (!initMatrix.matrixClient) initMatrix.init();
return () => {
initMatrix.removeListener('init_loading_finished', handleStart);
};
}, []);
return (
<SpecVersions baseUrl={baseUrl!}>
{loading ? (
<SplashScreen>
<Box direction="Column" grow="Yes" alignItems="Center" justifyContent="Center" gap="400">
<Spinner variant="Secondary" size="600" />
<Text>Heating up</Text>
</Box>
</SplashScreen>
) : (
<MatrixClientProvider value={initMatrix.matrixClient!}>
<CapabilitiesAndMediaConfigLoader>
{(capabilities, mediaConfig) => (
<CapabilitiesProvider value={capabilities ?? {}}>
<MediaConfigProvider value={mediaConfig ?? {}}>
<Outlet />
</MediaConfigProvider>
</CapabilitiesProvider>
)}
</CapabilitiesAndMediaConfigLoader>
</MatrixClientProvider>
)}
</SpecVersions>
);
}

View file

View file

@ -0,0 +1,46 @@
import React, { ReactNode } from 'react';
import { Box, Dialog, config, Text, Button, Spinner } from 'folds';
import { SpecVersionsLoader } from '../../components/SpecVersionsLoader';
import { SpecVersionsProvider } from '../../hooks/useSpecVersions';
import { SplashScreen } from '../../components/splash-screen';
export function SpecVersions({ baseUrl, children }: { baseUrl: string; children: ReactNode }) {
return (
<SpecVersionsLoader
baseUrl={baseUrl}
fallback={() => (
<SplashScreen>
<Box direction="Column" grow="Yes" alignItems="Center" justifyContent="Center" gap="400">
<Spinner variant="Secondary" size="600" />
<Text>Connecting to server</Text>
</Box>
</SplashScreen>
)}
error={(err, retry, ignore) => (
<SplashScreen>
<Box direction="Column" grow="Yes" alignItems="Center" justifyContent="Center" gap="400">
<Dialog>
<Box direction="Column" gap="400" style={{ padding: config.space.S400 }}>
<Text>
Failed to connect to homeserver. Either homeserver is down or your internet.
</Text>
<Button variant="Critical" onClick={retry}>
<Text as="span" size="B400">
Retry
</Text>
</Button>
<Button variant="Critical" onClick={ignore} fill="Soft">
<Text as="span" size="B400">
Continue
</Text>
</Button>
</Box>
</Dialog>
</Box>
</SplashScreen>
)}
>
{(versions) => <SpecVersionsProvider value={versions}>{children}</SpecVersionsProvider>}
</SpecVersionsLoader>
);
}

View file

@ -0,0 +1 @@
export * from './ClientLayout';

View file

@ -15,3 +15,14 @@ export type RegisterPathSearchParams = {
export const REGISTER_PATH = '/register/:server?/';
export const RESET_PASSWORD_PATH = '/reset-password/:server?/';
export const HOME_PATH = '/home/';
export const DIRECT_PATH = '/direct/';
export const NOTIFICATIONS_PATH = '/notifications/';
export const SPACE_PATH = '/:spaceIdOrAlias/';
export const EXPLORE_PATH = '/explore/';
export const _CREATE_PATH = './create/';
export const _LOBBY_PATH = './lobby/';
export const _SEARCH_PATH = './search/';
export const _ROOM_PATH = './:roomIdOrAlias/:eventId?/';

View file

@ -2,12 +2,15 @@ import { useCallback } from 'react';
import type * as PdfJsDist from 'pdfjs-dist';
import type { GetViewportParameters } from 'pdfjs-dist/types/src/display/api';
import { useAsyncCallback } from '../hooks/useAsyncCallback';
import { trimTrailingSlash } from '../utils/common';
export const usePdfJSLoader = () =>
useAsyncCallback(
useCallback(async () => {
const pdf = await import('pdfjs-dist');
pdf.GlobalWorkerOptions.workerSrc = 'pdf.worker.min.js';
pdf.GlobalWorkerOptions.workerSrc = `${trimTrailingSlash(
import.meta.env.BASE_URL
)}/pdf.worker.min.js`;
return pdf;
}, [])
);

View file

@ -1,24 +1,14 @@
import React, { useState, useEffect, useRef } from 'react';
import React, { useEffect, useRef } from 'react';
import './Client.scss';
import { initHotkeys } from '../../../client/event/hotkeys';
import { initRoomListListener } from '../../../client/event/roomList';
import Text from '../../atoms/text/Text';
import Spinner from '../../atoms/spinner/Spinner';
import Navigation from '../../organisms/navigation/Navigation';
import ContextMenu, { MenuItem } from '../../atoms/context-menu/ContextMenu';
import IconButton from '../../atoms/button/IconButton';
import ReusableContextMenu from '../../atoms/context-menu/ReusableContextMenu';
import Windows from '../../organisms/pw/Windows';
import Dialogs from '../../organisms/pw/Dialogs';
import initMatrix from '../../../client/initMatrix';
import navigation from '../../../client/state/navigation';
import cons from '../../../client/state/cons';
import VerticalMenuIC from '../../../../public/res/ic/outlined/vertical-menu.svg';
import { MatrixClientProvider } from '../../hooks/useMatrixClient';
import { ClientContent } from './ClientContent';
import { useSetting } from '../../state/hooks/settings';
import { settingsAtom } from '../../state/settings';
@ -36,8 +26,6 @@ function SystemEmojiFeature() {
}
function Client() {
const [isLoading, changeLoading] = useState(true);
const [loadingMsg, setLoadingMsg] = useState('Heating up');
const classNameHidden = 'client__item-hidden';
const navWrapperRef = useRef(null);
@ -62,76 +50,19 @@ function Client() {
};
}, []);
useEffect(() => {
changeLoading(true);
let counter = 0;
const iId = setInterval(() => {
const msgList = ['Almost there...', 'Looks like you have a lot of stuff to heat up!'];
if (counter === msgList.length - 1) {
setLoadingMsg(msgList[msgList.length - 1]);
clearInterval(iId);
return;
}
setLoadingMsg(msgList[counter]);
counter += 1;
}, 15000);
initMatrix.once('init_loading_finished', () => {
clearInterval(iId);
initHotkeys();
initRoomListListener(initMatrix.roomList);
changeLoading(false);
});
initMatrix.init();
}, []);
if (isLoading) {
return (
<div className="loading-display">
<div className="loading__menu">
<ContextMenu
placement="bottom"
content={
<>
<MenuItem onClick={() => initMatrix.clearCacheAndReload()}>
Clear cache & reload
</MenuItem>
<MenuItem onClick={() => initMatrix.logout()}>Logout</MenuItem>
</>
}
render={(toggle) => (
<IconButton size="extra-small" onClick={toggle} src={VerticalMenuIC} />
)}
/>
</div>
<Spinner />
<Text className="loading__message" variant="b2">
{loadingMsg}
</Text>
<div className="loading__appname">
<Text variant="h2" weight="medium">
Cinny
</Text>
</div>
</div>
);
}
return (
<MatrixClientProvider value={initMatrix.matrixClient}>
<div className="client-container">
<div className="navigation__wrapper" ref={navWrapperRef}>
<Navigation />
</div>
<div className={`room__wrapper ${classNameHidden}`} ref={roomWrapperRef}>
<ClientContent />
</div>
<Windows />
<Dialogs />
<ReusableContextMenu />
<SystemEmojiFeature />
<div className="client-container">
<div className="navigation__wrapper" ref={navWrapperRef}>
<Navigation />
</div>
</MatrixClientProvider>
<div className={`room__wrapper ${classNameHidden}`} ref={roomWrapperRef}>
<ClientContent />
</div>
<Windows />
<Dialogs />
<ReusableContextMenu />
<SystemEmojiFeature />
</div>
);
}

View file

@ -23,14 +23,20 @@ class InitMatrix extends EventEmitter {
}
async init() {
if (this.matrixClient) {
if (this.matrixClient || this.initializing) {
console.warn('Client is already initialized!')
return;
}
this.initializing = true;
await this.startClient();
this.setupSync();
this.listenEvents();
try {
await this.startClient();
this.setupSync();
this.listenEvents();
this.initializing = false;
} catch {
this.initializing = false;
}
}
async startClient() {

View file

@ -44,6 +44,12 @@ export default defineConfig({
server: {
port: 8080,
host: true,
proxy: {
"^\\/.*?\\/olm\\.wasm$": {
target: 'http://localhost:8080',
rewrite: () => '/olm.wasm'
}
}
},
plugins: [
viteStaticCopy(copyFiles),