mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-11-14 19:20:28 +03:00
handle error in loading screen (#1823)
* handle client boot error in loading screen * use sync state hook in client root * add loading screen options * removed extra condition in loading finish * add sync connection status bar
This commit is contained in:
parent
e046c59f7c
commit
e2228a18c1
62 changed files with 609 additions and 510 deletions
|
|
@ -7,7 +7,7 @@ type ClientLayoutProps = {
|
|||
};
|
||||
export function ClientLayout({ nav, children }: ClientLayoutProps) {
|
||||
return (
|
||||
<Box style={{ height: '100%' }}>
|
||||
<Box grow="Yes">
|
||||
<Box shrink="No">{nav}</Box>
|
||||
<Box grow="Yes">{children}</Box>
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,27 @@
|
|||
import { Box, Spinner, Text } from 'folds';
|
||||
import React, { ReactNode, useEffect, useState } from 'react';
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
config,
|
||||
Dialog,
|
||||
Icon,
|
||||
IconButton,
|
||||
Icons,
|
||||
Menu,
|
||||
MenuItem,
|
||||
PopOut,
|
||||
RectCords,
|
||||
Spinner,
|
||||
Text,
|
||||
} from 'folds';
|
||||
import { HttpApiEvent, HttpApiEventHandlerMap, MatrixClient } from 'matrix-js-sdk';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import React, { MouseEventHandler, ReactNode, useCallback, useEffect, useState } from 'react';
|
||||
import {
|
||||
clearCacheAndReload,
|
||||
initClient,
|
||||
logoutClient,
|
||||
startClient,
|
||||
} from '../../../client/initMatrix';
|
||||
import { getSecret } from '../../../client/state/auth';
|
||||
import { SplashScreen } from '../../components/splash-screen';
|
||||
import { CapabilitiesAndMediaConfigLoader } from '../../components/CapabilitiesAndMediaConfigLoader';
|
||||
|
|
@ -13,6 +34,10 @@ import Dialogs from '../../organisms/pw/Dialogs';
|
|||
import ReusableContextMenu from '../../atoms/context-menu/ReusableContextMenu';
|
||||
import { useSetting } from '../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../state/settings';
|
||||
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
||||
import { useSyncState } from '../../hooks/useSyncState';
|
||||
import { stopPropagation } from '../../utils/keyboard';
|
||||
import { SyncStatus } from './SyncStatus';
|
||||
|
||||
function SystemEmojiFeature() {
|
||||
const [twitterEmoji] = useSetting(settingsAtom, 'twitterEmoji');
|
||||
|
|
@ -37,6 +62,89 @@ function ClientRootLoading() {
|
|||
);
|
||||
}
|
||||
|
||||
function ClientRootOptions({ mx }: { mx: MatrixClient }) {
|
||||
const [menuAnchor, setMenuAnchor] = useState<RectCords>();
|
||||
|
||||
const handleToggle: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||
const cords = evt.currentTarget.getBoundingClientRect();
|
||||
setMenuAnchor((currentState) => {
|
||||
if (currentState) return undefined;
|
||||
return cords;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: config.space.S100,
|
||||
right: config.space.S100,
|
||||
}}
|
||||
variant="Background"
|
||||
fill="None"
|
||||
onClick={handleToggle}
|
||||
>
|
||||
<Icon size="200" src={Icons.VerticalDots} />
|
||||
<PopOut
|
||||
anchor={menuAnchor}
|
||||
position="Bottom"
|
||||
align="End"
|
||||
offset={6}
|
||||
content={
|
||||
<FocusTrap
|
||||
focusTrapOptions={{
|
||||
initialFocus: false,
|
||||
returnFocusOnDeactivate: false,
|
||||
onDeactivate: () => setMenuAnchor(undefined),
|
||||
clickOutsideDeactivates: true,
|
||||
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
|
||||
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
|
||||
escapeDeactivates: stopPropagation,
|
||||
}}
|
||||
>
|
||||
<Menu>
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||
<MenuItem onClick={() => clearCacheAndReload(mx)} size="300" radii="300">
|
||||
<Text as="span" size="T300" truncate>
|
||||
Clear Cache and Reload
|
||||
</Text>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => logoutClient(mx)}
|
||||
size="300"
|
||||
radii="300"
|
||||
variant="Critical"
|
||||
fill="None"
|
||||
>
|
||||
<Text as="span" size="T300" truncate>
|
||||
Logout
|
||||
</Text>
|
||||
</MenuItem>
|
||||
</Box>
|
||||
</Menu>
|
||||
</FocusTrap>
|
||||
}
|
||||
/>
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
|
||||
const useLogoutListener = (mx?: MatrixClient) => {
|
||||
useEffect(() => {
|
||||
const handleLogout: HttpApiEventHandlerMap[HttpApiEvent.SessionLoggedOut] = async () => {
|
||||
mx?.stopClient();
|
||||
await mx?.clearStores();
|
||||
window.localStorage.clear();
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
mx?.on(HttpApiEvent.SessionLoggedOut, handleLogout);
|
||||
return () => {
|
||||
mx?.removeListener(HttpApiEvent.SessionLoggedOut, handleLogout);
|
||||
};
|
||||
}, [mx]);
|
||||
};
|
||||
|
||||
type ClientRootProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
|
@ -44,30 +152,71 @@ export function ClientRoot({ children }: ClientRootProps) {
|
|||
const [loading, setLoading] = useState(true);
|
||||
const { baseUrl } = getSecret();
|
||||
|
||||
const [loadState, loadMatrix] = useAsyncCallback<MatrixClient, Error, []>(
|
||||
useCallback(() => initClient(getSecret() as any), [])
|
||||
);
|
||||
const mx = loadState.status === AsyncStatus.Success ? loadState.data : undefined;
|
||||
const [startState, startMatrix] = useAsyncCallback<void, Error, [MatrixClient]>(
|
||||
useCallback((m) => startClient(m), [])
|
||||
);
|
||||
|
||||
useLogoutListener(mx);
|
||||
|
||||
useEffect(() => {
|
||||
const handleStart = () => {
|
||||
setLoading(false);
|
||||
};
|
||||
initMatrix.once('init_loading_finished', handleStart);
|
||||
if (!initMatrix.matrixClient) initMatrix.init();
|
||||
return () => {
|
||||
initMatrix.removeListener('init_loading_finished', handleStart);
|
||||
};
|
||||
}, []);
|
||||
if (loadState.status === AsyncStatus.Idle) {
|
||||
loadMatrix();
|
||||
}
|
||||
}, [loadState, loadMatrix]);
|
||||
|
||||
useEffect(() => {
|
||||
if (mx && !mx.clientRunning) {
|
||||
startMatrix(mx);
|
||||
}
|
||||
}, [mx, startMatrix]);
|
||||
|
||||
useSyncState(
|
||||
mx,
|
||||
useCallback((state) => {
|
||||
if (state === 'PREPARED') {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [])
|
||||
);
|
||||
|
||||
return (
|
||||
<SpecVersions baseUrl={baseUrl!}>
|
||||
{loading ? (
|
||||
{mx && <SyncStatus mx={mx} />}
|
||||
{loading && mx && <ClientRootOptions mx={mx} />}
|
||||
{(loadState.status === AsyncStatus.Error || startState.status === AsyncStatus.Error) && (
|
||||
<SplashScreen>
|
||||
<Box direction="Column" grow="Yes" alignItems="Center" justifyContent="Center" gap="400">
|
||||
<Dialog>
|
||||
<Box direction="Column" gap="400" style={{ padding: config.space.S400 }}>
|
||||
{loadState.status === AsyncStatus.Error && (
|
||||
<Text>{`Failed to load. ${loadState.error.message}`}</Text>
|
||||
)}
|
||||
{startState.status === AsyncStatus.Error && (
|
||||
<Text>{`Failed to load. ${startState.error.message}`}</Text>
|
||||
)}
|
||||
<Button variant="Critical" onClick={loadMatrix}>
|
||||
<Text as="span" size="B400">
|
||||
Retry
|
||||
</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
</Dialog>
|
||||
</Box>
|
||||
</SplashScreen>
|
||||
)}
|
||||
{loading || !mx ? (
|
||||
<ClientRootLoading />
|
||||
) : (
|
||||
<MatrixClientProvider value={initMatrix.matrixClient!}>
|
||||
<MatrixClientProvider value={mx}>
|
||||
<CapabilitiesAndMediaConfigLoader>
|
||||
{(capabilities, mediaConfig) => (
|
||||
<CapabilitiesProvider value={capabilities ?? {}}>
|
||||
<MediaConfigProvider value={mediaConfig ?? {}}>
|
||||
{children}
|
||||
|
||||
{/* TODO: remove these components after navigation refactor */}
|
||||
<Windows />
|
||||
<Dialogs />
|
||||
<ReusableContextMenu />
|
||||
|
|
|
|||
87
src/app/pages/client/SyncStatus.tsx
Normal file
87
src/app/pages/client/SyncStatus.tsx
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import { MatrixClient, SyncState } from 'matrix-js-sdk';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { Box, config, Line, Text } from 'folds';
|
||||
import { useSyncState } from '../../hooks/useSyncState';
|
||||
import { ContainerColor } from '../../styles/ContainerColor.css';
|
||||
|
||||
type StateData = {
|
||||
current: SyncState | null;
|
||||
previous: SyncState | null | undefined;
|
||||
};
|
||||
|
||||
type SyncStatusProps = {
|
||||
mx: MatrixClient;
|
||||
};
|
||||
export function SyncStatus({ mx }: SyncStatusProps) {
|
||||
const [stateData, setStateData] = useState<StateData>({
|
||||
current: null,
|
||||
previous: undefined,
|
||||
});
|
||||
|
||||
useSyncState(
|
||||
mx,
|
||||
useCallback((current, previous) => {
|
||||
setStateData((s) => {
|
||||
if (s.current === current && s.previous === previous) {
|
||||
return s;
|
||||
}
|
||||
return { current, previous };
|
||||
});
|
||||
}, [])
|
||||
);
|
||||
|
||||
if (
|
||||
(stateData.current === SyncState.Prepared ||
|
||||
stateData.current === SyncState.Syncing ||
|
||||
stateData.current === SyncState.Catchup) &&
|
||||
stateData.previous !== SyncState.Syncing
|
||||
) {
|
||||
return (
|
||||
<Box direction="Column" shrink="No">
|
||||
<Box
|
||||
className={ContainerColor({ variant: 'Success' })}
|
||||
style={{ padding: `${config.space.S100} 0` }}
|
||||
alignItems="Center"
|
||||
justifyContent="Center"
|
||||
>
|
||||
<Text size="L400">Connecting...</Text>
|
||||
</Box>
|
||||
<Line variant="Success" size="300" />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (stateData.current === SyncState.Reconnecting) {
|
||||
return (
|
||||
<Box direction="Column" shrink="No">
|
||||
<Box
|
||||
className={ContainerColor({ variant: 'Warning' })}
|
||||
style={{ padding: `${config.space.S100} 0` }}
|
||||
alignItems="Center"
|
||||
justifyContent="Center"
|
||||
>
|
||||
<Text size="L400">Connection Lost! Reconnecting...</Text>
|
||||
</Box>
|
||||
<Line variant="Warning" size="300" />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (stateData.current === SyncState.Error) {
|
||||
return (
|
||||
<Box direction="Column" shrink="No">
|
||||
<Box
|
||||
className={ContainerColor({ variant: 'Critical' })}
|
||||
style={{ padding: `${config.space.S100} 0` }}
|
||||
alignItems="Center"
|
||||
justifyContent="Center"
|
||||
>
|
||||
<Text size="L400">Connection Lost!</Text>
|
||||
</Box>
|
||||
<Line variant="Critical" size="300" />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
@ -18,7 +18,7 @@ export function WelcomePage() {
|
|||
title="Welcome to Cinny"
|
||||
subTitle={
|
||||
<span>
|
||||
Yet anothor matrix client.{' '}
|
||||
Yet another matrix client.{' '}
|
||||
<a
|
||||
href="https://github.com/cinnyapp/cinny/releases"
|
||||
target="_blank"
|
||||
|
|
|
|||
|
|
@ -50,12 +50,13 @@ type DirectMenuProps = {
|
|||
requestClose: () => void;
|
||||
};
|
||||
const DirectMenu = forwardRef<HTMLDivElement, DirectMenuProps>(({ requestClose }, ref) => {
|
||||
const mx = useMatrixClient();
|
||||
const orphanRooms = useDirectRooms();
|
||||
const unread = useRoomsUnread(orphanRooms, roomToUnreadAtom);
|
||||
|
||||
const handleMarkAsRead = () => {
|
||||
if (!unread) return;
|
||||
orphanRooms.forEach((rId) => markAsRead(rId));
|
||||
orphanRooms.forEach((rId) => markAsRead(mx, rId));
|
||||
requestClose();
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -55,10 +55,11 @@ type HomeMenuProps = {
|
|||
const HomeMenu = forwardRef<HTMLDivElement, HomeMenuProps>(({ requestClose }, ref) => {
|
||||
const orphanRooms = useHomeRooms();
|
||||
const unread = useRoomsUnread(orphanRooms, roomToUnreadAtom);
|
||||
const mx = useMatrixClient();
|
||||
|
||||
const handleMarkAsRead = () => {
|
||||
if (!unread) return;
|
||||
orphanRooms.forEach((rId) => markAsRead(rId));
|
||||
orphanRooms.forEach((rId) => markAsRead(mx, rId));
|
||||
requestClose();
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -356,7 +356,7 @@ function RoomNotificationsGroupComp({
|
|||
onOpen(room.roomId, eventId);
|
||||
};
|
||||
const handleMarkAsRead = () => {
|
||||
markAsRead(room.roomId);
|
||||
markAsRead(mx, room.roomId);
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -30,10 +30,11 @@ type DirectMenuProps = {
|
|||
const DirectMenu = forwardRef<HTMLDivElement, DirectMenuProps>(({ requestClose }, ref) => {
|
||||
const orphanRooms = useDirectRooms();
|
||||
const unread = useRoomsUnread(orphanRooms, roomToUnreadAtom);
|
||||
const mx = useMatrixClient();
|
||||
|
||||
const handleMarkAsRead = () => {
|
||||
if (!unread) return;
|
||||
orphanRooms.forEach((rId) => markAsRead(rId));
|
||||
orphanRooms.forEach((rId) => markAsRead(mx, rId));
|
||||
requestClose();
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -31,10 +31,11 @@ type HomeMenuProps = {
|
|||
const HomeMenu = forwardRef<HTMLDivElement, HomeMenuProps>(({ requestClose }, ref) => {
|
||||
const orphanRooms = useHomeRooms();
|
||||
const unread = useRoomsUnread(orphanRooms, roomToUnreadAtom);
|
||||
const mx = useMatrixClient();
|
||||
|
||||
const handleMarkAsRead = () => {
|
||||
if (!unread) return;
|
||||
orphanRooms.forEach((rId) => markAsRead(rId));
|
||||
orphanRooms.forEach((rId) => markAsRead(mx, rId));
|
||||
requestClose();
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(
|
|||
const unread = useRoomsUnread(allChild, roomToUnreadAtom);
|
||||
|
||||
const handleMarkAsRead = () => {
|
||||
allChild.forEach((childRoomId) => markAsRead(childRoomId));
|
||||
allChild.forEach((childRoomId) => markAsRead(mx, childRoomId));
|
||||
requestClose();
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(({ room, requestClo
|
|||
const unread = useRoomsUnread(allChild, roomToUnreadAtom);
|
||||
|
||||
const handleMarkAsRead = () => {
|
||||
allChild.forEach((childRoomId) => markAsRead(childRoomId));
|
||||
allChild.forEach((childRoomId) => markAsRead(mx, childRoomId));
|
||||
requestClose();
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue