UNPKG

softchatjs-react

Version:
1 lines 202 kB
{"version":3,"sources":["../src/components/chat/Chat.tsx","../src/components/inputs/chat-input.tsx","../src/components/edit-panel/index.tsx","../src/components/text/text.tsx","../src/providers/chatClientProvider.tsx","../src/providers/clientStateProvider.tsx","../src/theme/index.tsx","../src/components/menu/index.tsx","../src/components/emoji/index.tsx","../src/helpers/date.ts","../src/components/audio/audio-player.tsx","../src/components/assets/icons.tsx","../src/components/Loaders/index.tsx","../src/components/message-list/message-list.tsx","../src/components/avartar/avartar.tsx","../src/components/message-item/index.tsx","../src/components/options-panel/options-panel.tsx","../src/components/user-conversations/index.tsx","../src/components/user-conversations/ConversationHeader.tsx","../src/components/modals/index.tsx","../src/components/chat/ChatTopNav.tsx","../src/components/navigation/index.tsx","../src/components/broadcast-lists/index.tsx"],"sourcesContent":["import React, { useContext, useEffect, useRef, useState, useMemo } from \"react\";\r\nimport ChatClient, {\r\n ChatEventGenerics,\r\n Conversation,\r\n Message,\r\n UserMeta,\r\n ConnectionEvent,\r\n ConversationListMeta,\r\n} from \"softchatjs-core\";\r\nimport styles from \"./chat.module.css\";\r\nimport ChatInput from \"../inputs/chat-input\";\r\nimport MessageList from \"../message-list/message-list\";\r\nimport { ConversationList as MainList } from \"../user-conversations\";\r\nimport {\r\n ConversationItem,\r\n useChatState,\r\n} from \"../../providers/clientStateProvider\";\r\nimport { useChatClient } from \"../../providers/chatClientProvider\";\r\nimport { ImageViewer } from \"../modals\";\r\nimport { ChatTopNav } from \"./ChatTopNav\";\r\nimport { ChatIcon } from \"../assets/icons\";\r\nimport Navbar, { NavButton } from \"../navigation\";\r\nimport BroadcastLists from \"../broadcast-lists\";\r\n\r\ntype ChatProps = {\r\n renderChatBubble?: (message: Message) => JSX.Element;\r\n renderConversationList?: (props: {\r\n conversations: ConversationItem[];\r\n onCoversationItemClick: (conversationItem: ConversationItem) => void;\r\n }) => JSX.Element;\r\n renderChatHeader?: () => JSX.Element;\r\n renderChatInput?: (props: { onChange: (e: string) => void }) => JSX.Element;\r\n renderNavbar?: (props: NavButton[]) => JSX.Element;\r\n onCreateBroadcastList?: () => void;\r\n user: UserMeta;\r\n userList?: UserMeta[];\r\n /**\r\n * use activeConversationId to preselect a conversation once the user enters the chat.\r\n */\r\n activeConversationId?: string;\r\n /**@note\r\n * This value calculates the the height off the container incase of an external header\r\n * Value should be in px i.e 300\r\n */\r\n headerHeightOffset?: number;\r\n /**\r\n * FCM token used to send push notifications to web users\r\n */\r\n webToken?: string;\r\n /**\r\n * Hide the broadcast icon on the navigation bar\r\n * NOTE: this doesn't disable the functionality itself\r\n */\r\n enableBroadcasts?: boolean;\r\n /**\r\n * Set a custom height for the chat container\r\n */\r\n height?: number;\r\n /**\r\n * Set a custom width for the chat container\r\n */\r\n width?: number;\r\n};\r\n\r\nconst Chat = (props: ChatProps) => {\r\n const {\r\n headerHeightOffset = 0,\r\n user,\r\n userList = [],\r\n onCreateBroadcastList,\r\n activeConversationId,\r\n webToken,\r\n enableBroadcasts = true,\r\n height,\r\n width,\r\n } = props;\r\n const chatUserId = user.uid;\r\n const { client, config } = useChatClient();\r\n const {\r\n activeConversation,\r\n showImageModal,\r\n setActiveConversation,\r\n setConversations,\r\n conversations,\r\n connectionStatus,\r\n setConnectionStatus,\r\n } = useChatState();\r\n const [isConnected, setIsConnected] = useState(false);\r\n const [loadingMessages, setLoadingMessages] = useState(false);\r\n const [fecthingmore, setFetchingMore] = useState(false);\r\n const [messages, setMessages] = useState<Message[]>([]);\r\n const [editDetails, setEditDetails] = useState<{\r\n message: Message;\r\n isEditing?: boolean;\r\n isReplying?: boolean;\r\n }>();\r\n // 47698942\r\n const [recipientTyping, setRecipientTyping] = useState(false);\r\n const [presentPage, setpresentPage] = useState(1);\r\n const [mainListOpen, setMainListOpen] = useState(true);\r\n const [showUserList, setShowUserList] = useState(false);\r\n const [scrollToKey, setScrollToKey] = useState<string>(\"\");\r\n const messagesEndRef = useRef<null | HTMLDivElement>(null);\r\n const [forceScrollCount, setForceScrollCount] = useState(0);\r\n const [recipientId, setRecipientId] = useState(\"\");\r\n\r\n const [isSmallScreen, setIsSmallScreen] = useState(false);\r\n const [inputContainerWidth, setInputContainerWidth] = useState(0);\r\n const [view, setView] = useState<\"conversation-list\" | \"broadcast-lists\">(\r\n \"conversation-list\"\r\n );\r\n const chatContainerRef = useRef<HTMLDivElement>(null);\r\n\r\n const { theme } = config;\r\n\r\n const [position, setPosition] = useState({ x: 0, y: 0 });\r\n const [menuDetails, setMenuDetails] = useState<{\r\n element: JSX.Element | null;\r\n }>({\r\n element: null,\r\n });\r\n\r\n const generalMenuRef: any = useRef(null);\r\n const textInputRef: any = useRef(null);\r\n const userListRef: any = useRef(null);\r\n\r\n const resetState = () => {\r\n setpresentPage(1);\r\n };\r\n\r\n const closeGeneralMenu = (e: any) => {\r\n if (menuDetails.element && !generalMenuRef.current?.contains(e.target)) {\r\n setMenuDetails({\r\n element: null,\r\n });\r\n }\r\n if (!userListRef.current?.contains(e.target)) {\r\n setShowUserList(false);\r\n }\r\n };\r\n\r\n const getPreSelectedConversation = () => {\r\n try {\r\n if (activeConversationId) {\r\n const converationListMeta = client.getConversations();\r\n const selectedConveration = converationListMeta[activeConversationId];\r\n if (selectedConveration) {\r\n setActiveConversation(selectedConveration);\r\n }\r\n }\r\n } catch (error) {\r\n console.error(error.message);\r\n }\r\n };\r\n\r\n useEffect(() => {\r\n getPreSelectedConversation();\r\n }, [activeConversationId, client, connectionStatus]);\r\n\r\n useEffect(() => {\r\n if (props.user && webToken) {\r\n client.initializeUser(props.user, {\r\n notificationConfig: { type: \"fcm\", token: webToken },\r\n });\r\n } else {\r\n client.initializeUser(props.user);\r\n }\r\n }, [props.user, webToken]);\r\n\r\n useEffect(() => {\r\n const checkScreenSize = () => {\r\n const screenWidth = window.innerWidth;\r\n const screenHeight = window.innerHeight;\r\n setIsSmallScreen(screenWidth < 1024 || screenHeight < 768);\r\n };\r\n \r\n if (!width || !height) {\r\n checkScreenSize();\r\n window.addEventListener(\"resize\", checkScreenSize);\r\n \r\n return () => window.removeEventListener(\"resize\", checkScreenSize);\r\n }\r\n \r\n const checkSize = () => {\r\n if (!chatContainerRef.current) return;\r\n \r\n const { offsetWidth, offsetHeight } = chatContainerRef.current;\r\n setIsSmallScreen(offsetWidth < 1024 || offsetHeight < 768);\r\n };\r\n \r\n const observer = new ResizeObserver(() => checkSize());\r\n \r\n if (chatContainerRef.current) {\r\n observer.observe(chatContainerRef.current);\r\n checkSize(); // Initial check\r\n }\r\n \r\n return () => observer.disconnect();\r\n }, [height, width]);\r\n \r\n\r\n useEffect(() => {\r\n document.addEventListener(\"mousedown\", closeGeneralMenu);\r\n }, [generalMenuRef, closeGeneralMenu]);\r\n\r\n const handleMessage = (event?: any) => {\r\n if (activeConversation) {\r\n if (\r\n event.message.conversationId ===\r\n activeConversation.conversation.conversationId\r\n ) {\r\n setMessages((prev) => {\r\n return [...prev, event.message];\r\n });\r\n setForceScrollCount(forceScrollCount + 1); //((:)\r\n setTimeout(() => {\r\n messagesEndRef?.current?.scrollIntoView({\r\n block: \"end\",\r\n behavior: \"smooth\",\r\n });\r\n }, 300);\r\n }\r\n }\r\n };\r\n\r\n const handleTypingStarted = (event: any) => {\r\n if (activeConversation) {\r\n if (\r\n activeConversation.conversation.conversationId === event.conversationId\r\n ) {\r\n setRecipientTyping(true);\r\n }\r\n }\r\n };\r\n\r\n const handleTypingStopped = (event: any) => {\r\n setRecipientTyping(false);\r\n };\r\n\r\n const handleDeletedMessage = (event: any) => {\r\n setMessages((prev) => {\r\n return prev.filter(\r\n (m: Message) => m.messageId !== event.message.messageId\r\n );\r\n });\r\n };\r\n\r\n const handleEditedMessage = (event: any) => {\r\n setMessages((prev: any) => {\r\n const newMessages = prev.map((item: Message) => {\r\n if (item?.messageId === event?.message?.messageId) {\r\n return { ...item, ...event.message };\r\n }\r\n return item;\r\n });\r\n return [...newMessages];\r\n });\r\n };\r\n\r\n const handleConnectionChanged = (\r\n event: ChatEventGenerics<ConnectionEvent>\r\n ) => {\r\n setConnectionStatus(event);\r\n };\r\n\r\n const handleConversationsListChanged = (e: {\r\n conversationListMeta: ConversationListMeta;\r\n }) => {\r\n const conversationList = Object.values(\r\n e.conversationListMeta\r\n ).flat() as ConversationItem[];\r\n conversationList.sort(\r\n (a, b) =>\r\n new Date(b.lastMessage?.createdAt).getTime() -\r\n new Date(a.lastMessage?.createdAt).getTime()\r\n );\r\n setConversations(conversationList);\r\n };\r\n\r\n const clearUnread = () => {\r\n if (client && activeConversation) {\r\n const messageIds = activeConversation.unread;\r\n const msClient = client.messageClient(\r\n activeConversation.conversation.conversationId\r\n );\r\n msClient.readMessages(activeConversation?.conversation.conversationId, {\r\n uid: chatUserId,\r\n messageIds,\r\n });\r\n }\r\n };\r\n\r\n useEffect(() => {\r\n if (client && activeConversation) {\r\n const recipients = activeConversation.conversation.participants.filter(\r\n (id) => id !== chatUserId\r\n );\r\n setRecipientId(recipients[0]);\r\n client\r\n .messageClient(activeConversation.conversation.conversationId)\r\n .setActiveConversation();\r\n clearUnread();\r\n }\r\n }, [activeConversation, client]);\r\n\r\n useEffect(() => {\r\n if (client) {\r\n resetState();\r\n return () => {\r\n client.unsubscribe(\"new_message\" as any, handleMessage);\r\n };\r\n }\r\n }, [client, activeConversation]);\r\n\r\n useEffect(() => {\r\n if (client) {\r\n client.subscribe(\"connection_changed\" as any, handleConnectionChanged);\r\n\r\n client.subscribe(\"new_message\" as any, handleMessage);\r\n\r\n client.subscribe(\"edited_message\" as any, handleEditedMessage);\r\n\r\n client.subscribe(\"started_typing\" as any, handleTypingStarted);\r\n\r\n client.subscribe(\"stopped_typing\" as any, handleTypingStopped);\r\n\r\n client.subscribe(\"deleted_message\" as any, handleDeletedMessage);\r\n\r\n client.subscribe(\r\n \"conversation_list_meta_changed\" as any,\r\n handleConversationsListChanged\r\n );\r\n }\r\n\r\n return () => {\r\n if (client) {\r\n client.unsubscribe(\"started_typing\" as any, handleTypingStarted);\r\n client.unsubscribe(\"stopped_typing\" as any, handleTypingStopped);\r\n client.unsubscribe(\"deleted_message\" as any, handleDeletedMessage);\r\n client.unsubscribe(\"edited_message\" as any, handleEditedMessage);\r\n client.unsubscribe(\"new_message\" as any, handleMessage);\r\n client.unsubscribe(\r\n \"connection_changed\" as any,\r\n handleConnectionChanged\r\n );\r\n client.unsubscribe(\r\n \"conversation_list_meta_changed\" as any,\r\n handleConversationsListChanged\r\n );\r\n }\r\n };\r\n }, [client, activeConversation]);\r\n\r\n useEffect(() => {\r\n if (activeConversation) {\r\n messagesEndRef.current.scrollIntoView({});\r\n setMessages(activeConversation.conversation.messages);\r\n setForceScrollCount(forceScrollCount + 1); //to trigger rerender\r\n }\r\n }, [isConnected, activeConversation]);\r\n\r\n useEffect(() => {\r\n if (messagesEndRef.current) {\r\n messagesEndRef.current.scrollIntoView({});\r\n }\r\n }, [forceScrollCount]);\r\n\r\n const getBroadcastMessages = async (conversationId: string) => {\r\n try {\r\n if (client) {\r\n const messageList = (await client\r\n .messageClient(conversationId)\r\n .getBroadcastListMessages(1)) as Array<Message>;\r\n setMessages(messageList);\r\n setpresentPage(2);\r\n if (messages[0]) {\r\n setScrollToKey(messages[0].messageId);\r\n }\r\n }\r\n } catch (error) {\r\n console.error(error.message);\r\n }\r\n };\r\n\r\n const getMessages = async (conversationId: string) => {\r\n try {\r\n if (client) {\r\n const messageList = (await client\r\n .messageClient(conversationId)\r\n .getMessages(1)) as Array<Message>;\r\n setMessages(messageList);\r\n setpresentPage(2);\r\n if (messages[0]) {\r\n setScrollToKey(messages[0].messageId);\r\n }\r\n }\r\n } catch (error) {\r\n console.error(error.message);\r\n }\r\n };\r\n\r\n const getOlderMessages = async (func: () => void) => {\r\n try {\r\n if (client && presentPage > 1) {\r\n setFetchingMore(true);\r\n const messageList = (await client\r\n .messageClient(activeConversation?.conversation?.conversationId!)\r\n .getMessages(presentPage)) as Array<Message>;\r\n setMessages((prev) => {\r\n return [...messageList, ...prev];\r\n });\r\n setpresentPage((p) => p + 1);\r\n if (messages[0]) {\r\n setScrollToKey(messages[0].messageId);\r\n }\r\n func();\r\n }\r\n } catch (error) {\r\n console.error(error.message);\r\n } finally {\r\n setFetchingMore(false);\r\n }\r\n };\r\n\r\n useEffect(() => {\r\n try {\r\n var conversationId = activeConversation?.conversation?.conversationId;\r\n if (conversationId) {\r\n if (\r\n activeConversation.conversation.conversationType === \"broadcast-chat\"\r\n ) {\r\n getBroadcastMessages(conversationId);\r\n } else {\r\n getMessages(conversationId);\r\n }\r\n }\r\n } catch (err) {\r\n console.error(err);\r\n } finally {\r\n setFetchingMore(false);\r\n }\r\n }, [activeConversation?.conversation?.conversationId]);\r\n\r\n const chatConverationStyles = useMemo(() => {\r\n if (isSmallScreen) {\r\n return {\r\n display: \"flex\",\r\n width: \"100%\",\r\n height: height? `${height}px` : \"100%\",\r\n };\r\n }\r\n return {\r\n display: \"flex\",\r\n width: \"30%\",\r\n height: \"100%\",\r\n };\r\n }, [width, height, isSmallScreen]);\r\n\r\n if (isSmallScreen && activeConversation) {\r\n return (\r\n <div\r\n ref={chatContainerRef}\r\n className={styles.chat__messages}\r\n // style={{ width: \"100%\", backgroundColor: theme?.background?.primary }}\r\n style={{\r\n width: width ? `${width}px` : \"100%\",\r\n height: height\r\n ? `${height}px`\r\n : `calc(100vh - ${headerHeightOffset}px)`,\r\n backgroundColor: theme?.background?.primary,\r\n }}\r\n >\r\n <ChatTopNav\r\n setMainListOpen={setMainListOpen}\r\n renderChatHeader={props.renderChatHeader}\r\n onClose={resetState}\r\n chatUserId={chatUserId}\r\n />\r\n <MessageList\r\n customHeight={height}\r\n headerHeightOffset={headerHeightOffset}\r\n setEditDetails={setEditDetails}\r\n messages={messages}\r\n mousePosition={position}\r\n conversationId={activeConversation?.conversation?.conversationId}\r\n client={client}\r\n textInputRef={textInputRef}\r\n presentPage={presentPage}\r\n setPresentPage={setpresentPage}\r\n recipientTyping={recipientTyping}\r\n setMainListOpen={setMainListOpen}\r\n recipientId={recipientId}\r\n scrollToKey={scrollToKey}\r\n fetchingMore={fecthingmore}\r\n messagesEndRef={messagesEndRef}\r\n renderChatBubble={props.renderChatBubble}\r\n renderChatHeader={props.renderChatHeader}\r\n getOlderMessages={(func) =>\r\n activeConversation?.conversation?.conversationType !==\r\n \"broadcast-chat\"\r\n ? getOlderMessages(func)\r\n : null\r\n }\r\n />\r\n <ChatInput\r\n closeGeneralMenu={() => setMenuDetails({ element: null })}\r\n generalMenuRef={generalMenuRef}\r\n setMenuDetails={setMenuDetails}\r\n menuDetails={menuDetails}\r\n setEditDetails={setEditDetails}\r\n editProps={editDetails as any}\r\n recipientId={recipientId}\r\n conversationId={activeConversation?.conversation?.conversationId}\r\n client={client}\r\n recipientTyping={recipientTyping}\r\n textInputRef={textInputRef}\r\n renderChatInput={props.renderChatInput}\r\n />\r\n {showImageModal.length > 0 && <ImageViewer />}\r\n </div>\r\n );\r\n }\r\n\r\n return (\r\n <div\r\n ref={chatContainerRef}\r\n style={{\r\n background: theme?.background?.primary,\r\n // height: `calc(100vh - ${headerHeightOffset}px)`,\r\n height: height\r\n ? `${height}px`\r\n : `calc(100vh - ${headerHeightOffset}px)`,\r\n width: width ? `${width}px` : \"100%\",\r\n }}\r\n className={styles.chat}\r\n >\r\n <div style={{ ...chatConverationStyles }}>\r\n {/* <div className={`${styles.chat__conversations}`} style={{ ...chatConverationStyles }}> */}\r\n <Navbar\r\n chatUser={user}\r\n userList={userList}\r\n renderNavbar={props.renderNavbar}\r\n activeView={view}\r\n onViewChanged={(value) => setView(value)}\r\n connectionStatus={connectionStatus}\r\n enableBroadcasts={enableBroadcasts}\r\n />\r\n {view === \"conversation-list\" ? (\r\n <MainList\r\n resetState={resetState}\r\n connectionStatus={connectionStatus}\r\n setShowUserList={setShowUserList}\r\n showUserList={showUserList}\r\n setMainListOpen={setMainListOpen}\r\n userListRef={userListRef}\r\n renderConversationList={props.renderConversationList}\r\n />\r\n ) : (\r\n <BroadcastLists onCreateBroadcastList={onCreateBroadcastList} />\r\n )}\r\n </div>\r\n {activeConversation ? (\r\n <div className={styles.chat__messages}>\r\n <ChatTopNav\r\n setMainListOpen={setMainListOpen}\r\n renderChatHeader={props.renderChatHeader}\r\n onClose={resetState}\r\n chatUserId={chatUserId}\r\n />\r\n <MessageList\r\n headerHeightOffset={headerHeightOffset}\r\n setEditDetails={setEditDetails}\r\n messages={messages}\r\n mousePosition={position}\r\n conversationId={activeConversation?.conversation?.conversationId}\r\n client={client}\r\n textInputRef={textInputRef}\r\n presentPage={presentPage}\r\n setPresentPage={setpresentPage}\r\n recipientTyping={recipientTyping}\r\n setMainListOpen={setMainListOpen}\r\n recipientId={recipientId}\r\n scrollToKey={scrollToKey}\r\n fetchingMore={fecthingmore}\r\n messagesEndRef={messagesEndRef}\r\n renderChatBubble={props.renderChatBubble}\r\n renderChatHeader={props.renderChatHeader}\r\n getOlderMessages={(func) => getOlderMessages(func)}\r\n />\r\n <ChatInput\r\n closeGeneralMenu={() => setMenuDetails({ element: null })}\r\n generalMenuRef={generalMenuRef}\r\n setMenuDetails={setMenuDetails}\r\n menuDetails={menuDetails}\r\n setEditDetails={setEditDetails}\r\n editProps={editDetails as any}\r\n recipientId={recipientId}\r\n conversationId={activeConversation?.conversation?.conversationId}\r\n client={client}\r\n recipientTyping={recipientTyping}\r\n textInputRef={textInputRef}\r\n renderChatInput={props.renderChatInput}\r\n />\r\n </div>\r\n ) : (\r\n <div\r\n style={{\r\n flex: 1,\r\n display: isSmallScreen ? \"none\" : \"flex\",\r\n alignItems: \"center\",\r\n flexDirection: \"column\",\r\n justifyContent: \"center\",\r\n }}\r\n >\r\n <ChatIcon color={theme.icon} size={100} />\r\n <p style={{ marginTop: \"30px\", color: theme.text.secondary }}>\r\n Select a conversation to get started\r\n </p>\r\n </div>\r\n )}\r\n {showImageModal.length > 0 && <ImageViewer />}\r\n </div>\r\n );\r\n};\r\n\r\nexport default Chat;\r\n","import React, {\r\n Dispatch,\r\n SetStateAction,\r\n useEffect,\r\n useState,\r\n useRef,\r\n} from \"react\";\r\nimport styles from \"./chat-input.module.css\";\r\nimport \"./chat-input.module.css\";\r\nimport ChatClient, {\r\n AttachmentTypes,\r\n Media,\r\n MediaType,\r\n Message,\r\n generateId,\r\n} from \"softchatjs-core\";\r\nimport {\r\n AiOutlineAudio,\r\n AiOutlineClose,\r\n AiOutlineDelete,\r\n AiOutlinePlus,\r\n} from \"react-icons/ai\";\r\nimport EditPanel from \"../edit-panel\";\r\nimport Text from \"../text/text\";\r\nimport { AttachmentMenu, Menu } from \"../menu\";\r\nimport { v4 as uuidv4 } from \"uuid\";\r\nimport { MdCancel } from \"react-icons/md\";\r\nimport { IoMdSend } from \"react-icons/io\";\r\nimport { text } from \"stream/consumers\";\r\nimport { VscSend } from \"react-icons/vsc\";\r\n// import AudioRecorder from \"../audio\";\r\n// import AudioReactRecorder, { RecordState } from \"audio-react-recorder\";\r\nimport { CiFaceSmile } from \"react-icons/ci\";\r\nimport { InputEmojis } from \"../emoji\";\r\nimport { useChatClient } from \"../../providers/chatClientProvider\";\r\nimport { convertToMinutes } from \"../../helpers/date\";\r\nimport AudioPlayer from \"../audio/audio-player\";\r\nimport TrashIcon, { LockIcon } from \"../assets/icons\";\r\n// import { AudioRecorder } from \"react-audio-voice-recorder\";\r\nimport { IoStopCircleOutline } from \"react-icons/io5\";\r\nimport { useChatState } from \"../../providers/clientStateProvider\";\r\nimport { LinearLoader } from \"../Loaders/index\";\r\n// import { convertWebmToMp3 } from \"@/src/helpers/toMp3\";\r\n\r\nconst ChatInput = ({\r\n client,\r\n conversationId,\r\n recipientId,\r\n editProps,\r\n setEditDetails,\r\n recipientTyping,\r\n setMenuDetails,\r\n menuDetails,\r\n generalMenuRef,\r\n closeGeneralMenu,\r\n textInputRef,\r\n renderChatInput,\r\n}: {\r\n client: ChatClient;\r\n conversationId: string;\r\n recipientId: string;\r\n editProps: {\r\n message: Message;\r\n isEditing?: boolean;\r\n isReplying?: boolean;\r\n };\r\n setEditDetails: Dispatch<\r\n SetStateAction<\r\n | { message: Message; isEditing?: boolean; isReplying?: boolean }\r\n | undefined\r\n >\r\n >;\r\n recipientTyping: boolean;\r\n menuDetails: { element: JSX.Element | null };\r\n setMenuDetails?: Dispatch<SetStateAction<{ element: JSX.Element | null }>>;\r\n generalMenuRef: any;\r\n closeGeneralMenu: () => void;\r\n textInputRef: any;\r\n renderChatInput?: (props: { onChange: (e: string) => void }) => JSX.Element;\r\n}) => {\r\n const [message, setMessage] = useState<Partial<Message>>();\r\n const [files, setFiles] = useState<File[]>([]);\r\n const [showEmojiPicker, setShowEmojiPicker] = useState(false);\r\n const [sending, setSending] = useState(false);\r\n const [isRecording, setIsRecording] = useState(false);\r\n const [audioChunks, setAudioChunks] = useState([]);\r\n const [audioRecorder, setAudioRecorder] = useState<MediaRecorder | null>(\r\n null\r\n );\r\n const inputContainerRef = useRef<HTMLDivElement>();\r\n const [voiceMessageDuration, setVoiceMessageDuration] = useState(0);\r\n const [audioBlob, setAudioBlob] = useState<Blob | null>(null);\r\n const [audioBlobPLaceHolder, setAudioBlobPlaceHolder] = useState<Blob | null>(\r\n null\r\n );\r\n const [inputContainerWidth, setInputContainerWidth] = useState(0);\r\n\r\n const msClient = client.messageClient(conversationId);\r\n const { config } = useChatClient();\r\n const { theme } = config;\r\n const { activeConversation } = useChatState();\r\n const [uploading, showUploading] = useState(false);\r\n const primaryActionColor = theme?.icon || \"white\";\r\n const inputBg = config?.theme?.input.bgColor || \"#222529\";\r\n\r\n const updateWidth = () => {\r\n if (inputContainerRef.current) {\r\n const { width } = inputContainerRef?.current?.getBoundingClientRect();\r\n setInputContainerWidth(width);\r\n }\r\n };\r\n\r\n useEffect(() => {\r\n updateWidth();\r\n }, []);\r\n\r\n useEffect(() => {\r\n if (editProps?.isEditing) {\r\n setMessage(editProps?.message);\r\n } else {\r\n setMessage({});\r\n }\r\n }, [editProps?.isEditing]);\r\n\r\n const prepareAudio = () => {\r\n if (navigator.mediaDevices) {\r\n const constraints = { audio: true };\r\n navigator.mediaDevices\r\n .getUserMedia(constraints)\r\n .then((stream) => {\r\n const mediaRecorder = new MediaRecorder(stream);\r\n mediaRecorder.start(1000);\r\n setIsRecording(true);\r\n setAudioRecorder(mediaRecorder);\r\n\r\n var chunks = [];\r\n\r\n mediaRecorder.onstop = (e) => {\r\n const blob = new Blob(chunks, { type: 'audio/mp3' });\r\n chunks = [];\r\n setAudioBlob(blob);\r\n };\r\n\r\n mediaRecorder.onstart = () => {};\r\n\r\n mediaRecorder.ondataavailable = (e) => {\r\n chunks.push(e.data);\r\n if (voiceMessageDuration >= 300) {\r\n mediaRecorder.stop();\r\n } else {\r\n setVoiceMessageDuration((t) => t + 1);\r\n }\r\n };\r\n })\r\n .catch((err) => {\r\n console.error(`The following error occurred: ${err}`);\r\n });\r\n } else {\r\n console.log(\"not media devices found\");\r\n }\r\n }\r\n\r\n // useEffect(() => {\r\n // if(audioRecorder) {\r\n\r\n // }\r\n // },[audioRecorder]);\r\n\r\n useEffect(() => {\r\n let debounceTimer: NodeJS.Timeout | undefined;\r\n let idleTimer: NodeJS.Timeout | undefined;\r\n if (message?.message && message.message.length > 0) {\r\n clearTimeout(debounceTimer);\r\n // set a new debounce timer to send a typing notification after 350ms\r\n debounceTimer = setTimeout(() => {\r\n if (client) {\r\n client\r\n .messageClient(conversationId)\r\n .sendTypingNotification(recipientId);\r\n debounceTimer = undefined; // clear debounce timer reference after sending the typing notification\r\n }\r\n }, 300);\r\n // clear the previous idle timer (stopped typing)\r\n clearTimeout(idleTimer);\r\n // set a new idle timer to send a stopped typing notification after 1300ms of inactivity\r\n idleTimer = setTimeout(() => {\r\n if (client) {\r\n client\r\n .messageClient(conversationId)\r\n .sendStoppedTypingNotification(recipientId);\r\n }\r\n }, 1300);\r\n }\r\n return () => clearTimeout(debounceTimer);\r\n }, [message?.message, conversationId]);\r\n\r\n const uploadMessageAttachment = async () => {\r\n try {\r\n let mediaData: Media[] = []\r\n if (files.length > 0) {\r\n // Wait for all uploads to complete using Promise.all\r\n const type = files[0].type.split(\"/\")[0];\r\n\r\n showUploading(true);\r\n const res = await msClient.uploadFile(files[0], {\r\n filename: files[0].name,\r\n mimeType: files[0].type,\r\n ext: type === \"image\" ? \".png\" : \".mp4\",\r\n });\r\n\r\n\r\n mediaData.push({\r\n type: type === \"image\" ? MediaType.IMAGE : MediaType.VIDEO,\r\n ext: type === \"image\" ? \".png\" : \".mp4\",\r\n mediaId: uuidv4(),\r\n mediaUrl: res.link,\r\n mimeType: files[0].type,\r\n });\r\n }\r\n\r\n if (audioBlob) {\r\n showUploading(true);\r\n // const mp3Blob = await convertWebmToMp3(audioBlob);\r\n // console.log(mp3Blob, \":::new blob\")\r\n console.log(audioBlob)\r\n const url = URL.createObjectURL(audioBlob);\r\n const res = await msClient.uploadFile(url, {\r\n filename: `${generateId()}.mp3`,\r\n mimeType: 'audio/mp3',\r\n ext: '.mp3'\r\n });\r\n mediaData.push({\r\n type: MediaType.AUDIO,\r\n ext: \".mp3\",\r\n mediaId: uuidv4(),\r\n mediaUrl: res.link as any,\r\n mimeType: \"audio/mp3\",\r\n meta: {\r\n audioDurationSec: voiceMessageDuration,\r\n },\r\n });\r\n setVoiceMessageDuration(0);\r\n }\r\n return mediaData;\r\n } catch (error) {\r\n console.error(error.message);\r\n return [];\r\n } finally {\r\n showUploading(false);\r\n }\r\n };\r\n\r\n const reset = () => {\r\n setMessage({\r\n message: \"\",\r\n });\r\n setEditDetails(undefined);\r\n };\r\n\r\n const sendMessage = async () => {\r\n var mediaData = await uploadMessageAttachment();\r\n msClient.sendMessage({\r\n conversationId,\r\n to: recipientId,\r\n message: message?.message as any,\r\n reactions: [],\r\n attachedMedia: mediaData,\r\n quotedMessage: editProps?.message,\r\n attachmentType:\r\n mediaData.length > 0 ? AttachmentTypes.MEDIA : AttachmentTypes.NONE,\r\n });\r\n reset();\r\n };\r\n\r\n const sendEditedMessage = async () => {\r\n msClient.editMessage({\r\n to: recipientId,\r\n conversationId,\r\n messageId: editProps?.message?.messageId as string,\r\n textMessage: message?.message as string,\r\n shouldEdit: true,\r\n });\r\n reset();\r\n };\r\n\r\n const broadcastMessage = async () => {\r\n var mediaData = await uploadMessageAttachment();\r\n client.messageClient(conversationId).broadcastMessage({\r\n broadcastListId: conversationId,\r\n participantsIds: activeConversation.conversation.participants,\r\n newMessage: {\r\n conversationId: conversationId,\r\n to: recipientId,\r\n message: message?.message,\r\n reactions: [],\r\n attachedMedia: mediaData,\r\n attachmentType:\r\n mediaData.length > 0 ? AttachmentTypes.MEDIA : AttachmentTypes.NONE,\r\n quotedMessage: editProps?.message,\r\n },\r\n });\r\n reset();\r\n };\r\n\r\n const sendHandler = async () => {\r\n setSending(true);\r\n try {\r\n \r\n if (!message?.message?.length && !files.length && !audioBlob) {\r\n return;\r\n }\r\n if (\r\n activeConversation?.conversation.conversationType === \"broadcast-chat\"\r\n ) {\r\n if(editProps?.isEditing){\r\n return sendEditedMessage()\r\n }\r\n return broadcastMessage();\r\n }\r\n if (editProps?.isEditing) {\r\n return sendEditedMessage();\r\n }\r\n sendMessage()\r\n \r\n } catch (err) {\r\n console.log(err);\r\n } finally {\r\n setSending(false);\r\n setFiles([]);\r\n setAudioBlob(null);\r\n }\r\n };\r\n\r\n const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {\r\n if (e.key === \"Enter\" && message?.message?.length) {\r\n // Check if Enter is pressed and message is not empty\r\n sendHandler();\r\n }\r\n };\r\n\r\n const addAudioElement = (blob: any) => {\r\n const url = URL.createObjectURL(blob);\r\n const audio = document.createElement(\"audio\");\r\n audio.src = url;\r\n audio.controls = true;\r\n document.body.appendChild(audio);\r\n };\r\n\r\n // if (1)\r\n // return (\r\n // <AudioRecorder\r\n // onRecordingComplete={addAudioElement}\r\n // audioTrackConstraints={{\r\n // noiseSuppression: true,\r\n // echoCancellation: true,\r\n // }}\r\n // downloadOnSavePress={true}\r\n // downloadFileExtension=\"webm\"\r\n // />\r\n // );\r\n\r\n const recordVoiceMessage = () => {\r\n prepareAudio();\r\n \r\n };\r\n\r\n const stopRecording = () => {\r\n audioRecorder.stop();\r\n setIsRecording(false);\r\n };\r\n\r\n const cancelAudioAttachments = () => {\r\n setAudioBlob(null);\r\n setAudioBlobPlaceHolder(null);\r\n };\r\n\r\n if (\r\n activeConversation?.conversation?.conversationType === \"admin-chat\"\r\n ) {\r\n return (\r\n <div\r\n style={{\r\n padding: \"20px\",\r\n flex: 1,\r\n display: \"flex\",\r\n flexDirection: \"row\",\r\n justifyContent: \"center\",\r\n alignItems: \"center\",\r\n }}\r\n >\r\n <LockIcon color=\"white\" size={20} />\r\n <Text\r\n size=\"xs\"\r\n styles={{ marginLeft: \"5px\" }}\r\n text={\"Only the Admin can send messages.\"}\r\n />\r\n </div>\r\n );\r\n }\r\n\r\n if (isRecording) {\r\n return (\r\n <div\r\n style={{\r\n backgroundColor: theme?.background?.secondary,\r\n justifyContent: \"flex-end\",\r\n width: \"100%\",\r\n flex: 1,\r\n }}\r\n className={styles.input}\r\n >\r\n <div\r\n className={styles.input__inner}\r\n style={{\r\n width: \"30%\",\r\n fontStyle: \"italic\",\r\n display: \"flex\",\r\n alignItems: \"center\",\r\n justifyContent: \"space-between\",\r\n padding: \"10px\",\r\n backgroundColor: theme?.background?.secondary,\r\n boxShadow: \"rgba(0, 0, 0, 0.35) 0px 5px 15px\",\r\n }}\r\n >\r\n <button\r\n onClick={stopRecording}\r\n style={{\r\n backgroundColor: \"transparent\",\r\n border: 0,\r\n marginRight: \"12px\",\r\n }}\r\n >\r\n <IoStopCircleOutline\r\n style={{ marginTop: \"5px\" }}\r\n color=\"red\"\r\n size={23}\r\n />\r\n </button>\r\n <div\r\n style={{\r\n flex: 1,\r\n width: \"100%\",\r\n height: \"2px\",\r\n backgroundColor: \"grey\",\r\n }}\r\n >\r\n <div\r\n style={{\r\n height: \"100%\",\r\n backgroundColor: \"white\",\r\n width: `${(voiceMessageDuration / 300) * 100}%`,\r\n }}\r\n />\r\n </div>\r\n <p\r\n style={{\r\n fontSize: \"11.5px\",\r\n marginLeft: \"15px\",\r\n color: theme?.text?.primary,\r\n }}\r\n >\r\n {convertToMinutes(voiceMessageDuration)} : {convertToMinutes(300)}\r\n </p>\r\n </div>\r\n </div>\r\n );\r\n }\r\n\r\n if (audioBlob && !audioBlobPLaceHolder) {\r\n return (\r\n <div\r\n style={{\r\n width: \"100%\",\r\n backgroundColor: theme?.background?.secondary || \"#1b1d21\",\r\n justifyContent: \"flex-start\",\r\n display: \"flex\",\r\n alignItems: \"center\",\r\n }}\r\n className={styles.input}\r\n >\r\n <div\r\n className={styles.input__inner}\r\n style={{\r\n width: \"30%\",\r\n fontStyle: \"italic\",\r\n display: \"flex\",\r\n alignItems: \"center\",\r\n justifyContent: \"space-between\",\r\n padding: \"10px\",\r\n marginRight: \"10px\",\r\n backgroundColor: theme?.background?.secondary || \"#1b1d21\",\r\n boxShadow: \"rgba(0, 0, 0, 0.35) 0px 5px 15px\",\r\n }}\r\n >\r\n <button\r\n onClick={() => {\r\n setAudioBlob(null);\r\n setVoiceMessageDuration(0);\r\n }}\r\n style={{ backgroundColor: \"transparent\", border: 0 }}\r\n >\r\n <AiOutlineDelete size={22} color={\"red\"} />\r\n </button>\r\n <AudioPlayer blob={audioBlob} duration={voiceMessageDuration} />\r\n </div>\r\n <VscSend\r\n onClick={() => {\r\n setAudioBlobPlaceHolder(audioBlob);\r\n }}\r\n size={22}\r\n color={primaryActionColor}\r\n />\r\n </div>\r\n );\r\n }\r\n\r\n return (\r\n <div ref={inputContainerRef} style={{ height: \"auto\", width: \"100%\" }}>\r\n {uploading && <LinearLoader />}\r\n <EditPanel\r\n width={inputContainerWidth}\r\n message={editProps?.message}\r\n isEditing={editProps?.isEditing}\r\n isReplying={editProps?.isReplying}\r\n closePanel={() => setEditDetails(undefined)}\r\n />\r\n {files.length || audioBlob ? (\r\n <ChatAttachments\r\n width={inputContainerWidth}\r\n files={files}\r\n setFiles={setFiles}\r\n audioBlob={audioBlobPLaceHolder}\r\n voiceMessageDuration={voiceMessageDuration}\r\n cancelAudioAttachment={cancelAudioAttachments}\r\n />\r\n ) : null}\r\n <div\r\n style={{ backgroundColor: theme?.background?.secondary }}\r\n className={styles.input}\r\n >\r\n <div className={styles.input__wrap}>\r\n <div className={styles.input__icon}>\r\n {!audioBlob && (\r\n <div>\r\n <AiOutlinePlus\r\n onClick={() =>\r\n setMenuDetails?.({\r\n element: (\r\n <AttachmentMenu\r\n closeGeneralMenu={closeGeneralMenu}\r\n setFiles={setFiles}\r\n />\r\n ),\r\n })\r\n }\r\n color={primaryActionColor}\r\n size={22}\r\n />\r\n </div>\r\n )}\r\n </div>\r\n <div\r\n className={styles.input__inner}\r\n style={{ flex: 1, fontStyle: \"italic\", background: inputBg }}\r\n >\r\n {renderChatInput ? (\r\n renderChatInput({\r\n onChange: (e) => {\r\n setMessage({\r\n ...message,\r\n message: e,\r\n });\r\n },\r\n })\r\n ) : (\r\n <div style={{ display: \"flex\", alignItems: \"center\" }}>\r\n <input\r\n style={{\r\n background: inputBg,\r\n color: theme?.input?.textColor || \"white\",\r\n }}\r\n onKeyDown={handleKeyDown}\r\n ref={textInputRef}\r\n value={message?.message}\r\n onChange={(e) =>\r\n setMessage({\r\n ...message,\r\n message: e.target.value,\r\n })\r\n }\r\n placeholder=\"Say something...\"\r\n />\r\n\r\n <CiFaceSmile\r\n onClick={() => setShowEmojiPicker(!showEmojiPicker)}\r\n className={styles.input__emoji}\r\n size={24}\r\n color={primaryActionColor}\r\n />\r\n </div>\r\n )}\r\n </div>\r\n <div className={styles.input__button}>\r\n {/* {audioBlob || message?.message || files.length > 0 ? (\r\n <div>\r\n {sending ? (\r\n \"...\"\r\n ) : (\r\n <VscSend\r\n onClick={sendHandler}\r\n size={20}\r\n color={primaryActionColor}\r\n />\r\n )}\r\n </div>\r\n ) : null} */}\r\n\r\n {!message?.message ? (\r\n <button\r\n onClick={recordVoiceMessage}\r\n style={{\r\n backgroundColor: \"transparent\",\r\n border: 0,\r\n cursor: \"pointer\",\r\n }}\r\n >\r\n <AiOutlineAudio color={primaryActionColor} size={20} />\r\n </button>\r\n ) : (\r\n <VscSend\r\n onClick={sendHandler}\r\n size={20}\r\n color={primaryActionColor}\r\n />\r\n )}\r\n </div>\r\n {menuDetails.element ? (\r\n <div className={styles.input__menu}>\r\n <Menu\r\n generalMenuRef={generalMenuRef}\r\n element={menuDetails.element}\r\n />\r\n </div>\r\n ) : null}\r\n {showEmojiPicker ? (\r\n <div className={styles.input__emoji__picker}>\r\n <InputEmojis\r\n onEmojiPick={(e) => {\r\n setMessage({\r\n ...message,\r\n message: e,\r\n });\r\n setShowEmojiPicker(false);\r\n }}\r\n />\r\n </div>\r\n ) : null}\r\n </div>\r\n </div>\r\n </div>\r\n );\r\n};\r\n\r\nconst ChatAttachments = ({\r\n audioBlob,\r\n voiceMessageDuration,\r\n cancelAudioAttachment,\r\n files,\r\n setFiles,\r\n width,\r\n}: {\r\n audioBlob: Blob;\r\n voiceMessageDuration: number;\r\n files: any[];\r\n setFiles: any;\r\n cancelAudioAttachment: () => void;\r\n width: number;\r\n}) => {\r\n const deleteAttachment = (id: string) => {\r\n const imgs = files.filter((i) => i.name !== id);\r\n setFiles(imgs);\r\n };\r\n\r\n const { config } = useChatClient();\r\n\r\n const { theme } = config;\r\n\r\n return (\r\n <div className={styles.chatPhotos} style={{ width, paddingBottom: \"10px\" }}>\r\n {audioBlob ? (\r\n <div\r\n style={{\r\n padding: \"10px\",\r\n background: theme?.background?.primary || \"#1b1d21\",\r\n borderRadius: \"30px\",\r\n cursor: \"pointer\",\r\n position: \"relative\",\r\n }}\r\n >\r\n <div onClick={cancelAudioAttachment} className={styles.audioCancel}>\r\n <MdCancel size={20} color=\"grey\" />\r\n </div>\r\n <div\r\n style={{\r\n border: `1px solid ${theme.divider}`,\r\n borderRadius: \"5px\",\r\n }}\r\n >\r\n <AudioPlayer\r\n style={{ padding: \"15px\" }}\r\n blob={audioBlob}\r\n duration={voiceMessageDuration}\r\n />\r\n </div>\r\n </div>\r\n ) : null}\r\n {files.length\r\n ? files.map((item, i) => {\r\n const url = URL.createObjectURL(item);\r\n return (\r\n <div key={i} className={styles.chatPhotos__item}>\r\n {item.type === \"video/quicktime\" ? (\r\n <video style={{}} src={url} />\r\n ) : (\r\n <img src={url as any} alt=\"\" />\r\n )}\r\n\r\n <div\r\n onClick={() => deleteAttachment(item.name)}\r\n className={styles.chatPhotos__cancel}\r\n >\r\n <MdCancel size={20} color=\"grey\" />\r\n </div>\r\n </div>\r\n );\r\n })\r\n : null}\r\n </div>\r\n );\r\n};\r\n\r\nexport default ChatInput;\r\n","import React, { useEffect, useRef, useState } from \"react\";\r\n\r\nimport styles from \"./edit.module.css\";\r\nimport Text from \"../text/text\";\r\nimport { Message } from \"softchatjs-core\";\r\nimport { AiOutlineClose } from \"react-icons/ai\";\r\nimport { useChatClient } from \"../../providers/chatClientProvider\";\r\n\r\ntype EditPanelProps = {\r\n message: Message;\r\n isEditing?: boolean;\r\n isReplying?: boolean;\r\n closePanel: () => void;\r\n width: number\r\n};\r\n\r\nconst EditPanel = (props: EditPanelProps) => {\r\n const { isEditing, message, isReplying, closePanel, width } = props;\r\n const { config } = useChatClient();\r\n const { theme } = config;\r\n\r\n const secondaryColor = theme?.background?.secondary;\r\n const textColor = theme?.text?.primary;\r\n const iconColor = theme?.icon;\r\n\r\n return (\r\n <div\r\n className={\r\n isEditing || isReplying\r\n ? `${styles.edit} ${styles.editOpen}`\r\n : `${styles.edit}`\r\n }\r\n style={{ background: secondaryColor || \"#1b1d21\", width: width }}\r\n >\r\n <div\r\n style={{ background: secondaryColor || \"#222529\" }}\r\n className={styles.edit__message}\r\n >\r\n <div style={{ width: \"90%\" }}>\r\n <Text text=\"You\" styles={{ color: textColor }} weight=\"bold\" />\r\n <Text\r\n text={message?.message}\r\n styles={{ color: textColor }}\r\n weight=\"medium\"\r\n />\r\n </div>\r\n\r\n <div style={{ width: \"10%\", marginRight: '15px' }}>\r\n {message?.attachedMedia[0]?.mediaUrl && (\r\n <img\r\n style={{ height: \"100%\", width: \"100%\", borderRadius: \"5px\" }}\r\n src={message?.attachedMedia[0]?.mediaUrl}\r\n alt=\"\"\r\n />\r\n )}\r\n </div>\r\n <AiOutlineClose\r\n onClick={closePanel}\r\n color={iconColor}\r\n size={20}\r\n style={{ cursor: \"pointer\" }}\r\n />\r\n </div>\r\n </div>\r\n );\r\n};\r\n\r\nexport default EditPanel;\r\n","import React from \"react\";\r\nimport styles from \"./text.module.css\";\r\n\r\ntype TextProps = {\r\n text: string;\r\n styles?: React.CSSProperties | undefined;\r\n weight?: \"bold\" | \"medium\";\r\n size?: \"sm\" | \"md\" | \"xs\";\r\n};\r\n\r\nconst Text = (props: TextProps) => {\r\n const textWeight = {\r\n bold: styles.textBold,\r\n medium: `${styles.textMedium}`,\r\n };\r\n\r\n const textSize: any = {\r\n sm: styles.textSmall,\r\n md: styles.textSizeMd,\r\n xs: styles.textExtraSmall,\r\n };\r\n\r\n return (\r\n <p\r\n style={props.styles}\r\n className={`${styles.text} ${textWeight[props.weight || \"medium\"]} ${\r\n textSize[props.size || \"md\"]\r\n }`}\r\n >\r\n {props.text}\r\n </p>\r\n );\r\n};\r\n\r\nexport default Text;\r\n","import { createContext, useContext } from \"react\";\r\nimport ChatClient from \"softchatjs-core\";\r\nimport { ChatStateProvider } from \"./clientStateProvider\";\r\nimport { defaulTheme } from \"../theme\";\r\nimport { ReactTheme } from \"../theme/type\";\r\n\r\ntype ContextType = {\r\n config: { theme: ReactTheme },\r\n client: ChatClient | null;\r\n};\r\n\r\nexport const ChatClientContext = createContext<ContextType>({\r\n config: { theme: defaulTheme },\r\n client: null,\r\n});\r\n\r\nexport const useChatClient = () => useContext(ChatClientContext);\r\n\r\nexport const ChatClientProvider = ({\r\n theme,\r\n children,\r\n client\r\n}: {\r\n theme?: ReactTheme\r\n children: JSX.Element;\r\n client: ChatClient | null\r\n}) => {\r\n\r\n return (\r\n <ChatClientContext.Provider value={{ config: { theme: theme? theme : defaulTheme }, client }}>\r\n <ChatStateProvider>\r\n {children}\r\n </ChatStateProvider>\r\n </ChatClientContext.Provider>\r\n );\r\n};\r\n","import React, { createContext, useContext, useState } from \"react\";\r\nimport { Conversation, Media, Message } from \"softchatjs-core\";\r\n\r\nexport type ConversationItem = {\r\n conversation: Conversation;\r\n lastMessage: Message;\r\n unread: string[];\r\n};\r\n\r\nexport type ConnectionStatus = {\r\n isConnected: boolean;\r\n fetchingConversations: boolean;\r\n connecting: boolean;\r\n};\r\n\r\ntype Context = {\r\n activeConversation: ConversationItem | null;\r\n setActiveConver