From de63b554c16a33c97dd8e0864a50decf991dfe28 Mon Sep 17 00:00:00 2001 From: Alois Date: Fri, 1 May 2026 11:16:23 +0200 Subject: [PATCH] (feat): move decryption to getMessages instead of letting it get handled by the message component --- packages/chat/src/components/input.tsx | 41 +-------- packages/chat/src/components/message.tsx | 98 +--------------------- packages/chat/src/context.tsx | 21 +++-- packages/chat/src/screen.tsx | 101 +++-------------------- 4 files changed, 31 insertions(+), 230 deletions(-) diff --git a/packages/chat/src/components/input.tsx b/packages/chat/src/components/input.tsx index 68b65d9..c42cf59 100644 --- a/packages/chat/src/components/input.tsx +++ b/packages/chat/src/components/input.tsx @@ -8,7 +8,6 @@ import { Plus, Laugh, Clapperboard } from "lucide-react"; import { useChat } from "../context"; import { useTTP } from "@tensamin/ttp"; import { log, toast } from "@tensamin/shared/log"; -import Message from "./message"; import { cn, useIsMobile } from "@tensamin/ui"; import { encryptText } from "@tensamin/crypto/worker"; @@ -20,7 +19,6 @@ export default function InputComponent({ setValue: (value: string) => void; }) { const [invertEnterBehavior, setInvertEnterBehavior] = React.useState(false); - const measurementRef = React.useRef(null); const { send } = useTTP(); const { addLiveMessage, sharedSecret, userId } = useChat(); @@ -32,21 +30,6 @@ export default function InputComponent({ }); }, [load]); - const getRenderedHeight = React.useCallback(() => { - const measuredHeight = - measurementRef.current?.getBoundingClientRect().height; - - if ( - typeof measuredHeight === "number" && - Number.isFinite(measuredHeight) && - measuredHeight > 0 - ) { - return Math.ceil(measuredHeight); - } - - return 40; - }, []); - /** * Executes handleSubmit. * @param none This function has no parameters. @@ -68,10 +51,8 @@ export default function InputComponent({ return; } - const height = getRenderedHeight(); - const reference = addLiveMessage({ - height, + height: 0, not_encrypted: true, send_time: time, content: currentValue, @@ -82,7 +63,7 @@ export default function InputComponent({ const encryptedContext = await encryptText(sharedSecret, currentValue); send("message_send", { - height, + height: 0, content: encryptedContext, receiver_id: userId, send_time: time, @@ -112,24 +93,6 @@ export default function InputComponent({ )} > - 0) { - return Math.ceil(height); - } - - return 40; -} - -/** - * Executes Message. - * @param props Parameter props. - * @returns unknown. - */ function MessageComponent({ message, - notEncrypted, - measureRef, }: { message: RawMessage & { failed?: boolean; }; - notEncrypted?: boolean; - measureRef?: React.Ref; }) { - const { sharedSecret } = useChat(); - const { decrypt } = useCrypto(); - const isMobile = useIsMobile(); - const [decodedContent, setDecodedContent] = React.useState(""); - const [isReady, setIsReady] = React.useState(false); - const safeMessageHeight = getSafeMessageHeight(message.height); - - React.useEffect(() => { - if (notEncrypted) { - setDecodedContent(message.content); - setIsReady(true); - return; - } - - const content = message.content; - const secret = sharedSecret; - let active = true; - - if (!secret) { - setIsReady(false); - return; - } - - decryptText(secret, content) - .then((value) => { - if (!active) { - return; - } - - setDecodedContent(value); - setIsReady(true); - }) - .catch((e) => { - if (!active) { - return; - } - - log(0, "Chat", "red", "Failed to decrypt message", e, { - content, - secret, - }); - - setDecodedContent("Failed to decrypt"); - setIsReady(true); - }); - - return () => { - active = false; - }; - }, [decrypt, message.content, notEncrypted, sharedSecret]); - return (
@@ -112,7 +35,7 @@ function MessageComponent({ ? "bg-destructive/75 text-destructive-foreground" : "bg-primary text-primary-foreground" : "bg-muted" - } ${!isReady && "animate-pulse"} ${isMobile && "select-none"}`} + } ${isMobile && "select-none"}`} > {message.failed && message.message_state === "awaiting" && ( @@ -125,26 +48,13 @@ function MessageComponent({ {!message.failed && message.message_state === "awaiting" && ( )} - {isReady ? ( - - ) : ( -
- )} +
} /> - +
@@ -154,8 +64,6 @@ function MessageComponent({ export default React.memo(MessageComponent, (prev, next) => { return ( - prev.notEncrypted === next.notEncrypted && - prev.measureRef === next.measureRef && prev.message.send_time === next.message.send_time && prev.message.content === next.message.content && prev.message.height === next.message.height && diff --git a/packages/chat/src/context.tsx b/packages/chat/src/context.tsx index 738211b..887fb4c 100644 --- a/packages/chat/src/context.tsx +++ b/packages/chat/src/context.tsx @@ -55,7 +55,7 @@ function updateMessageStateBySendTime< * @returns unknown. */ export default function Provider(props: { children: ReactNode }) { - const { getSharedSecret } = useCrypto(); + const { getSharedSecret, decryptText } = useCrypto(); const { get } = useUser(); const { load } = useStorage(); const { send, subscribePush } = useTTP(); @@ -128,7 +128,7 @@ export default function Provider(props: { children: ReactNode }) { }; }, [get, getSharedSecret, load, userIdValue]); - const customGetMessages = useCallback( + const getMessages = useCallback( async (amount: number, offset: number) => { const messages = await send("messages_get", { amount, @@ -155,9 +155,20 @@ export default function Provider(props: { children: ReactNode }) { }); } - return sorted; + return await Promise.all( + sorted.map(async (message) => { + try { + return { + ...message, + content: await decryptText(currentSharedSecret, message.content), + }; + } catch { + return message; + } + }), + ); }, - [send, userIdValue], + [send, userIdValue, currentSharedSecret, decryptText], ); const addLiveMessage = useCallback((message: RawMessage) => { @@ -287,7 +298,7 @@ export default function Provider(props: { children: ReactNode }) { liveMessagesState, addLiveMessage, clearLiveMessages, diff --git a/packages/chat/src/screen.tsx b/packages/chat/src/screen.tsx index 60e3f00..eb802ec 100644 --- a/packages/chat/src/screen.tsx +++ b/packages/chat/src/screen.tsx @@ -7,32 +7,23 @@ import InputComponent from "./components/input"; import Message from "./components/message"; import { PAGE_SIZE } from "./values"; -import { useLayoutEffect } from "react"; import { useIsMobile } from "@tensamin/ui"; function getDistanceFromBottom(element: HTMLDivElement) { return element.scrollHeight - (element.scrollTop + element.clientHeight); } -const FALLBACK_MESSAGE_HEIGHT = 40; +const MESSAGE_ROW_HEIGHT = 48; const ROW_VERTICAL_PADDING = 8; -function getSafeMessageHeight(height: number | undefined) { - if (typeof height === "number" && Number.isFinite(height) && height > 0) { - return Math.ceil(height); - } - - return FALLBACK_MESSAGE_HEIGHT; -} - /** * Renders the chat screen with virtualized history and live message updates. * @returns Chat screen JSX. */ export default function Screen() { - const { getMessages, liveMessages, clearLiveMessages, userId } = useChat(); + const { getMessages, liveMessages, clearLiveMessages, userId, sharedSecret } = + useChat(); - const inputBoxRef = React.useRef(null); const scrollRef = React.useRef(null); const isMobile = useIsMobile(); const stickyBottomThreshold = isMobile ? 220 : 140; @@ -46,12 +37,13 @@ export default function Screen() { } | null>(null); const hasValidChatUser = Number.isSafeInteger(userId) && userId > 0; + const hasSharedSecret = sharedSecret.length > 0; const messagesQuery = useInfiniteQuery({ - queryKey: ["chat-messages", String(userId)], + queryKey: ["chat-messages", String(userId), hasSharedSecret], initialPageParam: 0, queryFn: ({ pageParam }) => getMessages(PAGE_SIZE, Number(pageParam)), - enabled: hasValidChatUser, + enabled: hasValidChatUser && hasSharedSecret, getNextPageParam: (lastPage, allPages) => { if (lastPage.length < PAGE_SIZE) { return undefined; @@ -105,10 +97,7 @@ export default function Screen() { count: messages.length, getScrollElement: () => scrollRef.current, getItemKey, - estimateSize: (index) => { - const message = messages[index]; - return getSafeMessageHeight(message?.height) + ROW_VERTICAL_PADDING; - }, + estimateSize: () => MESSAGE_ROW_HEIGHT + ROW_VERTICAL_PADDING, overscan: isMobile ? 10 : 6, }); @@ -195,72 +184,8 @@ export default function Screen() { void onScroll(); }, [onScroll]); - /** - * Input box height changes & state - */ const [value, setValue] = React.useState(""); - const updateMaxHeight = React.useCallback(() => { - const el = inputBoxRef.current; - const host = scrollRef.current; - if (!host || !el || typeof window === "undefined") return; - - const shouldStickToBottom = - getDistanceFromBottom(host) <= stickyBottomThreshold; - - const viewportHeight = Math.max( - window.innerHeight, - window.visualViewport?.height ?? 0, - ); - const inputHeight = el.scrollHeight + 56; - - host.style.maxHeight = `calc(${viewportHeight}px - ${inputHeight - 62}px - env(safe-area-inset-bottom) - env(safe-area-inset-top))`; - - if (shouldStickToBottom) { - requestAnimationFrame(() => { - if (!scrollRef.current) { - return; - } - - scrollRef.current.scrollTop = scrollRef.current.scrollHeight; - }); - } - }, [stickyBottomThreshold]); - - useLayoutEffect(() => { - updateMaxHeight(); - }, [value, updateMaxHeight]); - - React.useEffect(() => { - updateMaxHeight(); - - if (typeof window === "undefined") { - return; - } - - const handleViewportChange = () => { - updateMaxHeight(); - }; - - const viewport = window.visualViewport; - viewport?.addEventListener("resize", handleViewportChange); - window.addEventListener("resize", handleViewportChange); - - const input = inputBoxRef.current; - const resizeObserver = input - ? new ResizeObserver(handleViewportChange) - : null; - if (input) { - resizeObserver?.observe(input); - } - - return () => { - viewport?.removeEventListener("resize", handleViewportChange); - window.removeEventListener("resize", handleViewportChange); - resizeObserver?.disconnect(); - }; - }, [updateMaxHeight]); - // Render if (!hasValidChatUser) { return ( @@ -275,7 +200,7 @@ export default function Screen() {
- +
); })}