softchatjs-react
Version:
Install the softchat-js SDKs
1 lines • 86.6 kB
Source Map (JSON)
{"version":3,"sources":["../../../src/components/message-list/message-list.tsx","../../../src/components/avartar/avartar.tsx","../../../src/providers/chatClientProvider.tsx","../../../src/providers/clientStateProvider.tsx","../../../src/theme/index.tsx","../../../src/components/message-item/index.tsx","../../../src/components/text/text.tsx","../../../src/helpers/date.ts","../../../src/components/options-panel/options-panel.tsx","../../../src/components/emoji/index.tsx","../../../src/components/audio/audio-player.tsx","../../../src/components/assets/icons.tsx"],"sourcesContent":["import React, {\r\n Dispatch,\r\n Ref,\r\n useMemo,\r\n SetStateAction,\r\n createRef,\r\n useCallback,\r\n useEffect,\r\n useRef,\r\n useState,\r\n useLayoutEffect,\r\n forwardRef,\r\n} from \"react\";\r\nimport styles from \"./message-list.module.css\";\r\nimport ChatClient, { Message } from \"softchatjs-core\";\r\nimport { MessageItem, TypingIndicator } from \"../message-item\";\r\nimport Text from \"../text/text\";\r\nimport { formatSectionTime } from \"../../helpers/date\";\r\nimport { useChatState } from \"../../providers/clientStateProvider\";\r\nimport Avartar from \"../avartar/avartar\";\r\nimport { useChatClient } from \"../../providers/chatClientProvider\";\r\nimport { MdOutlineMenu } from \"react-icons/md\";\r\nimport moment from \"moment\";\r\n\r\ntype MessageListProps = {\r\n messages: Message[];\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 mousePosition: {\r\n x: number;\r\n y: number;\r\n };\r\n customHeight?: number;\r\n client: ChatClient | null;\r\n conversationId: string;\r\n textInputRef: any;\r\n setPresentPage: Dispatch<SetStateAction<number>>;\r\n presentPage: number;\r\n setMainListOpen: any;\r\n recipientId: string;\r\n scrollToKey: string;\r\n fetchingMore: boolean;\r\n messagesEndRef: any;\r\n renderChatBubble?: (message: Message) => JSX.Element;\r\n renderChatHeader?: () => JSX.Element;\r\n headerHeightOffset: number;\r\n getOlderMessages: (func: () => void) => void;\r\n};\r\n\r\nconst MessageList = (props: MessageListProps) => {\r\n const {\r\n messages = [],\r\n setEditDetails,\r\n recipientTyping,\r\n mousePosition,\r\n conversationId,\r\n client,\r\n textInputRef,\r\n setPresentPage,\r\n presentPage,\r\n recipientId,\r\n scrollToKey,\r\n fetchingMore,\r\n messagesEndRef,\r\n renderChatBubble,\r\n renderChatHeader,\r\n headerHeightOffset,\r\n getOlderMessages,\r\n customHeight\r\n } = props;\r\n\r\n const ref = useRef<HTMLDivElement>()\r\n const [showOptions, setShowOPtions] = useState(false);\r\n const [showEmojiPanel, setShowEmojiPanel] = useState(false);\r\n const [activeIndex, setActiveIndex] = useState(\"\");\r\n const [quoteId, setQuoteId] = useState(\"\");\r\n const optionsMenuRef: any = useRef(null);\r\n const emojiPickerRef: any = useRef(null);\r\n // const ref: any = useRef<HTMLDivElement>(null);\r\n const [refMap, setRefMap] = useState<{ [key: string]: any }>({});\r\n const { activeConversation } = useChatState();\r\n const { config } = useChatClient();\r\n const [messagesPrepared, setMessagesPrepared] = useState(false);\r\n\r\n const itemRefs = useRef<Record<string, HTMLDivElement>>({});\r\n const theme = config.theme;\r\n const textColor = config?.theme?.text?.primary || \"white\";\r\n var scrollDebounce: NodeJS.Timeout | null = null\r\n\r\n function calculateHeight() {\r\n // Get the viewport height in pixels\r\n const viewportHeight = window.innerHeight;\r\n \r\n // Fixed pixel values in the calculation\r\n const fixedOffset1 = 80;\r\n const fixedOffset2 = 60;\r\n \r\n // Perform the calculation: (100vh - 80px - 60px - headerHeightOffset)\r\n const result = viewportHeight - fixedOffset1 - fixedOffset2 - headerHeightOffset;\r\n \r\n return result; // This gives you the pixel value of the calculation\r\n }\r\n\r\n const handlePress = (event: any, id: string) => {\r\n event.preventDefault();\r\n setShowEmojiPanel(false);\r\n setShowOPtions(true);\r\n setActiveIndex(id);\r\n };\r\n\r\n const closeOpenMenus = (e: any) => {\r\n if (showOptions && !optionsMenuRef.current?.contains(e.target)) {\r\n setShowOPtions(false);\r\n }\r\n if (showEmojiPanel && !emojiPickerRef.current?.contains(e.target)) {\r\n setShowEmojiPanel(false);\r\n }\r\n };\r\n\r\n const openEmojiPanel = (id: string) => {\r\n setShowEmojiPanel(true);\r\n setActiveIndex(id);\r\n };\r\n\r\n useEffect(() => {\r\n document.addEventListener(\"mousedown\", closeOpenMenus);\r\n }, [optionsMenuRef, emojiPickerRef, closeOpenMenus]);\r\n\r\n const scrollToBottom = (isInitial?: boolean) => {\r\n if (messagesEndRef.current) {\r\n messagesEndRef.current.scrollIntoView({\r\n behavior: isInitial === true ? \"instant\" : (\"smooth\" as any),\r\n });\r\n }\r\n };\r\n\r\n useEffect(() => {\r\n if (messages.length > 0) {\r\n messages.map((m) => {\r\n setRefMap((prev) => {\r\n const prevRefs = { ...prev };\r\n prevRefs[m.messageId] = createRef();\r\n return prevRefs;\r\n });\r\n });\r\n }\r\n }, [messages]);\r\n\r\n useEffect(() => {\r\n scrollToBottom();\r\n }, [activeConversation]);\r\n\r\n const scrollToQuote = (id: string) => {\r\n try {\r\n setQuoteId(id);\r\n const selectedRef = refMap[id];\r\n if (selectedRef.current) {\r\n selectedRef.current.style.backgroundColor = \"#282c34\";\r\n selectedRef.current.style.transition = \"0.5s\";\r\n setTimeout(() => {\r\n selectedRef.current.style.backgroundColor = \"transparent\";\r\n }, 1500);\r\n selectedRef?.current.scrollIntoView({\r\n behavior: \"smooth\",\r\n block: \"center\",\r\n });\r\n }\r\n } catch (error) {\r\n console.log(error);\r\n }\r\n };\r\n\r\n const sectionedMessages = messages.reduce(\r\n (list: { date: string; messages: Message[] }[], item) => {\r\n const date = moment(item.createdAt).format(\"MMMM DD, YYYY\");\r\n\r\n const dateExists = list.find((i) => i.date === date);\r\n\r\n if (dateExists) {\r\n dateExists.messages.push(item);\r\n } else {\r\n const newRec = {\r\n date,\r\n messages: [item],\r\n };\r\n list.push(newRec);\r\n }\r\n\r\n return list;\r\n },\r\n []\r\n );\r\n\r\n const handleScroll = () => {\r\n if (ref.current.scrollTop === 0) {\r\n const scrollHeightBeforeFetch = ref.current.scrollHeight;\r\n setPresentPage((prevPage) => prevPage + 1); // Increment page number\r\n const scrollHeightAfterFetch = ref.current.scrollHeight;\r\n ref.current.scrollTop = scrollHeightAfterFetch - scrollHeightBeforeFetch;\r\n }\r\n };\r\n\r\n const onScroll = (e: React.UIEvent<HTMLDivElement>) => {\r\n try {\r\n const scrollContainer = ref.current;\r\n const previousScrollHeight = scrollContainer.scrollHeight;\r\n const previousScrollTop = scrollContainer.scrollTop;\r\n const { scrollTop } = e.target as HTMLDivElement;\r\n const containerHeigh = calculateHeight();\r\n if (scrollTop <= 0) {\r\n getOlderMessages(() => {});\r\n }\r\n } catch (error) {\r\n console.error(error.message)\r\n }\r\n }\r\n \r\n\r\n // Get's position of last message before pagination\r\n useLayoutEffect(() => {\r\n try {\r\n if (!scrollToKey || (ref.current as HTMLDivElement).scrollTop !== 0) return;\r\n if (itemRefs.current[scrollToKey]) {\r\n itemRefs.current[scrollToKey].scrollIntoView();\r\n }\r\n } catch (error) {\r\n console.error(error.message)\r\n }\r\n \r\n }, [scrollToKey]);\r\n\r\n const getListHeight = useMemo(() => {\r\n if(customHeight){\r\n return `calc(${customHeight}px - 80px - 60px - ${headerHeightOffset}px)`\r\n }\r\n return `calc(100vh - 80px - 60px - ${headerHeightOffset}px)`\r\n },[customHeight])\r\n\r\n return (\r\n <div\r\n ref={ref}\r\n onScroll={onScroll}\r\n className={styles.wrapper}\r\n style={{ height: getListHeight }}\r\n >\r\n {fetchingMore && (\r\n <div className={styles.loading}>\r\n <Text styles={{ color: \"white\" }} size=\"sm\" text=\"Loading more...\" />\r\n </div>\r\n )}\r\n {sectionedMessages.map((_item, i) => {\r\n return (\r\n <div key={i} className={styles.wrapper__sec}>\r\n <div\r\n style={{\r\n justifyContent: \"center\",\r\n display: \"flex\",\r\n width: \"100%\",\r\n alignItems: \"center\",\r\n }}\r\n key={`${i}-sec`}\r\n >\r\n <div\r\n style={{\r\n flex: 1,\r\n height: \".5px\",\r\n width: \"100%\",\r\n backgroundColor: theme?.divider,\r\n }}\r\n />\r\n <div\r\n style={{\r\n backgroundColor:\r\n config?.theme?.background?.secondary || \"#1b1d21\",\r\n }}\r\n className={styles.wrapper__date}\r\n >\r\n <Text\r\n styles={{ color: textColor }}\r\n size=\"xs\"\r\n text={formatSectionTime(_item.date)}\r\n />\r\n </div>\r\n <div\r\n style={{\r\n flex: 1,\r\n height: \".5px\",\r\n width: \"100%\",\r\n backgroundColor: theme?.divider,\r\n }}\r\n />\r\n </div>\r\n\r\n {_item.messages.map((item, index) => (\r\n <div\r\n ref={(el: HTMLDivElement) =>\r\n (itemRefs.current[item.messageId] = el)\r\n }\r\n >\r\n <div ref={refMap[item?.messageId]}>\r\n {props.renderChatBubble ? (\r\n props.renderChatBubble(item)\r\n ) : (\r\n <MessageItem\r\n hideAvartar={\r\n item.messageOwner.uid ===\r\n _item.messages[index + 1]?.messageOwner.uid\r\n }\r\n textInputRef={textInputRef}\r\n show={showOptions && activeIndex == item.messageId}\r\n showEmojiPanel={\r\n showEmojiPanel && activeIndex == item.messageId\r\n }\r\n index={index}\r\n key={index}\r\n message={item}\r\n onPress={(e) => handlePress(e, item.messageId)}\r\n setEditDetails={setEditDetails}\r\n canEdit={item.from === client?.chatUserId}\r\n openEmojiPanel={() => openEmojiPanel(item.messageId)}\r\n optionsMenuRef={optionsMenuRef}\r\n emojiPickerRef={emojiPickerRef}\r\n mousePosition={mousePosition}\r\n client={client}\r\n conversationId={conversationId}\r\n closeOptionsMenu={() => setShowOPtions(false)}\r\n scrollToQuote={scrollToQuote}\r\n recipientId={recipientId}\r\n setShowEmojiPanel={setShowEmojiPanel}\r\n />\r\n )}\r\n </div>\r\n </div>\r\n ))}\r\n </div>\r\n );\r\n })}\r\n {recipientTyping && (\r\n <TypingIndicator message={sectionedMessages[0]?.messages[0]} />\r\n )}\r\n <div ref={messagesEndRef} style={{ height: \"1px\", width: \"100%\" }} />\r\n </div>\r\n );\r\n}\r\n\r\nexport default MessageList;\r\n","import React, { useEffect, useState } from \"react\";\r\n\r\nimport styles from \"./avartar.module.css\";\r\nimport { Message } from \"softchatjs-core\";\r\nimport Text from \"../text/text\";\r\nimport { useChatClient } from \"../../providers/chatClientProvider\";\r\n\r\nconst Avartar = ({\r\n initials,\r\n url,\r\n size = 35,\r\n}: {\r\n initials: string;\r\n url?: string;\r\n size?: number;\r\n}) => {\r\n const { client, config } = useChatClient();\r\n\r\n if (!url) {\r\n return (\r\n <div\r\n className={styles.avatar}\r\n style={{ height: size, width: size, borderRadius: size, backgroundColor: config.theme.background.disabled }}\r\n >\r\n <p\r\n style={{\r\n fontSize: 35 * 0.5,\r\n fontWeight: \"bold\",\r\n textTransform: \"capitalize\",\r\n color: config.theme.text.secondary\r\n }}\r\n >\r\n {initials}\r\n </p>\r\n </div>\r\n );\r\n }\r\n\r\n return (\r\n <div\r\n className={styles.avatar}\r\n style={{ height: size, width: size, borderRadius: size }}\r\n >\r\n <img\r\n src={url}\r\n alt=\"avatar\"\r\n style={{ height: \"100%\", width: \"100%\", borderRadius: \"100%\" }}\r\n />\r\n </div>\r\n );\r\n};\r\n\r\nexport default Avartar;\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 setActiveConversation: React.Dispatch<\r\n React.SetStateAction<ConversationItem | null>\r\n >;\r\n conversations: ConversationItem[];\r\n setConversations: React.Dispatch<React.SetStateAction<ConversationItem[]>>;\r\n showImageModal: Media[];\r\n setShowImageModal: React.Dispatch<React.SetStateAction<Media[]>>;\r\n connectionStatus: ConnectionStatus;\r\n setConnectionStatus: React.Dispatch<React.SetStateAction<ConnectionStatus>>;\r\n};\r\n\r\nexport const ChatStateContext = createContext<Context>({\r\n activeConversation: null,\r\n setActiveConversation: () => {},\r\n conversations: [],\r\n setConversations: () => {},\r\n showImageModal: [],\r\n setShowImageModal: () => {},\r\n connectionStatus: {\r\n isConnected: false,\r\n fetchingConversations: false,\r\n connecting: false,\r\n },\r\n setConnectionStatus: () => {},\r\n});\r\n\r\nexport const useChatState = () => useContext(ChatStateContext);\r\n\r\nexport const ChatStateProvider = ({ children }: { children: JSX.Element }) => {\r\n const [activeConversation, setActiveConversation] =\r\n useState<ConversationItem | null>(null);\r\n const [conversations, setConversations] = useState<ConversationItem[]>([]);\r\n const [showImageModal, setShowImageModal] = useState<Media[]>([]);\r\n const [connectionStatus, setConnectionStatus] = useState<ConnectionStatus>({\r\n isConnected: false,\r\n fetchingConversations: false,\r\n connecting: false,\r\n });\r\n\r\n return (\r\n <ChatStateContext.Provider\r\n value={{\r\n activeConversation,\r\n setActiveConversation,\r\n conversations,\r\n setConversations,\r\n showImageModal,\r\n setShowImageModal,\r\n connectionStatus,\r\n setConnectionStatus,\r\n }}\r\n >\r\n {children}\r\n </ChatStateContext.Provider>\r\n );\r\n};\r\n","import { ReactTheme } from \"./type\";\r\n\r\nexport const defaulTheme: ReactTheme = {\r\n background: {\r\n primary: \"#1b1d21\", // White for the primary background\r\n secondary: \"#202326\", // Light grey for secondary background\r\n disabled: \"#E0E0E0\", // Very light grey for disabled background\r\n },\r\n text: {\r\n primary: \"white\", // Black text for high contrast\r\n secondary: \"#4A4A4A\", // Dark grey for secondary text\r\n disabled: \"#9E9E9E\", // Light grey for disabled text\r\n },\r\n action: {\r\n primary: \"#007AFF\", // Bright blue for primary action buttons\r\n secondary: \"#5AA3FF\", // Light blue for secondary action buttons\r\n },\r\n chatBubble: {\r\n left: {\r\n bgColor: \"#343434\", // Light grey for incoming message background\r\n messageColor: \"white\", // Dark grey for incoming message text\r\n messageTimeColor: \"#6D6D6D\", // Medium grey for message time\r\n replyBorderColor: \"#D1D1D6\", // Slightly darker grey for reply border\r\n },\r\n right: {\r\n bgColor: \"#343434\", // Light blue for outgoing message background\r\n messageColor: \"white\", // Black for outgoing message text\r\n messageTimeColor: \"#6D6D6D\", // Medium grey for message time\r\n replyBorderColor: \"#A3D1FF\", // Medium blue for reply border\r\n },\r\n },\r\n icon: \"white\", // Dark grey for icons\r\n divider: \"rgba(128, 128, 128, 0.136)\", // Light grey for dividers\r\n hideDivider: false,\r\n input: {\r\n bgColor: \"#1b1d21\", // Light grey for input background\r\n textColor: \"white\", // Black for input text\r\n emojiPickerTheme: \"dark\", // Light theme for emoji picker\r\n },\r\n};\r\n","import React, {\r\n Dispatch,\r\n SetStateAction,\r\n useContext,\r\n useEffect,\r\n useState,\r\n} from \"react\";\r\nimport Avartar from \"../avartar/avartar\";\r\n\r\nimport styles from \"./message-item.module.css\";\r\nimport ChatClient, { Message } from \"softchatjs-core\";\r\nimport Text from \"../text/text\";\r\nimport dayjs from \"dayjs\";\r\nimport { formatMessageTime } from \"../../helpers/date\";\r\nimport OptionsPanel from \"../options-panel/options-panel\";\r\nimport { EmojiPanel, ReactionPanel } from \"../emoji\";\r\nimport { Media, Reaction } from \"softchatjs-core\";\r\nimport { useChatClient } from \"../../providers/chatClientProvider\";\r\nimport { ThreeDots } from \"react-loader-spinner\";\r\nimport { BsCheck, BsCheckAll, BsClock, BsX } from \"react-icons/bs\";\r\nimport { useChatState } from \"../../providers/clientStateProvider\";\r\nimport { MessageStates } from \"softchatjs-core\";\r\nimport AudioPlayer from \"../audio/audio-player\";\r\n\r\nexport const regex = {\r\n URL_REGEX:\r\n /^(http:\\/\\/www\\.|https:\\/\\/www\\.|http:\\/\\/|https:\\/\\/)?[a-z0-9]+([\\-\\.]{1}[a-z0-9]+)*\\.[a-z]{2,8}(:[0-9]{1,5})?(\\/.*)?$/gm,\r\n};\r\n\r\ntype ConversationProps = {\r\n message: Message;\r\n index: number;\r\n onPress: (event: any, index: number, message: Message) => void;\r\n show: boolean;\r\n setEditDetails: Dispatch<\r\n SetStateAction<\r\n | { message: Message; isEditing?: boolean; isReplying?: boolean }\r\n | undefined\r\n >\r\n >;\r\n canEdit?: boolean;\r\n recipientTyping?: boolean;\r\n showEmojiPanel?: boolean;\r\n openEmojiPanel: () => void;\r\n optionsMenuRef: any;\r\n emojiPickerRef: any;\r\n mousePosition: {\r\n x: number;\r\n y: number;\r\n };\r\n client: ChatClient;\r\n conversationId: string;\r\n closeOptionsMenu: () => void;\r\n scrollToQuote: (id: string) => void;\r\n textInputRef: any;\r\n recipientId: string;\r\n hideAvartar: boolean;\r\n setShowEmojiPanel: Dispatch<SetStateAction<boolean>>;\r\n};\r\n\r\nexport const MessageItem = (props: ConversationProps) => {\r\n const {\r\n message,\r\n index,\r\n onPress,\r\n show,\r\n setEditDetails,\r\n canEdit,\r\n showEmojiPanel,\r\n openEmojiPanel,\r\n optionsMenuRef,\r\n emojiPickerRef,\r\n mousePosition,\r\n client,\r\n conversationId,\r\n closeOptionsMenu,\r\n scrollToQuote,\r\n textInputRef,\r\n recipientId,\r\n hideAvartar,\r\n } = props;\r\n\r\n const hasAttachments = message?.attachedMedia?.length;\r\n const { config } = useChatClient();\r\n const stacked = true;\r\n var chatUserId = client.chatUserId\r\n\r\n const oppositBubbleBoxes = message.messageOwner.uid === chatUserId && stacked;\r\n\r\n const chatBoxColor =\r\n message.messageOwner.uid === chatUserId\r\n ? config.theme?.chatBubble?.right?.bgColor\r\n : config.theme?.chatBubble?.left?.bgColor || \"#343434\";\r\n\r\n const messageColor =\r\n message.messageOwner.uid === chatUserId\r\n ? config.theme?.chatBubble?.right?.messageColor\r\n : config.theme?.chatBubble?.left?.messageColor || \"white\";\r\n\r\n const messageDateColor =\r\n message.messageOwner.uid === chatUserId\r\n ? config.theme?.chatBubble?.right?.messageTimeColor\r\n : config.theme?.chatBubble?.left?.messageTimeColor || \"grey\";\r\n\r\n const messageState = () => {\r\n if (message.messageState === MessageStates.SENT) {\r\n return <BsCheck style={{ marginTop: \"8px\", color: \"grey\" }} />;\r\n }\r\n if (\r\n message.messageState === MessageStates.DELIVERED ||\r\n message.messageState === MessageStates.READ\r\n ) {\r\n return <BsCheckAll style={{ marginTop: \"8px\", color: \"grey\" }} />;\r\n }\r\n if (message.messageState === MessageStates.LOADING) {\r\n return <BsClock style={{ marginTop: \"8px\", color: \"grey\" }} />;\r\n }\r\n if (message.messageState === MessageStates.FAILED) {\r\n return <BsX style={{ marginTop: \"8px\", color: \"red\" }} />;\r\n }\r\n return <BsClock style={{ marginTop: \"8px\", color: \"grey\" }} />;\r\n };\r\n\r\n return (\r\n <div\r\n className={`${styles.conversation}`}\r\n onContextMenu={(event) => {\r\n onPress(event, index, message);\r\n }}\r\n >\r\n <div\r\n className={`${\r\n oppositBubbleBoxes\r\n ? styles.conversation__wrapper__alternate\r\n : styles.conversation__wrapper\r\n }`}\r\n >\r\n {!oppositBubbleBoxes && (\r\n <div className={styles.conversation__image}>\r\n {hideAvartar ? null : (\r\n <Avartar\r\n initials={message.messageOwner.username.substring(0, 1)}\r\n url={message.messageOwner.profileUrl}\r\n />\r\n )}\r\n </div>\r\n )}\r\n\r\n <div\r\n style={{\r\n display: \"flex\",\r\n alignItems: \"flex-start\",\r\n flexDirection: \"column\",\r\n justifyContent: \"flex-start\",\r\n }}\r\n >\r\n {showEmojiPanel ? (\r\n <EmojiPanel\r\n conversationId={conversationId}\r\n client={client}\r\n emojiPickerRef={emojiPickerRef}\r\n message={message}\r\n recipientId={recipientId}\r\n setShowEmojiPanel={props.setShowEmojiPanel}\r\n />\r\n ) : null}\r\n <div className={styles.conversation__text__container}>\r\n {oppositBubbleBoxes && (\r\n <div className={styles.conversation__text__container__reactions}>\r\n <ReactionPanel\r\n textInputRef={textInputRef}\r\n optionsMenuRef={optionsMenuRef}\r\n canEdit={canEdit}\r\n message={message}\r\n setEditDetails={setEditDetails}\r\n openEmojiPanel={openEmojiPanel}\r\n mousePosition={mousePosition}\r\n client={client}\r\n conversationId={conversationId}\r\n closeOptionsMenu={closeOptionsMenu}\r\n />\r\n </div>\r\n )}\r\n <div style={{ display: 'flex', alignItems: 'flex-end' }}>\r\n <div\r\n style={{ backgroundColor: chatBoxColor }}\r\n className={`${styles.conversation__text__wrap} ${\r\n hasAttachments\r\n ? styles.conversation__text__wrap__attach\r\n : styles.conversation__text__wrap__no__attach\r\n } ${\r\n hideAvartar\r\n ? styles.conversation__text__wrap__half__border\r\n : `${\r\n oppositBubbleBoxes\r\n ? styles.conversation__text__wrap__border__alternate\r\n : styles.conversation__text__wrap__border\r\n }`\r\n }`}\r\n >\r\n {message.quotedMessageId && (\r\n <QuotedMessage\r\n scrollToQuote={scrollToQuote}\r\n message={message?.quotedMessage as any}\r\n />\r\n )}\r\n {hasAttachments ? (\r\n <AttachmentList attachments={message.attachedMedia} />\r\n ) : null}\r\n {message.message ? (\r\n <div\r\n style={{\r\n alignItems: \"center\",\r\n paddingLeft: hasAttachments && \"10px\",\r\n paddingRight: hasAttachments && \"10px\",\r\n justifyContent: \"space-between\",\r\n wordWrap: 'break-word',\r\n }}\r\n >\r\n <Text\r\n styles={{\r\n textAlign: \"left\",\r\n marginRight: \"15px\",\r\n color: messageColor,\r\n }}\r\n size=\"sm\"\r\n text={message?.message}\r\n />\r\n <div\r\n style={{\r\n display: \"flex\",\r\n justifyContent: \"flex-end\",\r\n alignItems: \"center\",\r\n marginTop:\r\n message?.message?.length > 10 ? \"0px\" : \"-25px\",\r\n }}\r\n >\r\n {message.lastEdited && (\r\n <Text\r\n styles={{\r\n textAlign: \"right\",\r\n marginRight: \"5px\",\r\n color: messageDateColor,\r\n fontSize: \"9px\",\r\n }}\r\n size=\"sm\"\r\n text={\"(Edited)\"}\r\n />\r\n )}\r\n <Text\r\n size=\"xs\"\r\n styles={{ color: messageDateColor }}\r\n text={formatMessageTime(message.createdAt as string)}\r\n />\r\n {oppositBubbleBoxes && <>{messageState()}</>}\r\n </div>\r\n </div>\r\n ) : null}\r\n </div>\r\n {/* {!hideAvartar && (\r\n \r\n )} */}\r\n {/* <div style={{ height: '20px', width: '20px', backgroundColor: hideAvartar? 'transparent' : chatBoxColor }}>\r\n <div style={{ backgroundColor: hideAvartar? 'transparent' : config.theme.background.primary, height: '100%', width: '100%', borderBottomLeftRadius: '15px' }} />\r\n </div> */}\r\n\r\n </div>\r\n {!oppositBubbleBoxes && (\r\n <div className={styles.conversation__text__container__reactions}>\r\n <ReactionPanel\r\n textInputRef={textInputRef}\r\n optionsMenuRef={optionsMenuRef}\r\n canEdit={canEdit}\r\n message={message}\r\n setEditDetails={setEditDetails}\r\n openEmojiPanel={openEmojiPanel}\r\n mousePosition={mousePosition}\r\n client={client}\r\n conversationId={conversationId}\r\n closeOptionsMenu={closeOptionsMenu}\r\n // setEditDetails={setEditDetails}\r\n />\r\n </div>\r\n )}\r\n </div>\r\n\r\n {/* <div className={styles.conversation__wrapper__emojis}></div> */}\r\n <div\r\n style={{\r\n width: \"100%\",\r\n display: \"flex\",\r\n justifyContent: oppositBubbleBoxes ? \"flex-end\" : \"flex-start\",\r\n }}\r\n >\r\n <EmojiList reactions={message.reactions} message={message} />\r\n </div>\r\n </div>\r\n </div>\r\n {show ? (\r\n <OptionsPanel\r\n textInputRef={textInputRef}\r\n optionsMenuRef={optionsMenuRef}\r\n canEdit={canEdit}\r\n message={message}\r\n setEditDetails={setEditDetails}\r\n openEmojiPanel={openEmojiPanel}\r\n mousePosition={mousePosition}\r\n client={client}\r\n conversationId={conversationId}\r\n closeOptionsMenu={closeOptionsMenu}\r\n />\r\n ) : null}\r\n </div>\r\n );\r\n};\r\n\r\nconst EmojiList = ({\r\n reactions,\r\n message,\r\n}: {\r\n reactions: Reaction[];\r\n message: Message;\r\n}) => {\r\n const { client } = useChatClient();\r\n const { conversationId } = message;\r\n\r\n const msClient = client.messageClient(conversationId);\r\n\r\n const deleteReaction = ({ emojiId }: { emojiId: string }) => {\r\n const filteredEmojis = reactions.filter((i) => i.uid !== emojiId);\r\n msClient.reactToMessage({\r\n conversationId,\r\n messageId: message.messageId,\r\n reactions: filteredEmojis,\r\n to: \"30\", // Make this dynamic\r\n });\r\n };\r\n return (\r\n <div className={styles.emoji}>\r\n {reactions.map((item, index) => (\r\n <div\r\n key={index}\r\n onClick={() => deleteReaction({ emojiId: item.uid })}\r\n className={styles.emoji__wrap}\r\n >\r\n <p>{item.emoji}</p>\r\n </div>\r\n ))}\r\n </div>\r\n );\r\n};\r\n\r\nconst AttachmentList = ({ attachments }: { attachments: Media[] }) => {\r\n const { setShowImageModal } = useChatState();\r\n\r\n return (\r\n <div className={styles.attachments}>\r\n {attachments.map((i, index) => {\r\n if (i.type === \"image\") {\r\n return (\r\n <img\r\n onClick={() => setShowImageModal(attachments)}\r\n key={`${index}-attch`}\r\n src={i.mediaUrl}\r\n className={styles.image}\r\n alt=\"attached-images\"\r\n />\r\n );\r\n }\r\n if (i.type === \"video\") {\r\n return (\r\n <video\r\n onClick={() => setShowImageModal(attachments)}\r\n key={`${index}-attch`}\r\n src={i.mediaUrl}\r\n className={styles.image}\r\n controls\r\n />\r\n );\r\n }\r\n if (i.type === \"audio\") {\r\n // return <audio src={i.mediaUrl} controls />;\r\n return (\r\n <AudioPlayer\r\n url={i?.mediaUrl}\r\n duration={i?.meta?.audioDurationSec || 0}\r\n blob={null}\r\n />\r\n );\r\n }\r\n })}\r\n </div>\r\n );\r\n};\r\n\r\nexport const QuotedMessage = ({\r\n message,\r\n scrollToQuote,\r\n}: {\r\n message: Message;\r\n scrollToQuote: (id: string) => void;\r\n}) => {\r\n const msg = message?.messageOwner?.username;\r\n return (\r\n <div\r\n className={styles.quote}\r\n onClick={() => scrollToQuote(message.messageId)}\r\n >\r\n <div>\r\n <Text\r\n styles={{ color: \"greenyellow\", textAlign: \"left\" }}\r\n size=\"sm\"\r\n weight=\"bold\"\r\n text={msg?.length > 30 ? `${msg?.slice(0, 5)}...` : msg}\r\n />\r\n <Text\r\n size=\"xs\"\r\n text={message?.message}\r\n styles={{ textAlign: \"left\" }}\r\n />\r\n </div>\r\n <img src={message?.attachedMedia[0]?.mediaUrl} alt=\"\" />\r\n </div>\r\n );\r\n};\r\n\r\nexport const FormattedText = ({ content }: { content: string }) => {\r\n const lines = content?.split(\"/\");\r\n\r\n return (\r\n <>\r\n {lines?.map((line, lineIndex) => {\r\n return (\r\n <div style={{ display: \"flex\" }} key={lineIndex}>\r\n {line.split(\" \")?.map((word, index) => {\r\n if (word.match(regex.URL_REGEX)) {\r\n return (\r\n <>\r\n <a style={{ textDecoration: \"none\" }} href={word}>\r\n <Text\r\n key={index}\r\n styles={{\r\n textAlign: \"left\",\r\n marginRight: \"4px\",\r\n color: \"#1C9BEF\",\r\n }}\r\n size=\"sm\"\r\n text={word}\r\n />\r\n </a>\r\n </>\r\n );\r\n }\r\n\r\n return (\r\n <Text\r\n styles={{ textAlign: \"left\", marginRight: \"4px\" }}\r\n key={index}\r\n text={word}\r\n size=\"sm\"\r\n />\r\n );\r\n })}\r\n </div>\r\n );\r\n })}\r\n </>\r\n );\r\n};\r\n\r\nexport const TypingIndicator = ({ message }: { message: Message }) => {\r\n return (\r\n <div className={styles.typing__wrap}>\r\n <div style={{ marginRight: \"10px\" }}>\r\n <Avartar\r\n initials={message.messageOwner.username.substring(0, 1)}\r\n url={message.messageOwner.profileUrl}\r\n />\r\n </div>\r\n\r\n <ThreeDots\r\n visible={true}\r\n height=\"20\"\r\n width=\"50\"\r\n color=\"#343434\"\r\n radius=\"9\"\r\n ariaLabel=\"three-dots-loading\"\r\n wrapperClass=\"\"\r\n />\r\n </div>\r\n );\r\n};\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 dayjs from \"dayjs\";\r\nimport moment from 'moment';\r\n\r\n\r\nimport localizedFormat from \"dayjs/plugin/localizedFormat\";\r\nimport calendarFormat from \"dayjs/plugin/calendar\";\r\ndayjs.extend(localizedFormat);\r\ndayjs.extend(calendarFormat);\r\n\r\nexport const formatMessageTime = (date: string) => {\r\n return dayjs(date).format(\"LT\");\r\n};\r\n\r\nexport const formatSectionTime = (date: string) => {\r\n return dayjs(date).format(\"ll\");\r\n};\r\n\r\nexport function formatWhatsAppDate(dateInput: Date): string {\r\n const now = new Date();\r\n const messageDate = new Date(dateInput);\r\n\r\n const isSameDay = now.toDateString() === messageDate.toDateString();\r\n const isYesterday =\r\n now.getDate() - messageDate.getDate() === 1 &&\r\n now.getMonth() === messageDate.getMonth() &&\r\n now.getFullYear() === messageDate.getFullYear();\r\n\r\n const oneWeekAgo = new Date();\r\n oneWeekAgo.setDate(now.getDate() - 7);\r\n\r\n if (isSameDay) {\r\n return \"Today\";\r\n } else if (isYesterday) {\r\n return \"Yesterday\";\r\n } else if (messageDate > oneWeekAgo) {\r\n // Return the day of the week (e.g., \"Monday\")\r\n return messageDate.toLocaleDateString(\"en-US\", { weekday: \"long\" });\r\n } else {\r\n // Return the date in DD/MM/YYYY format\r\n const day = String(messageDate.getDate()).padStart(2, \"0\");\r\n const month = String(messageDate.getMonth() + 1).padStart(2, \"0\"); // Months are zero-indexed\r\n const year = messageDate.getFullYear();\r\n\r\n return `${day}/${month}/${year}`;\r\n }\r\n}\r\n\r\nexport function formatConversationTime(time: Date | string) {\r\n if(!time) return ''\r\n const now = moment();\r\n const then = moment(time);\r\n const duration = moment.duration(now.diff(then));\r\n\r\n // Get the largest unit\r\n const years = Math.floor(duration.asYears());\r\n if (years > 0) return years + 'yr';\r\n\r\n const months = Math.floor(duration.asMonths());\r\n if (months > 0) return months + 'mo';\r\n\r\n const weeks = Math.floor(duration.asWeeks());\r\n if (weeks > 0) return weeks + 'w';\r\n\r\n const days = Math.floor(duration.asDays());\r\n if (days > 0) return days + 'd';\r\n\r\n const hours = Math.floor(duration.asHours());\r\n if (hours > 0) return hours + 'h';\r\n\r\n const minutes = Math.floor(duration.asMinutes());\r\n if (minutes > 0) return minutes + 'm';\r\n\r\n // If duration is less than 1 minute\r\n return 'Just now';\r\n}\r\n\r\nexport function convertToMinutes(seconds: number) {\r\n if(seconds === 0) {\r\n return '00:00'\r\n }\r\n var _seconds = Number(seconds.toFixed(0))\r\n const minutes = Math.floor(_seconds / 60);\r\n const remainingSeconds = _seconds % 60;\r\n\r\n // Pad the numbers to always have two digits\r\n const paddedMinutes = String(minutes).padStart(2, '0');\r\n const paddedSeconds = String(remainingSeconds).padStart(2, '0');\r\n\r\n return `${paddedMinutes}:${paddedSeconds}`;\r\n}","import { Dispatch, SetStateAction, useEffect, useState } from \"react\";\r\nimport Avartar from \"../avartar/avartar\";\r\n\r\nimport styles from \"./options-panel.module.css\";\r\nimport Text from \"../text/text\";\r\nimport ChatClient, { Message } from \"softchatjs-core\";\r\n\r\ntype ConversationListProps = {\r\n message: Message;\r\n setEditDetails: Dispatch<\r\n SetStateAction<\r\n | { message: Message; isEditing?: boolean; isReplying?: boolean }\r\n | undefined\r\n >\r\n >;\r\n canEdit?: boolean;\r\n openEmojiPanel: () => void;\r\n optionsMenuRef: any;\r\n mousePosition: {\r\n x: number;\r\n y: number;\r\n };\r\n conversationId: string;\r\n client: ChatClient;\r\n closeOptionsMenu: () => void;\r\n textInputRef: any;\r\n};\r\n\r\nconst OptionsPanel = (props: ConversationListProps) => {\r\n const {\r\n setEditDetails,\r\n message,\r\n canEdit,\r\n openEmojiPanel,\r\n optionsMenuRef,\r\n mousePosition: position,\r\n client,\r\n conversationId,\r\n closeOptionsMenu,\r\n textInputRef,\r\n } = props;\r\n\r\n const options = [\r\n {\r\n title: \"Edit message\",\r\n onPress: () => {\r\n setEditDetails({\r\n message,\r\n isEditing: true,\r\n });\r\n closeOptionsMenu();\r\n },\r\n enabled: canEdit,\r\n },\r\n\r\n {\r\n title: \"Reply\",\r\n onPress: () => {\r\n setEditDetails({\r\n message,\r\n isReplying: true,\r\n });\r\n closeOptionsMenu();\r\n textInputRef.current?.focus();\r\n },\r\n enabled: true,\r\n },\r\n {\r\n title: \"Add reaction\",\r\n onPress: () => {\r\n openEmojiPanel();\r\n },\r\n enabled: true,\r\n },\r\n {\r\n title: \"Delete\",\r\n onPress: () => {\r\n const msClient = client.messageClient(conversationId);\r\n msClient.deleteMessage(message.messageId, message.to, conversationId);\r\n },\r\n enabled: canEdit,\r\n },\r\n ];\r\n\r\n return (\r\n <div ref={optionsMenuRef} className={`${styles.options}`}>\r\n <ul>\r\n {options.map((item, index) => {\r\n if (item.enabled) {\r\n return (\r\n <li key={index} onClick={item.onPress}>\r\n <Text size=\"sm\" text={item.title} />\r\n </li>\r\n );\r\n }\r\n })}\r\n </ul>\r\n </div>\r\n );\r\n};\r\n\r\nexport default OptionsPanel;\r\n","import React, {\r\n Dispatch,\r\n SetStateAction,\r\n useContext,\r\n useEffect,\r\n useState,\r\n} from \"react\";\r\nimport EmojiPicker from \"emoji-picker-react\";\r\nimport styles from \"./emoji.module.css\";\r\nimport ChatClient, { Message } from \"softchatjs-core\";\r\nimport { CiFaceSmile } from \"react-icons/ci\";\r\nimport { BsReply } from \"react-icons/bs\";\r\nimport { AiOutlineDelete } from \"react-icons/ai\";\r\nimport { useChatClient } from \"../../providers/chatClientProvider\";\r\nimport { FiEdit2 } from \"react-icons/fi\";\r\nimport { CiEdit } from \"react-icons/ci\";\r\n\r\nconst emojis = [\"👍\", \"😔\", \"🙂\", \"😮\", \"😃\"];\r\n\r\ntype EmojiPanelProps = {\r\n emojiPickerRef: any;\r\n conversationId: string;\r\n client: ChatClient;\r\n message: Message;\r\n recipientId: string;\r\n setShowEmojiPanel: Dispatch<SetStateAction<boolean>>;\r\n};\r\n\r\ntype ReactionPanelProps = {\r\n message: Message;\r\n setEditDetails: Dispatch<\r\n SetStateAction<\r\n | { message: Message; isEditing?: boolean; isReplying?: boolean }\r\n | undefined\r\n >\r\n >;\r\n canEdit?: boolean;\r\n openEmojiPanel: () => void;\r\n optionsMenuRef: any;\r\n mousePosition: {\r\n x: number;\r\n y: number;\r\n };\r\n conversationId: string;\r\n\r\n closeOptionsMenu: () => void;\r\n textInputRef: any;\r\n client: ChatClient;\r\n};\r\n\r\nexport const EmojiPanel = (props: EmojiPanelProps) => {\r\n const { client, message, conversationId, recipientId, setShowEmojiPanel } =\r\n props;\r\n const { config } = useChatClient();\r\n const bgColor = config?.theme?.background?.secondary || \"#222529\";\r\n\r\n const reactToMessage = ({ emoji }: { emoji: string }) => {\r\n const msClient = client.messageClient(conversationId);\r\n\r\n msClient.reactToMessage({\r\n conversationId,\r\n messageId: message.messageId,\r\n reactions: [\r\n {\r\n emoji,\r\n uid: client.chatUserId\r\n },\r\n ],\r\n to: recipientId,\r\n });\r\n setShowEmojiPanel(false);\r\n };\r\n return (\r\n <div\r\n ref={props.emojiPickerRef}\r\n style={{ background: bgColor }}\r\n className={styles.emoji}\r\n >\r\n {/* <EmojiPicker\r\n onEmojiClick={(e) => {\r\n reactToMessage({ emoji: e.emoji });\r\n }}\r\n theme={\"dark\" as any}\r\n /> */}\r\n {emojis.map((item, index) => (\r\n <div\r\n key={index}\r\n onClick={() => reactToMessage({ emoji: item })}\r\n className={styles.reaction__emoji}\r\n >\r\n {item}\r\n </div>\r\n ))}\r\n </div>\r\n );\r\n};\r\n\r\nexport const ReactionPanel = ({\r\n setEditDetails,\r\n message,\r\n closeOptionsMenu,\r\n textInputRef,\r\n openEmojiPanel,\r\n client,\r\n canEdit,\r\n conversationId,\r\n}: ReactionPanelProps) => {\r\n const { config } = useChatClient();\r\n const iconColor = config.theme?.icon || \"#72767D\";\r\n\r\n const emojiList = [\r\n {\r\n emoji: <FiEdit2 size={16} color={iconColor} />,\r\n onPress: () => {\r\n setEditDetails({\r\n message,\r\n isEditing: true,\r\n });\r\n closeOptionsMenu();\r\n },\r\n enabled: canEdit,\r\n },\r\n {\r\n emoji: <CiFaceSmile size={16} color={iconColor} />,\r\n onPress: () => {\r\n openEmojiPanel();\r\n },\r\n enabled: true,\r\n },\r\n {\r\n emoji: <BsReply size={16} color={iconColor} />,\r\n onPress: () => {\r\n setEditDetails({\r\n message,\r\n isReplying: true,\r\n });\r\n closeOptionsMenu();\r\n textInputRef.current?.focus();\r\n },\r\n enabled: true,\r\n },\r\n {\r\n emoji: <AiOutlineDelete size={16} color={iconColor} />,\r\n onPress: () => {\r\n const msClient = client.messageClient(conversationId);\r\n msClient.deleteMessage(message.messageId, message.to, conversationId);\r\n },\r\n enabled: canEdit,\r\n },\r\n ];\r\n return (\r\n <div className={styles.reactions}>\r\n {emojiList.map((item, index) => {\r\n if (item.enabled) {\r\n return (\r\n <div key={index} onClick={item.onPress} className={styles.reaction__emoji}>\r\n {item.emoji}\r\n </div>\r\n );\r\n }\r\n })}\r\n </div>\r\n );\r\n};\r\n\r\nexport const InputEmojis = ({\r\n onEmojiPick,\r\n}: {\r\n onEmojiPick: (emoji: string) => void;\r\n}) => {\r\n const { config } = useChatClient();\r\n return (\r\n <EmojiPicker\r\n height={350}\r\n width={300}\r\n onEmojiClick={(e) => {\r\n onEmojiPick(e.emoji);\r\n }}\r\n theme={config?.theme?.input?.emojiPickerTheme as any}\r\n />\r\n );\r\n};\r\n ","import React, { useState, useEffect, useRef, useCallback, CSSProperties } from \"react\";\r\n// import \"./audio-recorder.css\";\r\nimport { PauseIcon, PlayIcon } from \"../assets/icons\";\r\nimport { convertToMinutes } from \"../../helpers/date\";\r\nimport { FaSpinner } from \"react-icons/fa6\";\r\nimport { useChatClient } from \"../../providers/chatClientProvider\";\r\n\r\ntype AudioPlayerProps = {\r\n blob: Blob;\r\n url?: string;\r\n duration: number;\r\n style?: CSSProperties\r\n};\r\nexport default function AudioPlayer(props: AudioPlayerProps) {\r\n const { blob, duration, url, style } = props;\r\n\r\n const [audioUrl, setAudioUrl] = useState(\"\");\r\n const audioRef = useRef<HTMLAudioElement>(null);\r\n const [isPlaying, setIsPlaying] = useState(false);\r\n const [currentTime, setCurrentTime] = useState(0);\r\n const [isLoading, setIsLoading] = useState(true); // Set default to true\r\n const { config } = useChatClient();\r\n const theme = config.theme;\r\n const textColor = config?.theme?.text?.primary || \"white\";\r\n\r\n const togglePlayPause = () => {\r\n if (audioRef.current) {\r\n if (isPlaying) {\r\n audioRef.current.pause();\r\n } else {\r\n audioRef.current.play();\r\n }\r\n setIsPlaying(!isPlaying);\r\n }\r\n };\r\n\r\n const handleLoadedMetadata = () => {\r\n // if (audioRef.current) {\r\n // setDuration(audioRef.current.duration?? 0);\r\n // }\r\n };\r\n\r\n const handleTimeUpdate = () => {\r\n // if (audioRef.current) {\r\n setCurrentTime(audioRef.current.currentTime);\r\n // }\r\n };\r\n\r\n const handleEnded = () => {\r\n setIsPlaying(false);\r\n setCurrentTime(0);\r\n };\r\n\r\n useEffect(() => {\r\n if (url) {\r\n return setAudioUrl(url);\r\n }\r\n if (blob) {\r\n const url = URL.createObjectURL(blob);\r\n setAudioUrl(url);\r\n setIsLoading(true);\r\n }\r\n }, [blob, url]);\r\n\r\n const renderAction = useCallback(() => {\r\n if (isLoading) {\r\n return <FaSpinner style={{ marginRight: \"3px\", color:textColor} } />;\r\n }\r\n\r\n return (\r\n <button\r\n onClick={togglePlayPause}\r\n style={{\r\n backgroundColor: \"transparent\",\r\n border: 0,\r\n padding: 0,\r\n margin: '0px',\r\n marginTop: '4px'\r\n }}\r\n >\r\n {isPlaying ? (\r\n <PauseIcon size={15} color={textColor} />\r\n ) : (\r\n <PlayIcon size={15} color={textColor} />\r\n )}\r\n </button>\r\n );\r\n }, [isLoading, isPlaying]);\r\n\r\n return (\r\n <div style={{ display: \"flex\", alignItems: \"center\", padding: \"5px\", ...style }}>\r\n {renderAction()}\r\n <audio\r\n ref={audioRef}\r\n onLoadedMetadata={handleLoadedMetadata}\r\n onTimeUpdate={handleTimeUpdate}\r\n onEnded={handleEnded}\r\n onLoadStart={() => setIsLoading(true)}\r\n onCanPlay={() => setIsLoading(false)}\r\n src={audioUrl}\r\n >\r\n </audio>\r\n <p style={{ padding: 0, marginLeft: \"10px\", marginTop: 0, fontSize: \"11.5px\", color: textColor }}>\r\n {convertToMinutes(currentTime)} : {convertToMinutes(duration)}\r\n </p>\r\n </div>\r\n );\r\n}\r\n","type Icon = {\r\n size?: number;\r\n color?: string;\r\n};\r\n\r\nexport default function TrashIcon(props: Icon) {\r\n const { size = 25, color = \"red\" } = props;\r\n return (\r\n <svg width=