Msg edit
This commit is contained in:
@@ -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>
|
||||||
<p className="whitespace-pre-wrap break-words text-sm leading-snug">
|
|
||||||
{message.content}
|
{editing ? (
|
||||||
</p>
|
<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>
|
||||||
</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
|
||||||
|
|||||||
Reference in New Issue
Block a user