diff --git a/apps/web/src/components/navbar.tsx b/apps/web/src/components/navbar.tsx index 1030ca3..d97b3b1 100644 --- a/apps/web/src/components/navbar.tsx +++ b/apps/web/src/components/navbar.tsx @@ -62,7 +62,7 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) { )} - {pathname === "/chat" && ( + {pathname === "/chat" && id && ( ( diff --git a/apps/web/src/features/conversation/list/body.tsx b/apps/web/src/features/conversation/list/body.tsx index 7ed6665..cdbd35e 100644 --- a/apps/web/src/features/conversation/list/body.tsx +++ b/apps/web/src/features/conversation/list/body.tsx @@ -22,6 +22,8 @@ export default function List() { const scrollRef = React.useRef(null); + // TanStack Virtual is intentionally used here; React Compiler memoization is skipped. + // eslint-disable-next-line react-hooks/incompatible-library const virtualizer = useVirtualizer({ count: items?.length || 0, estimateSize: () => 60, diff --git a/packages/chat/src/components/message.tsx b/packages/chat/src/components/message.tsx index 5e03ab1..6815e13 100644 --- a/packages/chat/src/components/message.tsx +++ b/packages/chat/src/components/message.tsx @@ -36,7 +36,7 @@ function getSafeMessageHeight(height: number | undefined) { * @param props Parameter props. * @returns unknown. */ -function MessageComponent(props: { +function MessageComponent({ message, notEncrypted, measureRef }: { message: RawMessage & { failed?: boolean; }; @@ -50,16 +50,16 @@ function MessageComponent(props: { const [decodedContent, setDecodedContent] = React.useState(""); const [isReady, setIsReady] = React.useState(false); - const safeMessageHeight = getSafeMessageHeight(props.message.height); + const safeMessageHeight = getSafeMessageHeight(message.height); React.useEffect(() => { - if (props.notEncrypted) { - setDecodedContent(props.message.content); + if (notEncrypted) { + setDecodedContent(message.content); setIsReady(true); return; } - const content = props.message.content; + const content = message.content; const secret = sharedSecret; let active = true; @@ -94,27 +94,27 @@ function MessageComponent(props: { return () => { active = false; }; - }, [decrypt, props.message.content, props.notEncrypted, sharedSecret]); + }, [decrypt, message.content, notEncrypted, sharedSecret]); return (
- {props.message.failed && - props.message.message_state === "awaiting" && ( + {message.failed && + message.message_state === "awaiting" && (

Failed to send message

@@ -122,8 +122,8 @@ function MessageComponent(props: { } />
)} - {!props.message.failed && - props.message.message_state === "awaiting" && ( + {!message.failed && + message.message_state === "awaiting" && ( )} {isReady ? ( @@ -133,7 +133,7 @@ function MessageComponent(props: { className="block rounded-sm" style={{ width: generateFixedLoadingSize( - props.message.content.length, + message.content.length, safeMessageHeight, ), minHeight: safeMessageHeight, diff --git a/packages/chat/src/context.tsx b/packages/chat/src/context.tsx index 283d574..13a3a42 100644 --- a/packages/chat/src/context.tsx +++ b/packages/chat/src/context.tsx @@ -61,7 +61,13 @@ export default function Provider(props: { children: ReactNode }) { const { send, subscribePush } = useTTP(); const [liveMessagesState, setLiveMessagesState] = useState([]); - const [currentSharedSecret, setCurrentSharedSecret] = useState(""); + const [currentSharedSecretState, setCurrentSharedSecretState] = useState<{ + userId: number; + value: string; + }>({ + userId: 0, + value: "", + }); const inputBoxRef = useRef(null); @@ -75,16 +81,13 @@ export default function Provider(props: { children: ReactNode }) { return Number(rawId ?? 0); }, [locationSearch]); - const userIdValueFromLastRender = useRef(userIdValue); - - useEffect(() => { - if (userIdValue === userIdValueFromLastRender.current) { - return; + const currentSharedSecret = useMemo(() => { + if (currentSharedSecretState.userId !== userIdValue) { + return ""; } - userIdValueFromLastRender.current = userIdValue; - setCurrentSharedSecret(""); - }, [userIdValue]); + return currentSharedSecretState.value; + }, [currentSharedSecretState, userIdValue]); // Load shared secret useEffect(() => { @@ -105,11 +108,17 @@ export default function Provider(props: { children: ReactNode }) { ); if (active) { - setCurrentSharedSecret(sharedSecret); + setCurrentSharedSecretState({ + userId: userIdValue, + value: sharedSecret, + }); } } catch { if (active) { - setCurrentSharedSecret(""); + setCurrentSharedSecretState({ + userId: userIdValue, + value: "", + }); } } })(); diff --git a/packages/chat/src/screen.tsx b/packages/chat/src/screen.tsx index 6dd094e..395e518 100644 --- a/packages/chat/src/screen.tsx +++ b/packages/chat/src/screen.tsx @@ -100,6 +100,7 @@ export default function Screen() { return messagesRef.current[index]?.timestamp ?? index; }, []); + // eslint-disable-next-line react-hooks/incompatible-library const virtualizer = useVirtualizer({ count: messages.length, getScrollElement: () => scrollRef.current, diff --git a/packages/crypto/src/context.tsx b/packages/crypto/src/context.tsx index e5d3315..588aff1 100644 --- a/packages/crypto/src/context.tsx +++ b/packages/crypto/src/context.tsx @@ -33,8 +33,28 @@ export const context = React.createContext( export default function Provider(props: { children: React.ReactNode }) { const apiRef = React.useRef(null); - const value = React.useMemo( - () => createCryptoActions(() => apiRef.current), + const value = React.useMemo( + () => ({ + encrypt: async (secret, plaintext) => { + const api = apiRef.current; + if (!api) throw new Error("API not initialized"); + return await api.encrypt(secret, plaintext); + }, + decrypt: async (secret, ciphertext) => { + const api = apiRef.current; + if (!api) throw new Error("API not initialized"); + return await api.decrypt(secret, ciphertext); + }, + getSharedSecret: async (ownPrivateKey, ownPublicKey, otherPublicKey) => { + const api = apiRef.current; + if (!api) throw new Error("API not initialized"); + return await api.getSharedSecret( + ownPrivateKey, + ownPublicKey, + otherPublicKey, + ); + }, + }), [], ); @@ -56,11 +76,11 @@ export default function Provider(props: { children: React.ReactNode }) { /** * Creates crypto action functions that safely delegate to the worker API. - * @param getApiRef Function that returns worker API reference when initialized. + * @param apiRef Worker API reference object. * @returns Typed crypto action functions. */ export function createCryptoActions( - getApiRef: () => ApiRef | null, + apiRef: React.RefObject, ): CryptoContextType { /** * Encrypts plaintext by delegating to the crypto worker API. @@ -72,9 +92,9 @@ export function createCryptoActions( secret: string, plaintext: string, ): Promise => { - const apiRef = getApiRef(); - if (!apiRef) throw new Error("API not initialized"); - return await apiRef.encrypt(secret, plaintext); + const api = apiRef.current; + if (!api) throw new Error("API not initialized"); + return await api.encrypt(secret, plaintext); }; /** @@ -87,9 +107,9 @@ export function createCryptoActions( secret: string, ciphertext: string, ): Promise => { - const apiRef = getApiRef(); - if (!apiRef) throw new Error("API not initialized"); - return await apiRef.decrypt(secret, ciphertext); + const api = apiRef.current; + if (!api) throw new Error("API not initialized"); + return await api.decrypt(secret, ciphertext); }; /** @@ -104,9 +124,9 @@ export function createCryptoActions( ownPublicKey: string, otherPublicKey: string, ): Promise => { - const apiRef = getApiRef(); - if (!apiRef) throw new Error("API not initialized"); - return await apiRef.getSharedSecret( + const api = apiRef.current; + if (!api) throw new Error("API not initialized"); + return await api.getSharedSecret( ownPrivateKey, ownPublicKey, otherPublicKey, diff --git a/packages/user/src/wrapper.tsx b/packages/user/src/wrapper.tsx index 36ddf5a..5f13e54 100644 --- a/packages/user/src/wrapper.tsx +++ b/packages/user/src/wrapper.tsx @@ -6,7 +6,7 @@ import { useStorage } from "@tensamin/storage/context"; // Wrapper function to pass user data to some component export default function Wrapper(props: { - userId?: number | "own"; + userId: number | "own"; loading: React.ReactNode; component: (user: User) => React.ReactNode; }) { @@ -15,40 +15,34 @@ export default function Wrapper(props: { const [user, setUser] = useState(null); useEffect(() => { - if (props.userId == null) { - setUser(failedUser); - return; - } + if (props.userId == null) return; let active = true; - if (props.userId === "own") { - load("user_id") - .then((id) => { - get(id) - .then((user) => (active ? setUser(user) : null)) - .catch(() => setUser(failedUser)); - }) - .catch(() => setUser(failedUser)); - return; - } + void (async () => { + try { + const userId = + props.userId === "own" ? await load("user_id") : props.userId; + const value = await get(userId); - get(props.userId) - .then((value) => { if (active) { setUser(value); } - }) - .catch(() => { + } catch { if (active) { setUser(failedUser); } - }); + } + })(); return () => { active = false; }; }, [load, get, props.userId]); + if (props.userId == null) { + return <>{props.component(failedUser)}; + } + return <>{user ? props.component(user) : props.loading}; }