{userName}
{!user &&
Deleted}
diff --git a/frontend/src/components/feature/chat/ChatInput/Tiptap.tsx b/frontend/src/components/feature/chat/ChatInput/Tiptap.tsx
index 315e580f8..5eae63cf3 100644
--- a/frontend/src/components/feature/chat/ChatInput/Tiptap.tsx
+++ b/frontend/src/components/feature/chat/ChatInput/Tiptap.tsx
@@ -1,7 +1,7 @@
import { BubbleMenu, EditorContent, EditorContext, Extension, ReactRenderer, useEditor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import Underline from '@tiptap/extension-underline'
-import React, { Suspense, lazy, useContext, useEffect } from 'react'
+import React, { Suspense, lazy, useContext, useEffect, useMemo } from 'react'
import { TextFormattingMenu } from './TextFormattingMenu'
import Highlight from '@tiptap/extension-highlight'
import Link from '@tiptap/extension-link'
@@ -33,6 +33,7 @@ import { EmojiSuggestion } from './EmojiSuggestion'
import { useIsDesktop } from '@/hooks/useMediaQuery'
import { BiPlus } from 'react-icons/bi'
import clsx from 'clsx'
+import { ChannelMembers } from '@/hooks/fetchers/useFetchChannelMembers'
const MobileInputActions = lazy(() => import('./MobileActions/MobileInputActions'))
const lowlight = createLowlight(common)
@@ -59,7 +60,8 @@ type TiptapEditorProps = {
onMessageSend: (message: string, json: any) => Promise
,
messageSending: boolean,
defaultText?: string,
- replyMessage?: Message | null
+ replyMessage?: Message | null,
+ channelMembers?: ChannelMembers
}
export const UserMention = Mention.extend({
@@ -82,10 +84,19 @@ export const ChannelMention = Mention.extend({
}
})
-const Tiptap = ({ isEdit, slotBefore, fileProps, onMessageSend, replyMessage, clearReplyMessage, placeholder = 'Type a message...', messageSending, sessionStorageKey = 'tiptap-editor', disableSessionStorage = false, defaultText = '' }: TiptapEditorProps) => {
+const Tiptap = ({ isEdit, slotBefore, fileProps, onMessageSend, channelMembers, replyMessage, clearReplyMessage, placeholder = 'Type a message...', messageSending, sessionStorageKey = 'tiptap-editor', disableSessionStorage = false, defaultText = '' }: TiptapEditorProps) => {
const { enabledUsers } = useContext(UserListContext)
+ const channelMemberUsers = useMemo(() => {
+ if (channelMembers) {
+ // Filter enabled users to only include users that are in the channel
+ return enabledUsers.filter((user) => user.name in channelMembers)
+ } else {
+ return enabledUsers
+ }
+ }, [channelMembers, enabledUsers])
+
const { channels } = useContext(ChannelListContext) as ChannelListContextType
// this is a dummy extension only to create custom keydown behavior
@@ -301,7 +312,7 @@ const Tiptap = ({ isEdit, slotBefore, fileProps, onMessageSend, replyMessage, cl
},
suggestion: {
items: (query) => {
- return enabledUsers.filter((user) => user.full_name.toLowerCase().startsWith(query.query.toLowerCase()))
+ return channelMemberUsers.filter((user) => user.full_name.toLowerCase().startsWith(query.query.toLowerCase()))
.slice(0, 10);
},
// char: '@',
@@ -466,7 +477,7 @@ const Tiptap = ({ isEdit, slotBefore, fileProps, onMessageSend, replyMessage, cl
if (isDesktop) {
return (
-
+
{slotBefore}
diff --git a/frontend/src/components/feature/chat/ChatMessage/ActionModals/DeleteMessageModal.tsx b/frontend/src/components/feature/chat/ChatMessage/ActionModals/DeleteMessageModal.tsx
index f18990555..25be3d348 100644
--- a/frontend/src/components/feature/chat/ChatMessage/ActionModals/DeleteMessageModal.tsx
+++ b/frontend/src/components/feature/chat/ChatMessage/ActionModals/DeleteMessageModal.tsx
@@ -5,6 +5,7 @@ import { Loader } from "@/components/common/Loader"
import { FiAlertTriangle } from "react-icons/fi"
import { Message } from "../../../../../../../types/Messaging/Message"
import { toast } from "sonner"
+import { useNavigate } from "react-router-dom"
interface DeleteMessageModalProps {
onClose: (refresh?: boolean) => void,
@@ -14,11 +15,12 @@ interface DeleteMessageModalProps {
export const DeleteMessageModal = ({ onClose, message }: DeleteMessageModalProps) => {
const { deleteDoc, error, loading: deletingDoc } = useFrappeDeleteDoc()
+ const navigate = useNavigate()
const onSubmit = async () => {
-
return deleteDoc('Raven Message', message.name).then(() => {
toast('Message deleted')
+ message.is_thread && navigate(`/channel/${message.channel_id}`)
onClose()
})
}
@@ -26,7 +28,7 @@ export const DeleteMessageModal = ({ onClose, message }: DeleteMessageModalProps
return (
<>
- Delete Message
+ Delete {message.is_thread ? 'Thread' : 'Message'}
@@ -40,7 +42,8 @@ export const DeleteMessageModal = ({ onClose, message }: DeleteMessageModalProps
- Are you sure you want to delete this message? It will be deleted for all users.
+ {message.is_thread ? This message is a thread, deleting it will delete all messages in the thread. :
+ Are you sure you want to delete this message? It will be deleted for all users.}
diff --git a/frontend/src/components/feature/chat/ChatMessage/MessageActions/DeleteMessage.tsx b/frontend/src/components/feature/chat/ChatMessage/MessageActions/DeleteMessage.tsx
index 5bb75cc2e..68a965bad 100644
--- a/frontend/src/components/feature/chat/ChatMessage/MessageActions/DeleteMessage.tsx
+++ b/frontend/src/components/feature/chat/ChatMessage/MessageActions/DeleteMessage.tsx
@@ -5,14 +5,13 @@ import { DIALOG_CONTENT_CLASS } from "@/utils/layout/dialog"
import { DeleteMessageModal } from "@/components/feature/chat/ChatMessage/ActionModals/DeleteMessageModal"
export const useDeleteMessage = () => {
+
const [message, setMessage] = useState(null)
const onClose = useCallback(() => {
setMessage(null)
}, [])
-
-
return {
message,
setDeleteMessage: setMessage,
@@ -27,8 +26,6 @@ interface DeleteMessageDialogProps {
onClose: () => void
}
export const DeleteMessageDialog = ({ message, isOpen, onClose }: DeleteMessageDialogProps) => {
-
-
return
{message &&
diff --git a/frontend/src/components/feature/chat/ChatMessage/MessageActions/MessageActions.tsx b/frontend/src/components/feature/chat/ChatMessage/MessageActions/MessageActions.tsx
index 1ae811fb9..7a224638a 100644
--- a/frontend/src/components/feature/chat/ChatMessage/MessageActions/MessageActions.tsx
+++ b/frontend/src/components/feature/chat/ChatMessage/MessageActions/MessageActions.tsx
@@ -11,26 +11,27 @@ import { getErrorMessage } from '@/components/layout/AlertBanner/ErrorBanner'
import { AiOutlineEdit } from 'react-icons/ai'
import { LuForward, LuReply } from 'react-icons/lu'
import { MdOutlineEmojiEmotions } from "react-icons/md";
-import AttachFileToDocument from './AttachFileToDocument'
+import { CreateThreadContextItem } from './QuickActions/CreateThreadButton'
export interface MessageContextMenuProps {
message?: Message | null,
- onDelete: VoidFunction
+ onDelete: VoidFunction,
onEdit: VoidFunction,
onReply: VoidFunction,
onForward: VoidFunction,
- onViewReaction?: VoidFunction
- onAttachDocument: VoidFunction
+ onViewReaction?: VoidFunction,
+ onAttachDocument: VoidFunction,
+ showThreadButton?: boolean
}
-export const MessageContextMenu = ({ message, onDelete, onEdit, onReply, onForward, onAttachDocument, onViewReaction }: MessageContextMenuProps) => {
+export const MessageContextMenu = ({ message, onDelete, onEdit, onReply, onForward, showThreadButton, onAttachDocument, onViewReaction }: MessageContextMenuProps) => {
const copy = useMessageCopy(message)
const { currentUser } = useContext(UserContext)
- const isOwner = currentUser === message?.owner;
+ const isOwner = currentUser === message?.owner && !message?.is_bot_message
const isReactionsAvailable = Object.keys(JSON.parse(message?.message_reactions ?? '{}')).length !== 0
-
+
return (
{message ? <>
@@ -49,7 +50,8 @@ export const MessageContextMenu = ({ message, onDelete, onEdit, onReply, onForwa
Forward
- {/* */}
+ {message && !message.is_thread && showThreadButton && }
+
{message.message_type === 'Text' &&
@@ -87,29 +89,10 @@ export const MessageContextMenu = ({ message, onDelete, onEdit, onReply, onForwa
}
-
-
-
-
-
- {/*
-
-
- Link with document
-
-
-
-
-
-
- Send in an email
-
- */}
-
-
+
{isReactionsAvailable &&
@@ -173,7 +156,7 @@ const SaveMessageAction = ({ message }: { message: Message }) => {
{!isSaved && }
{isSaved && }
- {!isSaved ? "Save" : "Unsave"} message
+ {!isSaved ? "Save" : "Unsave"} Message
diff --git a/frontend/src/components/feature/chat/ChatMessage/MessageActions/QuickActions/CreateThreadButton.tsx b/frontend/src/components/feature/chat/ChatMessage/MessageActions/QuickActions/CreateThreadButton.tsx
new file mode 100644
index 000000000..cdc2e05e9
--- /dev/null
+++ b/frontend/src/components/feature/chat/ChatMessage/MessageActions/QuickActions/CreateThreadButton.tsx
@@ -0,0 +1,49 @@
+import { useFrappePostCall } from 'frappe-react-sdk'
+import { toast } from 'sonner'
+import { QuickActionButton } from './QuickActionButton'
+import { BiMessageDetail } from 'react-icons/bi'
+import { useNavigate } from 'react-router-dom'
+import { ContextMenu, Flex } from '@radix-ui/themes'
+
+const useCreateThread = (messageID: string) => {
+ const navigate = useNavigate()
+
+ const { call } = useFrappePostCall('raven.api.threads.create_thread')
+ const handleCreateThread = () => {
+ call({ 'message_id': messageID }).then((res) => {
+ toast.success('Thread created successfully!')
+ navigate(`/channel/${res.message.channel_id}/thread/${res.message.thread_id}`)
+ }).catch(() => {
+ toast.error('Failed to create thread')
+ })
+ }
+
+ return handleCreateThread
+}
+
+export const CreateThreadActionButton = ({ messageID }: { messageID: string }) => {
+
+ const handleCreateThread = useCreateThread(messageID)
+
+ return (
+
+
+
+ )
+}
+
+export const CreateThreadContextItem = ({ messageID }: { messageID: string }) => {
+
+ const handleCreateThread = useCreateThread(messageID)
+
+ return
+
+
+ Create Thread
+
+
+
+}
\ No newline at end of file
diff --git a/frontend/src/components/feature/chat/ChatMessage/MessageActions/QuickActions/QuickActions.tsx b/frontend/src/components/feature/chat/ChatMessage/MessageActions/QuickActions/QuickActions.tsx
index 824ffcadf..aafa26385 100644
--- a/frontend/src/components/feature/chat/ChatMessage/MessageActions/QuickActions/QuickActions.tsx
+++ b/frontend/src/components/feature/chat/ChatMessage/MessageActions/QuickActions/QuickActions.tsx
@@ -7,22 +7,23 @@ import { FrappeConfig, FrappeContext } from 'frappe-react-sdk'
import { EmojiPickerButton } from './EmojiPickerButton'
import { UserContext } from '@/utils/auth/UserProvider'
import { AiOutlineEdit } from 'react-icons/ai'
-import { LuForward, LuReply } from 'react-icons/lu'
+import { LuReply } from 'react-icons/lu'
import { toast } from 'sonner'
import { getErrorMessage } from '@/components/layout/AlertBanner/ErrorBanner'
+import { CreateThreadActionButton } from './CreateThreadButton'
const QUICK_EMOJIS = ['👍', '✅', '👀', '🎉']
-
interface QuickActionsProps extends MessageContextMenuProps {
isEmojiPickerOpen: boolean,
- setIsEmojiPickerOpen: (open: boolean) => void
+ setIsEmojiPickerOpen: (open: boolean) => void,
}
-export const QuickActions = ({ message, onReply, onEdit, onForward, isEmojiPickerOpen, setIsEmojiPickerOpen }: QuickActionsProps) => {
+
+export const QuickActions = ({ message, onReply, onEdit, isEmojiPickerOpen, setIsEmojiPickerOpen, showThreadButton = true }: QuickActionsProps) => {
const { currentUser } = useContext(UserContext)
- const isOwner = currentUser === message?.owner
+ const isOwner = currentUser === message?.owner && !message?.is_bot_message
const toolbarRef = useRef(null)
const { call } = useContext(FrappeContext) as FrappeConfig
@@ -107,12 +108,7 @@ export const QuickActions = ({ message, onReply, onEdit, onForward, isEmojiPicke
}
-
-
-
+ {message && !message.is_thread && showThreadButton && }
void,
isHighlighted?: boolean,
setReactionMessage: (message: Message) => void,
+ showThreadButton?: boolean
}
-export const MessageItem = ({ message, setDeleteMessage, isHighlighted, onReplyMessageClick, setEditMessage, replyToMessage, forwardMessage, onAttachDocument, setReactionMessage }: MessageBlockProps) => {
+export const MessageItem = ({ message, setDeleteMessage, isHighlighted, onReplyMessageClick, setEditMessage, replyToMessage, forwardMessage, onAttachDocument, setReactionMessage, showThreadButton = true }: MessageBlockProps) => {
const { name, owner: userID, is_bot_message, bot, creation: timestamp, message_reactions, is_continuation, linked_message, replied_message_details } = message
@@ -108,6 +109,18 @@ export const MessageItem = ({ message, setDeleteMessage, isHighlighted, onReplyM
return (
+ {!message.is_continuation && message.is_thread ?
+
+
: null}
-
: null}
@@ -154,7 +166,9 @@ export const MessageItem = ({ message, setDeleteMessage, isHighlighted, onReplyM
onClick={() => onReplyMessageClick(linked_message)}
message={replyMessageDetails} />
}
+
{ /* Show message according to type */}
+
}
+
+ {message.is_thread === 1 ? : null}
+
+
{(isHoveredDebounced || isEmojiPickerOpen) &&
}
@@ -190,6 +209,7 @@ export const MessageItem = ({ message, setDeleteMessage, isHighlighted, onReplyM
-
+
)
}
-interface MessageLeftElementProps extends BoxProps {
+type MessageLeftElementProps = BoxProps & {
message: MessageBlock['data'],
user?: UserFields,
isActive?: boolean
@@ -311,7 +331,7 @@ export const UserHoverCard = memo(({ user, userID, isActive }: UserProps) => {
})
-interface MessageContentProps extends BoxProps {
+type MessageContentProps = BoxProps & {
user?: UserFields
message: Message,
}
diff --git a/frontend/src/components/feature/chat/ChatMessage/Renderers/ThreadMessage.tsx b/frontend/src/components/feature/chat/ChatMessage/Renderers/ThreadMessage.tsx
new file mode 100644
index 000000000..a382feb01
--- /dev/null
+++ b/frontend/src/components/feature/chat/ChatMessage/Renderers/ThreadMessage.tsx
@@ -0,0 +1,46 @@
+import { Link } from "react-router-dom"
+import { Message } from "../../../../../../../types/Messaging/Message"
+import { Button, Flex, Text } from "@radix-ui/themes"
+import { useFrappeGetDocCount } from "frappe-react-sdk"
+import { RavenMessage } from "@/types/RavenMessaging/RavenMessage"
+import { useFrappeDocumentEventListener } from "frappe-react-sdk"
+import { useFrappeEventListener } from "frappe-react-sdk"
+
+export const ThreadMessage = ({ thread }: { thread: Message }) => {
+
+ return (
+
+
+
+
+
+
+ )
+}
+
+export const ThreadReplyCount = ({ thread }: { thread: Message }) => {
+
+ const { data, mutate } = useFrappeGetDocCount("Raven Message", [["channel_id", "=", thread.name]], undefined, undefined, undefined, {
+ revalidateOnFocus: false,
+ shouldRetryOnError: false,
+ keepPreviousData: false
+ })
+
+ // Listen to realtime event for new message count
+ useFrappeDocumentEventListener('Raven Message', thread.name, () => { })
+
+ useFrappeEventListener('thread_reply_created', () => {
+ mutate()
+ })
+
+ return
+ {data ?? 0}
+ {data && data === 1 ? 'Reply' : 'Replies'}
+
+}
\ No newline at end of file
diff --git a/frontend/src/components/feature/chat/ChatMessage/Renderers/TiptapRenderer/TiptapRenderer.tsx b/frontend/src/components/feature/chat/ChatMessage/Renderers/TiptapRenderer/TiptapRenderer.tsx
index e7bf3e96a..578992551 100644
--- a/frontend/src/components/feature/chat/ChatMessage/Renderers/TiptapRenderer/TiptapRenderer.tsx
+++ b/frontend/src/components/feature/chat/ChatMessage/Renderers/TiptapRenderer/TiptapRenderer.tsx
@@ -41,17 +41,17 @@ type TiptapRendererProps = BoxProps & {
user?: UserFields,
showLinkPreview?: boolean,
isScrolling?: boolean,
- isTruncated?: boolean
+ showMiniImage?: boolean,
}
-export const TiptapRenderer = ({ message, user, isScrolling = false, isTruncated = false, showLinkPreview = true, ...props }: TiptapRendererProps) => {
+export const TiptapRenderer = ({ message, user, isScrolling = false, showMiniImage = false, showLinkPreview = true, ...props }: TiptapRendererProps) => {
const editor = useEditor({
content: message.text,
editable: false,
editorProps: {
attributes: {
- class: isTruncated ? 'tiptap-renderer line-clamp-3' : 'tiptap-renderer'
+ class: clsx('tiptap-renderer'),
}
},
enableCoreExtensions: true,
@@ -122,7 +122,7 @@ export const TiptapRenderer = ({ message, user, isScrolling = false, isTruncated
}),
Image.configure({
HTMLAttributes: {
- class: 'w-full max-w-48 sm:max-w-96 mt-1 h-auto'
+ class: showMiniImage ? 'w-auto h-16 max-h-16' : 'w-full max-w-48 sm:max-w-96 mt-1 h-auto'
},
inline: true
}),
diff --git a/frontend/src/components/feature/chat/ChatStream/ChatBoxBody.tsx b/frontend/src/components/feature/chat/ChatStream/ChatBoxBody.tsx
index 4110da8e0..fc8e5d59e 100644
--- a/frontend/src/components/feature/chat/ChatStream/ChatBoxBody.tsx
+++ b/frontend/src/components/feature/chat/ChatStream/ChatBoxBody.tsx
@@ -14,6 +14,8 @@ import { BiX } from "react-icons/bi"
import ChatStream from "./ChatStream"
import Tiptap from "../ChatInput/Tiptap"
import useFetchChannelMembers from "@/hooks/fetchers/useFetchChannelMembers"
+import { useParams } from "react-router-dom"
+import clsx from "clsx"
const COOL_PLACEHOLDERS = [
"Delivering messages atop dragons 🐉 is available on a chargeable basis.",
@@ -79,8 +81,10 @@ export const ChatBoxBody = ({ channelData }: ChatBoxBodyProps) => {
const isDM = channelData?.is_direct_message === 1 || channelData?.is_self_message === 1
+ const { threadID } = useParams()
+
return (
-
+
{
maxFiles={10}
maxFileSize={10000000}>
{channelData?.is_archived == 0 && (isUserInChannel || channelData?.type === 'Open')
@@ -100,6 +105,7 @@ export const ChatBoxBody = ({ channelData }: ChatBoxBodyProps) => {
addFile
}}
clearReplyMessage={handleCancelReply}
+ channelMembers={channelMembers}
// placeholder={randomPlaceholder}
replyMessage={selectedMessage}
sessionStorageKey={`tiptap-${channelData.name}`}
diff --git a/frontend/src/components/feature/chat/ChatStream/ChatStream.tsx b/frontend/src/components/feature/chat/ChatStream/ChatStream.tsx
index c199a107b..4b8e6977b 100644
--- a/frontend/src/components/feature/chat/ChatStream/ChatStream.tsx
+++ b/frontend/src/components/feature/chat/ChatStream/ChatStream.tsx
@@ -3,7 +3,6 @@ import { DeleteMessageDialog, useDeleteMessage } from '../ChatMessage/MessageAct
import { EditMessageDialog, useEditMessage } from '../ChatMessage/MessageActions/EditMessage'
import { MessageItem } from '../ChatMessage/MessageItem'
import { ChannelHistoryFirstMessage } from '@/components/layout/EmptyState'
-import { useParams } from 'react-router-dom'
import useChatStream from './useChatStream'
import { useRef } from 'react'
import { Loader } from '@/components/common/Loader'
@@ -64,16 +63,17 @@ import { ReactionAnalyticsDialog, useMessageReactionAnalytics } from '../ChatMes
*/
type Props = {
+ channelID: string,
replyToMessage: (message: Message) => void,
+ showThreadButton?: boolean
}
-const ChatStream = ({ replyToMessage }: Props) => {
+const ChatStream = ({ channelID, replyToMessage, showThreadButton = true }: Props) => {
- const { channelID } = useParams()
const scrollRef = useRef(null)
- const { messages, hasOlderMessages, loadOlderMessages, goToLatestMessages, hasNewMessages, error, loadNewerMessages, isLoading, highlightedMessage, scrollToMessage } = useChatStream(scrollRef)
+ const { messages, hasOlderMessages, loadOlderMessages, goToLatestMessages, hasNewMessages, error, loadNewerMessages, isLoading, highlightedMessage, scrollToMessage } = useChatStream(channelID, scrollRef)
const { setDeleteMessage, ...deleteProps } = useDeleteMessage()
const { setEditMessage, ...editProps } = useEditMessage()
@@ -142,9 +142,10 @@ const ChatStream = ({ replyToMessage }: Props) => {
setEditMessage={setEditMessage}
replyToMessage={replyToMessage}
forwardMessage={setForwardMessage}
+ showThreadButton={showThreadButton}
onAttachDocument={setAttachDocument}
setDeleteMessage={setDeleteMessage}
- setReactionMessage={setReactionMessage}
+ setReactionMessage={setReactionMessage}
/>
diff --git a/frontend/src/components/feature/chat/ChatStream/useChatStream.ts b/frontend/src/components/feature/chat/ChatStream/useChatStream.ts
index 5500c7d2b..dd21778c2 100644
--- a/frontend/src/components/feature/chat/ChatStream/useChatStream.ts
+++ b/frontend/src/components/feature/chat/ChatStream/useChatStream.ts
@@ -1,6 +1,6 @@
import { useFrappeDocumentEventListener, useFrappeEventListener, useFrappeGetCall, useFrappePostCall } from 'frappe-react-sdk'
import { MutableRefObject, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
-import { useBeforeUnload, useLocation, useNavigate, useParams } from 'react-router-dom'
+import { useBeforeUnload, useLocation, useNavigate } from 'react-router-dom'
import { Message } from '../../../../../../types/Messaging/Message'
import { getDateObject } from '@/utils/dateConversions/utils'
import { useDebounce } from '@/hooks/useDebounce'
@@ -23,9 +23,7 @@ type MessageDateBlock = Message | {
/**
* Hook to fetch messages to be rendered on the chat interface
*/
-const useChatStream = (scrollRef: MutableRefObject