158 lines
4.5 KiB
TypeScript
158 lines
4.5 KiB
TypeScript
import { useCrypto } from "@tensamin/crypto/context";
|
|
import * as React from "react";
|
|
import { useChat } from "../context";
|
|
import type { RawMessage } from "../values";
|
|
import { log } from "@tensamin/shared/log";
|
|
import Text from "@tensamin/markdown/text";
|
|
import { AlertTriangle, Clock } from "lucide-react";
|
|
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipTrigger,
|
|
} from "@tensamin/ui/cmp/tooltip";
|
|
import { useIsMobile } from "@tensamin/ui/utils";
|
|
import {
|
|
ContextMenu,
|
|
ContextMenuContent,
|
|
ContextMenuItem,
|
|
ContextMenuTrigger,
|
|
} from "@tensamin/ui/cmp/context-menu";
|
|
|
|
function generateFixedLoadingSize(size: number, height: number) {
|
|
return size * 3 - (height / 20) * 11;
|
|
}
|
|
|
|
/**
|
|
* Executes Message.
|
|
* @param props Parameter props.
|
|
* @returns unknown.
|
|
*/
|
|
function MessageComponent(props: {
|
|
message: RawMessage & {
|
|
failed?: boolean;
|
|
};
|
|
notEncrypted?: boolean;
|
|
measureRef?: React.Ref<HTMLDivElement>;
|
|
}) {
|
|
const { sharedSecret } = useChat();
|
|
const { decrypt } = useCrypto();
|
|
|
|
const isMobile = useIsMobile();
|
|
|
|
const [decodedContent, setDecodedContent] = React.useState("");
|
|
const [isReady, setIsReady] = React.useState(false);
|
|
|
|
React.useEffect(() => {
|
|
if (props.notEncrypted) {
|
|
setDecodedContent(props.message.content);
|
|
setIsReady(true);
|
|
return;
|
|
}
|
|
|
|
const content = props.message.content;
|
|
const secret = sharedSecret;
|
|
let active = true;
|
|
|
|
if (!secret) {
|
|
setIsReady(false);
|
|
return;
|
|
}
|
|
|
|
decrypt(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, props.message.content, props.notEncrypted, sharedSecret]);
|
|
|
|
return (
|
|
<div
|
|
ref={props.measureRef}
|
|
className={`w-full flex justify-start transition-opacity duration-150 ${(props.message.failed || props.message.message_state === "awaiting") && "opacity-50"}`}
|
|
>
|
|
<ContextMenu>
|
|
<ContextMenuTrigger
|
|
render={
|
|
<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 ${
|
|
props.message.sent_by_self
|
|
? props.message.failed
|
|
? "bg-destructive/75 text-destructive-foreground"
|
|
: "bg-primary text-primary-foreground"
|
|
: "bg-muted"
|
|
} ${!isReady && "animate-pulse"} ${isMobile && "select-none"}`}
|
|
>
|
|
{props.message.failed &&
|
|
props.message.message_state === "awaiting" && (
|
|
<Tooltip>
|
|
<TooltipContent>
|
|
<p>Failed to send message</p>
|
|
</TooltipContent>
|
|
<TooltipTrigger render={<AlertTriangle size={17} />} />
|
|
</Tooltip>
|
|
)}
|
|
{!props.message.failed &&
|
|
props.message.message_state === "awaiting" && (
|
|
<Clock size={17} />
|
|
)}
|
|
{isReady ? (
|
|
<Text value={decodedContent} />
|
|
) : (
|
|
<div
|
|
className="h-8"
|
|
style={{
|
|
width: generateFixedLoadingSize(
|
|
props.message.content.length,
|
|
props.message.height,
|
|
),
|
|
height: props.message.height,
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
}
|
|
/>
|
|
<ContextMenuContent>
|
|
<ContextMenuItem>
|
|
<Text value={decodedContent} />
|
|
</ContextMenuItem>
|
|
</ContextMenuContent>
|
|
</ContextMenu>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default React.memo(MessageComponent, (prev, next) => {
|
|
return (
|
|
prev.notEncrypted === next.notEncrypted &&
|
|
prev.measureRef === next.measureRef &&
|
|
prev.message.timestamp === next.message.timestamp &&
|
|
prev.message.content === next.message.content &&
|
|
prev.message.height === next.message.height &&
|
|
prev.message.sent_by_self === next.message.sent_by_self &&
|
|
prev.message.message_state === next.message.message_state &&
|
|
prev.message.failed === next.message.failed
|
|
);
|
|
});
|