Fixed lint problems, fixed builds
This commit is contained in:
parent
39aa70a9a6
commit
f018dafa4f
7 changed files with 87 additions and 61 deletions
|
|
@ -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) => (
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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: "",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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}</>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue