mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2025-10-07 18:46:38 +08:00
chore: inline useLocalStorage
This commit is contained in:
parent
4c84d63ff4
commit
a0f11bff31
@ -48,7 +48,7 @@ import { Updater } from "../typing";
|
|||||||
import { ModelConfigList } from "./model-config";
|
import { ModelConfigList } from "./model-config";
|
||||||
import { FileName, Path } from "../constant";
|
import { FileName, Path } from "../constant";
|
||||||
import { BUILTIN_MASK_STORE } from "../masks";
|
import { BUILTIN_MASK_STORE } from "../masks";
|
||||||
import { useLocalStorage } from "foxact/use-local-storage";
|
import { useLocalStorage } from "../utils";
|
||||||
import {
|
import {
|
||||||
DragDropContext,
|
DragDropContext,
|
||||||
Droppable,
|
Droppable,
|
||||||
|
305
app/utils.ts
305
app/utils.ts
@ -1,4 +1,11 @@
|
|||||||
import { useEffect, useState } from "react";
|
import {
|
||||||
|
useEffect,
|
||||||
|
useLayoutEffect,
|
||||||
|
useState,
|
||||||
|
useSyncExternalStore,
|
||||||
|
useCallback,
|
||||||
|
useMemo,
|
||||||
|
} from "react";
|
||||||
import { showToast } from "./components/ui-lib";
|
import { showToast } from "./components/ui-lib";
|
||||||
import Locale from "./locales";
|
import Locale from "./locales";
|
||||||
import { RequestMessage } from "./client/api";
|
import { RequestMessage } from "./client/api";
|
||||||
@ -318,3 +325,299 @@ export function adapter(config: Record<string, unknown>) {
|
|||||||
: path;
|
: path;
|
||||||
return fetch(fetchUrl as string, { ...rest, responseType: "text" });
|
return fetch(fetchUrl as string, { ...rest, responseType: "text" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copyright 2024 Sukka (https://skk.moe) and the contributors of foxact (https://foxact.skk.moe)
|
||||||
|
* Licensed under the MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
const useIsomorphicLayoutEffect =
|
||||||
|
typeof window !== "undefined" ? useLayoutEffect : useEffect;
|
||||||
|
|
||||||
|
const noop = () => {};
|
||||||
|
|
||||||
|
const stlProp = Object.getOwnPropertyDescriptor(Error, "stackTraceLimit");
|
||||||
|
const hasSTL = stlProp?.writable && typeof stlProp.value === "number";
|
||||||
|
const noSSRError = (
|
||||||
|
errorMessage?: string | undefined,
|
||||||
|
nextjsDigest = "BAILOUT_TO_CLIENT_SIDE_RENDERING",
|
||||||
|
) => {
|
||||||
|
const originalStackTraceLimit = Error.stackTraceLimit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is *only* safe to do when we know that nothing at any point in the
|
||||||
|
* stack relies on the `Error.stack` property of the noSSRError. By removing
|
||||||
|
* the strack trace of the error, we can improve the performance of object
|
||||||
|
* creation by a lot.
|
||||||
|
*/
|
||||||
|
if (hasSTL) {
|
||||||
|
Error.stackTraceLimit = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const error = new Error(errorMessage);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore the stack trace limit to its original value after the error has
|
||||||
|
* been created.
|
||||||
|
*/
|
||||||
|
if (hasSTL) {
|
||||||
|
Error.stackTraceLimit = originalStackTraceLimit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next.js marks errors with `NEXT_DYNAMIC_NO_SSR_CODE` digest as recoverable:
|
||||||
|
// https://github.com/vercel/next.js/blob/bef716ad031591bdf94058aaf4b8d842e75900b5/packages/next/src/shared/lib/lazy-dynamic/bailout-to-csr.ts#L2
|
||||||
|
(error as any).digest = nextjsDigest;
|
||||||
|
|
||||||
|
(error as any).recoverableError = "NO_SSR";
|
||||||
|
|
||||||
|
return error;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StorageType = "localStorage" | "sessionStorage";
|
||||||
|
type NotUndefined<T> = T extends undefined ? never : T;
|
||||||
|
|
||||||
|
// StorageEvent is deliberately not fired on the same document, we do not want to change that
|
||||||
|
type CustomStorageEvent = CustomEvent<string>;
|
||||||
|
declare global {
|
||||||
|
interface WindowEventMap {
|
||||||
|
"foxact-use-local-storage": CustomStorageEvent;
|
||||||
|
"foxact-use-session-storage": CustomStorageEvent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Serializer<T> = (value: T) => string;
|
||||||
|
export type Deserializer<T> = (value: string) => T;
|
||||||
|
|
||||||
|
// This type utility is only used for workaround https://github.com/microsoft/TypeScript/issues/37663
|
||||||
|
const isFunction = (x: unknown): x is Function => typeof x === "function";
|
||||||
|
|
||||||
|
const identity = (x: any) => x;
|
||||||
|
|
||||||
|
export interface UseStorageRawOption {
|
||||||
|
raw: true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseStorageParserOption<T> {
|
||||||
|
raw?: false;
|
||||||
|
serializer: Serializer<T>;
|
||||||
|
deserializer: Deserializer<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getServerSnapshotWithoutServerValue = () => {
|
||||||
|
throw noSSRError(
|
||||||
|
"useLocalStorage cannot be used on the server without a serverValue",
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function createStorage(type: StorageType) {
|
||||||
|
const FOXACT_LOCAL_STORAGE_EVENT_KEY =
|
||||||
|
type === "localStorage"
|
||||||
|
? "foxact-use-local-storage"
|
||||||
|
: "foxact-use-session-storage";
|
||||||
|
|
||||||
|
const foxactHookName =
|
||||||
|
type === "localStorage"
|
||||||
|
? "foxact/use-local-storage"
|
||||||
|
: "foxact/use-session-storage";
|
||||||
|
|
||||||
|
const dispatchStorageEvent =
|
||||||
|
typeof window !== "undefined"
|
||||||
|
? (key: string) => {
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent<string>(FOXACT_LOCAL_STORAGE_EVENT_KEY, {
|
||||||
|
detail: key,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
: noop;
|
||||||
|
|
||||||
|
const setStorageItem =
|
||||||
|
typeof window !== "undefined"
|
||||||
|
? (key: string, value: string) => {
|
||||||
|
try {
|
||||||
|
window[type].setItem(key, value);
|
||||||
|
} catch {
|
||||||
|
console.warn(
|
||||||
|
`[${foxactHookName}] Failed to set value to ${type}, it might be blocked`,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
dispatchStorageEvent(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: noop;
|
||||||
|
|
||||||
|
const removeStorageItem =
|
||||||
|
typeof window !== "undefined"
|
||||||
|
? (key: string) => {
|
||||||
|
try {
|
||||||
|
window[type].removeItem(key);
|
||||||
|
} catch {
|
||||||
|
console.warn(
|
||||||
|
`[${foxactHookName}] Failed to remove value from ${type}, it might be blocked`,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
dispatchStorageEvent(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: noop;
|
||||||
|
|
||||||
|
const getStorageItem = (key: string) => {
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return window[type].getItem(key);
|
||||||
|
} catch {
|
||||||
|
console.warn(
|
||||||
|
`[${foxactHookName}] Failed to get value from ${type}, it might be blocked`,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const useSetStorage = <T>(key: string, serializer: Serializer<T>) =>
|
||||||
|
useCallback(
|
||||||
|
(v: T | null) => {
|
||||||
|
try {
|
||||||
|
if (v === null) {
|
||||||
|
removeStorageItem(key);
|
||||||
|
} else {
|
||||||
|
setStorageItem(key, serializer(v));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[key, serializer],
|
||||||
|
);
|
||||||
|
|
||||||
|
// ssr compatible
|
||||||
|
function useStorage<T>(
|
||||||
|
key: string,
|
||||||
|
serverValue: NotUndefined<T>,
|
||||||
|
options?: UseStorageRawOption | UseStorageParserOption<T>,
|
||||||
|
): readonly [T, React.Dispatch<React.SetStateAction<T | null>>];
|
||||||
|
// client-render only
|
||||||
|
function useStorage<T>(
|
||||||
|
key: string,
|
||||||
|
serverValue?: undefined,
|
||||||
|
options?: UseStorageRawOption | UseStorageParserOption<T>,
|
||||||
|
): readonly [T | null, React.Dispatch<React.SetStateAction<T | null>>];
|
||||||
|
function useStorage<T>(
|
||||||
|
key: string,
|
||||||
|
serverValue?: NotUndefined<T> | undefined,
|
||||||
|
options: UseStorageRawOption | UseStorageParserOption<T> = {
|
||||||
|
serializer: JSON.stringify,
|
||||||
|
deserializer: JSON.parse,
|
||||||
|
},
|
||||||
|
):
|
||||||
|
| readonly [T | null, React.Dispatch<React.SetStateAction<T | null>>]
|
||||||
|
| readonly [T, React.Dispatch<React.SetStateAction<T | null>>] {
|
||||||
|
const subscribeToSpecificKeyOfLocalStorage = useCallback(
|
||||||
|
(callback: () => void) => {
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
return noop;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleStorageEvent = (e: StorageEvent) => {
|
||||||
|
if (
|
||||||
|
!("key" in e) || // Some browsers' strange quirk where StorageEvent is missing key
|
||||||
|
e.key === key
|
||||||
|
) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleCustomStorageEvent = (e: CustomStorageEvent) => {
|
||||||
|
if (e.detail === key) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("storage", handleStorageEvent);
|
||||||
|
window.addEventListener(
|
||||||
|
FOXACT_LOCAL_STORAGE_EVENT_KEY,
|
||||||
|
handleCustomStorageEvent,
|
||||||
|
);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("storage", handleStorageEvent);
|
||||||
|
window.removeEventListener(
|
||||||
|
FOXACT_LOCAL_STORAGE_EVENT_KEY,
|
||||||
|
handleCustomStorageEvent,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[key],
|
||||||
|
);
|
||||||
|
|
||||||
|
const serializer: Serializer<T> = options.raw
|
||||||
|
? identity
|
||||||
|
: options.serializer;
|
||||||
|
const deserializer: Deserializer<T> = options.raw
|
||||||
|
? identity
|
||||||
|
: options.deserializer;
|
||||||
|
|
||||||
|
const getClientSnapshot = () => getStorageItem(key);
|
||||||
|
|
||||||
|
// If the serverValue is provided, we pass it to useSES' getServerSnapshot, which will be used during SSR
|
||||||
|
// If the serverValue is not provided, we don't pass it to useSES, which will cause useSES to opt-in client-side rendering
|
||||||
|
const getServerSnapshot =
|
||||||
|
serverValue !== undefined
|
||||||
|
? () => serializer(serverValue)
|
||||||
|
: getServerSnapshotWithoutServerValue;
|
||||||
|
|
||||||
|
const store = useSyncExternalStore(
|
||||||
|
subscribeToSpecificKeyOfLocalStorage,
|
||||||
|
getClientSnapshot,
|
||||||
|
getServerSnapshot,
|
||||||
|
);
|
||||||
|
|
||||||
|
const deserialized = useMemo(
|
||||||
|
() => (store === null ? null : deserializer(store)),
|
||||||
|
[store, deserializer],
|
||||||
|
);
|
||||||
|
|
||||||
|
const setState = useCallback<
|
||||||
|
React.Dispatch<React.SetStateAction<T | null>>
|
||||||
|
>(
|
||||||
|
(v) => {
|
||||||
|
try {
|
||||||
|
const nextState = isFunction(v) ? v(deserialized ?? null) : v;
|
||||||
|
|
||||||
|
if (nextState === null) {
|
||||||
|
removeStorageItem(key);
|
||||||
|
} else {
|
||||||
|
setStorageItem(key, serializer(nextState));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[key, serializer, deserialized],
|
||||||
|
);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (getStorageItem(key) === null && serverValue !== undefined) {
|
||||||
|
setStorageItem(key, serializer(serverValue));
|
||||||
|
}
|
||||||
|
}, [deserializer, key, serializer, serverValue]);
|
||||||
|
|
||||||
|
const finalValue: T | null =
|
||||||
|
deserialized === null
|
||||||
|
? // storage doesn't have value
|
||||||
|
serverValue === undefined
|
||||||
|
? // no default value provided
|
||||||
|
null
|
||||||
|
: (serverValue satisfies NotUndefined<T>)
|
||||||
|
: // storage has value
|
||||||
|
(deserialized satisfies T);
|
||||||
|
|
||||||
|
return [finalValue, setState] as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
useStorage,
|
||||||
|
useSetStorage,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useLocalStorage = createStorage("localStorage").useStorage;
|
||||||
|
@ -26,7 +26,6 @@
|
|||||||
"@vercel/speed-insights": "^1.0.2",
|
"@vercel/speed-insights": "^1.0.2",
|
||||||
"axios": "^1.7.5",
|
"axios": "^1.7.5",
|
||||||
"emoji-picker-react": "^4.9.2",
|
"emoji-picker-react": "^4.9.2",
|
||||||
"foxact": "^0.2.37",
|
|
||||||
"fuse.js": "^7.0.0",
|
"fuse.js": "^7.0.0",
|
||||||
"heic2any": "^0.0.4",
|
"heic2any": "^0.0.4",
|
||||||
"html-to-image": "^1.11.11",
|
"html-to-image": "^1.11.11",
|
||||||
|
15
yarn.lock
15
yarn.lock
@ -2378,7 +2378,7 @@ cli-truncate@^3.1.0:
|
|||||||
slice-ansi "^5.0.0"
|
slice-ansi "^5.0.0"
|
||||||
string-width "^5.0.0"
|
string-width "^5.0.0"
|
||||||
|
|
||||||
client-only@0.0.1, client-only@^0.0.1:
|
client-only@0.0.1:
|
||||||
version "0.0.1"
|
version "0.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"
|
resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"
|
||||||
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
|
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
|
||||||
@ -3627,14 +3627,6 @@ formdata-polyfill@^4.0.10:
|
|||||||
dependencies:
|
dependencies:
|
||||||
fetch-blob "^3.1.2"
|
fetch-blob "^3.1.2"
|
||||||
|
|
||||||
foxact@^0.2.37:
|
|
||||||
version "0.2.37"
|
|
||||||
resolved "https://registry.yarnpkg.com/foxact/-/foxact-0.2.37.tgz#9af5c8b56c96bb70c84760e79c8e949a6fa28f04"
|
|
||||||
integrity sha512-nzK7n3JAnmCWO3GJXe4ry196s7wlOUnJVBn/RQ9Og1FJ/pEaRi94atLGFmzM0CVDgIbSFdMnH49PtkWjSqtMSw==
|
|
||||||
dependencies:
|
|
||||||
client-only "^0.0.1"
|
|
||||||
server-only "^0.0.1"
|
|
||||||
|
|
||||||
fs.realpath@^1.0.0:
|
fs.realpath@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
|
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
|
||||||
@ -5786,11 +5778,6 @@ serialize-javascript@^6.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
randombytes "^2.1.0"
|
randombytes "^2.1.0"
|
||||||
|
|
||||||
server-only@^0.0.1:
|
|
||||||
version "0.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/server-only/-/server-only-0.0.1.tgz#0f366bb6afb618c37c9255a314535dc412cd1c9e"
|
|
||||||
integrity sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==
|
|
||||||
|
|
||||||
shebang-command@^2.0.0:
|
shebang-command@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
|
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
|
||||||
|
Loading…
Reference in New Issue
Block a user