Fixed lint problems, fixed builds

This commit is contained in:
Alois 2026-04-12 00:20:24 +02:00
commit f018dafa4f
7 changed files with 87 additions and 61 deletions

View file

@ -62,7 +62,7 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
</Button> </Button>
</> </>
)} )}
{pathname === "/chat" && ( {pathname === "/chat" && id && (
<Wrapper <Wrapper
userId={id} userId={id}
component={(user) => ( component={(user) => (

View file

@ -22,6 +22,8 @@ export default function List() {
const scrollRef = React.useRef<HTMLDivElement | null>(null); const scrollRef = React.useRef<HTMLDivElement | null>(null);
// TanStack Virtual is intentionally used here; React Compiler memoization is skipped.
// eslint-disable-next-line react-hooks/incompatible-library
const virtualizer = useVirtualizer({ const virtualizer = useVirtualizer({
count: items?.length || 0, count: items?.length || 0,
estimateSize: () => 60, estimateSize: () => 60,

View file

@ -36,7 +36,7 @@ function getSafeMessageHeight(height: number | undefined) {
* @param props Parameter props. * @param props Parameter props.
* @returns unknown. * @returns unknown.
*/ */
function MessageComponent(props: { function MessageComponent({ message, notEncrypted, measureRef }: {
message: RawMessage & { message: RawMessage & {
failed?: boolean; failed?: boolean;
}; };
@ -50,16 +50,16 @@ function MessageComponent(props: {
const [decodedContent, setDecodedContent] = React.useState(""); const [decodedContent, setDecodedContent] = React.useState("");
const [isReady, setIsReady] = React.useState(false); const [isReady, setIsReady] = React.useState(false);
const safeMessageHeight = getSafeMessageHeight(props.message.height); const safeMessageHeight = getSafeMessageHeight(message.height);
React.useEffect(() => { React.useEffect(() => {
if (props.notEncrypted) { if (notEncrypted) {
setDecodedContent(props.message.content); setDecodedContent(message.content);
setIsReady(true); setIsReady(true);
return; return;
} }
const content = props.message.content; const content = message.content;
const secret = sharedSecret; const secret = sharedSecret;
let active = true; let active = true;
@ -94,27 +94,27 @@ function MessageComponent(props: {
return () => { return () => {
active = false; active = false;
}; };
}, [decrypt, props.message.content, props.notEncrypted, sharedSecret]); }, [decrypt, message.content, notEncrypted, sharedSecret]);
return ( return (
<div <div
ref={props.measureRef} ref={measureRef}
className={`w-full flex justify-start transition-opacity duration-150 ${(props.message.failed || props.message.message_state === "awaiting") && "opacity-50"}`} className={`w-full flex justify-start transition-opacity duration-150 ${(message.failed || message.message_state === "awaiting") && "opacity-50"}`}
> >
<ContextMenu> <ContextMenu>
<ContextMenuTrigger <ContextMenuTrigger
render={ render={
<div <div
className={`selection-foreground flex gap-1 items-center justify-center max-w-[80%] rounded-lg px-2 py-px whitespace-pre-wrap break-all ${ className={`selection-foreground flex gap-1 items-center justify-center max-w-[80%] rounded-lg px-2 py-px whitespace-pre-wrap break-all ${
props.message.sent_by_self message.sent_by_self
? props.message.failed ? message.failed
? "bg-destructive/75 text-destructive-foreground" ? "bg-destructive/75 text-destructive-foreground"
: "bg-primary text-primary-foreground" : "bg-primary text-primary-foreground"
: "bg-muted" : "bg-muted"
} ${!isReady && "animate-pulse"} ${isMobile && "select-none"}`} } ${!isReady && "animate-pulse"} ${isMobile && "select-none"}`}
> >
{props.message.failed && {message.failed &&
props.message.message_state === "awaiting" && ( message.message_state === "awaiting" && (
<Tooltip> <Tooltip>
<TooltipContent> <TooltipContent>
<p>Failed to send message</p> <p>Failed to send message</p>
@ -122,8 +122,8 @@ function MessageComponent(props: {
<TooltipTrigger render={<AlertTriangle size={17} />} /> <TooltipTrigger render={<AlertTriangle size={17} />} />
</Tooltip> </Tooltip>
)} )}
{!props.message.failed && {!message.failed &&
props.message.message_state === "awaiting" && ( message.message_state === "awaiting" && (
<Clock size={17} /> <Clock size={17} />
)} )}
{isReady ? ( {isReady ? (
@ -133,7 +133,7 @@ function MessageComponent(props: {
className="block rounded-sm" className="block rounded-sm"
style={{ style={{
width: generateFixedLoadingSize( width: generateFixedLoadingSize(
props.message.content.length, message.content.length,
safeMessageHeight, safeMessageHeight,
), ),
minHeight: safeMessageHeight, minHeight: safeMessageHeight,

View file

@ -61,7 +61,13 @@ export default function Provider(props: { children: ReactNode }) {
const { send, subscribePush } = useTTP(); const { send, subscribePush } = useTTP();
const [liveMessagesState, setLiveMessagesState] = useState<LiveMessage[]>([]); const [liveMessagesState, setLiveMessagesState] = useState<LiveMessage[]>([]);
const [currentSharedSecret, setCurrentSharedSecret] = useState(""); const [currentSharedSecretState, setCurrentSharedSecretState] = useState<{
userId: number;
value: string;
}>({
userId: 0,
value: "",
});
const inputBoxRef = useRef<HTMLDivElement>(null); const inputBoxRef = useRef<HTMLDivElement>(null);
@ -75,16 +81,13 @@ export default function Provider(props: { children: ReactNode }) {
return Number(rawId ?? 0); return Number(rawId ?? 0);
}, [locationSearch]); }, [locationSearch]);
const userIdValueFromLastRender = useRef(userIdValue); const currentSharedSecret = useMemo(() => {
if (currentSharedSecretState.userId !== userIdValue) {
useEffect(() => { return "";
if (userIdValue === userIdValueFromLastRender.current) {
return;
} }
userIdValueFromLastRender.current = userIdValue; return currentSharedSecretState.value;
setCurrentSharedSecret(""); }, [currentSharedSecretState, userIdValue]);
}, [userIdValue]);
// Load shared secret // Load shared secret
useEffect(() => { useEffect(() => {
@ -105,11 +108,17 @@ export default function Provider(props: { children: ReactNode }) {
); );
if (active) { if (active) {
setCurrentSharedSecret(sharedSecret); setCurrentSharedSecretState({
userId: userIdValue,
value: sharedSecret,
});
} }
} catch { } catch {
if (active) { if (active) {
setCurrentSharedSecret(""); setCurrentSharedSecretState({
userId: userIdValue,
value: "",
});
} }
} }
})(); })();

View file

@ -100,6 +100,7 @@ export default function Screen() {
return messagesRef.current[index]?.timestamp ?? index; return messagesRef.current[index]?.timestamp ?? index;
}, []); }, []);
// eslint-disable-next-line react-hooks/incompatible-library
const virtualizer = useVirtualizer({ const virtualizer = useVirtualizer({
count: messages.length, count: messages.length,
getScrollElement: () => scrollRef.current, getScrollElement: () => scrollRef.current,

View file

@ -33,8 +33,28 @@ export const context = React.createContext<CryptoContextType | undefined>(
export default function Provider(props: { children: React.ReactNode }) { export default function Provider(props: { children: React.ReactNode }) {
const apiRef = React.useRef<ApiRef | null>(null); const apiRef = React.useRef<ApiRef | null>(null);
const value = React.useMemo( const value = React.useMemo<CryptoContextType>(
() => createCryptoActions(() => apiRef.current), () => ({
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. * 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. * @returns Typed crypto action functions.
*/ */
export function createCryptoActions( export function createCryptoActions(
getApiRef: () => ApiRef | null, apiRef: React.RefObject<ApiRef | null>,
): CryptoContextType { ): CryptoContextType {
/** /**
* Encrypts plaintext by delegating to the crypto worker API. * Encrypts plaintext by delegating to the crypto worker API.
@ -72,9 +92,9 @@ export function createCryptoActions(
secret: string, secret: string,
plaintext: string, plaintext: string,
): Promise<string> => { ): Promise<string> => {
const apiRef = getApiRef(); const api = apiRef.current;
if (!apiRef) throw new Error("API not initialized"); if (!api) throw new Error("API not initialized");
return await apiRef.encrypt(secret, plaintext); return await api.encrypt(secret, plaintext);
}; };
/** /**
@ -87,9 +107,9 @@ export function createCryptoActions(
secret: string, secret: string,
ciphertext: string, ciphertext: string,
): Promise<string> => { ): Promise<string> => {
const apiRef = getApiRef(); const api = apiRef.current;
if (!apiRef) throw new Error("API not initialized"); if (!api) throw new Error("API not initialized");
return await apiRef.decrypt(secret, ciphertext); return await api.decrypt(secret, ciphertext);
}; };
/** /**
@ -104,9 +124,9 @@ export function createCryptoActions(
ownPublicKey: string, ownPublicKey: string,
otherPublicKey: string, otherPublicKey: string,
): Promise<string> => { ): Promise<string> => {
const apiRef = getApiRef(); const api = apiRef.current;
if (!apiRef) throw new Error("API not initialized"); if (!api) throw new Error("API not initialized");
return await apiRef.getSharedSecret( return await api.getSharedSecret(
ownPrivateKey, ownPrivateKey,
ownPublicKey, ownPublicKey,
otherPublicKey, otherPublicKey,

View file

@ -6,7 +6,7 @@ import { useStorage } from "@tensamin/storage/context";
// Wrapper function to pass user data to some component // Wrapper function to pass user data to some component
export default function Wrapper(props: { export default function Wrapper(props: {
userId?: number | "own"; userId: number | "own";
loading: React.ReactNode; loading: React.ReactNode;
component: (user: User) => React.ReactNode; component: (user: User) => React.ReactNode;
}) { }) {
@ -15,40 +15,34 @@ export default function Wrapper(props: {
const [user, setUser] = useState<User | null>(null); const [user, setUser] = useState<User | null>(null);
useEffect(() => { useEffect(() => {
if (props.userId == null) { if (props.userId == null) return;
setUser(failedUser);
return;
}
let active = true; let active = true;
if (props.userId === "own") { void (async () => {
load("user_id") try {
.then((id) => { const userId =
get(id) props.userId === "own" ? await load("user_id") : props.userId;
.then((user) => (active ? setUser(user) : null)) const value = await get(userId);
.catch(() => setUser(failedUser));
})
.catch(() => setUser(failedUser));
return;
}
get(props.userId)
.then((value) => {
if (active) { if (active) {
setUser(value); setUser(value);
} }
}) } catch {
.catch(() => {
if (active) { if (active) {
setUser(failedUser); setUser(failedUser);
} }
}); }
})();
return () => { return () => {
active = false; active = false;
}; };
}, [load, get, props.userId]); }, [load, get, props.userId]);
if (props.userId == null) {
return <>{props.component(failedUser)}</>;
}
return <>{user ? props.component(user) : props.loading}</>; return <>{user ? props.component(user) : props.loading}</>;
} }