feat(status): make the set-status menu work
feat(user): optimise get() function for react qol(todos): update todos
This commit is contained in:
parent
dbb407606b
commit
1c90a0c859
30 changed files with 572 additions and 324 deletions
|
|
@ -11,6 +11,7 @@
|
|||
"scripts": {
|
||||
"format": "pnpm exec prettier --write .",
|
||||
"lint": "eslint src",
|
||||
"test": "vitest run",
|
||||
"build": "tsc -p tsconfig.json --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
|
|
|
|||
|
|
@ -4,8 +4,10 @@ import {
|
|||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
useSyncExternalStore,
|
||||
} from "react";
|
||||
import { useMTP, type ProtocolMessage } from "@tensamin/mtp";
|
||||
|
||||
|
|
@ -19,9 +21,21 @@ import type z from "zod";
|
|||
import { createCache } from "@tensamin/cache";
|
||||
import { useStorage } from "@tensamin/storage/context";
|
||||
import { useSession } from "@tensamin/storage/session";
|
||||
import { getChangedUserFields, selectUserFields } from "./selection";
|
||||
|
||||
export { getChangedUserFields, selectUserFields } from "./selection";
|
||||
|
||||
export type User = z.infer<typeof schemas.GetUserData.response>;
|
||||
type ClientUserState = z.infer<typeof clientUserStateSchema>;
|
||||
export type UserField = keyof User;
|
||||
export type UserFields = readonly [UserField, ...UserField[]];
|
||||
export type SelectedUser<Fields extends readonly UserField[]> = Readonly<
|
||||
Pick<User, Fields[number]>
|
||||
>;
|
||||
export type UserProfilePatch = Omit<
|
||||
z.infer<typeof schemas.ChangeUserData.request>,
|
||||
"OnlineStatus"
|
||||
>;
|
||||
|
||||
export function mergeTransientPresence<T extends { UserId: number }>(
|
||||
user: T,
|
||||
|
|
@ -34,8 +48,22 @@ export function mergeTransientPresence<T extends { UserId: number }>(
|
|||
const USER_CACHE_MAX_AGE = 5 * 60 * 1000;
|
||||
|
||||
interface contextValue {
|
||||
get(userId: number): Promise<User>;
|
||||
update(user: User): Promise<void>;
|
||||
get<const Fields extends UserFields>(
|
||||
userId: number,
|
||||
fields: Fields,
|
||||
): Promise<SelectedUser<Fields>>;
|
||||
peek<const Fields extends UserFields>(
|
||||
userId: number,
|
||||
fields: Fields,
|
||||
): SelectedUser<Fields> | undefined;
|
||||
subscribe(
|
||||
userId: number,
|
||||
fields: readonly UserField[],
|
||||
listener: () => void,
|
||||
): () => void;
|
||||
getVersion(userId: number, fields: readonly UserField[]): string;
|
||||
updateProfile(userId: number, patch: UserProfilePatch): Promise<void>;
|
||||
updateState(userId: number, state: ClientUserState): void;
|
||||
}
|
||||
|
||||
const UserContext = createContext<contextValue | undefined>(undefined);
|
||||
|
|
@ -48,16 +76,24 @@ const UserContext = createContext<contextValue | undefined>(undefined);
|
|||
export default function UserProvider(props: { children: ReactNode }) {
|
||||
const storageRef = useRef<Record<number, User>>({});
|
||||
const pendingRef = useRef<Record<number, Promise<User> | undefined>>({});
|
||||
const profileUpdateQueuesRef = useRef(new Map<number, Promise<void>>());
|
||||
const profileGenerationsRef = useRef(new Map<number, number>());
|
||||
const checkedAtRef = useRef<Record<number, number>>({});
|
||||
const presenceRef = useRef(new Map<number, ClientUserState>());
|
||||
const durableProfileRef = useRef<Record<number, User>>({});
|
||||
const listenersRef = useRef(
|
||||
new Map<
|
||||
number,
|
||||
Set<{ fields: ReadonlySet<UserField>; listener: () => void }>
|
||||
>(),
|
||||
);
|
||||
const revisionsRef = useRef(new Map<number, Map<UserField, number>>());
|
||||
|
||||
const { send, subscribePush } = useMTP();
|
||||
const { load } = useStorage();
|
||||
const { contacts } = useSession();
|
||||
const [accountId, setAccountId] = useState<number | null>(null);
|
||||
const [cacheVersion, setCacheVersion] = useState(0);
|
||||
const [sessionId, setSessionId] = useState<number | null>(null);
|
||||
const accountIdRef = useRef<number | null>(null);
|
||||
const contactsRef = useRef(contacts);
|
||||
const initialStatesRef = useRef(
|
||||
new Map<number, z.infer<typeof publicUserStateSchema>>(),
|
||||
|
|
@ -71,40 +107,78 @@ export default function UserProvider(props: { children: ReactNode }) {
|
|||
return mergeTransientPresence(user, presenceRef.current);
|
||||
}, []);
|
||||
|
||||
const publishUser = useCallback((userId: number, user?: User) => {
|
||||
const previous = storageRef.current[userId];
|
||||
if (user) storageRef.current[userId] = user;
|
||||
else delete storageRef.current[userId];
|
||||
|
||||
const changedFields = getChangedUserFields(previous, user);
|
||||
if (changedFields.length === 0) return;
|
||||
|
||||
let revisions = revisionsRef.current.get(userId);
|
||||
if (!revisions) {
|
||||
revisions = new Map();
|
||||
revisionsRef.current.set(userId, revisions);
|
||||
}
|
||||
for (const field of changedFields) {
|
||||
revisions.set(field, (revisions.get(field) ?? 0) + 1);
|
||||
}
|
||||
|
||||
const changed = new Set<UserField>(changedFields);
|
||||
for (const entry of listenersRef.current.get(userId) ?? []) {
|
||||
if ([...entry.fields].some((field) => changed.has(field))) {
|
||||
entry.listener();
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const installProfile = useCallback(
|
||||
(profile: User) => {
|
||||
durableProfileRef.current[profile.UserId] = profile;
|
||||
publishUser(profile.UserId, mergeUserPresence(profile));
|
||||
},
|
||||
[mergeUserPresence, publishUser],
|
||||
);
|
||||
|
||||
const applyUserState = useCallback(
|
||||
(userId: number, state: ClientUserState, privateState = false) => {
|
||||
const currentAccountId = accountIdRef.current;
|
||||
const known =
|
||||
userId === accountId ||
|
||||
userId === currentAccountId ||
|
||||
durableProfileRef.current[userId] !== undefined ||
|
||||
contactsRef.current.some((contact) => contact.UserId === userId);
|
||||
if (!known) return false;
|
||||
|
||||
if (state === "user_invisible" && userId !== accountId) return false;
|
||||
if (userId === accountId && !privateState) return false;
|
||||
if (state === "user_invisible" && userId !== currentAccountId)
|
||||
return false;
|
||||
if (userId === currentAccountId && !privateState) return false;
|
||||
if (presenceRef.current.get(userId) === state) return true;
|
||||
|
||||
presenceRef.current.set(userId, state);
|
||||
const current = storageRef.current[userId];
|
||||
if (current) storageRef.current[userId] = mergeUserPresence(current);
|
||||
setCacheVersion((version) => version + 1);
|
||||
const profile = durableProfileRef.current[userId];
|
||||
if (profile) publishUser(userId, mergeUserPresence(profile));
|
||||
return true;
|
||||
},
|
||||
[accountId, mergeUserPresence],
|
||||
[mergeUserPresence, publishUser],
|
||||
);
|
||||
|
||||
const removePresence = useCallback((userId: number) => {
|
||||
presenceRef.current.delete(userId);
|
||||
initialStatesRef.current.delete(userId);
|
||||
delete checkedAtRef.current[userId];
|
||||
setCacheVersion((version) => version + 1);
|
||||
}, []);
|
||||
const removePresence = useCallback(
|
||||
(userId: number) => {
|
||||
const removed = presenceRef.current.delete(userId);
|
||||
initialStatesRef.current.delete(userId);
|
||||
delete checkedAtRef.current[userId];
|
||||
if (!removed) return;
|
||||
const profile = durableProfileRef.current[userId];
|
||||
if (profile) publishUser(userId, profile);
|
||||
},
|
||||
[publishUser],
|
||||
);
|
||||
|
||||
const handleStatePush = useCallback(
|
||||
async (message: ProtocolMessage) => {
|
||||
if (!accountId || !sessionId) return;
|
||||
if (!accountId) return;
|
||||
const data = message.data as Record<string, unknown>;
|
||||
if (message.type === "GetStates") {
|
||||
if (Number(data.SessionId) !== sessionId) {
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(data.MissingUserIds)) {
|
||||
for (const userId of data.MissingUserIds) {
|
||||
if (typeof userId === "number") removePresence(userId);
|
||||
|
|
@ -126,36 +200,34 @@ export default function UserProvider(props: { children: ReactNode }) {
|
|||
return;
|
||||
}
|
||||
if (message.type !== "ClientChanged") return;
|
||||
if (Number(data.SessionId) !== sessionId) return;
|
||||
const parsed = schemas.ClientChanged.response.safeParse(data);
|
||||
if (!parsed.success) return;
|
||||
applyUserState(parsed.data.UserId, parsed.data.UserState, true);
|
||||
},
|
||||
[accountId, applyUserState, removePresence, sessionId],
|
||||
[accountId, applyUserState, removePresence],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
void load("user_id").then((accountId) => {
|
||||
accountIdRef.current = accountId;
|
||||
setAccountId(accountId);
|
||||
});
|
||||
void load("session_id").then((value) => {
|
||||
setSessionId(value);
|
||||
});
|
||||
}, [load]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!accountId || !sessionId) return;
|
||||
return subscribePush(handleStatePush);
|
||||
}, [accountId, handleStatePush, sessionId, subscribePush]);
|
||||
const getAccountId = useCallback(async () => {
|
||||
if (accountIdRef.current !== null) return accountIdRef.current;
|
||||
const value = await load("user_id");
|
||||
accountIdRef.current = value;
|
||||
return value;
|
||||
}, [load]);
|
||||
|
||||
/**
|
||||
* Executes get.
|
||||
* @param userId Parameter userId.
|
||||
* @returns Promise<User>.
|
||||
*/
|
||||
const get = useCallback(
|
||||
useEffect(() => {
|
||||
if (!accountId) return;
|
||||
return subscribePush(handleStatePush);
|
||||
}, [accountId, handleStatePush, subscribePush]);
|
||||
|
||||
const loadUser = useCallback(
|
||||
async (userId: number): Promise<User> => {
|
||||
void cacheVersion;
|
||||
if (userId == null) {
|
||||
throw new Error("userId is required");
|
||||
}
|
||||
|
|
@ -166,39 +238,45 @@ export default function UserProvider(props: { children: ReactNode }) {
|
|||
}
|
||||
|
||||
const request = (async () => {
|
||||
const cache = createCache(String(accountId ?? userId));
|
||||
const cache = createCache(String(await getAccountId()));
|
||||
const cachedValue =
|
||||
storageRef.current[userId] ?? (await cache.profiles.get(userId));
|
||||
durableProfileRef.current[userId] ??
|
||||
(await cache.profiles.get(userId));
|
||||
const cachedResult =
|
||||
schemas.GetUserData.response.safeParse(cachedValue);
|
||||
const cached = cachedResult.success ? cachedResult.data : undefined;
|
||||
if (cached) {
|
||||
durableProfileRef.current[userId] = cached;
|
||||
const merged = mergeUserPresence(cached);
|
||||
storageRef.current[userId] = merged;
|
||||
installProfile(cached);
|
||||
const checkedAt = checkedAtRef.current[userId];
|
||||
if (!checkedAt || Date.now() - checkedAt < USER_CACHE_MAX_AGE) {
|
||||
checkedAtRef.current[userId] = Date.now();
|
||||
return merged;
|
||||
return storageRef.current[userId];
|
||||
}
|
||||
}
|
||||
try {
|
||||
const profileGeneration =
|
||||
profileGenerationsRef.current.get(userId) ?? 0;
|
||||
const userData = await send("GetUserData", { UserId: userId });
|
||||
if (userData.type === "ErrorNotFound" || userData.data.UserId === 0) {
|
||||
delete storageRef.current[userId];
|
||||
delete durableProfileRef.current[userId];
|
||||
delete checkedAtRef.current[userId];
|
||||
publishUser(userId);
|
||||
await cache.profiles.delete(userId);
|
||||
throw new Error("GetUserData failed: user not found");
|
||||
}
|
||||
if (userData.type.startsWith("Error")) {
|
||||
throw new Error(`GetUserData failed: ${userData.type}`);
|
||||
}
|
||||
const user = mergeUserPresence(userData.data);
|
||||
durableProfileRef.current[userId] = userData.data;
|
||||
storageRef.current[userId] = user;
|
||||
if (
|
||||
profileGeneration !==
|
||||
(profileGenerationsRef.current.get(userId) ?? 0) &&
|
||||
storageRef.current[userId]
|
||||
) {
|
||||
return storageRef.current[userId];
|
||||
}
|
||||
installProfile(userData.data);
|
||||
checkedAtRef.current[userId] = Date.now();
|
||||
return user;
|
||||
return storageRef.current[userId];
|
||||
} catch (error) {
|
||||
if (cached && storageRef.current[userId]) {
|
||||
checkedAtRef.current[userId] = Date.now();
|
||||
|
|
@ -216,26 +294,91 @@ export default function UserProvider(props: { children: ReactNode }) {
|
|||
delete pendingRef.current[userId];
|
||||
}
|
||||
},
|
||||
[accountId, cacheVersion, mergeUserPresence, send],
|
||||
[getAccountId, installProfile, publishUser, send],
|
||||
);
|
||||
|
||||
const update = useCallback(
|
||||
async (user: User) => {
|
||||
const durableProfile = {
|
||||
...user,
|
||||
OnlineStatus:
|
||||
durableProfileRef.current[user.UserId]?.OnlineStatus ??
|
||||
user.OnlineStatus,
|
||||
} as User;
|
||||
await createCache(String(accountId ?? user.UserId)).profiles.put(
|
||||
durableProfile,
|
||||
);
|
||||
durableProfileRef.current[user.UserId] = durableProfile;
|
||||
storageRef.current[user.UserId] = mergeUserPresence(durableProfile);
|
||||
checkedAtRef.current[user.UserId] = Date.now();
|
||||
setCacheVersion((version) => version + 1);
|
||||
const get = useCallback(
|
||||
async <const Fields extends UserFields>(
|
||||
userId: number,
|
||||
fields: Fields,
|
||||
): Promise<SelectedUser<Fields>> => {
|
||||
return selectUserFields(await loadUser(userId), fields);
|
||||
},
|
||||
[accountId, mergeUserPresence],
|
||||
[loadUser],
|
||||
);
|
||||
|
||||
const peek = useCallback(
|
||||
<const Fields extends UserFields>(userId: number, fields: Fields) => {
|
||||
const user = storageRef.current[userId];
|
||||
return user ? selectUserFields(user, fields) : undefined;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const subscribe = useCallback(
|
||||
(userId: number, fields: readonly UserField[], listener: () => void) => {
|
||||
let listeners = listenersRef.current.get(userId);
|
||||
if (!listeners) {
|
||||
listeners = new Set();
|
||||
listenersRef.current.set(userId, listeners);
|
||||
}
|
||||
const entry = { fields: new Set(fields), listener };
|
||||
listeners.add(entry);
|
||||
return () => {
|
||||
listeners.delete(entry);
|
||||
if (listeners.size === 0) listenersRef.current.delete(userId);
|
||||
};
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const getVersion = useCallback(
|
||||
(userId: number, fields: readonly UserField[]) => {
|
||||
const revisions = revisionsRef.current.get(userId);
|
||||
return fields
|
||||
.map((field) => `${field}:${revisions?.get(field) ?? 0}`)
|
||||
.join("|");
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const updateProfile = useCallback(
|
||||
async (userId: number, patch: UserProfilePatch) => {
|
||||
const previousUpdate = profileUpdateQueuesRef.current.get(userId);
|
||||
const update = (
|
||||
previousUpdate?.catch(() => undefined) ?? Promise.resolve()
|
||||
).then(async () => {
|
||||
const current = durableProfileRef.current[userId];
|
||||
if (!current) throw new Error(`User ${userId} is not loaded`);
|
||||
profileGenerationsRef.current.set(
|
||||
userId,
|
||||
(profileGenerationsRef.current.get(userId) ?? 0) + 1,
|
||||
);
|
||||
const profile = schemas.GetUserData.response.parse({
|
||||
...current,
|
||||
...patch,
|
||||
});
|
||||
installProfile(profile);
|
||||
checkedAtRef.current[userId] = Date.now();
|
||||
await createCache(String(await getAccountId())).profiles.put(profile);
|
||||
});
|
||||
profileUpdateQueuesRef.current.set(userId, update);
|
||||
try {
|
||||
await update;
|
||||
} finally {
|
||||
if (profileUpdateQueuesRef.current.get(userId) === update) {
|
||||
profileUpdateQueuesRef.current.delete(userId);
|
||||
}
|
||||
}
|
||||
},
|
||||
[getAccountId, installProfile],
|
||||
);
|
||||
|
||||
const updateState = useCallback(
|
||||
(userId: number, state: ClientUserState) => {
|
||||
applyUserState(userId, state, true);
|
||||
},
|
||||
[applyUserState],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -247,7 +390,7 @@ export default function UserProvider(props: { children: ReactNode }) {
|
|||
].filter((userId, index, all) => all.indexOf(userId) === index);
|
||||
for (const userId of userIds) {
|
||||
try {
|
||||
await get(userId);
|
||||
await loadUser(userId);
|
||||
const state = initialStatesRef.current.get(userId);
|
||||
if (state && applyUserState(userId, state)) {
|
||||
initialStatesRef.current.delete(userId);
|
||||
|
|
@ -257,12 +400,22 @@ export default function UserProvider(props: { children: ReactNode }) {
|
|||
}
|
||||
}
|
||||
})();
|
||||
}, [accountId, applyUserState, contacts, get]);
|
||||
}, [accountId, applyUserState, contacts, loadUser]);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
get,
|
||||
peek,
|
||||
subscribe,
|
||||
getVersion,
|
||||
updateProfile,
|
||||
updateState,
|
||||
}),
|
||||
[get, getVersion, peek, subscribe, updateProfile, updateState],
|
||||
);
|
||||
|
||||
return (
|
||||
<UserContext.Provider value={{ get, update }}>
|
||||
{props.children}
|
||||
</UserContext.Provider>
|
||||
<UserContext.Provider value={value}>{props.children}</UserContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -278,3 +431,63 @@ export function useUser(): contextValue {
|
|||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export type UserLoadState<Fields extends readonly UserField[]> =
|
||||
| { data: undefined; error: undefined; loading: true }
|
||||
| { data: undefined; error: unknown; loading: false }
|
||||
| { data: SelectedUser<Fields>; error: undefined; loading: false };
|
||||
|
||||
export function useUserFields<const Fields extends UserFields>(
|
||||
userId: number | null,
|
||||
fields: Fields,
|
||||
): UserLoadState<Fields> {
|
||||
const { get, getVersion, peek, subscribe } = useUser();
|
||||
const fieldKey = fields.join("|");
|
||||
const selectedFields = useMemo(
|
||||
() => fieldKey.split("|") as unknown as Fields,
|
||||
[fieldKey],
|
||||
);
|
||||
const requestKey = `${userId ?? "unresolved"}:${fieldKey}`;
|
||||
const [failure, setFailure] = useState<{
|
||||
error: unknown;
|
||||
requestKey: string;
|
||||
}>();
|
||||
|
||||
const version = useSyncExternalStore(
|
||||
useCallback(
|
||||
(listener) =>
|
||||
userId === null
|
||||
? () => undefined
|
||||
: subscribe(userId, selectedFields, listener),
|
||||
[selectedFields, subscribe, userId],
|
||||
),
|
||||
useCallback(
|
||||
() =>
|
||||
userId === null ? "unresolved" : getVersion(userId, selectedFields),
|
||||
[getVersion, selectedFields, userId],
|
||||
),
|
||||
() => "server",
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (userId === null) return;
|
||||
let active = true;
|
||||
void get(userId, selectedFields).catch((nextError: unknown) => {
|
||||
if (active) setFailure({ error: nextError, requestKey });
|
||||
});
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [get, requestKey, selectedFields, userId]);
|
||||
|
||||
const data = useMemo(() => {
|
||||
void version;
|
||||
return userId === null ? undefined : peek(userId, selectedFields);
|
||||
}, [peek, selectedFields, userId, version]);
|
||||
|
||||
if (data) return { data, error: undefined, loading: false };
|
||||
if (failure?.requestKey === requestKey) {
|
||||
return { data: undefined, error: failure.error, loading: false };
|
||||
}
|
||||
return { data: undefined, error: undefined, loading: true };
|
||||
}
|
||||
|
|
|
|||
28
packages/user/src/selection.test.ts
Normal file
28
packages/user/src/selection.test.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { User } from "./context";
|
||||
import { getChangedUserFields, selectUserFields } from "./selection";
|
||||
|
||||
const user = {
|
||||
Avatar: undefined,
|
||||
Display: "Alice",
|
||||
IotaId: 1,
|
||||
OmikronConnections: [],
|
||||
OnlineStatus: "user_online",
|
||||
PublicKey: "AA==",
|
||||
SubEnd: 0,
|
||||
SubLevel: 0,
|
||||
UserId: 1,
|
||||
Username: "alice",
|
||||
} satisfies User;
|
||||
|
||||
describe("presence selection", () => {
|
||||
it("notifies OnlineStatus selections when presence changes", () => {
|
||||
const next = { ...user, OnlineStatus: "user_dnd" as const };
|
||||
|
||||
expect(getChangedUserFields(user, next)).toEqual(["OnlineStatus"]);
|
||||
expect(selectUserFields(next, ["OnlineStatus"])).toEqual({
|
||||
OnlineStatus: "user_dnd",
|
||||
});
|
||||
});
|
||||
});
|
||||
40
packages/user/src/selection.ts
Normal file
40
packages/user/src/selection.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import type { User, UserField, UserFields, SelectedUser } from "./context";
|
||||
|
||||
const USER_FIELD_MAP = {
|
||||
About: true,
|
||||
Avatar: true,
|
||||
Display: true,
|
||||
IotaId: true,
|
||||
OmikronConnections: true,
|
||||
OmikronId: true,
|
||||
OnlineStatus: true,
|
||||
PublicKey: true,
|
||||
Status: true,
|
||||
SubEnd: true,
|
||||
SubLevel: true,
|
||||
UserId: true,
|
||||
Username: true,
|
||||
} satisfies Record<UserField, true>;
|
||||
|
||||
const USER_FIELDS = Object.keys(USER_FIELD_MAP) as UserField[];
|
||||
|
||||
export function selectUserFields<const Fields extends UserFields>(
|
||||
user: User,
|
||||
fields: Fields,
|
||||
): SelectedUser<Fields> {
|
||||
return Object.fromEntries(
|
||||
fields.map((field) => [field, user[field]]),
|
||||
) as SelectedUser<Fields>;
|
||||
}
|
||||
|
||||
export function getChangedUserFields(
|
||||
previous: User | undefined,
|
||||
next: User | undefined,
|
||||
): UserField[] {
|
||||
if ((previous === undefined) !== (next === undefined)) {
|
||||
return [...USER_FIELDS];
|
||||
}
|
||||
return USER_FIELDS.filter(
|
||||
(field) => !Object.is(previous?.[field], next?.[field]),
|
||||
);
|
||||
}
|
||||
|
|
@ -1,48 +1,47 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { useUser, type User } from "./context";
|
||||
import { type SelectedUser, type UserFields, useUserFields } from "./context";
|
||||
|
||||
import { failedUser } from "@tensamin/shared/data";
|
||||
import { useStorage } from "@tensamin/storage/context";
|
||||
|
||||
// Wrapper function to pass user data to some component
|
||||
export default function Wrapper(props: {
|
||||
export default function Wrapper<const Fields extends UserFields>(props: {
|
||||
userId: number | "own";
|
||||
fields: Fields;
|
||||
loading: React.ReactNode;
|
||||
component: (user: User) => React.ReactNode;
|
||||
component: (user: SelectedUser<Fields>) => React.ReactNode;
|
||||
}) {
|
||||
const { get } = useUser();
|
||||
const { load } = useStorage();
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [ownUserId, setOwnUserId] = useState<number | null>(null);
|
||||
const [ownUserError, setOwnUserError] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.userId == null) return;
|
||||
|
||||
if (props.userId !== "own") return;
|
||||
let active = true;
|
||||
|
||||
void (async () => {
|
||||
try {
|
||||
const userId =
|
||||
props.userId === "own" ? await load("user_id") : props.userId;
|
||||
const value = await get(userId);
|
||||
|
||||
void load("user_id").then(
|
||||
(value) => {
|
||||
if (active) {
|
||||
setUser(value);
|
||||
setOwnUserId(value);
|
||||
setOwnUserError(false);
|
||||
}
|
||||
} catch {
|
||||
if (active) {
|
||||
setUser(failedUser);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
},
|
||||
() => {
|
||||
if (active) setOwnUserError(true);
|
||||
},
|
||||
);
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [load, get, props.userId]);
|
||||
}, [load, props.userId]);
|
||||
|
||||
if (props.userId == null) {
|
||||
return <>{props.component(failedUser)}</>;
|
||||
const userId = props.userId === "own" ? ownUserId : props.userId;
|
||||
const result = useUserFields(userId, props.fields);
|
||||
if (
|
||||
(props.userId === "own" && ownUserError) ||
|
||||
(!result.loading && !result.data)
|
||||
) {
|
||||
return <>{props.component(failedUser as SelectedUser<Fields>)}</>;
|
||||
}
|
||||
|
||||
return <>{user ? props.component(user) : props.loading}</>;
|
||||
return <>{result.data ? props.component(result.data) : props.loading}</>;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue