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
+184 -119
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,69 +84,95 @@ 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"
<div key={index} className="h-max break-words"> onKeyDown={(e) => {
{index > 0 && <br />} if (e.key === "Escape" && isEditing) {
<ReactMarkdown setIsEditing(false);
remarkPlugins={[remarkGfm]} setEditText(message.contents);
components={{ }
h1: ({ node, ...props }) => ( }}
<h1 >
className="text-3xl font-bold my-2 break-words" {isEditing ? (
{...props} <MessageInput
/> text={editText}
), setText={setEditText}
h2: ({ node, ...props }) => ( onEnter={() => {
<h2 sendRequest({
className="text-2xl font-bold my-2 break-words" type: "edit_message",
{...props} params: {
/> message_id: message.id,
), new_contents: editText,
h3: ({ node, ...props }) => ( },
<h3 });
className="text-xl font-bold my-2 break-words"
{...props} setIsEditing(false);
/> }}
), />
h4: ({ node, ...props }) => ( ) : (
<h4 message.contents.split("\n\n").map((line, index) => (
className="text-lg font-bold my-2 break-words" <div key={index} className="h-max break-words">
{...props} {index > 0 && <br />}
/> <ReactMarkdown
), remarkPlugins={[remarkGfm]}
h5: ({ node, ...props }) => ( components={{
<h5 h1: ({ node, ...props }) => (
className="text-base font-bold my-2 break-words" <h1
{...props} className="text-3xl font-bold my-2 break-words"
/> {...props}
), />
ul: ({ node, ...props }) => ( ),
<ul h2: ({ node, ...props }) => (
className="list-disc list-inside my-2 break-words" <h2
{...props} className="text-2xl font-bold my-2 break-words"
/> {...props}
), />
ol: ({ node, ...props }) => ( ),
<ol h3: ({ node, ...props }) => (
className="list-decimal list-inside my-2 break-words" <h3
{...props} className="text-xl font-bold my-2 break-words"
/> {...props}
), />
a: ({ node, ...props }) => ( ),
<a h4: ({ node, ...props }) => (
className="text-accent-foreground underline underline-offset-2 break-all" <h4
target="_blank" className="text-lg font-bold my-2 break-words"
rel="noopener noreferrer" {...props}
{...props} />
/> ),
), h5: ({ node, ...props }) => (
}} <h5
> className="text-base font-bold my-2 break-words"
{line} {...props}
</ReactMarkdown> />
</div> ),
))} ul: ({ node, ...props }) => (
<ul
className="list-disc list-inside my-2 break-words"
{...props}
/>
),
ol: ({ node, ...props }) => (
<ol
className="list-decimal list-inside my-2 break-words"
{...props}
/>
),
a: ({ node, ...props }) => (
<a
className="text-accent-foreground underline underline-offset-2 break-all"
target="_blank"
rel="noopener noreferrer"
{...props}
/>
),
}}
>
{line}
</ReactMarkdown>
</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 }>;