Skip to content

Commit bf7cf76

Browse files
committed
feat: require login before sending messages, allow typing in composer
1 parent 82b5c78 commit bf7cf76

File tree

22 files changed

+718
-72
lines changed

22 files changed

+718
-72
lines changed

packages/webapp/components/TipTap/extentions/plugins/headingButtonsPlugin.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import * as PubSub from 'pubsub-js'
77
import { copyToClipboard } from '../helper'
88
import { CHAT_OPEN } from '@services/eventsHub'
99
import { ChatLeftSVG, ArrowDownSVG } from '@icons'
10-
import { db } from '../../../../db'
10+
import { db } from '@db/headingCrinckleDB'
1111

1212
// Plugin-specific types
1313
interface HeadingBlock {

packages/webapp/components/chatroom/ChatroomContext.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react'
1+
import React, { createContext, useContext, useState, useEffect, useCallback, useMemo } from 'react'
22
import {
33
ChatroomContextValue,
44
ChatroomProps,
@@ -7,6 +7,7 @@ import {
77
} from './types/chatroom.types'
88
import { useChannelInitialData, useMessageSubscription } from '@components/chatroom/hooks'
99
import { Modal, ModalContent } from '@components/ui/Dialog'
10+
import { useChatStore } from '@stores'
1011

1112
const ChatroomContext = createContext<ChatroomContextValue | null>(null)
1213

@@ -23,9 +24,8 @@ export const ChatroomProvider: React.FC<{
2324
variant: keyof ChatroomVariant
2425
children: React.ReactNode
2526
}> = ({ channelId, variant, children }) => {
26-
const [isLoading, setIsLoading] = useState(false)
2727
const [error, setError] = useState<string | null>(null)
28-
28+
const { isReadyToDisplayMessages } = useChatStore((state) => state.chatRoom)
2929
// Dialog state
3030
const [isDialogOpen, setIsDialogOpen] = useState(false)
3131
const [dialogContent, setDialogContent] = useState<React.ReactNode>(null)
@@ -53,16 +53,20 @@ export const ChatroomProvider: React.FC<{
5353
setDialogConfig({})
5454
}, [])
5555

56+
const initLoadMessages = useMemo(() => {
57+
return !isDbSubscriptionReady || !isChannelDataLoaded || !isReadyToDisplayMessages
58+
}, [isDbSubscriptionReady, isChannelDataLoaded, isReadyToDisplayMessages])
59+
5660
const value: ChatroomContextValue = {
5761
channelId,
5862
variant,
59-
isLoading,
6063
error,
6164
isChannelDataLoaded,
6265
isDbSubscriptionReady,
6366
openDialog,
6467
closeDialog,
65-
isDialogOpen
68+
isDialogOpen,
69+
initLoadMessages
6670
}
6771

6872
return (

packages/webapp/components/chatroom/components/ChannelComposer/ChannelComposer.tsx

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,20 +55,14 @@ const AccessControl = () => {
5555

5656
const channels = useChatStore((state: any) => state.channels)
5757

58-
// Early returns for special cases
59-
if (!user) {
60-
return <ChannelComposer.SignInPrompt />
61-
}
62-
63-
if (!channelId) {
64-
return null
65-
}
58+
if (!channelId) return null
6659

6760
// Thread channels always allow messaging (no permission checks needed)
6861
if (
6962
(channels.has(channelId) && channels.get(channelId).type === 'THREAD') ||
7063
!channelInfo ||
71-
channelInfo.type === 'THREAD'
64+
channelInfo.type === 'THREAD' ||
65+
!user
7266
) {
7367
return <MsgComposer.Editor />
7468
}

packages/webapp/components/chatroom/components/MessageComposer/MessageComposer.tsx

Lines changed: 58 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { MessageComposerContext } from './context/MessageComposerContext'
66
import { useAuthStore, useChatStore, useStore } from '@stores'
77
import { TChannelSettings } from '@types'
88
import { toolbarStorage } from './helpers/toolbarStorage'
9+
import { getComposerState, clearComposerState, ComposerState } from '@db/messageComposerDB'
10+
import { SignInDialog } from '@components/ui/dialogs'
911

1012
import { EditorContent } from '@tiptap/react'
1113
import {
@@ -58,10 +60,11 @@ const MessageComposer = ({
5860
const usersPresence = useStore((state: any) => state.usersPresence)
5961
const startThreadMessage = useChatStore((state) => state.startThreadMessage)
6062
const channels = useChatStore((state) => state.channels)
61-
const { workspaceId } = useChatStore((state) => state.workspaceSettings)
63+
const { workspaceId } = useStore((state) => state.settings)
6264
const editorRef = useRef<HTMLDivElement | null>(null)
6365
const [isToolbarOpen, setIsToolbarOpen] = useState(() => toolbarStorage.get())
6466
const setOrUpdateChatRoom = useChatStore((state) => state.setOrUpdateChatRoom)
67+
const openDialog = useStore((state) => state.openDialog)
6568

6669
const setEditMsgMemory = useChatStore((state) => state.setEditMessageMemory)
6770
const setReplyMsgMemory = useChatStore((state) => state.setReplyMessageMemory)
@@ -89,7 +92,9 @@ const MessageComposer = ({
8992

9093
const { editor, text, html, isEmojiOnly } = useTiptapEditor({
9194
loading,
92-
onSubmit: () => submitRef.current?.()
95+
onSubmit: () => submitRef.current?.(),
96+
workspaceId,
97+
channelId
9398
})
9499

95100
const chatChannels = useChatStore((state) => state.workspaceSettings.channels)
@@ -117,6 +122,20 @@ const MessageComposer = ({
117122
users.forEach((user) => user && setOrUpdateUserPresence(user.id, user))
118123
}, [replyMessageMemory, editMessageMemory, usersPresence, setOrUpdateUserPresence])
119124

125+
// Load persisted draft from IndexedDB on mount or channel change
126+
useEffect(() => {
127+
if (!editor || !workspaceId || !channelId) return
128+
129+
// Don't load draft if we're editing/replying/commenting
130+
if (editMessageMemory || replyMessageMemory || commentMessageMemory) return
131+
132+
getComposerState(workspaceId, channelId).then((draft: ComposerState | null) => {
133+
if (draft?.html) {
134+
editor.chain().setContent(draft.html).focus('end').run()
135+
}
136+
})
137+
}, [editor, workspaceId, channelId, editMessageMemory, replyMessageMemory, commentMessageMemory])
138+
120139
// set the editor content if it is a reply message
121140
useEffect(() => {
122141
if (!editor || !editMessageMemory || editMessageMemory.channel_id !== channelId) return
@@ -130,6 +149,8 @@ const MessageComposer = ({
130149
// Validation helpers
131150
const validateSubmission = useCallback(() => {
132151
if (!editor || !user) return { isValid: false, error: 'Editor or user not available' }
152+
const html = editor?.getHTML()
153+
const text = editor?.getText()
133154

134155
const isContentEmpty =
135156
!html || !text || html.replace(/<[^>]*>/g, '').trim() === '' || text.trim() === ''
@@ -138,10 +159,14 @@ const MessageComposer = ({
138159
if (loading) return { isValid: false, error: 'Already loading' }
139160

140161
return { isValid: true }
141-
}, [editor, user, html, text, loading])
162+
}, [editor, user, loading])
142163

143164
// Content preparation
144165
const prepareContent = useCallback(() => {
166+
const html = editor?.getHTML()
167+
const text = editor?.getText()
168+
if (!html || !text) return { sanitizedHtml: '', sanitizedText: '', chunks: [] }
169+
145170
const { sanitizedHtml, sanitizedText } = sanitizeMessageContent(html, text)
146171

147172
if (!sanitizedHtml || !sanitizedText) {
@@ -153,7 +178,7 @@ const MessageComposer = ({
153178
sanitizedText,
154179
chunks: chunkHtmlContent(sanitizedHtml, 3000)
155180
}
156-
}, [html, text])
181+
}, [editor])
157182

158183
// Message type handlers
159184
const handleThreadMessage = useCallback(
@@ -292,13 +317,21 @@ const MessageComposer = ({
292317
text: null,
293318
html: null
294319
})
295-
// document.dispatchEvent(new CustomEvent('messages:container:scroll:down'))
320+
321+
// Clear IndexedDB draft
322+
if (workspaceId && channelId) {
323+
clearComposerState(workspaceId, channelId)
324+
}
325+
326+
// Clear editor content
327+
editor?.chain().clearContent(true).focus('start').run()
296328
}, [
297329
editor,
298330
replyMessageMemory,
299331
editMessageMemory,
300332
commentMessageMemory,
301333
channelId,
334+
workspaceId,
302335
setReplyMsgMemory,
303336
setEditMsgMemory,
304337
setCommentMsgMemory,
@@ -325,23 +358,38 @@ const MessageComposer = ({
325358
[]
326359
)
327360

361+
const openSignInModalHandler = useCallback(() => {
362+
// append search query to the URL, for when they back to the page they will be redirected to the channel
363+
const url = new URL(window.location.href)
364+
url.searchParams.set('open_heading_chat', channelId)
365+
window.history.pushState({}, '', url.href)
366+
367+
openDialog(<SignInDialog />, { size: 'sm', dismissible: true })
368+
}, [openDialog])
369+
328370
// Main submit function - now clean and focused
329371
const submitMessage = useCallback(
330372
async (e?: any) => {
331373
e?.preventDefault()
332374
editor?.view.focus()
333375

376+
// 0. check if user is signed in
377+
if (!user) {
378+
openSignInModalHandler()
379+
return
380+
}
381+
334382
// 1. Validate
335383
const validation = validateSubmission()
336384
if (!validation.isValid) return
337385

338386
try {
339387
// 2. Prepare content
340388
const { sanitizedHtml, sanitizedText, chunks } = prepareContent()
341-
const { htmlChunks, textChunks } = chunks
389+
const { htmlChunks, textChunks } = chunks as { htmlChunks: string[]; textChunks: string[] }
342390

343-
// 3. Update user presence
344-
updateUserPresence()
391+
// 3. Update user presence // TODO: review this part, we do not need it anymore
392+
// updateUserPresence()
345393

346394
// 4. Send message(s)
347395
if (htmlChunks.length === 0) {
@@ -357,6 +405,7 @@ const MessageComposer = ({
357405
}
358406
},
359407
[
408+
user,
360409
editor,
361410
validateSubmission,
362411
prepareContent,
@@ -372,21 +421,6 @@ const MessageComposer = ({
372421
submitRef.current = () => submitMessage()
373422
}, [submitMessage])
374423

375-
// Handle Draft Memory
376-
useEffect(() => {
377-
return () => {
378-
// Save draft before unmounting
379-
const html = editor?.getHTML()
380-
const text = editor?.getText()
381-
if (html && text) {
382-
setMsgDraftMemory(channelId, {
383-
text: text,
384-
html: html
385-
})
386-
}
387-
}
388-
}, [editor, channelId, setMsgDraftMemory])
389-
390424
useEffect(() => {
391425
const setAttributes = () => {
392426
const firstChild = editorRef.current?.firstChild as HTMLElement | null
@@ -429,7 +463,7 @@ const MessageComposer = ({
429463
replyMessageMemory,
430464
editMessageMemory,
431465
commentMessageMemory,
432-
messageDraftMemory,
466+
messageDraftMemory: messageDraftMemory ?? null,
433467
setEditMsgMemory,
434468
setReplyMsgMemory,
435469
setCommentMsgMemory,

packages/webapp/components/chatroom/components/MessageComposer/hooks/useTiptapEditor.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useState, useEffect } from 'react'
22
import { useEditor, Editor } from '@tiptap/react'
33
import { TextSelection } from 'prosemirror-state'
4+
import { setComposerStateDebounced } from '@db/messageComposerDB'
45

56
// Code and Syntax Highlighting
67
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'
@@ -45,10 +46,14 @@ import { isOnlyEmoji } from '@utils/emojis'
4546

4647
export const useTiptapEditor = ({
4748
loading,
48-
onSubmit
49+
onSubmit,
50+
workspaceId,
51+
channelId
4952
}: {
5053
loading: boolean
5154
onSubmit: () => void
55+
workspaceId?: string
56+
channelId: string
5257
}) => {
5358
const [html, setHtml] = useState('')
5459
const [text, setText] = useState('')
@@ -97,10 +102,17 @@ export const useTiptapEditor = ({
97102
],
98103
onUpdate: ({ editor }) => {
99104
const text = editor?.getText()
100-
setHtml(editor?.getHTML())
101-
setText(editor?.getText())
105+
const html = editor?.getHTML()
106+
107+
setHtml(html)
108+
setText(text)
102109
setIsEmojiOnly(isOnlyEmoji(text))
103110
if (text.length) handleTypingIndicator(TypingIndicatorType.StartTyping)
111+
112+
// Persist draft to IndexedDB with debouncing (500ms)
113+
if (workspaceId && channelId && text && html) {
114+
setComposerStateDebounced(workspaceId, channelId, { text, html })
115+
}
104116
},
105117
onBlur: () => {
106118
if (text.length) handleTypingIndicator(TypingIndicatorType.StopTyping)

packages/webapp/components/chatroom/components/MessageFeed/MessageFeedContext.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
useHighlightMessage
88
} from '@components/chatroom/hooks'
99
import type { Virtualizer } from '@tanstack/react-virtual'
10+
import { useChatStore } from '@stores'
1011

1112
interface MessageFeedContextValue {
1213
isLoadingMore: boolean

packages/webapp/components/chatroom/components/MessageFeed/components/OverLayers/MessageFeedLoading.tsx

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,18 @@
11
import { useChatroomContext } from '@components/chatroom/ChatroomContext'
2-
import { useMemo } from 'react'
3-
import { useChatStore } from '@stores'
42

53
interface Props {
64
children: React.ReactNode
75
}
86

97
// Overlayer for loading state of the message feed
108
export const MessageFeedLoading = ({ children }: Props) => {
11-
const { isDbSubscriptionReady, isChannelDataLoaded } = useChatroomContext()
12-
const { isReadyToDisplayMessages } = useChatStore((state) => state.chatRoom)
13-
14-
const loading = useMemo(() => {
15-
return !isDbSubscriptionReady || !isChannelDataLoaded || !isReadyToDisplayMessages
16-
}, [isDbSubscriptionReady, isChannelDataLoaded, isReadyToDisplayMessages])
9+
const { initLoadMessages } = useChatroomContext()
1710

1811
return (
1912
<>
2013
<div
2114
className="bg-base-100 absolute z-50 flex size-full items-center justify-center"
22-
style={{ display: loading ? 'flex' : 'none' }}>
15+
style={{ display: initLoadMessages ? 'flex' : 'none' }}>
2316
<div className="flex w-full items-center justify-center">
2417
<span className="loading loading-spinner text-primary"></span>
2518
</div>

0 commit comments

Comments
 (0)