Skip to content

Commit

Permalink
chore: optimize login state (#17)
Browse files Browse the repository at this point in the history
  • Loading branch information
xc2 authored Oct 15, 2024
1 parent d587e26 commit cec45d9
Show file tree
Hide file tree
Showing 14 changed files with 310 additions and 81 deletions.
14 changes: 14 additions & 0 deletions src/backend/app.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { inboxesAsyncAtom, inboxesAtom, settingsAtom } from "@/backend/atoms";
import { authStateAsyncAtom, authStateAtom } from "@/backend/atoms/follow-client";
import { container } from "@/backend/container";
import { sendToInbox } from "@/backend/inbox";
import { baseUrl } from "@/gen/internal";
Expand Down Expand Up @@ -46,3 +47,16 @@ app.openapi(r.RefreshInboxes, async (c) => {
container.set(inboxesAsyncAtom);
return c.json(await container.get(inboxesAsyncAtom), 200);
});

app.openapi(r.GetAuthState, async (c) => {
let authState = container.get(authStateAtom);
let needRefresh = false;
if (authState === false) {
container.set(inboxesAsyncAtom);
authState = await container.get(authStateAsyncAtom);
needRefresh = authState === true;
} else if (authState === undefined) {
authState = await container.get(authStateAsyncAtom);
}
return c.json({ logged: authState, changed: needRefresh }, 200);
});
18 changes: 18 additions & 0 deletions src/backend/atoms/follow-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { inboxesAsyncAtom } from "@/backend/atoms/inboxes";
import { isFollowError } from "@/lib/follow";
import { atom } from "jotai";
import { unwrap } from "jotai/utils";

export const authStateAsyncAtom = atom(async (get) => {
try {
await get(inboxesAsyncAtom);
return true;
} catch (e) {
if (isFollowError(e) && e.name === "AuthError") {
return false;
}
return true;
}
});

export const authStateAtom = unwrap(authStateAsyncAtom, (prev) => prev);
7 changes: 4 additions & 3 deletions src/backend/atoms/inboxes.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { asyncAtom } from "@/lib/jotai";
import { follow, handleFollowResult } from "@/services/follow";
import { atom } from "jotai";
import { atomWithRefresh, selectAtom, unwrap } from "jotai/utils";
import { selectAtom } from "jotai/utils";
import { settingsAtom } from "./settings";

export const inboxesAsyncAtom = atomWithRefresh(async () => {
export const inboxesAsyncAtom = asyncAtom(async () => {
const r = await follow.GET("/inboxes/list");
return handleFollowResult(r);
});

export const inboxesAtom = unwrap(inboxesAsyncAtom, (prev) => prev);
export const inboxesAtom = inboxesAsyncAtom.unwrap();
export const defaultInboxIdAtom = selectAtom(settingsAtom, (s) => s.DefaultInbox);

export const defaultInboxAtom = atom((get) => {
Expand Down
7 changes: 3 additions & 4 deletions src/backend/container.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { inboxesAtom, settingsAtom } from "@/backend/atoms";
import { settingsAtom } from "@/backend/atoms";
import { authStateAtom } from "@/backend/atoms/follow-client";
import { createStore } from "jotai";

export const container = createStore();
container.sub(settingsAtom, () => {});

container.sub(inboxesAtom, () => {
console.log("inboxesAtom changed", container.get(inboxesAtom));
});
container.sub(authStateAtom, () => {});
5 changes: 5 additions & 0 deletions src/backend/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ export const RefreshInboxes = createRoute({
path: "/inboxes",
responses: res(z.array(InboxItemSchema)),
});
export const GetAuthState = createRoute({
method: "get",
path: "/auth-state",
responses: res(z.object({ logged: z.boolean(), changed: z.boolean().optional() })),
});

function res<T extends ZodType<unknown>>(schema: T) {
return {
Expand Down
102 changes: 102 additions & 0 deletions src/gen/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,108 @@
export const baseUrl = "/internal";

export interface paths {
"/auth-state": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
logged: boolean;
changed?: boolean;
};
};
};
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
/** @enum {boolean} */
ok: false;
message: string;
name?: string;
};
};
};
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
/** @enum {boolean} */
ok: false;
message: string;
name?: string;
};
};
};
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
/** @enum {boolean} */
ok: false;
message: string;
name?: string;
};
};
};
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
/** @enum {boolean} */
ok: false;
message: string;
name?: string;
};
};
};
500: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
/** @enum {boolean} */
ok: false;
message: string;
name?: string;
};
};
};
};
};
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/inboxes": {
parameters: {
query?: never;
Expand Down
30 changes: 30 additions & 0 deletions src/lib/follow.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { baseUrl, type paths } from "@/gen/follow";
import { destructPromise } from "@/lib/lang";
import createClient, { type ClientOptions, type FetchResponse } from "openapi-fetch";

export interface FollowError extends Error {
response: Response;
Expand Down Expand Up @@ -45,3 +47,31 @@ export async function handleFollowResponse<T>(res: Response | PromiseLike<Respon
}
return data;
}

export function createFollowClient(clientOptions?: ClientOptions) {
const mani = chrome.runtime.getManifest();
return createClient<paths>({
baseUrl,
mode: "cors",
credentials: "include",
...clientOptions,
headers: {
"X-User-Agent": `${mani.name.replace(/ +/g, "-")}/${mani.version} (+${mani.homepage_url})`,
...clientOptions?.headers,
},
});
}
type ExtractData<T> = T extends { data: infer K } ? K : never;
export async function handleFollowResult<T extends FetchResponse<any, any, any>>(
res: T | PromiseLike<T>
): Promise<ExtractData<T["data"]>> {
res = await res;
const data = handleFollowData<ExtractData<T["data"]>>(res.data, res.response);
if (res.error) {
throw makeHttpError(res.response, {
name: "JSONError",
message: "Cannot decode the response message.",
});
}
return data;
}
29 changes: 29 additions & 0 deletions src/lib/jotai.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { addProperties } from "@/lib/lang";
import { atomWithRefresh, loadable as jotaiLoadable, selectAtom } from "jotai/utils";
import type { Atom, WritableAtom } from "jotai/vanilla";
import type { Loadable } from "jotai/vanilla/utils/loadable";

type Read<Value, Args extends unknown[], Result> = WritableAtom<Value, Args, Result>["read"];

export function asyncAtom<Value>(read: Read<Value, [], void>) {
const baseAtom = atomWithRefresh(read);
let loadableAtom: Atom<Loadable<Value>>;
let unwrappedAtom: Atom<Awaited<Value> | undefined>;
return addProperties(baseAtom, { loadable, unwrap });

function loadable() {
loadableAtom = loadableAtom || jotaiLoadable(baseAtom);
return loadableAtom;
}

function unwrap() {
unwrappedAtom =
unwrappedAtom ||
selectAtom(loadable(), (s, prevSlice) => {
if (s.state === "loading") return prevSlice;
if (s.state === "hasData") return s.data;
return undefined;
});
return unwrappedAtom;
}
}
10 changes: 10 additions & 0 deletions src/lib/use-last-swr-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { useRef } from "react";
import type { SWRResponse } from "swr";

export function useLastSWRData<T>(swr: SWRResponse<T>): T | undefined {
const ref = useRef(swr.data);
if (!swr.isLoading) {
ref.current = swr.data;
}
return ref.current;
}
20 changes: 6 additions & 14 deletions src/pages/popup.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,24 @@
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Card } from "@/components/ui/card";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { chainProviders } from "@/lib/providers";
import { Pencil2Icon } from "@radix-ui/react-icons";
import { LazyMotion, MotionConfig, domMax } from "framer-motion";
import { SendScreen, SendScreenActions } from "./screens/send-screen";
import { SWRConfig } from "swr";
import { SendScreen } from "./screens/send-screen";

export function Popup() {
const config = chainProviders(
(c) => <TooltipProvider>{c}</TooltipProvider>,
(c) => <MotionConfig transition={{ duration: 0.15 }}>{c}</MotionConfig>,
(c) => <LazyMotion features={domMax}>{c}</LazyMotion>
(c) => <LazyMotion features={domMax}>{c}</LazyMotion>,
(c) => <SWRConfig value={{ errorRetryCount: 0 }}>{c}</SWRConfig>
);

return config(
<div className="min-w-96">
<Card className="border-none rounded-none shadow-none">
<CardHeader>
<div className="flex items-center">
<CardTitle className="text-xl">Follow it later</CardTitle>
<div className="ml-auto">
<SendScreenActions />
</div>
</div>
</CardHeader>
<CardContent>
<SendScreen />
</CardContent>
<SendScreen />
</Card>
<div className="flex border-t-[1px] border-zinc-50 py-2">
<div className="min-w-0 flex-initial text-zinc-500 pl-2">
Expand Down
Loading

0 comments on commit cec45d9

Please sign in to comment.