mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-24 02:02:36 -04:00
160 lines
4.9 KiB
TypeScript
160 lines
4.9 KiB
TypeScript
/*
|
|
* 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 { ReactNode, createContext, useContext, useEffect, useMemo, useRef } from "react";
|
|
import { User, UserP } from "./resources";
|
|
import { z } from "zod";
|
|
import { zdate } from "./utils";
|
|
import { removeAccounts, setCookie, updateAccount } from "./account-internal";
|
|
import { useMMKVString } from "react-native-mmkv";
|
|
import { Platform } from "react-native";
|
|
import { useFetch } from "./query";
|
|
import { useQueryClient } from "@tanstack/react-query";
|
|
import { KyooErrors } from "./kyoo-errors";
|
|
|
|
export const TokenP = z.object({
|
|
token_type: z.literal("Bearer"),
|
|
access_token: z.string(),
|
|
refresh_token: z.string(),
|
|
expire_in: z.string(),
|
|
expire_at: zdate(),
|
|
});
|
|
export type Token = z.infer<typeof TokenP>;
|
|
|
|
export const AccountP = UserP.merge(
|
|
z.object({
|
|
// set it optional for accounts logged in before the kind was present
|
|
kind: z.literal("user").optional(),
|
|
token: TokenP,
|
|
apiUrl: z.string(),
|
|
selected: z.boolean(),
|
|
}),
|
|
);
|
|
export type Account = z.infer<typeof AccountP>;
|
|
|
|
const AccountContext = createContext<(Account & { select: () => void; remove: () => void })[]>([]);
|
|
export const ConnectionErrorContext = createContext<{
|
|
error: KyooErrors | null;
|
|
loading: boolean;
|
|
retry?: () => void;
|
|
}>({ error: null, loading: true });
|
|
|
|
/* eslint-disable react-hooks/rules-of-hooks */
|
|
export const AccountProvider = ({
|
|
children,
|
|
ssrAccount,
|
|
}: {
|
|
children: ReactNode;
|
|
ssrAccount?: Account;
|
|
}) => {
|
|
if (Platform.OS === "web" && typeof window === "undefined") {
|
|
const accs = ssrAccount
|
|
? [{ ...ssrAccount, selected: true, select: () => {}, remove: () => {} }]
|
|
: [];
|
|
return (
|
|
<AccountContext.Provider value={accs}>
|
|
<ConnectionErrorContext.Provider
|
|
value={{
|
|
error: null,
|
|
loading: false,
|
|
retry: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["auth", "me"] });
|
|
},
|
|
}}
|
|
>
|
|
{children}
|
|
</ConnectionErrorContext.Provider>
|
|
</AccountContext.Provider>
|
|
);
|
|
}
|
|
|
|
const [accStr] = useMMKVString("accounts");
|
|
const acc = accStr ? z.array(AccountP).parse(JSON.parse(accStr)) : null;
|
|
const accounts = useMemo(
|
|
() =>
|
|
acc?.map((account) => ({
|
|
...account,
|
|
select: () => updateAccount(account.id, { ...account, selected: true }),
|
|
remove: () => removeAccounts((x) => x.id == x.id),
|
|
})) ?? [],
|
|
[acc],
|
|
);
|
|
|
|
// update user's data from kyoo un startup, it could have changed.
|
|
const selected = useMemo(() => accounts.find((x) => x.selected), [accounts]);
|
|
const controller = useMemo(() => {
|
|
const ret = new AbortController();
|
|
setTimeout(() => ret.abort(), 5_000);
|
|
return ret;
|
|
}, [selected]);
|
|
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;
|
|
// The id is different when user is stale data, we need to wait for the use effect to invalidate the query.
|
|
if (user.data.id !== selected.id) return;
|
|
const nUser = { ...selected, ...user.data };
|
|
if (!Object.is(selected, nUser)) updateAccount(nUser.id, nUser);
|
|
}, [selected, user]);
|
|
|
|
const queryClient = useQueryClient();
|
|
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();
|
|
oldSelectedId.current = selected?.id;
|
|
|
|
// update cookies for ssr (needs to contains token, theme, language...)
|
|
if (Platform.OS === "web") setCookie("account", selected);
|
|
}, [selected, queryClient]);
|
|
|
|
return (
|
|
<AccountContext.Provider value={accounts}>
|
|
<ConnectionErrorContext.Provider
|
|
value={{
|
|
error: user.error,
|
|
loading: user.isLoading,
|
|
retry: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["auth", "me"] });
|
|
},
|
|
}}
|
|
>
|
|
{children}
|
|
</ConnectionErrorContext.Provider>
|
|
</AccountContext.Provider>
|
|
);
|
|
};
|
|
|
|
export const useAccount = () => {
|
|
const acc = useContext(AccountContext);
|
|
return acc.find((x) => x.selected) || null;
|
|
};
|
|
|
|
export const useAccounts = () => {
|
|
return useContext(AccountContext);
|
|
};
|