diff --git a/package-lock.json b/package-lock.json index 0d2a329..6a01c88 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@lexical/rich-text": "^0.34.0", "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-context-menu": "^2.2.16", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.7", @@ -38,7 +39,7 @@ "js-cookie": "^3.0.5", "lexical": "^0.34.0", "lucide-react": "^0.541.0", - "next": "15.5.0", + "next": "15.5.7", "next-themes": "^0.4.6", "pg": "^8.16.3", "react": "19.1.0", @@ -883,15 +884,15 @@ } }, "node_modules/@next/env": { - "version": "15.5.0", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.0.tgz", - "integrity": "sha512-sDaprBAfzCQiOgo2pO+LhnV0Wt2wBgartjrr+dpcTORYVnnXD0gwhHhiiyIih9hQbq+JnbqH4odgcFWhqCGidw==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.7.tgz", + "integrity": "sha512-4h6Y2NyEkIEN7Z8YxkA27pq6zTkS09bUSYC0xjd0NpwFxjnIKeZEeH591o5WECSmjpUhLn3H2QLJcDye3Uzcvg==", "license": "MIT" }, "node_modules/@next/swc-darwin-arm64": { - "version": "15.5.0", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.0.tgz", - "integrity": "sha512-v7Jj9iqC6enxIRBIScD/o0lH7QKvSxq2LM8UTyqJi+S2w2QzhMYjven4vgu/RzgsdtdbpkyCxBTzHl/gN5rTRg==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.7.tgz", + "integrity": "sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw==", "cpu": [ "arm64" ], @@ -905,9 +906,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "15.5.0", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.0.tgz", - "integrity": "sha512-s2Nk6ec+pmYmAb/utawuURy7uvyYKDk+TRE5aqLRsdnj3AhwC9IKUBmhfnLmY/+P+DnwqpeXEFIKe9tlG0p6CA==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.7.tgz", + "integrity": "sha512-UP6CaDBcqaCBuiq/gfCEJw7sPEoX1aIjZHnBWN9v9qYHQdMKvCKcAVs4OX1vIjeE+tC5EIuwDTVIoXpUes29lg==", "cpu": [ "x64" ], @@ -921,9 +922,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.5.0", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.0.tgz", - "integrity": "sha512-mGlPJMZReU4yP5fSHjOxiTYvZmwPSWn/eF/dcg21pwfmiUCKS1amFvf1F1RkLHPIMPfocxLViNWFvkvDB14Isg==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.7.tgz", + "integrity": "sha512-NCslw3GrNIw7OgmRBxHtdWFQYhexoUCq+0oS2ccjyYLtcn1SzGzeM54jpTFonIMUjNbHmpKpziXnpxhSWLcmBA==", "cpu": [ "arm64" ], @@ -937,9 +938,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.5.0", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.0.tgz", - "integrity": "sha512-biWqIOE17OW/6S34t1X8K/3vb1+svp5ji5QQT/IKR+VfM3B7GvlCwmz5XtlEan2ukOUf9tj2vJJBffaGH4fGRw==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.7.tgz", + "integrity": "sha512-nfymt+SE5cvtTrG9u1wdoxBr9bVB7mtKTcj0ltRn6gkP/2Nu1zM5ei8rwP9qKQP0Y//umK+TtkKgNtfboBxRrw==", "cpu": [ "arm64" ], @@ -953,9 +954,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.5.0", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.0.tgz", - "integrity": "sha512-zPisT+obYypM/l6EZ0yRkK3LEuoZqHaSoYKj+5jiD9ESHwdr6QhnabnNxYkdy34uCigNlWIaCbjFmQ8FY5AlxA==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.7.tgz", + "integrity": "sha512-hvXcZvCaaEbCZcVzcY7E1uXN9xWZfFvkNHwbe/n4OkRhFWrs1J1QV+4U1BN06tXLdaS4DazEGXwgqnu/VMcmqw==", "cpu": [ "x64" ], @@ -969,9 +970,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "15.5.0", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.0.tgz", - "integrity": "sha512-+t3+7GoU9IYmk+N+FHKBNFdahaReoAktdOpXHFIPOU1ixxtdge26NgQEEkJkCw2dHT9UwwK5zw4mAsURw4E8jA==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.7.tgz", + "integrity": "sha512-4IUO539b8FmF0odY6/SqANJdgwn1xs1GkPO5doZugwZ3ETF6JUdckk7RGmsfSf7ws8Qb2YB5It33mvNL/0acqA==", "cpu": [ "x64" ], @@ -985,9 +986,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.5.0", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.0.tgz", - "integrity": "sha512-d8MrXKh0A+c9DLiy1BUFwtg3Hu90Lucj3k6iKTUdPOv42Ve2UiIG8HYi3UAb8kFVluXxEfdpCoPPCSODk5fDcw==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.7.tgz", + "integrity": "sha512-CpJVTkYI3ZajQkC5vajM7/ApKJUOlm6uP4BknM3XKvJ7VXAvCqSjSLmM0LKdYzn6nBJVSjdclx8nYJSa3xlTgQ==", "cpu": [ "arm64" ], @@ -1001,9 +1002,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.5.0", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.0.tgz", - "integrity": "sha512-Fe1tGHxOWEyQjmygWkkXSwhFcTJuimrNu52JEuwItrKJVV4iRjbWp9I7zZjwqtiNnQmxoEvoisn8wueFLrNpvQ==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.7.tgz", + "integrity": "sha512-gMzgBX164I6DN+9/PGA+9dQiwmTkE4TloBNx8Kv9UiGARsr9Nba7IpcBRA1iTV9vwlYnrE3Uy6I7Aj6qLjQuqw==", "cpu": [ "x64" ], @@ -1164,6 +1165,34 @@ } } }, + "node_modules/@radix-ui/react-context-menu": { + "version": "2.2.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz", + "integrity": "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-dialog": { "version": "1.1.15", "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", @@ -4458,12 +4487,13 @@ } }, "node_modules/next": { - "version": "15.5.0", - "resolved": "https://registry.npmjs.org/next/-/next-15.5.0.tgz", - "integrity": "sha512-N1lp9Hatw3a9XLt0307lGB4uTKsXDhyOKQo7uYMzX4i0nF/c27grcGXkLdb7VcT8QPYLBa8ouIyEoUQJ2OyeNQ==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.7.tgz", + "integrity": "sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ==", + "deprecated": "This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/security-update-2025-12-11 for more details.", "license": "MIT", "dependencies": { - "@next/env": "15.5.0", + "@next/env": "15.5.7", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", @@ -4476,14 +4506,14 @@ "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "15.5.0", - "@next/swc-darwin-x64": "15.5.0", - "@next/swc-linux-arm64-gnu": "15.5.0", - "@next/swc-linux-arm64-musl": "15.5.0", - "@next/swc-linux-x64-gnu": "15.5.0", - "@next/swc-linux-x64-musl": "15.5.0", - "@next/swc-win32-arm64-msvc": "15.5.0", - "@next/swc-win32-x64-msvc": "15.5.0", + "@next/swc-darwin-arm64": "15.5.7", + "@next/swc-darwin-x64": "15.5.7", + "@next/swc-linux-arm64-gnu": "15.5.7", + "@next/swc-linux-arm64-musl": "15.5.7", + "@next/swc-linux-x64-gnu": "15.5.7", + "@next/swc-linux-x64-musl": "15.5.7", + "@next/swc-win32-arm64-msvc": "15.5.7", + "@next/swc-win32-x64-msvc": "15.5.7", "sharp": "^0.34.3" }, "peerDependencies": { diff --git a/package.json b/package.json index 107b8ec..8909f37 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@lexical/rich-text": "^0.34.0", "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-context-menu": "^2.2.16", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.7", diff --git a/src/app/chat/[id]/page.tsx b/src/app/chat/[id]/page.tsx index d62c919..84d1259 100644 --- a/src/app/chat/[id]/page.tsx +++ b/src/app/chat/[id]/page.tsx @@ -19,13 +19,13 @@ export default function DMs() { useEffect(() => { async function connectTargetNode() { if (!target) return; - auth( - target.node_address, - targetNode, - () => {}, - app.addMessage, - () => {} - ); + auth({ + id: target.node_address, + wsRef: targetNode, + addMessage: app.addMessage, + deleteMessage: app.deleteMessage, + editMessage: app.editMessage, + }); } connectTargetNode(); @@ -42,16 +42,15 @@ export default function DMs() { (m.channel_id === id && m.from === app.profile?.id) || (m.from === id && m.channel_id === app.profile?.id) )} - sendMessage={(message) => { - targetNode.current?.send( - JSON.stringify({ - type: "send_message", - params: { - channel_id: id, - contents: message, - }, - }) - ); + sendRequest={(req) => { + switch (req.type) { + case "send_message": + targetNode.current?.send(JSON.stringify(req)); + break; + default: + app.node.current?.send(JSON.stringify(req)); + break; + } }} /> diff --git a/src/app/server/[ip]/page.tsx b/src/app/server/[ip]/page.tsx index 901e145..e10152d 100644 --- a/src/app/server/[ip]/page.tsx +++ b/src/app/server/[ip]/page.tsx @@ -16,20 +16,23 @@ export default function Server() { const searchParams = useSearchParams(); const serverRef = useRef(null); const app = useApp(); - const { messages, addMessage } = useServerMessages(); + const { messages, addMessage, editMessage, deleteMessage } = + useServerMessages(); useEffectOnceWhenReady( () => { if (!ip) return; const msgs = messages[ip]; - auth( - ip, - serverRef, - app.setServer, - (m) => addMessage(ip, m), - () => {}, - msgs ? msgs[msgs.length - 1]?.id : undefined - ); + auth({ + id: ip, + wsRef: serverRef, + setServer: app.setServer, + lastMessage: msgs ? msgs[msgs.length - 1]?.id : undefined, + + addMessage: (m) => addMessage(ip, m), + editMessage: (msg_id, contents) => editMessage(ip, msg_id, contents), + deleteMessage: (msg_id) => deleteMessage(ip, msg_id), + }); }, [ip, messages], [undefined, (v) => v] diff --git a/src/components/app/AppLayout.tsx b/src/components/app/AppLayout.tsx index cac3790..4a2d628 100644 --- a/src/components/app/AppLayout.tsx +++ b/src/components/app/AppLayout.tsx @@ -7,7 +7,7 @@ import { toast } from "sonner"; import auth from "@/lib/auth"; import { useIsMobile } from "@/hooks/is-mobile"; import App from "@/types/app"; -import { Server, Message } from "@/types/types"; +import { Message } from "@/types/types"; import SidebarServers from "./SidebarServers"; import SidebarDMs from "./SidebarDMs"; @@ -31,12 +31,10 @@ export default function AppLayout({ useEffect(() => { if (!app.profile?.node_address) return; - auth( - app.profile.node_address, - app.node, - () => {}, - app.addMessage, - (m) => { + auth({ + id: app.profile.node_address, + wsRef: app.node, + onNewMessage: (m) => { if (m.from !== chatWith && m.from !== app.profile?.id) toast( `New message from ${app.profiles[m.from]?.display_name || m.from}`, @@ -52,8 +50,12 @@ export default function AppLayout({ if (onNewMessage) onNewMessage(m); }, - app.messages?.[app.messages.length - 1]?.id - ); + lastMessage: app.messages?.[app.messages.length - 1]?.id, + + addMessage: app.addMessage, + deleteMessage: app.deleteMessage, + editMessage: app.editMessage, + }); app.setProfiles((prev) => { if (app.profile) prev[app.profile.id] = app.profile; diff --git a/src/components/channel/ChannelView.tsx b/src/components/channel/ChannelView.tsx index d77542e..68d563c 100644 --- a/src/components/channel/ChannelView.tsx +++ b/src/components/channel/ChannelView.tsx @@ -30,17 +30,7 @@ export default function ChannelView({ (m) => m.channel_id === app.currentChannel?.id ) || [] } - sendMessage={(message) => { - serverRef.current?.send( - JSON.stringify({ - type: "send_message", - params: { - channel_id: app.currentChannel?.id, - contents: message, - }, - }) - ); - }} + sendRequest={(req) => serverRef.current?.send(JSON.stringify(req))} /> ); diff --git a/src/components/channel/MessageBox.tsx b/src/components/channel/MessageBox.tsx index 73d7f61..1ccbb59 100644 --- a/src/components/channel/MessageBox.tsx +++ b/src/components/channel/MessageBox.tsx @@ -11,126 +11,295 @@ import { DropdownMenuTrigger, } from "../ui/dropdown-menu"; 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 ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import { UserProfile } from "@/hooks/get-user"; import ProfilePicture from "../ProfilePicture"; -import App from "@/types/app"; +import App, { SetState } from "@/types/app"; import { Textarea } from "../ui/textarea"; +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuTrigger, +} from "@/components/ui/context-menu"; +import { ClientMessage } from "@/types/protocol"; +import { AnimatePresence, motion } from "framer-motion"; -function MessageContainer({ message, app }: { message: Message; app: App }) { +function MessageContainer({ + message, + app, + sendRequest, +}: { + message: Message; + app: App; + sendRequest: (content: ClientMessage) => void; +}) { const [profile, setProfile] = useState(); + 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 ( -
-
- {/*
*/} - -
- - {profile?.display_name || message.from} + + +
+
+ {/*
*/} + +
+ + {profile?.display_name || message.from} -

- {isToday(message.timestamp * 1000) - ? format(message.timestamp * 1000, "p") - : isYesterday(message.timestamp * 1000) - ? "Yesterday, " + format(message.timestamp * 1000, "p") - : format(message.timestamp * 1000, "PP, p")} -

-
+

+ {isToday(message.timestamp * 1000) + ? format(message.timestamp * 1000, "p") + : isYesterday(message.timestamp * 1000) + ? "Yesterday, " + format(message.timestamp * 1000, "p") + : format(message.timestamp * 1000, "PP, p")} +

+ -
- {message.contents.split("\n\n").map((line, index) => ( -
- {index > 0 &&
} - ( -

- ), - h2: ({ node, ...props }) => ( -

- ), - h3: ({ node, ...props }) => ( -

- ), - h4: ({ node, ...props }) => ( -

- ), - h5: ({ node, ...props }) => ( -

- ), - ul: ({ node, ...props }) => ( -
-
-
+ + + Copy message + { + setEditText(message.contents); + setIsEditing(true); + }} + disabled={message.from !== app.profile?.id} + > + Edit + + { + sendRequest({ + type: "delete_message", + params: { + message_id: message.id, + }, + }); + }} + disabled={message.from !== app.profile?.id} + > + Delete + + + + ); +} + +function MessageInput({ + text, + setText, + onEnter, +}: { + text: string; + setText: SetState; + onEnter: () => void; +}) { + const inputRef = useRef(null); + + useEffect(() => { + inputRef.current?.focus(); + }, [inputRef]); + + return text.includes("\n") ? ( +