softchatjs-react
Version:
Install the softchat-js SDKs
1 lines • 79.6 kB
Source Map (JSON)
{"version":3,"sources":["../../../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"],"sourcesContent":["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 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 { Dispatch, SetStateAction, useEffect, useRef, useState } from \"react\";\r\nimport styles from \"./menu.module.css\";\r\nimport { HiPhoto } from \"react-icons/hi2\";\r\nimport Text from \"../text/text\";\r\n\r\ntype MenuProps = {\r\n element: JSX.Element;\r\n generalMenuRef: any;\r\n};\r\n\r\nexport const Menu = (props: MenuProps) => {\r\n return (\r\n <div ref={props.generalMenuRef} className={styles.menu}>\r\n {props.element}\r\n </div>\r\n );\r\n};\r\n\r\nexport const AttachmentMenu = ({\r\n onChange,\r\n setFiles,\r\n closeGeneralMenu,\r\n}: {\r\n onChange?: (event: any) => void;\r\n closeGeneralMenu: () => void;\r\n setFiles:Dispatch<SetStateAction<any[]>>\r\n}) => {\r\n const fileInputRef: any = useRef();\r\n const options = [\r\n {\r\n icon: <HiPhoto size={24} color=\"white\" />,\r\n title: \"Photo\",\r\n },\r\n ];\r\n\r\n const handleChange = (event: any) => {\r\n closeGeneralMenu();\r\n const files = event.target.files;\r\n setFiles(Array.from(files))\r\n };\r\n\r\n return (\r\n <div className={styles.attachment}>\r\n {options.map((item, index) => (\r\n <label htmlFor=\"upload\" key={index}>\r\n <div className={styles.attachment__item}>\r\n <div style={{ marginRight: \"10px\" }}> {item.icon}</div>\r\n <input\r\n onChange={handleChange}\r\n \r\n ref={fileInputRef}\r\n type=\"file\"\r\n hidden\r\n id=\"upload\"\r\n />\r\n <Text\r\n styles={{ marginBottom: \"3px\" }}\r\n size=\"sm\"\r\n text={item.title}\r\n />\r\n </div>\r\n </label>\r\n ))}\r\n </div>\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 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 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 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={size} height={size} viewBox=\"0 0 20 20\" fill=\"none\">\r\n <path\r\n fillRule=\"evenodd\"\r\n clipRule=\"evenodd\"\r\n d=\"M8.75009 1C8.02075 1 7.32127 1.28973 6.80555 1.80546C6.28982 2.32118 6.00009 3.02065 6.00009 3.75V4.193C5.20476 4.26967 4.41643 4.369 3.63509 4.491C3.53634 4.50445 3.44126 4.53745 3.35541 4.58807C3.26956 4.63869 3.19465 4.70591 3.13508 4.78581C3.0755 4.86571 3.03245 4.95667 3.00843 5.0534C2.98441 5.15013 2.97992 5.25067 2.9952 5.34916C3.01048 5.44764 3.04524 5.54209 3.09745 5.62699C3.14965 5.71189 3.21825 5.78553 3.29924 5.84361C3.38023 5.90169 3.47199 5.94305 3.56914 5.96526C3.6663 5.98748 3.76691 5.99011 3.86509 5.973L4.01409 5.951L4.85509 16.469C4.91015 17.1582 5.22279 17.8014 5.73075 18.2704C6.23871 18.7394 6.9047 18.9999 7.59609 19H12.4031C13.0945 19.0002 13.7606 18.74 14.2687 18.2711C14.7769 17.8022 15.0898 17.1592 15.1451 16.47L15.9861 5.95L16.1351 5.973C16.3298 5.99952 16.5271 5.94858 16.6847 5.83111C16.8422 5.71365 16.9473 5.53906 16.9775 5.34488C17.0076 5.15071 16.9603 4.95246 16.8458 4.79278C16.7313 4.6331 16.5587 4.52474 16.3651 4.491C15.5798 4.36877 14.7911 4.26939 14.0001 4.193V3.75C14.0001 3.02065 13.7104 2.32118 13.1946 1.80546C12.6789 1.28973 11.9794 1 11.2501 1H8.75009ZM10.0001 4C10.8401 4 11.6734 4.025 12.5001 4.075V3.75C12.5001 3.06 11.9401 2.5 11.2501 2.5H8.75009C8.06009 2.5 7.50009 3.06 7.50009 3.75V4.075C8.32676 4.025 9.16009 4 10.0001 4ZM8.58009 7.72C8.57213 7.52109 8.48549 7.33348 8.33921 7.19846C8.19293 7.06343 7.999 6.99204 7.80009 7C7.60118 7.00796 7.41357 7.0946 7.27855 7.24088C7.14352 7.38716 7.07213 7.58109 7.08009 7.78L7.38009 15.28C7.38403 15.3785 7.40733 15.4752 7.44866 15.5647C7.48999 15.6542 7.54854 15.7347 7.62097 15.8015C7.6934 15.8684 7.77829 15.9203 7.8708 15.9544C7.9633 15.9884 8.0616 16.0039 8.16009 16C8.25858 15.9961 8.35533 15.9728 8.44482 15.9314C8.53431 15.8901 8.61478 15.8315 8.68163 15.7591C8.74849 15.6867 8.80043 15.6018 8.83448 15.5093C8.86853 15.4168 8.88403 15.3185 8.88009 15.22L8.58009 7.72ZM12.9201 7.78C12.924 7.68151 12.9085 7.58321 12.8745 7.4907C12.8404 7.3982 12.7885 7.31331 12.7216 7.24088C12.6548 7.16845 12.5743 7.1099 12.4848 7.06857C12.3953 7.02724 12.2986 7.00394 12.2001 7C12.0012 6.99204 11.8073 7.06343 11.661 7.19846C11.5147 7.33348 11.428 7.52109 11.4201 7.72L11.1201 15.22C11.1162 15.3185 11.1317 15.4168 11.1657 15.5093C11.1998 15.6018 11.2517 15.6867 11.3185 15.7591C11.3854 15.8315 11.4659 15.8901 11.5554 15.9314C11.6448 15.9728 11.7416 15.9961 11.8401 16C11.9386 16.0039 12.0369 15.9884 12.1294 15.9544C12.2219 15.9203 12.3068 15.8684 12.3792 15.8015C12.4516 15.7347 12.5102 15.6542 12.5515 15.5647C12.5929 15.4752 12.6162 15.3785 12.6201 15.28L12.9201 7.78Z\"\r\n fill={color}\r\n />\r\n </svg>\r\n );\r\n}\r\n\r\nexport const PlayIcon = (props: Icon) => {\r\n const { size = 25, color = \"black\" } = props;\r\n return (\r\n <svg width={size} height={size} viewBox=\"0 0 256 256\" fill=\"none\">\r\n <path\r\n d=\"M240 128C240.007 130.716 239.31 133.388 237.978 135.756C236.647 138.123 234.725 140.105 232.4 141.51L88.32 229.65C85.8909 231.138 83.1087 231.95 80.2608 232.002C77.4129 232.055 74.6025 231.347 72.12 229.95C69.6611 228.575 67.6128 226.57 66.1856 224.141C64.7585 221.712 64.0041 218.947 64 216.13V39.8701C64.0041 37.053 64.7585 34.2877 66.1856 31.8588C67.6128 29.4299 69.6611 27.4249 72.12 26.0501C74.6025 24.6536 77.4129 23.9451 80.2608 23.9979C83.1087 24.0506 85.8909 24.8626 88.32 26.3501L232.4 114.49C234.725 115.895 236.647 117.877 237.978 120.245C239.31 122.612 240.007 125.284 240 128Z\"\r\n fill={color}\r\n />\r\n </svg>\r\n );\r\n};\r\n\r\nexport const PauseIcon = (props: Icon) => {\r\n const { size = 25, color = \"black\" } = props;\r\n return (\r\n <svg width={size} height={size} viewBox=\"0 0 24 24\" fill=\"none\">\r\n <path\r\n fill-rule=\"evenodd\"\r\n clip-rule=\"evenodd\"\r\n d=\"M8 5C7.46957 5 6.96086 5.21071 6.58579 5.58579C6.21071 5.96086 6 6.46957 6 7V17C6 17.5304 6.21071 18.0391 6.58579 18.4142C6.96086 18.7893 7.46957 19 8 19H9C9.53043 19 10.0391 18.7893 10.4142 18.4142C10.7893 18.0391 11 17.5304 11 17V7C11 6.46957 10.7893 5.96086 10.4142 5.58579C10.0391 5.21071 9.53043 5 9 5H8ZM15 5C14.4696 5 13.9609 5.21071 13.5858 5.58579C13.2107 5.96086 13 6.46957 13 7V17C13 17.5304 13.2107 18.0391 13.5858 18.4142C13.9609 18.7893 14.4696 19 15 19H16C16.5304 19 17.0391 18.7893 17.4142 18.4142C17.7893 18.0391 18 17.5304 18 17V7C18 6.46957 17.7893 5.96086 17.4142 5.58579C17.0391 5.21071 16.5304 5 16 5H15Z\"\r\n fill={color}\r\n />\r\n </svg>\r\n );\r\n};\r\n\r\nexport const LockIcon = (props: Icon) => {\r\n const { size = 25, color = \"black\" } = props;\r\n\r\n return (\r\n <svg width={size} height={size} viewBox=\"0