diff --git a/front/app/(app)/_layout.tsx b/front/app/(app)/_layout.tsx
new file mode 100644
index 00000000..b43d4dec
--- /dev/null
+++ b/front/app/(app)/_layout.tsx
@@ -0,0 +1,10 @@
+import { Slot } from "one";
+import { ErrorConsumer } from "~/providers/error-provider";
+
+export default function Layout() {
+ return (
+
+
+
+ );
+}
diff --git a/front/app/index.tsx b/front/app/(app)/index.tsx
similarity index 100%
rename from front/app/index.tsx
rename to front/app/(app)/index.tsx
diff --git a/front/routes.d.ts b/front/routes.d.ts
index f2889912..103b2b71 100644
--- a/front/routes.d.ts
+++ b/front/routes.d.ts
@@ -6,7 +6,7 @@ import type { OneRouter } from 'one'
declare module 'one' {
export namespace OneRouter {
export interface __routes extends Record {
- StaticRoutes: `/` | `/_sitemap`
+ StaticRoutes: `/` | `/(app)` | `/_sitemap`
DynamicRoutes: never
DynamicRouteTemplate: never
IsTyped: true
diff --git a/front/src/providers/account-provider.tsx b/front/src/providers/account-provider.tsx
index 58bc6aaa..94739b15 100644
--- a/front/src/providers/account-provider.tsx
+++ b/front/src/providers/account-provider.tsx
@@ -1,5 +1,5 @@
import { useQueryClient } from "@tanstack/react-query";
-import { type ReactNode, createContext, useEffect, useMemo, useRef } from "react";
+import { type ReactNode, createContext, useContext, useEffect, useMemo, useRef } from "react";
import { Platform } from "react-native";
import { z } from "zod";
import { type Account, AccountP, type Token, UserP } from "~/models";
@@ -8,14 +8,12 @@ import { removeAccounts, updateAccount } from "./account-store";
import { useSetError } from "./error-provider";
import { useStoreValue } from "./settings";
-export const ssrApiUrl = process.env.KYOO_URL ?? "http://back/api";
-
export const AccountContext = createContext<{
apiUrl: string;
authToken: Token | null;
selectedAccount: Account | null;
accounts: (Account & { select: () => void; remove: () => void })[];
-}>({ apiUrl: ssrApiUrl, authToken: null, selectedAccount: null, accounts: [] });
+}>({ apiUrl: "/api", authToken: null, selectedAccount: null, accounts: [] });
export const AccountProvider = ({ children }: { children: ReactNode }) => {
const [setError, clearError] = useSetError("account");
@@ -26,7 +24,7 @@ export const AccountProvider = ({ children }: { children: ReactNode }) => {
return {
apiUrl: Platform.OS === "web" ? "/api" : acc?.apiUrl!,
authToken: acc?.token ?? null,
- selectedAccount: acc,
+ selectedAccount: acc ?? null,
accounts: accounts.map((account) => ({
...account,
select: () => updateAccount(account.id, { ...account, selected: true }),
@@ -51,6 +49,7 @@ export const AccountProvider = ({ children }: { children: ReactNode }) => {
authToken: ret.authToken?.access_token,
},
});
+ console.log(user);
// Use a ref here because we don't want the effect to trigger when the selected
// value has changed, only when the fetch result changed
// If we trigger the effect when the selected value change, we enter an infinite render loop
@@ -59,7 +58,7 @@ export const AccountProvider = ({ children }: { children: ReactNode }) => {
useEffect(() => {
if (!selectedRef.current || !userIsSuccess || userIsPlaceholder) return;
// The id is different when user is stale data, we need to wait for the use effect to invalidate the query.
- if (user.id !== selectedRef.current.id) return;
+ if (user?.id !== selectedRef.current.id) return;
const nUser = { ...selectedRef.current, ...user };
updateAccount(nUser.id, nUser);
}, [user, userIsSuccess, userIsPlaceholder]);
@@ -83,3 +82,8 @@ export const AccountProvider = ({ children }: { children: ReactNode }) => {
return {children};
};
+
+export const useAccount = () => {
+ const { selectedAccount } = useContext(AccountContext);
+ return selectedAccount;
+};
diff --git a/front/src/providers/error-provider.tsx b/front/src/providers/error-provider.tsx
index 901e9c02..cc0f73e1 100644
--- a/front/src/providers/error-provider.tsx
+++ b/front/src/providers/error-provider.tsx
@@ -1,4 +1,12 @@
-import { type ReactNode, createContext, useContext, useState } from "react";
+import {
+ type ReactNode,
+ createContext,
+ useCallback,
+ useContext,
+ useMemo,
+ useRef,
+ useState,
+} from "react";
import type { KyooError } from "~/models";
import { ErrorView, errorHandlers } from "~/ui/errors";
@@ -8,41 +16,49 @@ type Error = {
retry?: () => void;
};
-const ErrorContext = createContext<{
- error: Error | null;
+const ErrorContext = createContext(null);
+const ErrorSetterContext = createContext<{
setError: (error: Error | null) => void;
-}>({ error: null, setError: () => {} });
+ clearError: (key: string) => void;
+}>(null!);
export const ErrorProvider = ({ children }: { children: ReactNode }) => {
const [error, setError] = useState(null);
+ const currentKey = useRef(error?.key);
+ currentKey.current = error?.key;
+ const clearError = useCallback((key: string) => {
+ if (key === currentKey.current) setError(null);
+ }, []);
+ const provider = useMemo(
+ () => ({
+ setError,
+ clearError,
+ }),
+ [clearError],
+ );
+
return (
-
- {children}
-
+
+ {children}
+
);
};
export const ErrorConsumer = ({ children, scope }: { children: ReactNode; scope: string }) => {
- const { error } = useContext(ErrorContext);
+ const error = useContext(ErrorContext);
if (!error) return children;
const handler = errorHandlers[error.key] ?? { view: ErrorView };
if (handler.forbid && handler.forbid !== scope) return children;
const Handler = handler.view;
- return ;
+ const { key, ...val } = error;
+ return ;
};
export const useSetError = (key: string) => {
- const { error, setError } = useContext(ErrorContext);
- const set = ({ key: nKey, ...obj }: Error & { key?: Error["key"] }) =>
+ const { setError, clearError } = useContext(ErrorSetterContext);
+ const set = ({ key: nKey, ...obj }: Omit & { key?: Error["key"] }) =>
setError({ key: nKey ?? key, ...obj });
- const clearError = () => {
- if (error?.key === key) setError(null);
- };
- return [set, clearError] as const;
+ const clear = () => clearError(key);
+ return [set, clear] as const;
};
diff --git a/front/src/query/fetch.tsx b/front/src/query/fetch.tsx
index e41ff10a..f73c6330 100644
--- a/front/src/query/fetch.tsx
+++ b/front/src/query/fetch.tsx
@@ -1,4 +1,4 @@
-import type { ReactElement } from "react";
+import { useLayoutEffect, type ReactElement } from "react";
import { useSetError } from "~/providers/error-provider";
import { ErrorView } from "~/ui/errors";
import { type QueryIdentifier, useFetch } from "./query";
@@ -15,15 +15,18 @@ export const Fetch = ({
const { data, isPaused, error } = useFetch(query);
const [setError] = useSetError("fetch");
- if (error) {
- if (error.status === 401 || error.status === 403) {
+ useLayoutEffect(() => {
+ if (isPaused) {
+ setError({ key: "offline" });
+ }
+ if (error && (error.status === 401 || error.status === 403)) {
setError({ key: "unauthorized", error });
}
+ }, [error, isPaused]);
+
+ if (error) {
return ;
}
- if (isPaused) {
- setError({ key: "offline" });
- }
if (!data) return ;
return ;
};
diff --git a/front/src/query/query.tsx b/front/src/query/query.tsx
index ace2d699..3cf6ae41 100644
--- a/front/src/query/query.tsx
+++ b/front/src/query/query.tsx
@@ -1,9 +1,12 @@
import { QueryClient, dehydrate, useInfiniteQuery, useQuery } from "@tanstack/react-query";
import { setServerData } from "one";
import { useContext } from "react";
+import { Platform } from "react-native";
import type { z } from "zod";
import { type KyooError, type Page, Paged } from "~/models";
-import { AccountContext, ssrApiUrl } from "~/providers/account-provider";
+import { AccountContext } from "~/providers/account-provider";
+
+const ssrApiUrl = process.env.KYOO_URL ?? "http://back/api";
const cleanSlash = (str: string | null, keepFirst = false) => {
if (!str) return null;
@@ -21,6 +24,8 @@ export const queryFn = async (context: {
parser?: Parser;
signal: AbortSignal;
}): Promise> => {
+ if (Platform.OS === "web" && typeof window === "undefined" && context.url.startsWith("/api"))
+ context.url = `${ssrApiUrl}/${context.url.substring(4)}`;
let resp: Response;
try {
resp = await fetch(context.url, {
@@ -55,11 +60,7 @@ export const queryFn = async (context: {
data = { message: error } as KyooError;
}
data.status = resp.status;
- console.trace(
- `Invalid response (${context.method ?? "GET"} ${context.url}):`,
- data,
- resp.status,
- );
+ console.log(`Invalid response (${context.method ?? "GET"} ${context.url}):`, data, resp.status);
throw data as KyooError;
}
@@ -199,39 +200,41 @@ export const prefetch = async (...queries: QueryIdentifier[]) => {
const authToken = undefined;
await Promise.all(
- queries.map((query) => {
- const key = toQueryKey({
- apiUrl: ssrApiUrl,
- path: query.path,
- params: query.params,
- });
+ queries
+ .filter((x) => x.enabled !== false)
+ .map((query) => {
+ const key = toQueryKey({
+ apiUrl: ssrApiUrl,
+ path: query.path,
+ params: query.params,
+ });
- if (query.infinite) {
- return client.prefetchInfiniteQuery({
+ if (query.infinite) {
+ return client.prefetchInfiniteQuery({
+ queryKey: key,
+ queryFn: (ctx) =>
+ queryFn({
+ url: key.join("/").replace("/?", "?"),
+ parser: Paged(query.parser),
+ signal: ctx.signal,
+ authToken: authToken?.access_token ?? null,
+ ...query.options,
+ }),
+ initialPageParam: undefined,
+ });
+ }
+ return client.prefetchQuery({
queryKey: key,
queryFn: (ctx) =>
queryFn({
url: key.join("/").replace("/?", "?"),
- parser: Paged(query.parser),
+ parser: query.parser,
signal: ctx.signal,
authToken: authToken?.access_token ?? null,
...query.options,
}),
- initialPageParam: undefined,
});
- }
- return client.prefetchQuery({
- queryKey: key,
- queryFn: (ctx) =>
- queryFn({
- url: key.join("/").replace("/?", "?"),
- parser: query.parser,
- signal: ctx.signal,
- authToken: authToken?.access_token ?? null,
- ...query.options,
- }),
- });
- }),
+ }),
);
setServerData("queryState", dehydrate(client));
return client;
diff --git a/front/src/ui/errors/unauthorized.tsx b/front/src/ui/errors/unauthorized.tsx
index 57f9069c..a7d4737c 100644
--- a/front/src/ui/errors/unauthorized.tsx
+++ b/front/src/ui/errors/unauthorized.tsx
@@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next";
import { View } from "react-native";
import { useYoshiki } from "yoshiki/native";
import { Button, Icon, Link, P, ts } from "~/primitives";
+import { useAccount } from "~/providers/account-provider";
export const Unauthorized = ({ missing }: { missing: string[] }) => {
const { t } = useTranslation();