@@ -12,15 +12,21 @@ import {
1212 type Message ,
1313} from "../../Contexts/EditorContext" ;
1414import { useTheme } from "../../Contexts/ThemeProvider" ;
15- import { ChevronRight } from "lucide-react" ;
15+ import { ChevronRight , Trash2 , Reply } from "lucide-react" ;
1616import Avatar from "../../components/Avatar" ;
1717
1818export function ChatSpace ( ) {
1919 const [ inputMessage , setInputMessage ] = useState ( "" ) ;
2020 const [ currentUser , setCurrentUser ] = useState < CollaborationUser | null > (
2121 null
2222 ) ;
23+ const [ hoveredMessageId , setHoveredMessageId ] = useState < string | null > ( null ) ;
24+ const [ showDeleteConfirm , setShowDeleteConfirm ] = useState < string | null > (
25+ null
26+ ) ;
27+ const [ replyToMessage , setReplyToMessage ] = useState < Message | null > ( null ) ;
2328 const scrollAreaRef = useRef < HTMLDivElement > ( null ) ;
29+ const inputRef = useRef < HTMLTextAreaElement > ( null ) ;
2430 const { theme } = useTheme ( ) ;
2531
2632 const { messages, getAwareness, sendChatMessage } = useEditorCollaboration ( ) ;
@@ -53,10 +59,29 @@ export function ChatSpace() {
5359 }
5460 } , [ messages ] ) ;
5561
62+ // Close delete confirmation when clicking outside
63+ useEffect ( ( ) => {
64+ const handleClickOutside = ( event : MouseEvent ) => {
65+ if (
66+ showDeleteConfirm &&
67+ ! ( event . target as Element ) . closest ( ".delete-confirmation" )
68+ ) {
69+ setShowDeleteConfirm ( null ) ;
70+ }
71+ } ;
72+
73+ if ( showDeleteConfirm ) {
74+ document . addEventListener ( "mousedown" , handleClickOutside ) ;
75+ return ( ) =>
76+ document . removeEventListener ( "mousedown" , handleClickOutside ) ;
77+ }
78+ } , [ showDeleteConfirm ] ) ;
79+
5680 const handleSendMessage = useCallback ( ( ) => {
5781 if ( inputMessage . trim ( ) ) {
5882 sendChatMessage ( inputMessage ) ;
5983 setInputMessage ( "" ) ;
84+ setReplyToMessage ( null ) ; // Clear reply after sending
6085 }
6186 } , [ inputMessage , sendChatMessage ] ) ;
6287
@@ -75,6 +100,35 @@ export function ChatSpace() {
75100 setInputMessage ( e . target . value ) ;
76101 } ;
77102
103+ const handleDeleteMessage = useCallback (
104+ ( messageId : string ) => {
105+ const success = collaboration . deleteChatMessage ( messageId ) ;
106+ if ( success ) {
107+ setShowDeleteConfirm ( null ) ;
108+ }
109+ } ,
110+ [ collaboration ]
111+ ) ;
112+
113+ const confirmDelete = ( messageId : string ) => {
114+ setShowDeleteConfirm ( messageId ) ;
115+ } ;
116+
117+ const cancelDelete = ( ) => {
118+ setShowDeleteConfirm ( null ) ;
119+ } ;
120+
121+ const handleReplyToMessage = useCallback ( ( message : Message ) => {
122+ setReplyToMessage ( message ) ;
123+ setShowDeleteConfirm ( null ) ; // Close any open delete confirmations
124+ // Focus input after setting reply
125+ setTimeout ( ( ) => inputRef . current ?. focus ( ) , 100 ) ;
126+ } , [ ] ) ;
127+
128+ const cancelReply = ( ) => {
129+ setReplyToMessage ( null ) ;
130+ } ;
131+
78132 return (
79133 < div className = { `flex flex-col ${ theme . surface } ` } >
80134 { /* Header */ }
@@ -94,6 +148,7 @@ export function ChatSpace() {
94148 < div className = "space-y-3" >
95149 { messages . map ( ( msg : Message ) => {
96150 const isCurrentUser = msg . user === currentUser ?. name ;
151+ const isDeleteConfirmOpen = showDeleteConfirm === msg . id ;
97152
98153 return (
99154 < div
@@ -103,19 +158,21 @@ export function ChatSpace() {
103158 ? "justify-start flex-row-reverse"
104159 : "justify-start"
105160 } `}
161+ onMouseEnter = { ( ) => setHoveredMessageId ( msg . id ) }
162+ onMouseLeave = { ( ) => setHoveredMessageId ( null ) }
106163 >
107164 < Avatar
108165 name = { msg . user }
109166 src = { msg . avatar }
110- color = { undefined } // Blue for current user
167+ color = { undefined }
111168 size = "medium"
112169 />
113170
114171 { /* Message bubble */ }
115172 < div
116173 className = { `flex flex-col max-w-[75%] ${
117174 isCurrentUser ? "items-end" : "items-start"
118- } `}
175+ } relative group `}
119176 >
120177 { /* Username (only for other users) */ }
121178 { ! isCurrentUser && (
@@ -126,17 +183,92 @@ export function ChatSpace() {
126183 </ span >
127184 ) }
128185
129- { /* Message content */ }
130- < div
131- className = { `px-4 py-2 text-sm shadow-sm ${
132- isCurrentUser
133- ? "bg-blue-500 text-white rounded-br-md"
134- : `${ theme . surfaceSecondary } ${ theme . text } rounded-bl-md`
135- } `}
136- >
137- < p className = "break-words" > { msg . text } </ p >
186+ { /* Message content with reply and delete buttons */ }
187+ < div className = "relative" >
188+ < div
189+ className = { `px-4 py-2 text-sm shadow-sm ${
190+ isCurrentUser
191+ ? "bg-blue-500 text-white rounded-br-md"
192+ : `${ theme . surfaceSecondary } ${ theme . text } rounded-bl-md`
193+ } `}
194+ >
195+ { /* Reply indicator */ }
196+ { msg . replyTo && (
197+ < div
198+ className = { `mb-2 pb-2 border-l-2 pl-2 text-xs opacity-75 ${
199+ isCurrentUser
200+ ? "border-blue-200"
201+ : `border-gray-300 ${ theme . textSecondary } `
202+ } `}
203+ >
204+ < div className = "font-medium" >
205+ Replying to { msg . replyTo . user }
206+ </ div >
207+ { /* <div className="truncate">{msg.replyTo.text}</div> */ }
208+ </ div >
209+ ) }
210+ < p className = "break-words" > { msg . text } </ p >
211+ </ div >
212+
213+ { hoveredMessageId === msg . id && (
214+ < div
215+ className = { `absolute -top-2 ${
216+ isCurrentUser ? "-left-16" : "-right-16"
217+ } flex gap-1`}
218+ >
219+ { /* Reply button (for all messages) */ }
220+ < button
221+ onClick = { ( ) => handleReplyToMessage ( msg ) }
222+ className = "p-1 bg-gray-500 text-white rounded-full opacity-0 group-hover:opacity-100 hover:bg-gray-600 transition-all duration-200 shadow-lg"
223+ title = "Reply to message"
224+ >
225+ < Reply className = "w-3 h-3" />
226+ </ button >
227+
228+ { /* Delete button */ }
229+ { isCurrentUser && (
230+ < button
231+ onClick = { ( ) => confirmDelete ( msg . id ) }
232+ className = "p-1 bg-red-500 text-white rounded-full opacity-0 group-hover:opacity-100 hover:bg-red-600 transition-all duration-200 shadow-lg"
233+ title = "Delete message"
234+ >
235+ < Trash2 className = "w-3 h-3" />
236+ </ button >
237+ ) }
238+ </ div >
239+ ) }
138240 </ div >
139241
242+ { /* Delete confirmation dialog */ }
243+ { isDeleteConfirmOpen && (
244+ < div
245+ className = { `delete-confirmation absolute z-10 mt-1 p-3 ${
246+ theme . surface
247+ } ${ theme . border } border rounded-lg shadow-lg ${
248+ isCurrentUser ? "right-0" : "left-0"
249+ } `}
250+ style = { { top : "100%" } }
251+ >
252+ < p className = { `text-xs ${ theme . text } mb-2` } >
253+ Delete this message?
254+ </ p >
255+ < div className = "flex gap-2" >
256+ < button
257+ onClick = { ( ) => handleDeleteMessage ( msg . id ) }
258+ className = "px-2 py-1 text-xs bg-red-500 text-white rounded hover:bg-red-600 transition-colors"
259+ >
260+ Delete
261+ </ button >
262+ < button
263+ onClick = { cancelDelete }
264+ className = { `px-2 py-1 text-xs ${ theme . surfaceSecondary } ${ theme . text } rounded hover:${ theme . hover } transition-colors` }
265+ >
266+ Cancel
267+ </ button >
268+ </ div >
269+ </ div >
270+ ) }
271+
140272 { /* Timestamp */ }
141273 < span className = { `text-xs ${ theme . textMuted } mt-1 px-1` } >
142274 { new Date ( msg . timestamp ) . toLocaleTimeString ( [ ] , {
@@ -153,35 +285,68 @@ export function ChatSpace() {
153285 </ div >
154286
155287 { /* Input Area */ }
156- < div
157- className = { `flex-shrink-0 min-h-20 flex items-end gap-2 pt-4 p-4 ${ theme . border } border-t` }
158- >
159- < textarea
160- placeholder = "Type a message... "
161- value = { inputMessage }
162- onChange = { handleInputChange }
163- onKeyDown = { handleKeyDown }
164- rows = { 1 }
165- className = { `flex-1 px-4 py-2 ${ theme . border } border ${ theme . surface } focus:outline-none focus:ring-2 focus:ring-blue-500 ${ theme . text } transition-all resize-none min-h-[40px] max-h-[120px]` }
166- style = { {
167- height : "auto" ,
168- minHeight : "40px" ,
169- maxHeight : "120px" ,
170- overflowY : inputMessage . split ( "\n" ) . length > 3 ? "auto" : "hidden" ,
171- } }
172- onInput = { ( e ) => {
173- const target = e . target as HTMLTextAreaElement ;
174- target . style . height = "auto" ;
175- target . style . height = Math . min ( target . scrollHeight , 120 ) + "px" ;
176- } }
177- />
178- < button
179- onClick = { handleSendMessage }
180- disabled = { ! inputMessage . trim ( ) }
181- className = { `p-2 bg-blue-500 text-white rounded-full hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-colors disabled:opacity-50 disabled:cursor-not-allowed ${ theme . hover } ` }
182- >
183- < ChevronRight className = "w-5 h-5" />
184- </ button >
288+ < div className = { `flex-shrink-0 ${ theme . border } border-t` } >
289+ { /* Reply Preview */ }
290+ { replyToMessage && (
291+ < div
292+ className = { `p-3 ${ theme . surfaceSecondary } border-l-4 border-blue-500 mx-4 mt-3 rounded` }
293+ >
294+ < div className = "flex items-center justify-between" >
295+ < div className = "flex-1" >
296+ < div
297+ className = { `text-xs font-medium ${ theme . textSecondary } mb-1` }
298+ >
299+ Replying to { replyToMessage . user }
300+ </ div >
301+ < div className = { `text-sm ${ theme . text } truncate` } >
302+ { replyToMessage . text }
303+ </ div >
304+ </ div >
305+ < button
306+ onClick = { cancelReply }
307+ className = { `ml-2 p-1 hover:${ theme . hover } rounded` }
308+ title = "Cancel reply"
309+ >
310+ < span className = { `text-lg ${ theme . textSecondary } ` } > ×</ span >
311+ </ button >
312+ </ div >
313+ </ div >
314+ ) }
315+
316+ < div className = "min-h-20 flex items-end gap-2 pt-4 p-4" >
317+ < textarea
318+ ref = { inputRef }
319+ placeholder = {
320+ replyToMessage
321+ ? `Replying to ${ replyToMessage . user } ...`
322+ : "Type a message... "
323+ }
324+ value = { inputMessage }
325+ onChange = { handleInputChange }
326+ onKeyDown = { handleKeyDown }
327+ rows = { 1 }
328+ className = { `flex-1 px-4 py-2 ${ theme . border } border ${ theme . surface } focus:outline-none focus:ring-2 focus:ring-blue-500 ${ theme . text } transition-all resize-none min-h-[40px] max-h-[120px]` }
329+ style = { {
330+ height : "auto" ,
331+ minHeight : "40px" ,
332+ maxHeight : "120px" ,
333+ overflowY :
334+ inputMessage . split ( "\n" ) . length > 3 ? "auto" : "hidden" ,
335+ } }
336+ onInput = { ( e ) => {
337+ const target = e . target as HTMLTextAreaElement ;
338+ target . style . height = "auto" ;
339+ target . style . height = Math . min ( target . scrollHeight , 120 ) + "px" ;
340+ } }
341+ />
342+ < button
343+ onClick = { handleSendMessage }
344+ disabled = { ! inputMessage . trim ( ) }
345+ className = { `p-2 bg-blue-500 text-white rounded-full hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-colors disabled:opacity-50 disabled:cursor-not-allowed ${ theme . hover } ` }
346+ >
347+ < ChevronRight className = "w-5 h-5" />
348+ </ button >
349+ </ div >
185350 </ div >
186351 </ div >
187352 ) ;
0 commit comments