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,
ContextMenuTrigger,
} from "@/components/ui/context-menu";
import { Textarea } from "@/components/ui/textarea";
export default function TextChannel({
appRef,
@@ -28,10 +29,9 @@ export default function TextChannel({
const account = appRef.current?.getAccount();
const userPubKey =
(account &&
base58.encode(ed.getPublicKey(base58.decode(account.privateKey)))) ||
null;
const secretKey = (account && base58.decode(account.privateKey)) || null;
const userPubKey = secretKey && base58.encode(ed.getPublicKey(secretKey));
useEffect(() => {
if (!intersectionRef.current) return;
@@ -88,6 +88,7 @@ export default function TextChannel({
message={message}
channel={channel}
userPubKey={userPubKey}
secretKey={secretKey}
/>
))}
<div ref={intersectionRef} />
@@ -116,28 +117,36 @@ export default function TextChannel({
export function TextMessage({
appRef,
message,
channel,
userPubKey,
secretKey,
}: {
appRef: React.RefObject<Enclave<ChannelPageProps> | null>;
message: StoredMessage;
channel: Channel;
userPubKey: string | null;
secretKey: Uint8Array | null;
}) {
const time = new Date(message.timestamp).toLocaleTimeString([], {
hour: "numeric",
minute: "2-digit",
});
const author = appRef.current?.server?.users[message.author];
const [verified, setVerified] = useState(true);
const [editing, setEditing] = useState(false);
const [draft, setDraft] = useState(message.content);
useEffect(() => {
const serverPubKey =
appRef.current?.server?.serverPublicKey &&
base58.encode(appRef.current?.server?.serverPublicKey);
base58.encode(appRef.current.server.serverPublicKey);
if (!serverPubKey) return;
const authorPubKey = base58.decode(message.author);
setVerified(
ed.verify(
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 = (
<div className="rounded-lg flex gap-3 px-3 py-3 hover:bg-muted/40">
<Avatar className="h-10 w-10">
@@ -168,6 +227,7 @@ export function TextMessage({
message.author.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<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)}
</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>
)}
</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">
{message.content}
</p>
)}
</div>
</div>
);
if (!isOwnMessage) {
// Don't allow the context menu while editing
if (!isOwnMessage || editing) {
return content;
}
return (
<ContextMenu>
<ContextMenuTrigger>{content}</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem onClick={() => {}}>
<ContextMenuItem onClick={startEditing}>
<PencilIcon className="mr-2 h-4 w-4" />
Edit
</ContextMenuItem>
<ContextMenuItem onClick={handleDelete} variant="destructive">
<Trash2Icon className="mr-2 h-4 w-4" />
Delete