From 980c74af2cc5f9d631c06b5cad6e218f0b84b440 Mon Sep 17 00:00:00 2001 From: Final Deterrence Date: Thu, 4 Sep 2025 15:49:49 +0800 Subject: [PATCH 1/4] change .env --- frontend/.env | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/.env b/frontend/.env index c005e86..ac56177 100644 --- a/frontend/.env +++ b/frontend/.env @@ -1,3 +1,3 @@ REACT_APP_BACKEND_URL=http://localhost:8888 -REACT_APP_HASURA_HTTPLINK=https://web-workshop.hasura.app/v1/graphql -REACT_APP_HASURA_WSLINK=wss://web-workshop.hasura.app/v1/graphql +REACT_APP_HASURA_HTTPLINK=http://localhost:23333/v1/graphql +REACT_APP_HASURA_WSLINK=ws://localhost:23333/v1/graphql From db0311de1d9ebec896c02c3d08165d49551dd804 Mon Sep 17 00:00:00 2001 From: Final Deterrence Date: Thu, 4 Sep 2025 17:25:56 +0800 Subject: [PATCH 2/4] reset .env to original --- frontend/.env | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/.env b/frontend/.env index ac56177..6b25edb 100644 --- a/frontend/.env +++ b/frontend/.env @@ -1,3 +1,3 @@ -REACT_APP_BACKEND_URL=http://localhost:8888 -REACT_APP_HASURA_HTTPLINK=http://localhost:23333/v1/graphql -REACT_APP_HASURA_WSLINK=ws://localhost:23333/v1/graphql +REACT_APP_BACKEND_URL=https://workshop.eesast.com +REACT_APP_HASURA_HTTPLINK=https://workshop.eesast.com/v1/graphql +REACT_APP_HASURA_WSLINK=wss://workshop.eesast.com/v1/graphql From 6d41937ed8287b6c4d8c727fa42904207e747b72 Mon Sep 17 00:00:00 2001 From: Final Deterrence Date: Thu, 4 Sep 2025 20:04:18 +0800 Subject: [PATCH 3/4] finish reply messages --- frontend/package.json | 1 + frontend/src/ChatBox.tsx | 173 ++++++++++++++++------- frontend/src/Components.tsx | 266 +++++++++++++++++++----------------- frontend/yarn.lock | 44 +++++- 4 files changed, 304 insertions(+), 180 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index b832b0d..df1a008 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,6 +10,7 @@ "jwt-decode": "4.0.0", "md5": "2.3.0", "react": "18.3.1", + "react-contextify": "^0.1.0", "react-dom": "18.3.1", "react-draggable": "4.4.6", "react-router-dom": "6.26.1" diff --git a/frontend/src/ChatBox.tsx b/frontend/src/ChatBox.tsx index b2619b7..b551505 100644 --- a/frontend/src/ChatBox.tsx +++ b/frontend/src/ChatBox.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from "react"; import { Button, Input, message, Spin } from "antd"; import { user } from "./getUser"; import * as graphql from "./graphql"; -import { Bubble, Card, Container, Scroll, Text } from "./Components"; +import { Bubble, Card, Container, Scroll, Text, ContextMenu } from "./Components"; interface ChatBoxProps { user: user | null; @@ -13,13 +13,20 @@ interface ChatBoxProps { const ChatBox: React.FC = ({ user, room, handleClose }) => { const [text, setText] = useState(""); const [loading, setLoading] = useState(false); + const [replyingMessage, setReplyingMessage] = + useState(null); + const [menuPosition, setMenuPosition] = + useState<{ x: number; y: number } | null>(null); + const [menuMessage, setMenuMessage] = + useState(null); + + const chatBoxRef = useRef(null); const { data, error } = graphql.useGetMessagesByRoomSubscription({ skip: !room, - variables: { - room_uuid: room?.uuid, - }, + variables: { room_uuid: room?.uuid }, }); + useEffect(() => { if (error) { console.error(error); @@ -33,20 +40,30 @@ const ChatBox: React.FC = ({ user, room, handleClose }) => { setLoading(true); if (!text) { message.error("消息不能为空!"); - return setLoading(false); + setLoading(false); + return; + } + + let contentToSend = text; + if (replyingMessage) { + const header = `↩︎ 回复 @${replyingMessage.user.username}: ${replyingMessage.content}`; + contentToSend = `${header}\n${text}`; } + const result = await addMessageMutation({ variables: { user_uuid: user?.uuid, room_uuid: room?.uuid, - content: text, + content: contentToSend, }, }); + if (result.errors) { console.error(result.errors); message.error("发送消息失败!"); } setText(""); + setReplyingMessage(null); setLoading(false); }; @@ -68,11 +85,19 @@ const ChatBox: React.FC = ({ user, room, handleClose }) => { ); - if (!user || !room) { - return null; - } + if (!user || !room) return null; + return ( - + @@ -82,23 +107,36 @@ const ChatBox: React.FC = ({ user, room, handleClose }) => { {room.intro} - -
+ + + { + e.preventDefault(); + setMenuPosition({ x: e.clientX, y: e.clientY }); + setMenuMessage(msg); + }} + /> + + + {replyingMessage && ( + + + 正在回复 @{replyingMessage.user.username}: {replyingMessage.content} + + + )} + +
setText(e.target.value)} - style={{ fontSize: "18px", height: "40px" }} + style={{ fontSize: "18px", height: "40px", flex: 1 }} />
+ + { + setMenuPosition(null); + setMenuMessage(null); + }} + items={[ + { + label: "回复", + onClick: () => { + if (menuMessage) setReplyingMessage(menuMessage); + }, + }, + ]} + /> ); }; @@ -113,45 +168,51 @@ const ChatBox: React.FC = ({ user, room, handleClose }) => { interface MessageFeedProps { user: user; messages: graphql.GetMessagesByRoomSubscription["message"] | undefined; + onRightClick: ( + e: React.MouseEvent, + msg: graphql.GetMessagesByRoomSubscription["message"][0] + ) => void; } -const MessageFeed: React.FC = ({ user, messages }) => { +const MessageFeed: React.FC = ({ user, messages, onRightClick }) => { const bottomRef = useRef(null); useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]); - return ( - - {messages ? ( - messages.map((message, index) => ( -
- -
- )) - ) : ( - - - - )} -
+ return messages ? ( + <> + {messages.map((message, index) => ( +
+ +
+ ))} + + ) : ( + + + ); }; interface MessageBubbleProps { user: user; message: graphql.GetMessagesByRoomSubscription["message"][0]; + onRightClick: ( + e: React.MouseEvent, + msg: graphql.GetMessagesByRoomSubscription["message"][0] + ) => void; } -const MessageBubble: React.FC = ({ user, message }) => { +const MessageBubble: React.FC = ({ user, message, onRightClick }) => { const isSelf = user.uuid === message.user.uuid; const dateUTC = new Date(message.created_at); - const date = new Date( - dateUTC.getTime() - dateUTC.getTimezoneOffset() * 60000 - ); + const date = new Date(dateUTC.getTime() - dateUTC.getTimezoneOffset() * 60000); + + const [firstLine, ...restLines] = message.content.split("\n"); + const isReplyLike = firstLine.startsWith("↩︎ 回复 @"); + const bodyText = isReplyLike ? restLines.join("\n") : message.content; + return (
= ({ user, message }) => { flexWrap: "nowrap", alignItems: isSelf ? "flex-end" : "flex-start", }} + onContextMenu={(e) => onRightClick(e, message)} >
{message.user.username} @@ -168,17 +230,32 @@ const MessageBubble: React.FC = ({ user, message }) => { {date.toLocaleString("zh-CN")}
+ + {isReplyLike && ( + + + {firstLine} + + + )} + - {message.content} + {bodyText}
); diff --git a/frontend/src/Components.tsx b/frontend/src/Components.tsx index 22f9d35..1d39653 100644 --- a/frontend/src/Components.tsx +++ b/frontend/src/Components.tsx @@ -1,30 +1,26 @@ +import React, { forwardRef } from "react"; import { Button as AntdButton, Typography } from "antd"; const { Text: AntdText, Link: AntdLink } = Typography; -export const Container: React.FC< - React.PropsWithChildren<{ style?: React.CSSProperties }> -> = ({ children, style }) => { - return ( -
- {children} -
- ); -}; +export const Container: React.FC> = ({ children, style }) => ( +
+ {children} +
+); -export const Card: React.FC< - React.PropsWithChildren<{ style?: React.CSSProperties }> -> = ({ children, style }) => { - return ( - >( + ({ children, style }, ref) => ( +
{children} - - ); -}; - -export const Bubble: React.FC< - React.PropsWithChildren<{ style?: React.CSSProperties }> -> = ({ children, style }) => { - return ( -
- {children}
- ); -}; + ) +); -export const fontFamilies = [ - "Times New Roman", - "Times", - "Nimbus Roman No9 L", - "Liberation Serif", - "FreeSerif", - "Hoefler Text", - "Microsoft YaHei", // 微软雅黑 - "Hiragino Sans GB", // 冬青黑体 - "WenQuanYi Micro Hei", // 文泉驿微米黑 - "STHeiti", // 华文黑体 - "sans-serif", // 无衬线 -]; +export const Bubble: React.FC> = ({ children, style }) => ( +
+ {children} +
+); export const Text: React.FC< - React.PropsWithChildren<{ - style?: React.CSSProperties; - size?: string; - editable?: any; - copyable?: any; - }> + React.PropsWithChildren<{ style?: React.CSSProperties; size?: string; editable?: any; copyable?: any }> > = ({ children, style, size, editable, copyable }) => { switch (size) { case "small": @@ -102,76 +75,111 @@ export const Text: React.FC< default: size = "18px"; } - return ( - - {children} - - ); + return {children}; }; -export const Link: React.FC< - React.PropsWithChildren<{ - style?: React.CSSProperties; - onClick?: () => void; - danger?: boolean; - }> -> = ({ children, style, onClick, danger }) => { - return ( - - {children} - - ); -}; +export const Link: React.FC void; danger?: boolean }>> = ({ + children, + style, + onClick, + danger, +}) => ( + + {children} + +); + +export const Button: React.FC void }>> = ({ + children, + style, + onClick, +}) => ( + + {children} + +); + +export const Scroll: React.FC> = ({ children, style }) => ( +
+ {children} +
+); -export const Button: React.FC< - React.PropsWithChildren<{ style?: React.CSSProperties; onClick?: () => void }> -> = ({ children, style, onClick }) => { - return ( - - {children} - - ); -}; -export const Scroll: React.FC< - React.PropsWithChildren<{ style?: React.CSSProperties }> -> = ({ children, style }) => { +export const fontFamilies = [ + "Times New Roman", + "Times", + "Nimbus Roman No9 L", + "Liberation Serif", + "FreeSerif", + "Hoefler Text", + "Microsoft YaHei", + "Hiragino Sans GB", + "WenQuanYi Micro Hei", + "STHeiti", + "sans-serif", +]; + +interface ContextMenuProps { + position: { x: number; y: number } | null; + parentRef?: React.RefObject; + onClose: () => void; + items: { label: string; onClick: () => void }[]; +} + +export const ContextMenu: React.FC = ({ position, parentRef, onClose, items }) => { + if (!position) return null; + + let top = position.y; + let left = position.x; + + if (parentRef?.current) { + const rect = parentRef.current.getBoundingClientRect(); + top = position.y - rect.top; + left = position.x - rect.left; + } + return ( -
- {children} -
+ <> +
+
+ {items.map((item, idx) => ( +
{ item.onClick(); onClose(); }} + onMouseEnter={(e) => (e.currentTarget.style.backgroundColor = "rgba(0,0,0,0.05)")} + onMouseLeave={(e) => (e.currentTarget.style.backgroundColor = "transparent")} + > + {item.label} +
+ ))} +
+ ); }; diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 54e0ff8..d8d6eb7 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -5693,6 +5693,11 @@ he@^1.2.0: resolved "https://registry.npmmirror.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== +hoist-non-react-statics@^1.0.5: + version "1.2.0" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz#aa448cf0986d55cc40773b17174b7dd066cb7cfb" + integrity sha512-r8huvKK+m+VraiRipdZYc+U4XW43j6OFG/oIafe7GfDbRpCduRoX9JI/DRxqgtBSCeL+et6N6ibZoedHS2NyOQ== + hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.npmmirror.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" @@ -8870,6 +8875,14 @@ react-app-polyfill@^3.0.0: regenerator-runtime "^0.13.9" whatwg-fetch "^3.6.2" +react-contextify@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/react-contextify/-/react-contextify-0.1.0.tgz#f6a7843d4bab95c6fa46342a5f0440c846d15ddd" + integrity sha512-1cufyI0T13MKmEO/pqCh4qpG2Ok9mpmQ0EnruZ9rT9BBrFx0BGz1baeA2S5tdokNL3oK3qwEscgUfb67Ok9INw== + dependencies: + hoist-non-react-statics "^1.0.5" + loose-envify "^1.1.0" + react-dev-utils@^12.0.1: version "12.0.1" resolved "https://registry.npmmirror.com/react-dev-utils/-/react-dev-utils-12.0.1.tgz#ba92edb4a1f379bd46ccd6bcd4e7bc398df33e73" @@ -9749,7 +9762,16 @@ string-natural-compare@^3.0.1: resolved "https://registry.npmmirror.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0, string-width@^4.2.0: version "4.2.3" resolved "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -9852,7 +9874,14 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -11007,7 +11036,16 @@ workbox-window@6.6.1: "@types/trusted-types" "^2.0.2" workbox-core "6.6.1" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== From c1001701ff23b21a3576083292ba37f55b739bcd Mon Sep 17 00:00:00 2001 From: Final Deterrence Date: Thu, 4 Sep 2025 21:36:22 +0800 Subject: [PATCH 4/4] change component card to fix 'left' problem --- frontend/src/Components.tsx | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/frontend/src/Components.tsx b/frontend/src/Components.tsx index 1d39653..73eba75 100644 --- a/frontend/src/Components.tsx +++ b/frontend/src/Components.tsx @@ -23,6 +23,10 @@ export const Card = forwardRef ); +export const fontFamilies = [ + "Times New Roman", + "Times", + "Nimbus Roman No9 L", + "Liberation Serif", + "FreeSerif", + "Hoefler Text", + "Microsoft YaHei", + "Hiragino Sans GB", + "WenQuanYi Micro Hei", + "STHeiti", + "sans-serif", +]; + export const Text: React.FC< React.PropsWithChildren<{ style?: React.CSSProperties; size?: string; editable?: any; copyable?: any }> > = ({ children, style, size, editable, copyable }) => { @@ -115,21 +133,6 @@ export const Scroll: React.FC ); - -export const fontFamilies = [ - "Times New Roman", - "Times", - "Nimbus Roman No9 L", - "Liberation Serif", - "FreeSerif", - "Hoefler Text", - "Microsoft YaHei", - "Hiragino Sans GB", - "WenQuanYi Micro Hei", - "STHeiti", - "sans-serif", -]; - interface ContextMenuProps { position: { x: number; y: number } | null; parentRef?: React.RefObject;