This commit is contained in:
2026-08-22 13:03:25 +02:00
parent e5ffc51189
commit f7b7b5b637
+101 -9
View File
@@ -14,6 +14,7 @@ import {
ContextMenuItem, ContextMenuItem,
ContextMenuTrigger, ContextMenuTrigger,
} from "@/components/ui/context-menu"; } from "@/components/ui/context-menu";
import { Textarea } from "@/components/ui/textarea";
export default function TextChannel({ export default function TextChannel({
appRef, appRef,
@@ -28,10 +29,9 @@ export default function TextChannel({
const account = appRef.current?.getAccount(); const account = appRef.current?.getAccount();
const userPubKey = const secretKey = (account && base58.decode(account.privateKey)) || null;
(account &&
base58.encode(ed.getPublicKey(base58.decode(account.privateKey)))) || const userPubKey = secretKey && base58.encode(ed.getPublicKey(secretKey));
null;
useEffect(() => { useEffect(() => {
if (!intersectionRef.current) return; if (!intersectionRef.current) return;
@@ -88,6 +88,7 @@ export default function TextChannel({
message={message} message={message}
channel={channel} channel={channel}
userPubKey={userPubKey} userPubKey={userPubKey}
secretKey={secretKey}
/> />
))} ))}
<div ref={intersectionRef} /> <div ref={intersectionRef} />
@@ -116,28 +117,36 @@ export default function TextChannel({
export function TextMessage({ export function TextMessage({
appRef, appRef,
message, message,
channel, channel,
userPubKey, userPubKey,
secretKey,
}: { }: {
appRef: React.RefObject<Enclave<ChannelPageProps> | null>; appRef: React.RefObject<Enclave<ChannelPageProps> | null>;
message: StoredMessage; message: StoredMessage;
channel: Channel; channel: Channel;
userPubKey: string | null; userPubKey: string | null;
secretKey: Uint8Array | null;
}) { }) {
const time = new Date(message.timestamp).toLocaleTimeString([], { const time = new Date(message.timestamp).toLocaleTimeString([], {
hour: "numeric", hour: "numeric",
minute: "2-digit", minute: "2-digit",
}); });
const author = appRef.current?.server?.users[message.author]; const author = appRef.current?.server?.users[message.author];
const [verified, setVerified] = useState(true); const [verified, setVerified] = useState(true);
const [editing, setEditing] = useState(false);
const [draft, setDraft] = useState(message.content);
useEffect(() => { useEffect(() => {
const serverPubKey = const serverPubKey =
appRef.current?.server?.serverPublicKey && appRef.current?.server?.serverPublicKey &&
base58.encode(appRef.current?.server?.serverPublicKey); base58.encode(appRef.current.server.serverPublicKey);
if (!serverPubKey) return; if (!serverPubKey) return;
const authorPubKey = base58.decode(message.author); const authorPubKey = base58.decode(message.author);
setVerified( setVerified(
ed.verify( ed.verify(
base58.decode(message.signature), base58.decode(message.signature),
@@ -159,6 +168,56 @@ export function TextMessage({
}); });
} }
function startEditing() {
setDraft(message.content);
setEditing(true);
}
function cancelEditing() {
setDraft(message.content);
setEditing(false);
}
function handleSave() {
const content = draft.trim();
if (
!content ||
content === message.content ||
!secretKey ||
!appRef.current?.server?.serverPublicKey
) {
cancelEditing();
return;
}
const signature = new TextEncoder().encode(
`${message.timestamp}@${base58.encode(appRef.current.server?.serverPublicKey)}@${content}`,
);
appRef.current?.server?.websocket?.send({
method: "EditMessage",
message_id: message.id,
channel_id: channel.id,
signature: base58.encode(ed.sign(signature, secretKey)),
content,
});
setEditing(false);
}
function handleKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>) {
if (e.key === "Escape") {
e.preventDefault();
cancelEditing();
}
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSave();
}
}
const content = ( const content = (
<div className="rounded-lg flex gap-3 px-3 py-3 hover:bg-muted/40"> <div className="rounded-lg flex gap-3 px-3 py-3 hover:bg-muted/40">
<Avatar className="h-10 w-10"> <Avatar className="h-10 w-10">
@@ -168,6 +227,7 @@ export function TextMessage({
message.author.slice(0, 2).toUpperCase()} message.author.slice(0, 2).toUpperCase()}
</AvatarFallback> </AvatarFallback>
</Avatar> </Avatar>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-sm font-medium flex items-center gap-1"> <span className="text-sm font-medium flex items-center gap-1">
@@ -176,30 +236,62 @@ export function TextMessage({
)} )}
{author?.displayName || message.author.slice(0, 8)} {author?.displayName || message.author.slice(0, 8)}
</span> </span>
<span className="text-xs text-muted-foreground">{time}</span> <span className="text-xs text-muted-foreground">{time}</span>
{message.is_edited && (
{message.is_edited && !editing && (
<span className="text-xs text-muted-foreground">(edited)</span> <span className="text-xs text-muted-foreground">(edited)</span>
)} )}
</div> </div>
{editing ? (
<div className="mt-2">
<Textarea
value={draft}
onChange={(e) => setDraft(e.target.value)}
onKeyDown={handleKeyDown}
autoFocus
className="min-h-20 resize-none"
/>
<div className="mt-2 flex items-center gap-2">
<Button size="sm" onClick={handleSave}>
Save
</Button>
<Button size="sm" variant="ghost" onClick={cancelEditing}>
Cancel
</Button>
<span className="text-xs text-muted-foreground">
Enter to save · Shift + Enter for new line · Esc to cancel
</span>
</div>
</div>
) : (
<p className="whitespace-pre-wrap break-words text-sm leading-snug"> <p className="whitespace-pre-wrap break-words text-sm leading-snug">
{message.content} {message.content}
</p> </p>
)}
</div> </div>
</div> </div>
); );
if (!isOwnMessage) { // Don't allow the context menu while editing
if (!isOwnMessage || editing) {
return content; return content;
} }
return ( return (
<ContextMenu> <ContextMenu>
<ContextMenuTrigger>{content}</ContextMenuTrigger> <ContextMenuTrigger>{content}</ContextMenuTrigger>
<ContextMenuContent> <ContextMenuContent>
<ContextMenuItem onClick={() => {}}> <ContextMenuItem onClick={startEditing}>
<PencilIcon className="mr-2 h-4 w-4" /> <PencilIcon className="mr-2 h-4 w-4" />
Edit Edit
</ContextMenuItem> </ContextMenuItem>
<ContextMenuItem onClick={handleDelete} variant="destructive"> <ContextMenuItem onClick={handleDelete} variant="destructive">
<Trash2Icon className="mr-2 h-4 w-4" /> <Trash2Icon className="mr-2 h-4 w-4" />
Delete Delete