mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-31 04:04:21 -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/>.
|
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ConnectionErrorContext } from "@kyoo/models";
|
import { ConnectionError } from "@kyoo/ui";
|
||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ConnectionError;
|
export default ConnectionError;
|
||||||
|
@ -33,14 +33,16 @@ import { WebTooltip } from "@kyoo/primitives/src/tooltip.web";
|
|||||||
import {
|
import {
|
||||||
AccountP,
|
AccountP,
|
||||||
AccountProvider,
|
AccountProvider,
|
||||||
|
ConnectionErrorContext,
|
||||||
createQueryClient,
|
createQueryClient,
|
||||||
fetchQuery,
|
fetchQuery,
|
||||||
getTokenWJ,
|
getTokenWJ,
|
||||||
QueryIdentifier,
|
QueryIdentifier,
|
||||||
QueryPage,
|
QueryPage,
|
||||||
|
UserP,
|
||||||
useUserTheme,
|
useUserTheme,
|
||||||
} from "@kyoo/models";
|
} from "@kyoo/models";
|
||||||
import { useState } from "react";
|
import { Component, ComponentType, useContext, useState } from "react";
|
||||||
import NextApp, { AppContext, type AppProps } from "next/app";
|
import NextApp, { AppContext, type AppProps } from "next/app";
|
||||||
import { Poppins } from "next/font/google";
|
import { Poppins } from "next/font/google";
|
||||||
import { useTheme, useMobileHover, useStyleRegistry, StyleRegistryProvider } from "yoshiki/web";
|
import { useTheme, useMobileHover, useStyleRegistry, StyleRegistryProvider } from "yoshiki/web";
|
||||||
@ -51,6 +53,7 @@ import arrayShuffle from "array-shuffle";
|
|||||||
import { Tooltip } from "react-tooltip";
|
import { Tooltip } from "react-tooltip";
|
||||||
import { getCurrentAccount, readCookie, updateAccount } from "@kyoo/models/src/account-internal";
|
import { getCurrentAccount, readCookie, updateAccount } from "@kyoo/models/src/account-internal";
|
||||||
import { PortalProvider } from "@gorhom/portal";
|
import { PortalProvider } from "@gorhom/portal";
|
||||||
|
import { ConnectionError } from "@kyoo/ui";
|
||||||
|
|
||||||
const font = Poppins({ weight: ["300", "400", "900"], subsets: ["latin"], display: "swap" });
|
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>;
|
return <StyleRegistryProvider registry={registry}>{children}</StyleRegistryProvider>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const App = ({ Component, pageProps }: AppProps) => {
|
const ConnectionErrorVerifier = ({ children }: { children: JSX.Element }) => {
|
||||||
const [queryClient] = useState(() => createQueryClient());
|
const { error } = useContext(ConnectionErrorContext);
|
||||||
const { queryState, token, randomItems, account, theme, ...props } = superjson.deserialize<any>(
|
|
||||||
pageProps ?? { json: {} },
|
if (!error) return children;
|
||||||
);
|
return <WithLayout Component={ConnectionError} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const WithLayout = ({ Component, ...props }: { Component: ComponentType }) => {
|
||||||
const layoutInfo = (Component as QueryPage).getLayout ?? (({ page }) => page);
|
const layoutInfo = (Component as QueryPage).getLayout ?? (({ page }) => page);
|
||||||
const { Layout, props: layoutProps } =
|
const { Layout, props: layoutProps } =
|
||||||
typeof layoutInfo === "function" ? { Layout: layoutInfo, props: {} } : layoutInfo;
|
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);
|
const userTheme = useUserTheme(theme);
|
||||||
useMobileHover();
|
useMobileHover();
|
||||||
|
|
||||||
@ -140,25 +152,22 @@ const App = ({ Component, pageProps }: AppProps) => {
|
|||||||
<meta name="description" content="A portable and vast media library solution." />
|
<meta name="description" content="A portable and vast media library solution." />
|
||||||
</Head>
|
</Head>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<AccountProvider ssrAccount={account}>
|
<AccountProvider ssrAccount={account} ssrError={ssrError}>
|
||||||
<HydrationBoundary state={queryState}>
|
<HydrationBoundary state={queryState}>
|
||||||
<ThemeSelector theme={userTheme} font={{ normal: "inherit" }}>
|
<ThemeSelector theme={userTheme} font={{ normal: "inherit" }}>
|
||||||
<PortalProvider>
|
<PortalProvider>
|
||||||
<SnackbarProvider>
|
<SnackbarProvider>
|
||||||
<GlobalCssTheme />
|
<GlobalCssTheme />
|
||||||
<Layout
|
<ConnectionErrorVerifier>
|
||||||
page={
|
<WithLayout
|
||||||
<Component
|
Component={Component}
|
||||||
randomItems={
|
randomItems={
|
||||||
randomItems[Component.displayName!] ??
|
randomItems[Component.displayName!] ??
|
||||||
arrayShuffle((Component as QueryPage).randomItems ?? [])
|
arrayShuffle((Component as QueryPage).randomItems ?? [])
|
||||||
}
|
}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
}
|
</ConnectionErrorVerifier>
|
||||||
randomItems={[]}
|
|
||||||
{...layoutProps}
|
|
||||||
/>
|
|
||||||
<Tooltip id="tooltip" positionStrategy={"fixed"} />
|
<Tooltip id="tooltip" positionStrategy={"fixed"} />
|
||||||
</SnackbarProvider>
|
</SnackbarProvider>
|
||||||
</PortalProvider>
|
</PortalProvider>
|
||||||
@ -194,8 +203,10 @@ App.getInitialProps = async (ctx: AppContext) => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const account = readCookie(ctx.ctx.req?.headers.cookie, "account", AccountP);
|
const account = readCookie(ctx.ctx.req?.headers.cookie, "account", AccountP);
|
||||||
const [authToken, token] = await getTokenWJ(account);
|
if (account) urls.push({ path: ["auth", "me"], parser: UserP });
|
||||||
appProps.pageProps.queryState = await fetchQuery(urls, authToken);
|
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.token = token;
|
||||||
appProps.pageProps.account = account;
|
appProps.pageProps.account = account;
|
||||||
appProps.pageProps.theme = readCookie(ctx.ctx.req?.headers.cookie, "theme") ?? "auto";
|
appProps.pageProps.theme = readCookie(ctx.ctx.req?.headers.cookie, "theme") ?? "auto";
|
||||||
|
@ -60,9 +60,11 @@ export const ConnectionErrorContext = createContext<{
|
|||||||
export const AccountProvider = ({
|
export const AccountProvider = ({
|
||||||
children,
|
children,
|
||||||
ssrAccount,
|
ssrAccount,
|
||||||
|
ssrError,
|
||||||
}: {
|
}: {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
ssrAccount?: Account;
|
ssrAccount?: Account;
|
||||||
|
ssrError?: KyooErrors;
|
||||||
}) => {
|
}) => {
|
||||||
if (Platform.OS === "web" && typeof window === "undefined") {
|
if (Platform.OS === "web" && typeof window === "undefined") {
|
||||||
const accs = ssrAccount
|
const accs = ssrAccount
|
||||||
@ -72,7 +74,7 @@ export const AccountProvider = ({
|
|||||||
<AccountContext.Provider value={accs}>
|
<AccountContext.Provider value={accs}>
|
||||||
<ConnectionErrorContext.Provider
|
<ConnectionErrorContext.Provider
|
||||||
value={{
|
value={{
|
||||||
error: null,
|
error: ssrError || null,
|
||||||
loading: false,
|
loading: false,
|
||||||
retry: () => {
|
retry: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["auth", "me"] });
|
queryClient.invalidateQueries({ queryKey: ["auth", "me"] });
|
||||||
@ -85,6 +87,8 @@ export const AccountProvider = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const initialSsrError = useRef(ssrError);
|
||||||
|
|
||||||
const [accStr] = useMMKVString("accounts");
|
const [accStr] = useMMKVString("accounts");
|
||||||
const acc = accStr ? z.array(AccountP).parse(JSON.parse(accStr)) : null;
|
const acc = accStr ? z.array(AccountP).parse(JSON.parse(accStr)) : null;
|
||||||
const accounts = useMemo(
|
const accounts = useMemo(
|
||||||
@ -99,16 +103,11 @@ export const AccountProvider = ({
|
|||||||
|
|
||||||
// update user's data from kyoo un startup, it could have changed.
|
// update user's data from kyoo un startup, it could have changed.
|
||||||
const selected = useMemo(() => accounts.find((x) => x.selected), [accounts]);
|
const selected = useMemo(() => accounts.find((x) => x.selected), [accounts]);
|
||||||
const controller = new AbortController();
|
|
||||||
setTimeout(() => controller.abort(), 5_000);
|
|
||||||
const user = useFetch({
|
const user = useFetch({
|
||||||
path: ["auth", "me"],
|
path: ["auth", "me"],
|
||||||
parser: UserP,
|
parser: UserP,
|
||||||
placeholderData: selected as User,
|
placeholderData: selected as User,
|
||||||
enabled: !!selected,
|
enabled: !!selected,
|
||||||
options: {
|
|
||||||
signal: controller.signal,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selected || !user.isSuccess || user.isPlaceholderData) return;
|
if (!selected || !user.isSuccess || user.isPlaceholderData) return;
|
||||||
@ -122,7 +121,10 @@ export const AccountProvider = ({
|
|||||||
const oldSelectedId = useRef<string | undefined>(selected?.id);
|
const oldSelectedId = useRef<string | undefined>(selected?.id);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// if the user change account (or connect/disconnect), reset query cache.
|
// 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;
|
oldSelectedId.current = selected?.id;
|
||||||
|
|
||||||
// update cookies for ssr (needs to contains token, theme, language...)
|
// update cookies for ssr (needs to contains token, theme, language...)
|
||||||
@ -136,7 +138,7 @@ export const AccountProvider = ({
|
|||||||
<AccountContext.Provider value={accounts}>
|
<AccountContext.Provider value={accounts}>
|
||||||
<ConnectionErrorContext.Provider
|
<ConnectionErrorContext.Provider
|
||||||
value={{
|
value={{
|
||||||
error: selected ? user.error : null,
|
error: selected ? initialSsrError.current ?? user.error : null,
|
||||||
loading: user.isLoading,
|
loading: user.isLoading,
|
||||||
retry: () => {
|
retry: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["auth", "me"] });
|
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> => {
|
export const getTokenWJ = async (account?: Account | null): ReturnType<typeof run> => {
|
||||||
async function run() {
|
async function run() {
|
||||||
if (account === undefined) account = getCurrentAccount();
|
if (account === undefined) account = getCurrentAccount();
|
||||||
if (!account) return [null, null] as const;
|
if (!account) return [null, null, null] as const;
|
||||||
|
|
||||||
let token = account.token;
|
let token = account.token;
|
||||||
|
|
||||||
@ -115,10 +115,10 @@ export const getTokenWJ = async (account?: Account | null): ReturnType<typeof ru
|
|||||||
updateAccount(account.id, { ...account, token });
|
updateAccount(account.id, { ...account, token });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error refreshing token durring ssr:", 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.
|
// 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 "./error";
|
||||||
export * from "./unauthorized";
|
export * from "./unauthorized";
|
||||||
|
export * from "./connection";
|
||||||
|
Loading…
x
Reference in New Issue
Block a user