@@ -11,26 +11,38 @@ import {
11
11
Text ,
12
12
Badge ,
13
13
Skeleton ,
14
- Spinner
14
+ Spinner ,
15
+ Select
15
16
} from '@radix-ui/themes' ;
16
17
import Link from 'next/link' ;
17
18
import { type TypeId , validateTypeId } from '@u22n/utils' ;
18
19
import { useGlobalStore } from '@/src/providers/global-store-provider' ;
19
- import { memo , useCallback , useMemo , useState } from 'react' ;
20
+ import { memo , useCallback , useEffect , useMemo , useRef , useState } from 'react' ;
20
21
import { type JSONContent , generateHTML } from '@u22n/tiptap/react' ;
21
22
import { tipTapExtensions } from '@u22n/tiptap/extensions' ;
22
- import { formatParticipantData } from '../utils' ;
23
+ import {
24
+ formatParticipantData ,
25
+ useUpdateConvoMessageList$Cache
26
+ } from '../utils' ;
23
27
import { cn , generateAvatarUrl , getInitials } from '@/src/lib/utils' ;
24
28
import ChatSideBar from './_components/chat-sidebar' ;
25
29
import useTimeAgo from '@/src/hooks/use-time-ago' ;
26
30
import { ArrowLeft , Ellipsis } from 'lucide-react' ;
27
- import { atom , useAtom } from 'jotai' ;
31
+ import { atom , useAtom , useAtomValue , useSetAtom } from 'jotai' ;
28
32
import { useCopyToClipboard } from '@uidotdev/usehooks' ;
29
33
import { toast } from 'sonner' ;
30
34
import { ms } from 'itty-time' ;
31
35
import { Virtuoso } from 'react-virtuoso' ;
36
+ import Editor from '../new/editor' ;
37
+ import { emptyTiptapEditorContent } from '@u22n/tiptap' ;
38
+ import AttachmentButton , {
39
+ type ConvoAttachmentUpload
40
+ } from '../new/attachment-button' ;
41
+ import { stringify } from 'superjson' ;
42
+ import { type Editor as EditorType } from '@u22n/tiptap/react' ;
32
43
33
44
const replyToMessageAtom = atom < null | TypeId < 'convoEntries' > > ( null ) ;
45
+ const selectedEmailIdentityAtom = atom < null | TypeId < 'emailIdentities' > > ( null ) ;
34
46
35
47
export default function Page ( {
36
48
params : { convoId }
@@ -53,6 +65,8 @@ function ConvoView({ convoId }: { convoId: TypeId<'convos'> }) {
53
65
INVERSE_LIST_START_INDEX
54
66
) ;
55
67
const [ scrollParent , setScrollParent ] = useState < HTMLElement | null > ( null ) ;
68
+ const setReplyTo = useSetAtom ( replyToMessageAtom ) ;
69
+ const [ emailIdentity , setEmailIdentity ] = useAtom ( selectedEmailIdentityAtom ) ;
56
70
57
71
const {
58
72
data : convoData ,
@@ -63,6 +77,11 @@ function ConvoView({ convoId }: { convoId: TypeId<'convos'> }) {
63
77
convoPublicId : convoId
64
78
} ) ;
65
79
80
+ const { data : emailIdentities , isLoading : emailIdentitiesLoading } =
81
+ api . org . mail . emailIdentities . getUserEmailIdentities . useQuery ( {
82
+ orgShortCode
83
+ } ) ;
84
+
66
85
const { data, isLoading, hasNextPage, fetchNextPage, isFetchingNextPage } =
67
86
api . convos . entries . getConvoEntries . useInfiniteQuery (
68
87
{
@@ -83,6 +102,18 @@ function ConvoView({ convoId }: { convoId: TypeId<'convos'> }) {
83
102
return messages ;
84
103
} , [ data ] ) ;
85
104
105
+ useEffect ( ( ) => {
106
+ const lastMessage = allMessages . at ( - 1 ) ;
107
+ setReplyTo ( ( ) => lastMessage ?. publicId ?? null ) ;
108
+ } , [ allMessages , setReplyTo ] ) ;
109
+
110
+ useEffect ( ( ) => {
111
+ setEmailIdentity ( ( prev ) => {
112
+ if ( ! emailIdentities ) return prev ;
113
+ return prev ?? emailIdentities . emailIdentities [ 0 ] ?. publicId ?? null ;
114
+ } ) ;
115
+ } , [ emailIdentities , setEmailIdentity ] ) ;
116
+
86
117
const participantOwnPublicId = convoData ?. ownParticipantPublicId ;
87
118
const convoHidden = useMemo (
88
119
( ) =>
@@ -177,7 +208,36 @@ function ConvoView({ convoId }: { convoId: TypeId<'convos'> }) {
177
208
/>
178
209
</ ScrollArea >
179
210
) }
180
- < Flex className = "h-20" > Editor box</ Flex >
211
+ < Flex
212
+ className = "border-top min-h-32"
213
+ direction = "column"
214
+ justify = "end"
215
+ gap = "1" >
216
+ { ! emailIdentitiesLoading ? (
217
+ < div className = "flex items-center gap-1" >
218
+ < span className = "text-gray-9 px-2 text-sm" > Reply as</ span >
219
+ < Select . Root
220
+ value = { emailIdentity ?? undefined }
221
+ onValueChange = { ( email ) =>
222
+ setEmailIdentity ( email as TypeId < 'emailIdentities' > )
223
+ } >
224
+ < Select . Trigger className = "text-xs" />
225
+ < Select . Content >
226
+ { emailIdentities ?. emailIdentities . map ( ( email ) => (
227
+ < Select . Item
228
+ key = { email . publicId }
229
+ value = { email . publicId } >
230
+ < span >
231
+ { email . username } @{ email . domainName }
232
+ </ span >
233
+ </ Select . Item >
234
+ ) ) }
235
+ </ Select . Content >
236
+ </ Select . Root >
237
+ </ div >
238
+ ) : null }
239
+ < MessageReplyBox convoId = { convoId } />
240
+ </ Flex >
181
241
</ Flex >
182
242
< ChatSideBar
183
243
participants = { allParticipants }
@@ -188,6 +248,109 @@ function ConvoView({ convoId }: { convoId: TypeId<'convos'> }) {
188
248
) ;
189
249
}
190
250
251
+ const attachmentsAtom = atom < ConvoAttachmentUpload [ ] > ( [ ] ) ;
252
+
253
+ function MessageReplyBox ( { convoId } : { convoId : TypeId < 'convos' > } ) {
254
+ const [ editorText , setEditorText ] = useState < JSONContent > (
255
+ emptyTiptapEditorContent
256
+ ) ;
257
+ const [ attachments , setAttachments ] = useAtom ( attachmentsAtom ) ;
258
+ const orgShortCode = useGlobalStore ( ( state ) => state . currentOrg . shortCode ) ;
259
+ const replyTo = useAtomValue ( replyToMessageAtom ) ;
260
+ const addConvoToCache = useUpdateConvoMessageList$Cache ( ) ;
261
+ const editorRef = useRef < EditorType | null > ( null ) ;
262
+ const emailIdentity = useAtomValue ( selectedEmailIdentityAtom ) ;
263
+
264
+ // TODO: Find a better way to handle this
265
+ const [ loadingType , setLoadingType ] = useState < 'comment' | 'message' > (
266
+ 'message'
267
+ ) ;
268
+
269
+ const replyToConvoMutation = api . convos . replyToConvo . useMutation ( {
270
+ onSuccess : ( ) => {
271
+ editorRef . current ?. commands . clearContent ( ) ;
272
+ setEditorText ( emptyTiptapEditorContent ) ;
273
+ setAttachments ( [ ] ) ;
274
+ } ,
275
+ onError : ( err ) => {
276
+ toast . error ( err . message ) ;
277
+ }
278
+ } ) ;
279
+
280
+ const isEditorEmpty = useMemo ( ( ) => {
281
+ const contentArray = editorText ?. content ;
282
+ if ( ! contentArray ) return true ;
283
+ if ( contentArray . length === 0 ) return true ;
284
+ if (
285
+ contentArray [ 0 ] &&
286
+ ( ! contentArray [ 0 ] . content || contentArray [ 0 ] . content . length === 0 )
287
+ )
288
+ return true ;
289
+ return false ;
290
+ } , [ editorText ] ) ;
291
+
292
+ return (
293
+ < div className = "flex max-h-[250px] w-full flex-col gap-1 p-1" >
294
+ < Editor
295
+ initialValue = { editorText }
296
+ onChange = { setEditorText }
297
+ setEditor = { ( editor ) => {
298
+ editorRef . current = editor ;
299
+ } }
300
+ />
301
+ < div className = "align-center flex justify-end gap-2" >
302
+ < AttachmentButton attachmentsAtom = { attachmentsAtom } />
303
+ < Button
304
+ variant = "soft"
305
+ loading = { replyToConvoMutation . isLoading && loadingType === 'comment' }
306
+ disabled = {
307
+ ! replyTo ||
308
+ isEditorEmpty ||
309
+ ! emailIdentity ||
310
+ replyToConvoMutation . isLoading
311
+ }
312
+ onClick = { async ( ) => {
313
+ if ( ! replyTo || ! emailIdentity ) return ;
314
+ setLoadingType ( 'comment' ) ;
315
+ const { publicId } = await replyToConvoMutation . mutateAsync ( {
316
+ attachments,
317
+ orgShortCode,
318
+ message : stringify ( editorText ) ,
319
+ replyToMessagePublicId : replyTo ,
320
+ messageType : 'comment'
321
+ } ) ;
322
+ await addConvoToCache ( convoId , publicId ) ;
323
+ } } >
324
+ Comment
325
+ </ Button >
326
+ < Button
327
+ loading = { replyToConvoMutation . isLoading && loadingType === 'message' }
328
+ disabled = {
329
+ ! replyTo ||
330
+ isEditorEmpty ||
331
+ ! emailIdentity ||
332
+ replyToConvoMutation . isLoading
333
+ }
334
+ onClick = { async ( ) => {
335
+ if ( ! replyTo || ! emailIdentity ) return ;
336
+ setLoadingType ( 'message' ) ;
337
+ const { publicId } = await replyToConvoMutation . mutateAsync ( {
338
+ attachments,
339
+ orgShortCode,
340
+ message : stringify ( editorText ) ,
341
+ replyToMessagePublicId : replyTo ,
342
+ messageType : 'message' ,
343
+ sendAsEmailIdentityPublicId : emailIdentity
344
+ } ) ;
345
+ await addConvoToCache ( convoId , publicId ) ;
346
+ } } >
347
+ Send
348
+ </ Button >
349
+ </ div >
350
+ </ div >
351
+ ) ;
352
+ }
353
+
191
354
function MessageItem ( {
192
355
message,
193
356
participantOwnPublicId,
@@ -246,7 +409,9 @@ function MessageItem({
246
409
< Flex
247
410
className = { cn (
248
411
'w-full max-w-full overflow-hidden rounded-lg p-2' ,
249
- isUserAuthor ? 'bg-blue-10' : 'bg-gray-11'
412
+ isUserAuthor
413
+ ? 'dark:bg-blue-10 bg-blue-8'
414
+ : 'dark:bg-gray-10 bg-gray-8'
250
415
) } >
251
416
< HTMLMessage html = { messageHtml } />
252
417
</ Flex >
@@ -304,7 +469,7 @@ const HTMLMessage = memo(
304
469
return (
305
470
< div
306
471
dangerouslySetInnerHTML = { { __html } }
307
- className = "w-full max-w-full overflow-clip break-words"
472
+ className = "prose dark:prose-invert prose-p:my-1 prose-a:decoration-blue-8 prose-img:my-1 w-full max-w-full overflow-clip break-words text-black dark:text-white "
308
473
/>
309
474
) ;
310
475
} ,
0 commit comments