mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-12-06 21:44:13 +03:00
add new hooks
This commit is contained in:
parent
97b29d177a
commit
2cb94f173f
@ -1,6 +1,8 @@
|
|||||||
import { useCallback, useEffect, useMemo } from "react";
|
import { useCallback, useContext, useEffect, useMemo } from "react";
|
||||||
import { useLocation, useNavigate, useSearchParams } from "react-router-dom";
|
import { useLocation, useNavigate, useSearchParams } from "react-router-dom";
|
||||||
import { usePersistence } from "./use-persistence";
|
import { usePersistence } from "./use-persistence";
|
||||||
|
import { useUserPersistence } from "./use-user-persistence";
|
||||||
|
import { AuthContext } from "@/context/auth-context";
|
||||||
|
|
||||||
export function useOverlayState<S>(
|
export function useOverlayState<S>(
|
||||||
key: string,
|
key: string,
|
||||||
@ -79,6 +81,60 @@ export function usePersistedOverlayState<S extends string>(
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like usePersistedOverlayState, but namespaces the persistence key by username.
|
||||||
|
* This ensures different users on the same browser don't share state.
|
||||||
|
* Automatically migrates data from legacy (non-namespaced) keys on first use.
|
||||||
|
*/
|
||||||
|
export function useUserPersistedOverlayState<S extends string>(
|
||||||
|
key: string,
|
||||||
|
defaultValue: S | undefined = undefined,
|
||||||
|
): [
|
||||||
|
S | undefined,
|
||||||
|
(value: S | undefined, replace?: boolean) => void,
|
||||||
|
boolean,
|
||||||
|
() => void,
|
||||||
|
] {
|
||||||
|
const { auth } = useContext(AuthContext);
|
||||||
|
const location = useLocation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const currentLocationState = useMemo(() => location.state, [location]);
|
||||||
|
|
||||||
|
// currently selected value from URL state
|
||||||
|
const overlayStateValue = useMemo<S | undefined>(
|
||||||
|
() => location.state && location.state[key],
|
||||||
|
[location, key],
|
||||||
|
);
|
||||||
|
|
||||||
|
// saved value from previous session (user-namespaced with migration)
|
||||||
|
const [persistedValue, setPersistedValue, loaded, deletePersistedValue] =
|
||||||
|
useUserPersistence<S>(key, overlayStateValue);
|
||||||
|
|
||||||
|
const setOverlayStateValue = useCallback(
|
||||||
|
(value: S | undefined, replace: boolean = false) => {
|
||||||
|
setPersistedValue(value);
|
||||||
|
const newLocationState = { ...currentLocationState };
|
||||||
|
newLocationState[key] = value;
|
||||||
|
navigate(location.pathname, { state: newLocationState, replace });
|
||||||
|
},
|
||||||
|
// we know that these deps are correct
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[key, currentLocationState, navigate, setPersistedValue],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Don't return a value until auth has finished loading
|
||||||
|
if (auth.isLoading) {
|
||||||
|
return [undefined, setOverlayStateValue, false, deletePersistedValue];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
overlayStateValue ?? persistedValue ?? defaultValue,
|
||||||
|
setOverlayStateValue,
|
||||||
|
loaded,
|
||||||
|
deletePersistedValue,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
export function useHashState<S extends string>(): [
|
export function useHashState<S extends string>(): [
|
||||||
S | undefined,
|
S | undefined,
|
||||||
(value: S) => void,
|
(value: S) => void,
|
||||||
|
|||||||
159
web/src/hooks/use-user-persistence.ts
Normal file
159
web/src/hooks/use-user-persistence.ts
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
import { useEffect, useState, useCallback, useContext, useRef } from "react";
|
||||||
|
import { get as getData, set as setData, del as delData } from "idb-keyval";
|
||||||
|
import { AuthContext } from "@/context/auth-context";
|
||||||
|
|
||||||
|
type useUserPersistenceReturn<S> = [
|
||||||
|
value: S | undefined,
|
||||||
|
setValue: (value: S | undefined) => void,
|
||||||
|
loaded: boolean,
|
||||||
|
deleteValue: () => void,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Key used to track which keys have been migrated to prevent re-reading old keys
|
||||||
|
const MIGRATED_KEYS_STORAGE_KEY = "frigate-migrated-user-keys";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the set of keys that have already been migrated for a specific user.
|
||||||
|
*/
|
||||||
|
async function getMigratedKeys(username: string): Promise<Set<string>> {
|
||||||
|
const allMigrated =
|
||||||
|
(await getData<Record<string, string[]>>(MIGRATED_KEYS_STORAGE_KEY)) || {};
|
||||||
|
return new Set(allMigrated[username] || []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark a key as migrated for a specific user.
|
||||||
|
*/
|
||||||
|
async function markKeyAsMigrated(username: string, key: string): Promise<void> {
|
||||||
|
const allMigrated =
|
||||||
|
(await getData<Record<string, string[]>>(MIGRATED_KEYS_STORAGE_KEY)) || {};
|
||||||
|
const userMigrated = new Set(allMigrated[username] || []);
|
||||||
|
userMigrated.add(key);
|
||||||
|
allMigrated[username] = Array.from(userMigrated);
|
||||||
|
await setData(MIGRATED_KEYS_STORAGE_KEY, allMigrated);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for user-namespaced persistence with automatic migration from legacy keys.
|
||||||
|
*
|
||||||
|
* This hook:
|
||||||
|
* 1. Namespaces storage keys by username to isolate per-user preferences
|
||||||
|
* 2. Automatically migrates data from legacy (non-namespaced) keys on first use
|
||||||
|
* 3. Tracks migrated keys to prevent re-reading stale data after migration
|
||||||
|
* 4. Waits for auth to load before returning values to prevent race conditions
|
||||||
|
*
|
||||||
|
* @param key - The base key name (will be namespaced with username)
|
||||||
|
* @param defaultValue - Default value if no persisted value exists
|
||||||
|
*/
|
||||||
|
export function useUserPersistence<S>(
|
||||||
|
key: string,
|
||||||
|
defaultValue: S | undefined = undefined,
|
||||||
|
): useUserPersistenceReturn<S> {
|
||||||
|
const { auth } = useContext(AuthContext);
|
||||||
|
const [value, setInternalValue] = useState<S | undefined>(defaultValue);
|
||||||
|
const [loaded, setLoaded] = useState<boolean>(false);
|
||||||
|
const migrationAttemptedRef = useRef(false);
|
||||||
|
|
||||||
|
// Compute the user-namespaced key
|
||||||
|
const username = auth?.user?.username;
|
||||||
|
const isAuthenticated =
|
||||||
|
username && username !== "anonymous" && !auth.isLoading;
|
||||||
|
const namespacedKey = isAuthenticated ? `${key}:${username}` : key;
|
||||||
|
|
||||||
|
const setValue = useCallback(
|
||||||
|
(newValue: S | undefined) => {
|
||||||
|
setInternalValue(newValue);
|
||||||
|
async function update() {
|
||||||
|
await setData(namespacedKey, newValue);
|
||||||
|
}
|
||||||
|
update();
|
||||||
|
},
|
||||||
|
[namespacedKey],
|
||||||
|
);
|
||||||
|
|
||||||
|
const deleteValue = useCallback(async () => {
|
||||||
|
await delData(namespacedKey);
|
||||||
|
setInternalValue(defaultValue);
|
||||||
|
}, [namespacedKey, defaultValue]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Don't load until auth is resolved
|
||||||
|
if (auth.isLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset migration flag when key changes
|
||||||
|
migrationAttemptedRef.current = false;
|
||||||
|
setLoaded(false);
|
||||||
|
|
||||||
|
async function loadWithMigration() {
|
||||||
|
// For authenticated users, check if we need to migrate from legacy key
|
||||||
|
if (isAuthenticated && username && !migrationAttemptedRef.current) {
|
||||||
|
migrationAttemptedRef.current = true;
|
||||||
|
|
||||||
|
const migratedKeys = await getMigratedKeys(username);
|
||||||
|
|
||||||
|
// Check if we already have data in the namespaced key
|
||||||
|
const existingNamespacedValue = await getData<S>(namespacedKey);
|
||||||
|
|
||||||
|
if (typeof existingNamespacedValue !== "undefined") {
|
||||||
|
// Already have namespaced data, use it
|
||||||
|
setInternalValue(existingNamespacedValue);
|
||||||
|
setLoaded(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this key has already been migrated (even if value was deleted)
|
||||||
|
if (migratedKeys.has(key)) {
|
||||||
|
// Already migrated, don't read from legacy key
|
||||||
|
setInternalValue(defaultValue);
|
||||||
|
setLoaded(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to migrate from legacy key
|
||||||
|
const legacyValue = await getData<S>(key);
|
||||||
|
if (typeof legacyValue !== "undefined") {
|
||||||
|
// Migrate: copy to namespaced key, delete legacy key, mark as migrated
|
||||||
|
await setData(namespacedKey, legacyValue);
|
||||||
|
await delData(key);
|
||||||
|
await markKeyAsMigrated(username, key);
|
||||||
|
setInternalValue(legacyValue);
|
||||||
|
setLoaded(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No legacy value, just mark as migrated so we don't check again
|
||||||
|
await markKeyAsMigrated(username, key);
|
||||||
|
setInternalValue(defaultValue);
|
||||||
|
setLoaded(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For unauthenticated users or after migration check, just load normally
|
||||||
|
const storedValue = await getData<S>(namespacedKey);
|
||||||
|
if (typeof storedValue !== "undefined") {
|
||||||
|
setInternalValue(storedValue);
|
||||||
|
} else {
|
||||||
|
setInternalValue(defaultValue);
|
||||||
|
}
|
||||||
|
setLoaded(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadWithMigration();
|
||||||
|
}, [
|
||||||
|
auth.isLoading,
|
||||||
|
isAuthenticated,
|
||||||
|
username,
|
||||||
|
key,
|
||||||
|
namespacedKey,
|
||||||
|
defaultValue,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Don't return a value until auth has finished loading
|
||||||
|
if (auth.isLoading) {
|
||||||
|
return [undefined, setValue, false, deleteValue];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [value, setValue, loaded, deleteValue];
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user