mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-24 02:02:36 -04:00
Add connection error page on the web
This commit is contained in:
parent
af422e62e1
commit
e60e2306b7
@ -18,33 +18,6 @@
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { ConnectionErrorContext } from "@kyoo/models";
|
||||
import { Button, H1, P, ts } from "@kyoo/primitives";
|
||||
import { useRouter } from "expo-router";
|
||||
import { useContext } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { View } from "react-native";
|
||||
import { useYoshiki } from "yoshiki/native";
|
||||
|
||||
const ConnectionError = () => {
|
||||
const { css } = useYoshiki();
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const { error, retry } = useContext(ConnectionErrorContext);
|
||||
|
||||
return (
|
||||
<View {...css({ padding: ts(2) })}>
|
||||
<H1 {...css({ textAlign: "center" })}>{t("errors.connection")}</H1>
|
||||
<P>{error?.errors[0] ?? t("error.unknown")}</P>
|
||||
<P>{t("errors.connection-tips")}</P>
|
||||
<Button onPress={retry} text={t("errors.try-again")} {...css({ m: ts(1) })} />
|
||||
<Button
|
||||
onPress={() => router.push("/login")}
|
||||
text={t("errors.re-login")}
|
||||
{...css({ m: ts(1) })}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
import { ConnectionError } from "@kyoo/ui";
|
||||
|
||||
export default ConnectionError;
|
||||
|
@ -33,14 +33,16 @@ import { WebTooltip } from "@kyoo/primitives/src/tooltip.web";
|
||||
import {
|
||||
AccountP,
|
||||
AccountProvider,
|
||||
ConnectionErrorContext,
|
||||
createQueryClient,
|
||||
fetchQuery,
|
||||
getTokenWJ,
|
||||
QueryIdentifier,
|
||||
QueryPage,
|
||||
UserP,
|
||||
useUserTheme,
|
||||
} from "@kyoo/models";
|
||||
import { useState } from "react";
|
||||
import { Component, ComponentType, useContext, useState } from "react";
|
||||
import NextApp, { AppContext, type AppProps } from "next/app";
|
||||
import { Poppins } from "next/font/google";
|
||||
import { useTheme, useMobileHover, useStyleRegistry, StyleRegistryProvider } from "yoshiki/web";
|
||||
@ -51,6 +53,7 @@ import arrayShuffle from "array-shuffle";
|
||||
import { Tooltip } from "react-tooltip";
|
||||
import { getCurrentAccount, readCookie, updateAccount } from "@kyoo/models/src/account-internal";
|
||||
import { PortalProvider } from "@gorhom/portal";
|
||||
import { ConnectionError } from "@kyoo/ui";
|
||||
|
||||
const font = Poppins({ weight: ["300", "400", "900"], subsets: ["latin"], display: "swap" });
|
||||
|
||||
@ -114,15 +117,24 @@ const YoshikiDebug = ({ children }: { children: JSX.Element }) => {
|
||||
return <StyleRegistryProvider registry={registry}>{children}</StyleRegistryProvider>;
|
||||
};
|
||||
|
||||
const App = ({ Component, pageProps }: AppProps) => {
|
||||
const [queryClient] = useState(() => createQueryClient());
|
||||
const { queryState, token, randomItems, account, theme, ...props } = superjson.deserialize<any>(
|
||||
pageProps ?? { json: {} },
|
||||
);
|
||||
const ConnectionErrorVerifier = ({ children }: { children: JSX.Element }) => {
|
||||
const { error } = useContext(ConnectionErrorContext);
|
||||
|
||||
if (!error) return children;
|
||||
return <WithLayout Component={ConnectionError} />;
|
||||
};
|
||||
|
||||
const WithLayout = ({ Component, ...props }: { Component: ComponentType }) => {
|
||||
const layoutInfo = (Component as QueryPage).getLayout ?? (({ page }) => page);
|
||||
const { Layout, props: layoutProps } =
|
||||
typeof layoutInfo === "function" ? { Layout: layoutInfo, props: {} } : layoutInfo;
|
||||
return <Layout page={<Component {...props} />} randomItems={[]} {...layoutProps} />;
|
||||
};
|
||||
|
||||
const App = ({ Component, pageProps }: AppProps) => {
|
||||
const [queryClient] = useState(() => createQueryClient());
|
||||
const { queryState, ssrError, token, randomItems, account, theme, ...props } =
|
||||
superjson.deserialize<any>(pageProps ?? { json: {} });
|
||||
const userTheme = useUserTheme(theme);
|
||||
useMobileHover();
|
||||
|
||||
@ -140,25 +152,22 @@ const App = ({ Component, pageProps }: AppProps) => {
|
||||
<meta name="description" content="A portable and vast media library solution." />
|
||||
</Head>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AccountProvider ssrAccount={account}>
|
||||
<AccountProvider ssrAccount={account} ssrError={ssrError}>
|
||||
<HydrationBoundary state={queryState}>
|
||||
<ThemeSelector theme={userTheme} font={{ normal: "inherit" }}>
|
||||
<PortalProvider>
|
||||
<SnackbarProvider>
|
||||
<GlobalCssTheme />
|
||||
<Layout
|
||||
page={
|
||||
<Component
|
||||
randomItems={
|
||||
randomItems[Component.displayName!] ??
|
||||
arrayShuffle((Component as QueryPage).randomItems ?? [])
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
}
|
||||
randomItems={[]}
|
||||
{...layoutProps}
|
||||
/>
|
||||
<ConnectionErrorVerifier>
|
||||
<WithLayout
|
||||
Component={Component}
|
||||
randomItems={
|
||||
randomItems[Component.displayName!] ??
|
||||
arrayShuffle((Component as QueryPage).randomItems ?? [])
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
</ConnectionErrorVerifier>
|
||||
<Tooltip id="tooltip" positionStrategy={"fixed"} />
|
||||
</SnackbarProvider>
|
||||
</PortalProvider>
|
||||
@ -194,8 +203,10 @@ App.getInitialProps = async (ctx: AppContext) => {
|
||||
];
|
||||
|
||||
const account = readCookie(ctx.ctx.req?.headers.cookie, "account", AccountP);
|
||||
const [authToken, token] = await getTokenWJ(account);
|
||||
appProps.pageProps.queryState = await fetchQuery(urls, authToken);
|
||||
if (account) urls.push({ path: ["auth", "me"], parser: UserP });
|
||||
const [authToken, token, error] = await getTokenWJ(account);
|
||||
if (error) appProps.pageProps.ssrError = error;
|
||||
else appProps.pageProps.queryState = await fetchQuery(urls, authToken);
|
||||
appProps.pageProps.token = token;
|
||||
appProps.pageProps.account = account;
|
||||
appProps.pageProps.theme = readCookie(ctx.ctx.req?.headers.cookie, "theme") ?? "auto";
|
||||
|
@ -60,9 +60,11 @@ export const ConnectionErrorContext = createContext<{
|
||||
export const AccountProvider = ({
|
||||
children,
|
||||
ssrAccount,
|
||||
ssrError,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
ssrAccount?: Account;
|
||||
ssrError?: KyooErrors;
|
||||
}) => {
|
||||
if (Platform.OS === "web" && typeof window === "undefined") {
|
||||
const accs = ssrAccount
|
||||
@ -72,7 +74,7 @@ export const AccountProvider = ({
|
||||
<AccountContext.Provider value={accs}>
|
||||
<ConnectionErrorContext.Provider
|
||||
value={{
|
||||
error: null,
|
||||
error: ssrError || null,
|
||||
loading: false,
|
||||
retry: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["auth", "me"] });
|
||||
@ -85,6 +87,8 @@ export const AccountProvider = ({
|
||||
);
|
||||
}
|
||||
|
||||
const initialSsrError = useRef(ssrError);
|
||||
|
||||
const [accStr] = useMMKVString("accounts");
|
||||
const acc = accStr ? z.array(AccountP).parse(JSON.parse(accStr)) : null;
|
||||
const accounts = useMemo(
|
||||
@ -99,16 +103,11 @@ export const AccountProvider = ({
|
||||
|
||||
// update user's data from kyoo un startup, it could have changed.
|
||||
const selected = useMemo(() => accounts.find((x) => x.selected), [accounts]);
|
||||
const controller = new AbortController();
|
||||
setTimeout(() => controller.abort(), 5_000);
|
||||
const user = useFetch({
|
||||
path: ["auth", "me"],
|
||||
parser: UserP,
|
||||
placeholderData: selected as User,
|
||||
enabled: !!selected,
|
||||
options: {
|
||||
signal: controller.signal,
|
||||
},
|
||||
});
|
||||
useEffect(() => {
|
||||
if (!selected || !user.isSuccess || user.isPlaceholderData) return;
|
||||
@ -122,7 +121,10 @@ export const AccountProvider = ({
|
||||
const oldSelectedId = useRef<string | undefined>(selected?.id);
|
||||
useEffect(() => {
|
||||
// if the user change account (or connect/disconnect), reset query cache.
|
||||
if (selected?.id !== oldSelectedId.current) queryClient.invalidateQueries();
|
||||
if (selected?.id !== oldSelectedId.current) {
|
||||
initialSsrError.current = undefined;
|
||||
queryClient.invalidateQueries();
|
||||
}
|
||||
oldSelectedId.current = selected?.id;
|
||||
|
||||
// update cookies for ssr (needs to contains token, theme, language...)
|
||||
@ -136,7 +138,7 @@ export const AccountProvider = ({
|
||||
<AccountContext.Provider value={accounts}>
|
||||
<ConnectionErrorContext.Provider
|
||||
value={{
|
||||
error: selected ? user.error : null,
|
||||
error: selected ? initialSsrError.current ?? user.error : null,
|
||||
loading: user.isLoading,
|
||||
retry: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["auth", "me"] });
|
||||
|
@ -96,7 +96,7 @@ let running: ReturnType<typeof getTokenWJ> | null = null;
|
||||
export const getTokenWJ = async (account?: Account | null): ReturnType<typeof run> => {
|
||||
async function run() {
|
||||
if (account === undefined) account = getCurrentAccount();
|
||||
if (!account) return [null, null] as const;
|
||||
if (!account) return [null, null, null] as const;
|
||||
|
||||
let token = account.token;
|
||||
|
||||
@ -115,10 +115,10 @@ export const getTokenWJ = async (account?: Account | null): ReturnType<typeof ru
|
||||
updateAccount(account.id, { ...account, token });
|
||||
} catch (e) {
|
||||
console.error("Error refreshing token durring ssr:", e);
|
||||
return [null, null];
|
||||
return [null, null, e as KyooErrors] as const;
|
||||
}
|
||||
}
|
||||
return [`${token.token_type} ${token.access_token}`, token] as const;
|
||||
return [`${token.token_type} ${token.access_token}`, token, null] as const;
|
||||
}
|
||||
|
||||
// Do not cache promise durring ssr.
|
||||
|
51
front/packages/ui/src/errors/connection.tsx
Normal file
51
front/packages/ui/src/errors/connection.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { ConnectionErrorContext } from "@kyoo/models";
|
||||
import { Button, H1, P, ts } from "@kyoo/primitives";
|
||||
import { useRouter } from "solito/router";
|
||||
import { useContext } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { View } from "react-native";
|
||||
import { useYoshiki } from "yoshiki/native";
|
||||
import { DefaultLayout } from "../layout";
|
||||
|
||||
export const ConnectionError = () => {
|
||||
const { css } = useYoshiki();
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const { error, retry } = useContext(ConnectionErrorContext);
|
||||
|
||||
return (
|
||||
<View {...css({ padding: ts(2) })}>
|
||||
<H1 {...css({ textAlign: "center" })}>{t("errors.connection")}</H1>
|
||||
<P>{error?.errors[0] ?? t("error.unknown")}</P>
|
||||
<P>{t("errors.connection-tips")}</P>
|
||||
<Button onPress={retry} text={t("errors.try-again")} {...css({ m: ts(1) })} />
|
||||
<Button
|
||||
onPress={() => router.push("/login")}
|
||||
text={t("errors.re-login")}
|
||||
{...css({ m: ts(1) })}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
ConnectionError.getLayout = DefaultLayout;
|
@ -20,3 +20,4 @@
|
||||
|
||||
export * from "./error";
|
||||
export * from "./unauthorized";
|
||||
export * from "./connection";
|
||||
|
Loading…
x
Reference in New Issue
Block a user