Message edit

This commit is contained in:
2025-12-22 14:16:19 +01:00
parent f32b8aebe5
commit 9975ac17a6
2 changed files with 186 additions and 120 deletions
+124 -59
View File
@@ -11,13 +11,13 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from "../ui/dropdown-menu"; } from "../ui/dropdown-menu";
import { ChevronLeftIcon, SendIcon, SmilePlusIcon } from "lucide-react"; import { ChevronLeftIcon, SendIcon, SmilePlusIcon } from "lucide-react";
import { useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { format, isToday, isYesterday } from "date-fns"; import { format, isToday, isYesterday } from "date-fns";
import ReactMarkdown from "react-markdown"; import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm"; import remarkGfm from "remark-gfm";
import { UserProfile } from "@/hooks/get-user"; import { UserProfile } from "@/hooks/get-user";
import ProfilePicture from "../ProfilePicture"; import ProfilePicture from "../ProfilePicture";
import App from "@/types/app"; import App, { SetState } from "@/types/app";
import { Textarea } from "../ui/textarea"; import { Textarea } from "../ui/textarea";
import { import {
ContextMenu, ContextMenu,
@@ -46,8 +46,20 @@ function MessageContainer({
sendRequest: (content: ClientMessage) => void; sendRequest: (content: ClientMessage) => void;
}) { }) {
const [profile, setProfile] = useState<UserProfile | null>(); const [profile, setProfile] = useState<UserProfile | null>();
const [isEditing, setIsEditing] = useState(false);
const [editText, setEditText] = useState(message.contents);
app.getUserById(message.from).then(setProfile); useEffect(() => {
let mounted = true;
app.getUserById(message.from).then((p) => {
if (mounted) setProfile(p);
});
return () => {
mounted = false;
};
}, [app, message.from]);
return ( return (
<ContextMenu> <ContextMenu>
@@ -72,8 +84,33 @@ function MessageContainer({
</p> </p>
</span> </span>
<div className="text-foreground/85 flex flex-col break-words whitespace-pre-wrap"> <div
{message.contents.split("\n\n").map((line, index) => ( className="text-foreground/85 flex flex-col break-words whitespace-pre-wrap"
onKeyDown={(e) => {
if (e.key === "Escape" && isEditing) {
setIsEditing(false);
setEditText(message.contents);
}
}}
>
{isEditing ? (
<MessageInput
text={editText}
setText={setEditText}
onEnter={() => {
sendRequest({
type: "edit_message",
params: {
message_id: message.id,
new_contents: editText,
},
});
setIsEditing(false);
}}
/>
) : (
message.contents.split("\n\n").map((line, index) => (
<div key={index} className="h-max break-words"> <div key={index} className="h-max break-words">
{index > 0 && <br />} {index > 0 && <br />}
<ReactMarkdown <ReactMarkdown
@@ -134,7 +171,8 @@ function MessageContainer({
{line} {line}
</ReactMarkdown> </ReactMarkdown>
</div> </div>
))} ))
)}
</div> </div>
</div> </div>
</div> </div>
@@ -142,6 +180,16 @@ function MessageContainer({
</ContextMenuTrigger> </ContextMenuTrigger>
<ContextMenuContent className="w-52"> <ContextMenuContent className="w-52">
<ContextMenuItem inset>Copy message</ContextMenuItem> <ContextMenuItem inset>Copy message</ContextMenuItem>
<ContextMenuItem
inset
onClick={() => {
setEditText(message.contents);
setIsEditing(true);
}}
disabled={message.from !== app.profile?.id}
>
Edit
</ContextMenuItem>
<ContextMenuItem <ContextMenuItem
inset inset
variant="destructive" variant="destructive"
@@ -162,6 +210,75 @@ function MessageContainer({
); );
} }
function MessageInput({
text,
setText,
onEnter,
}: {
text: string;
setText: SetState<string>;
onEnter: () => void;
}) {
const inputRef = useRef<any>(null);
useEffect(() => {
inputRef.current?.focus();
}, [inputRef]);
return text.includes("\n") ? (
<Textarea
ref={inputRef}
className="resize-none"
placeholder="Type a message..."
value={text}
onChange={(e) => {
if (!e.target.value.includes("\n") && text.includes("\n")) {
setTimeout(() => {
if (inputRef.current) {
inputRef.current.selectionStart = text.length + 1;
inputRef.current.selectionEnd = text.length + 1;
inputRef.current.focus();
}
}, 10);
}
setText(e.target.value);
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
if (!e.shiftKey) {
e.preventDefault();
onEnter();
}
}
}}
/>
) : (
<Input
placeholder="Type a message..."
ref={inputRef}
value={text}
onChange={(e) => setText(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
if (e.shiftKey) {
setText((p) => p + "\n");
setTimeout(() => {
if (inputRef.current) {
inputRef.current.selectionStart = text.length + 1;
inputRef.current.selectionEnd = text.length + 1;
inputRef.current.focus();
}
}, 10);
} else {
onEnter();
}
}
}}
/>
);
}
export default function MessageBox({ export default function MessageBox({
app, app,
sendRequest, sendRequest,
@@ -176,7 +293,6 @@ export default function MessageBox({
messages: Message[]; messages: Message[];
}) { }) {
const [text, setText] = useState(""); const [text, setText] = useState("");
const inputRef = useRef<any>(null);
const sendMessage = () => { const sendMessage = () => {
if (!app.currentChannel) { if (!app.currentChannel) {
@@ -226,58 +342,7 @@ export default function MessageBox({
))} ))}
</div> </div>
<footer className="flex gap-3 pr-5 w-full h-max"> <footer className="flex gap-3 pr-5 w-full h-max">
{text.includes("\n") ? ( <MessageInput text={text} setText={setText} onEnter={sendMessage} />
<Textarea
ref={inputRef}
className="resize-none"
placeholder="Type a message..."
value={text}
onChange={(e) => {
if (!e.target.value.includes("\n") && text.includes("\n")) {
setTimeout(() => {
if (inputRef.current) {
inputRef.current.selectionStart = text.length + 1;
inputRef.current.selectionEnd = text.length + 1;
inputRef.current.focus();
}
}, 10);
}
setText(e.target.value);
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
if (!e.shiftKey) {
e.preventDefault();
sendMessage();
}
}
}}
/>
) : (
<Input
placeholder="Type a message..."
ref={inputRef}
value={text}
onChange={(e) => setText(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
if (e.shiftKey) {
setText((p) => p + "\n");
setTimeout(() => {
if (inputRef.current) {
inputRef.current.selectionStart = text.length + 1;
inputRef.current.selectionEnd = text.length + 1;
inputRef.current.focus();
}
}, 10);
} else {
sendMessage();
}
}
}}
/>
)}
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
+2 -1
View File
@@ -5,4 +5,5 @@ export interface MessageType<T = string, P = object> {
export type ClientMessage = export type ClientMessage =
| MessageType<"send_message", { channel_id: string; contents: string }> | MessageType<"send_message", { channel_id: string; contents: string }>
| MessageType<"delete_message", { message_id: number }>; | MessageType<"delete_message", { message_id: number }>
| MessageType<"edit_message", { message_id: number; new_contents: string }>;