diff --git a/front/packages/models/src/login.ts b/front/packages/models/src/login.ts index cad71fd4..0bd89d1f 100644 --- a/front/packages/models/src/login.ts +++ b/front/packages/models/src/login.ts @@ -19,7 +19,7 @@ */ import { z } from "zod"; -import { getSecureItem, setSecureItem } from "./secure-store"; +import { deleteSecureItem, getSecureItem, setSecureItem } from "./secure-store"; import { zdate } from "./utils"; import { queryFn } from "./query"; import { KyooErrors } from "./kyoo-errors"; @@ -79,3 +79,7 @@ export const getTokenWJ = async (cookies?: string): Promise<[string, Token] | [n export const getToken = async (cookies?: string): Promise => (await getTokenWJ(cookies))[0] + +export const logout = async () =>{ + deleteSecureItem("auth") +} diff --git a/front/packages/models/src/secure-store.ts b/front/packages/models/src/secure-store.ts index 03c9aa26..a4fd300d 100644 --- a/front/packages/models/src/secure-store.ts +++ b/front/packages/models/src/secure-store.ts @@ -18,4 +18,8 @@ * along with Kyoo. If not, see . */ -export { setItemAsync as setSecureItem, getItemAsync as getSecureItem } from "expo-secure-store"; +export { + setItemAsync as setSecureItem, + deleteItemAsync as deleteSecureItem, + getItemAsync as getSecureItem, +} from "expo-secure-store"; diff --git a/front/packages/models/src/secure-store.web.ts b/front/packages/models/src/secure-store.web.ts index 3a45da84..7a832104 100644 --- a/front/packages/models/src/secure-store.web.ts +++ b/front/packages/models/src/secure-store.web.ts @@ -18,7 +18,7 @@ * along with Kyoo. If not, see . */ -export const setSecureItemSync = (key: string, value?: string) => { +export const setSecureItemSync = (key: string, value: string | null) => { const d = new Date(); // A year d.setTime(d.getTime() + 365 * 24 * 60 * 60 * 1000); @@ -27,9 +27,11 @@ export const setSecureItemSync = (key: string, value?: string) => { return null; }; -export const setSecureItem = async (key: string, value: string): Promise => +export const setSecureItem = async (key: string, value: string | null): Promise => setSecureItemSync(key, value); +export const deleteSecureItem = async (key: string) => setSecureItem(key, null); + export const getSecureItem = async (key: string, cookies?: string): Promise => { // Don't try to use document's cookies on SSR. if (!cookies && typeof window === "undefined") return null; diff --git a/front/packages/primitives/src/avatar.tsx b/front/packages/primitives/src/avatar.tsx index d746e2b9..a9496a81 100644 --- a/front/packages/primitives/src/avatar.tsx +++ b/front/packages/primitives/src/avatar.tsx @@ -25,31 +25,30 @@ import { Icon } from "./icons"; import AccountCircle from "@material-symbols/svg-400/rounded/account_circle-fill.svg"; import { YoshikiStyle } from "yoshiki/dist/type"; import { P } from "@expo/html-elements"; +import { forwardRef } from "react"; -export const Avatar = ({ - src, - alt, - size = px(24), - color, - placeholder, - isLoading = false, - fill = false, - ...props -}: { - src?: string | null; - alt?: string; - size?: YoshikiStyle; - placeholder?: string; - color?: string; - isLoading?: boolean; - fill?: boolean; -} & Stylable) => { +export const Avatar = forwardRef< + View, + { + src?: string | null; + alt?: string; + size?: YoshikiStyle; + placeholder?: string; + color?: string; + isLoading?: boolean; + fill?: boolean; + } & Stylable +>(function _Avatar( + { src, alt, size = px(24), color, placeholder, isLoading = false, fill = false, ...props }, + ref, +) { const { css, theme } = useYoshiki(); const col = color ?? theme.overlay0; // TODO: Support dark themes when fill === true return ( ); -}; +}); diff --git a/front/packages/primitives/src/menu.tsx b/front/packages/primitives/src/menu.tsx index 0bdeb5b2..9c4a509c 100644 --- a/front/packages/primitives/src/menu.tsx +++ b/front/packages/primitives/src/menu.tsx @@ -30,20 +30,21 @@ import { P } from "./text"; import { ContrastArea } from "./themes"; import { ts } from "./utils"; import Check from "@material-symbols/svg-400/rounded/check-fill.svg"; +import { useRouter } from "solito/router"; const MenuContext = createContext<((open: boolean) => void) | undefined>(undefined); const Menu = ({ - Triger, + Trigger, onMenuOpen, onMenuClose, children, ...props }: { - Triger: ComponentType; + Trigger: ComponentType; children: ReactNode | ReactNode[] | null; - onMenuOpen: () => void; - onMenuClose: () => void; + onMenuOpen?: () => void; + onMenuClose?: () => void; } & Omit) => { const [isOpen, setOpen] = useState(false); @@ -55,7 +56,7 @@ const Menu = ({ return ( <> {/* @ts-ignore */} - setOpen(true)} {...props} /> + setOpen(true)} {...props} /> {isOpen && ( @@ -112,20 +113,22 @@ const MenuItem = ({ label, selected, onSelect, + href, ...props }: { label: string; selected?: boolean; - onSelect: () => void; -}) => { +} & ({ onSelect: () => void; href?: undefined } | { href: string; onSelect?: undefined })) => { const { css, theme } = useYoshiki(); const setOpen = useContext(MenuContext); + const router = useRouter(); return ( { setOpen?.call(null, false); onSelect?.call(null); + if (href) router.push(href); }} {...css( { diff --git a/front/packages/primitives/src/menu.web.tsx b/front/packages/primitives/src/menu.web.tsx index d3c26e07..fd539f8a 100644 --- a/front/packages/primitives/src/menu.web.tsx +++ b/front/packages/primitives/src/menu.web.tsx @@ -19,7 +19,8 @@ */ import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; -import { ComponentType, forwardRef, ReactNode } from "react"; +import { ComponentProps, ComponentType, forwardRef, ReactNode } from "react"; +import Link from "next/link"; import { PressableProps } from "react-native"; import { useYoshiki } from "yoshiki/web"; import { px, useYoshiki as useNativeYoshiki } from "yoshiki/native"; @@ -43,16 +44,16 @@ const InternalTriger = forwardRef(function _Triger( }); const Menu = ({ - Triger, + Trigger, onMenuOpen, onMenuClose, children, ...props }: { - Triger: ComponentType; + Trigger: ComponentType; children: ReactNode | ReactNode[] | null; - onMenuOpen: () => void; - onMenuClose: () => void; + onMenuOpen?: () => void; + onMenuClose?: () => void; } & Omit) => { return ( ({ }} > - + @@ -90,18 +91,38 @@ const Menu = ({ ); }; +const Item = forwardRef< + HTMLDivElement, + ComponentProps & { href?: string } +>(function _Item({ children, href, ...props }, ref) { + if (href) { + return ( + + + {children} + + + ); + } + return ( + + {children} + + ); +}); + const MenuItem = ({ label, icon, selected, onSelect, + href, ...props }: { label: string; icon?: JSX.Element; selected?: boolean; - onSelect: () => void; -}) => { +} & ({ onSelect: () => void; href?: undefined } | { href: string; onSelect?: undefined })) => { const { css: nCss } = useNativeYoshiki(); const { css, theme } = useYoshiki(); @@ -112,8 +133,9 @@ const MenuItem = ({ background: ${theme.alternate.accent}; } `} - theme.paragraph, focus: { boxShadow: "none", }, @@ -138,8 +159,8 @@ const MenuItem = ({ {...nCss({ paddingRight: px(8) })} /> )} - {

{label}

} -
+ {

{label}

} + ); }; diff --git a/front/packages/ui/src/login/login.tsx b/front/packages/ui/src/login/login.tsx index 49f2b04f..0ffa7adf 100644 --- a/front/packages/ui/src/login/login.tsx +++ b/front/packages/ui/src/login/login.tsx @@ -29,6 +29,7 @@ import { percent, px, useYoshiki } from "yoshiki/native"; import { DefaultLayout } from "../layout"; import { FormPage } from "./form"; import { PasswordInput } from "./password-input"; +import { useQueryClient } from "@tanstack/react-query"; export const LoginPage: QueryPage = () => { const [username, setUsername] = useState(""); @@ -36,6 +37,7 @@ export const LoginPage: QueryPage = () => { const [error, setError] = useState(undefined); const router = useRouter(); + const queryClient = useQueryClient(); const { t } = useTranslation(); const { css } = useYoshiki(); @@ -66,7 +68,9 @@ export const LoginPage: QueryPage = () => { onPress={async () => { const { error } = await loginFunc("login", { username, password }); setError(error); - if (!error) router.push("/"); + if (error) return; + queryClient.invalidateQueries(["auth", "me"]); + router.push("/"); }} {...css({ m: ts(1), diff --git a/front/packages/ui/src/login/register.tsx b/front/packages/ui/src/login/register.tsx index 5ad54e0d..5cb61564 100644 --- a/front/packages/ui/src/login/register.tsx +++ b/front/packages/ui/src/login/register.tsx @@ -29,6 +29,7 @@ import { percent, px, useYoshiki } from "yoshiki/native"; import { DefaultLayout } from "../layout"; import { FormPage } from "./form"; import { PasswordInput } from "./password-input"; +import { useQueryClient } from "@tanstack/react-query"; export const RegisterPage: QueryPage = () => { const [email, setEmail] = useState(""); @@ -38,6 +39,7 @@ export const RegisterPage: QueryPage = () => { const [error, setError] = useState(undefined); const router = useRouter(); + const queryClient = useQueryClient(); const { t } = useTranslation(); const { css } = useYoshiki(); @@ -81,7 +83,9 @@ export const RegisterPage: QueryPage = () => { onPress={async () => { const { error } = await loginFunc("register", { email, username, password }); setError(error); - if (!error) router.push("/"); + if (error) return; + queryClient.invalidateQueries(["auth", "me"]); + router.push("/"); }} {...css({ m: ts(1), diff --git a/front/packages/ui/src/navbar/index.tsx b/front/packages/ui/src/navbar/index.tsx index bc84c38e..bee2f84f 100644 --- a/front/packages/ui/src/navbar/index.tsx +++ b/front/packages/ui/src/navbar/index.tsx @@ -18,7 +18,7 @@ * along with Kyoo. If not, see . */ -import { Library, LibraryP, Page, Paged, QueryIdentifier, User, UserP } from "@kyoo/models"; +import { Library, LibraryP, logout, Page, Paged, QueryIdentifier, User, UserP } from "@kyoo/models"; import { Input, IconButton, @@ -29,17 +29,19 @@ import { tooltip, ts, Link, + Menu, } from "@kyoo/primitives"; import { Platform, TextInput, View, ViewProps } from "react-native"; import { useTranslation } from "react-i18next"; import { createParam } from "solito"; import { useRouter } from "solito/router"; import { rem, Stylable, useYoshiki } from "yoshiki/native"; -import Menu from "@material-symbols/svg-400/rounded/menu-fill.svg"; +import MenuIcon from "@material-symbols/svg-400/rounded/menu-fill.svg"; import Search from "@material-symbols/svg-400/rounded/search-fill.svg"; import { Fetch, FetchNE } from "../fetch"; import { KyooLongLogo } from "./icon"; import { forwardRef, useRef, useState } from "react"; +import { useQueryClient } from "@tanstack/react-query"; export const NavbarTitle = (props: Stylable & { onLayout?: ViewProps["onLayout"] }) => { const { t } = useTranslation(); @@ -90,23 +92,37 @@ export const MeQuery: QueryIdentifier = { export const NavbarProfile = () => { const { css, theme } = useYoshiki(); const { t } = useTranslation(); + const queryClient = useQueryClient(); - // TODO: show logged in user. return ( - {({ username }) => ( - ( + - - + {isGuest ? ( + <> + + + + ) : ( + <> + { + logout(); + queryClient.invalidateQueries(["auth", "me"]); + }} + /> + + )} + )} ); @@ -175,13 +191,12 @@ export const Navbar = (props: Stylable) => { shadowOpacity: 0.3, shadowRadius: 4.65, elevation: 8, - zIndex: 1, }, props, )} > {subtitles && (