|
@ -1,28 +1,28 @@ |
|
|
import { PiTrash } from "react-icons/pi"; |
|
|
|
|
|
import Image from "next/image"; |
|
|
|
|
|
import SendIcon from "public/image/send-01.svg"; |
|
|
|
|
|
import MicIcon from "public/image/microphone-01.svg"; |
|
|
|
|
|
import LinkIcon from "public/image/link-simple.svg"; |
|
|
|
|
|
import SmileIcon from "public/image/face-smile.svg"; |
|
|
|
|
|
import { MdClose } from "react-icons/md"; |
|
|
|
|
|
import { useEffect, useState, useRef } from "react"; |
|
|
|
|
|
import { useWebSocket } from "@/contexts/WebSocket.context"; |
|
|
|
|
|
import FileInput from "./file-input"; |
|
|
|
|
|
import dynamic from "next/dynamic"; |
|
|
|
|
|
import EmojiPicker from "emoji-picker-react"; |
|
|
|
|
|
import { BsFillReplyFill } from "react-icons/bs"; |
|
|
|
|
|
|
|
|
import { PiTrash } from 'react-icons/pi'; |
|
|
|
|
|
import Image from 'next/image'; |
|
|
|
|
|
import SendIcon from 'public/image/send-01.svg'; |
|
|
|
|
|
import MicIcon from 'public/image/microphone-01.svg'; |
|
|
|
|
|
import LinkIcon from 'public/image/link-simple.svg'; |
|
|
|
|
|
import SmileIcon from 'public/image/face-smile.svg'; |
|
|
|
|
|
import { MdClose } from 'react-icons/md'; |
|
|
|
|
|
import { useEffect, useState, useRef } from 'react'; |
|
|
|
|
|
import { useWebSocket } from '@/contexts/WebSocket.context'; |
|
|
|
|
|
import FileInput from './file-input'; |
|
|
|
|
|
import dynamic from 'next/dynamic'; |
|
|
|
|
|
import EmojiPicker from 'emoji-picker-react'; |
|
|
|
|
|
import { BsFillReplyFill } from 'react-icons/bs'; |
|
|
// Dynamically import ReactMediaRecorder to prevent SSR issues
|
|
|
// Dynamically import ReactMediaRecorder to prevent SSR issues
|
|
|
const ReactMediaRecorder = dynamic( |
|
|
const ReactMediaRecorder = dynamic( |
|
|
() => import("react-media-recorder").then((mod) => mod.ReactMediaRecorder), |
|
|
|
|
|
{ ssr: false } |
|
|
|
|
|
|
|
|
() => import('react-media-recorder').then((mod) => mod.ReactMediaRecorder), |
|
|
|
|
|
{ ssr: false }, |
|
|
); |
|
|
); |
|
|
|
|
|
|
|
|
// Helper function to format recording time
|
|
|
// Helper function to format recording time
|
|
|
const formatTime = (seconds) => { |
|
|
const formatTime = (seconds) => { |
|
|
const mins = Math.floor(seconds / 60) |
|
|
const mins = Math.floor(seconds / 60) |
|
|
.toString() |
|
|
.toString() |
|
|
.padStart(2, "0"); |
|
|
|
|
|
const secs = (seconds % 60).toString().padStart(2, "0"); |
|
|
|
|
|
|
|
|
.padStart(2, '0'); |
|
|
|
|
|
const secs = (seconds % 60).toString().padStart(2, '0'); |
|
|
return `${mins}:${secs}`; |
|
|
return `${mins}:${secs}`; |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
@ -39,7 +39,7 @@ const Input = ({ product = {} }) => { |
|
|
const [showProduct, setShowProduct] = useState(!!product.id); |
|
|
const [showProduct, setShowProduct] = useState(!!product.id); |
|
|
const [isRecording, setIsRecording] = useState(false); |
|
|
const [isRecording, setIsRecording] = useState(false); |
|
|
const [recordingTime, setRecordingTime] = useState(0); |
|
|
const [recordingTime, setRecordingTime] = useState(0); |
|
|
const [message, setMessage] = useState(""); |
|
|
|
|
|
|
|
|
const [message, setMessage] = useState(''); |
|
|
const [showEmojiPicker, setShowEmojiPicker] = useState(false); // Emoji picker state
|
|
|
const [showEmojiPicker, setShowEmojiPicker] = useState(false); // Emoji picker state
|
|
|
|
|
|
|
|
|
// Ref for the timer interval
|
|
|
// Ref for the timer interval
|
|
@ -60,11 +60,11 @@ const Input = ({ product = {} }) => { |
|
|
id: editingMessage.id, |
|
|
id: editingMessage.id, |
|
|
}; |
|
|
}; |
|
|
editMessage(msg); |
|
|
editMessage(msg); |
|
|
setEditingMessage({}) |
|
|
|
|
|
setMessage(""); |
|
|
|
|
|
|
|
|
setEditingMessage({}); |
|
|
|
|
|
setMessage(''); |
|
|
} else { |
|
|
} else { |
|
|
sendMessage(message, product?.id); |
|
|
sendMessage(message, product?.id); |
|
|
setMessage(""); |
|
|
|
|
|
|
|
|
setMessage(''); |
|
|
setShowProduct(false); |
|
|
setShowProduct(false); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
@ -108,9 +108,9 @@ const Input = ({ product = {} }) => { |
|
|
} |
|
|
} |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
document.addEventListener("mousedown", handleClickOutside); |
|
|
|
|
|
|
|
|
document.addEventListener('mousedown', handleClickOutside); |
|
|
return () => { |
|
|
return () => { |
|
|
document.removeEventListener("mousedown", handleClickOutside); |
|
|
|
|
|
|
|
|
document.removeEventListener('mousedown', handleClickOutside); |
|
|
}; |
|
|
}; |
|
|
}, []); |
|
|
}, []); |
|
|
useEffect(() => { |
|
|
useEffect(() => { |
|
@ -119,7 +119,7 @@ const Input = ({ product = {} }) => { |
|
|
messageInputRef.current?.focus(); // Set focus on the input
|
|
|
messageInputRef.current?.focus(); // Set focus on the input
|
|
|
} |
|
|
} |
|
|
}, [editingMessage]); |
|
|
}, [editingMessage]); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return ( |
|
|
return ( |
|
|
<ReactMediaRecorder |
|
|
<ReactMediaRecorder |
|
|
audio |
|
|
audio |
|
@ -133,166 +133,126 @@ const Input = ({ product = {} }) => { |
|
|
stopTimer(); |
|
|
stopTimer(); |
|
|
if (sendRecordingRef.current) { |
|
|
if (sendRecordingRef.current) { |
|
|
let selectedFile = blob; |
|
|
let selectedFile = blob; |
|
|
selectedFile.status = "loading"; |
|
|
|
|
|
|
|
|
selectedFile.status = 'loading'; |
|
|
selectedFile.name = `audio ${new Date()}.wav`; |
|
|
selectedFile.name = `audio ${new Date()}.wav`; |
|
|
setLoadingMessage((prev) => [...prev, selectedFile]); |
|
|
setLoadingMessage((prev) => [...prev, selectedFile]); |
|
|
} else { |
|
|
} else { |
|
|
console.log("Recording discarded:", blobUrl); |
|
|
|
|
|
|
|
|
console.log('Recording discarded:', blobUrl); |
|
|
} |
|
|
} |
|
|
sendRecordingRef.current = false; // Reset the flag after handling
|
|
|
sendRecordingRef.current = false; // Reset the flag after handling
|
|
|
}} |
|
|
}} |
|
|
render={({ startRecording, stopRecording }) => ( |
|
|
render={({ startRecording, stopRecording }) => ( |
|
|
<div |
|
|
<div |
|
|
className={`${ |
|
|
className={`${ |
|
|
product.id && showProduct ? "mb-28" : "" |
|
|
|
|
|
|
|
|
product.id && showProduct ? 'mb-28' : '' |
|
|
} z-10 m-7 border border-[#D9D9D9] self-end w-full`}
|
|
|
} z-10 m-7 border border-[#D9D9D9] self-end w-full`}
|
|
|
> |
|
|
> |
|
|
{/* Product Preview */} |
|
|
|
|
|
{product.id && showProduct && ( |
|
|
|
|
|
<div className="w-full flex gap-2 bg-white border p-4"> |
|
|
|
|
|
<Image src={LinkIcon} alt="Link Icon" width={24} height={24} /> |
|
|
|
|
|
<div className="flex justify-between w-full items-center"> |
|
|
|
|
|
<div className="flex gap-2"> |
|
|
|
|
|
{product?.images && product.images.length > 0 && ( |
|
|
|
|
|
<Image |
|
|
|
|
|
width={35} |
|
|
|
|
|
height={35} |
|
|
|
|
|
src={product.images[0].image_url.sm} |
|
|
|
|
|
alt={product.name || "Product Image"} |
|
|
|
|
|
className="object-cover rounded" |
|
|
|
|
|
/> |
|
|
|
|
|
)} |
|
|
|
|
|
<div className="flex flex-col justify-between"> |
|
|
|
|
|
<p className="text-xs leading-none mb-0"> |
|
|
|
|
|
{product.name || "Unnamed Product"} |
|
|
|
|
|
</p> |
|
|
|
|
|
<p className="text-[10px]">{product.price}$</p> |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> |
|
|
|
|
|
<MdClose |
|
|
|
|
|
onClick={() => {setShowProduct(false)}} |
|
|
|
|
|
size={20} |
|
|
|
|
|
aria-label="Close product preview" |
|
|
|
|
|
className="cursor-pointer" |
|
|
|
|
|
/> |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> |
|
|
|
|
|
)} |
|
|
|
|
|
|
|
|
|
|
|
{!!Object.keys(editingMessage).length && ( |
|
|
|
|
|
<div className=" w-full flex bg-white border gap-2 p-4"> |
|
|
|
|
|
<BsFillReplyFill size={24} /> |
|
|
|
|
|
|
|
|
|
|
|
<div className="flex justify-between w-full items-center"> |
|
|
|
|
|
<div className="flex gap-2"> |
|
|
|
|
|
<div className="flex flex-col justify-between"> |
|
|
|
|
|
<p className="text-xs leading-none mb-0"> |
|
|
|
|
|
{editingMessage.mime_type === "text" |
|
|
|
|
|
? editingMessage.content |
|
|
|
|
|
: editingMessage.mime_type} |
|
|
|
|
|
</p> |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> |
|
|
|
|
|
<MdClose |
|
|
|
|
|
onClick={() => {setEditingMessage({}); setMessage("")}} |
|
|
|
|
|
size={20} |
|
|
|
|
|
aria-label="Close product preview" |
|
|
|
|
|
className="cursor-pointer" |
|
|
|
|
|
/> |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> |
|
|
|
|
|
)} |
|
|
|
|
|
{/* Message Input Area */} |
|
|
{/* Message Input Area */} |
|
|
<div className="relative h-14 max-h-40 z-10 border-2 bg-white flex items-center p-1"> |
|
|
<div className="relative h-14 max-h-40 z-10 border-2 bg-white flex items-center p-1"> |
|
|
{/* Emoji Button */} |
|
|
|
|
|
<button |
|
|
|
|
|
ref={emojiButtonRef} // Attach ref to the emoji toggle button
|
|
|
|
|
|
onClick={() => { |
|
|
|
|
|
setShowEmojiPicker((prev) => !prev); // Toggle emoji picker state
|
|
|
|
|
|
}} |
|
|
|
|
|
className="text-white px-3 py-2 rounded-md transition flex items-center hover:bg-slate-100" |
|
|
|
|
|
aria-label="Insert emoji" |
|
|
|
|
|
> |
|
|
|
|
|
<Image width={25} height={25} src={SmileIcon} alt="Smile" /> |
|
|
|
|
|
</button> |
|
|
|
|
|
|
|
|
|
|
|
{/* Emoji Picker */} |
|
|
|
|
|
{showEmojiPicker && ( |
|
|
|
|
|
<div |
|
|
|
|
|
ref={emojiPickerRef} |
|
|
|
|
|
className="absolute bottom-14 left-0 z-20 bg-white border rounded shadow-md" |
|
|
|
|
|
> |
|
|
|
|
|
<EmojiPicker |
|
|
|
|
|
onEmojiClick={(emojiObject) => { |
|
|
|
|
|
setMessage( |
|
|
|
|
|
(prevMessage) => prevMessage + emojiObject.emoji |
|
|
|
|
|
); |
|
|
|
|
|
messageInputRef.current?.focus(); |
|
|
|
|
|
}} |
|
|
|
|
|
emojiStyle="native" |
|
|
|
|
|
skinTonesDisabled |
|
|
|
|
|
previewConfig={{ |
|
|
|
|
|
showPreview: false, |
|
|
|
|
|
}} |
|
|
|
|
|
/> |
|
|
|
|
|
</div> |
|
|
|
|
|
)} |
|
|
|
|
|
|
|
|
{isRecording ? ( |
|
|
|
|
|
<div className="flex w-full justify-between items-center px-4"> |
|
|
|
|
|
{/* Trash Button */} |
|
|
|
|
|
<div className='flex gap-4 items-center'> |
|
|
|
|
|
<button |
|
|
|
|
|
onClick={() => { |
|
|
|
|
|
sendRecordingRef.current = false; // Discard recording
|
|
|
|
|
|
stopRecording(); |
|
|
|
|
|
}} |
|
|
|
|
|
aria-label="Discard recording" |
|
|
|
|
|
className="text-red-600 flex items-center gap-2 hover:text-red-800" |
|
|
|
|
|
> |
|
|
|
|
|
<PiTrash size={25} /> |
|
|
|
|
|
</button> |
|
|
|
|
|
|
|
|
{/* Textarea Input */} |
|
|
|
|
|
<textarea |
|
|
|
|
|
className="flex-1 h-full rounded-md p-2 text-base focus:outline-none resize-none overflow-hidden" |
|
|
|
|
|
ref={messageInputRef} |
|
|
|
|
|
placeholder="Type your message..." |
|
|
|
|
|
value={message} |
|
|
|
|
|
onChange={(e) => setMessage(e.target.value)} |
|
|
|
|
|
rows={1} |
|
|
|
|
|
onKeyDown={(e) => { |
|
|
|
|
|
if (e.key === "Enter" && !e.shiftKey) { |
|
|
|
|
|
handleSendMessage(); |
|
|
|
|
|
e.preventDefault(); |
|
|
|
|
|
} |
|
|
|
|
|
}} |
|
|
|
|
|
aria-label="Message input" |
|
|
|
|
|
/> |
|
|
|
|
|
|
|
|
{/* Timer with Red Dot */} |
|
|
|
|
|
<div className="flex items-center gap-2 py-1 px-2 rounded-md bg-red-100 text-red-600 font-medium"> |
|
|
|
|
|
<div className="w-3 h-3 bg-red-500 rounded-full animate-pulse"></div> |
|
|
|
|
|
<span className='text-black'>{formatTime(recordingTime)}</span> |
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
{/* Send Button or File/Mic Buttons */} |
|
|
|
|
|
{message.trim() || isRecording ? ( |
|
|
|
|
|
<button |
|
|
|
|
|
onClick={() => { |
|
|
|
|
|
if (isRecording) { |
|
|
|
|
|
|
|
|
{/* Stop/Send Button */} |
|
|
|
|
|
</div> |
|
|
|
|
|
<button |
|
|
|
|
|
onClick={() => { |
|
|
sendRecordingRef.current = true; // Indicate sending
|
|
|
sendRecordingRef.current = true; // Indicate sending
|
|
|
stopRecording(); |
|
|
stopRecording(); |
|
|
} else { |
|
|
|
|
|
handleSendMessage(); |
|
|
|
|
|
} |
|
|
|
|
|
}} |
|
|
|
|
|
className="text-white px-3 py-2 rounded-md transition flex items-center hover:bg-slate-100" |
|
|
|
|
|
aria-label={isRecording ? "Send recording" : "Send message"} |
|
|
|
|
|
> |
|
|
|
|
|
<Image src={SendIcon} alt="Send" width={24} height={24} /> |
|
|
|
|
|
</button> |
|
|
|
|
|
|
|
|
}} |
|
|
|
|
|
className="text-red-600 flex items-center gap-2 hover:text-red-800" |
|
|
|
|
|
aria-label="Stop recording" |
|
|
|
|
|
> |
|
|
|
|
|
<Image src={SendIcon} alt="Send" width={24} height={24} /> |
|
|
|
|
|
</button> |
|
|
|
|
|
</div> |
|
|
) : ( |
|
|
) : ( |
|
|
<> |
|
|
<> |
|
|
{!isRecording && ( |
|
|
|
|
|
<> |
|
|
|
|
|
<FileInput /> |
|
|
|
|
|
<button |
|
|
|
|
|
onClick={() => { |
|
|
|
|
|
startRecording(); |
|
|
|
|
|
|
|
|
{/* Emoji Button */} |
|
|
|
|
|
<button |
|
|
|
|
|
ref={emojiButtonRef} |
|
|
|
|
|
onClick={() => { |
|
|
|
|
|
setShowEmojiPicker((prev) => !prev); |
|
|
|
|
|
}} |
|
|
|
|
|
className="text-white px-3 py-2 rounded-md transition flex items-center hover:bg-slate-100" |
|
|
|
|
|
aria-label="Insert emoji" |
|
|
|
|
|
> |
|
|
|
|
|
<Image width={25} height={25} src={SmileIcon} alt="Smile" /> |
|
|
|
|
|
</button> |
|
|
|
|
|
|
|
|
|
|
|
{/* Emoji Picker */} |
|
|
|
|
|
{showEmojiPicker && ( |
|
|
|
|
|
<div |
|
|
|
|
|
ref={emojiPickerRef} |
|
|
|
|
|
className="absolute bottom-14 left-0 z-20 bg-white border rounded shadow-md" |
|
|
|
|
|
> |
|
|
|
|
|
<EmojiPicker |
|
|
|
|
|
onEmojiClick={(emojiObject) => { |
|
|
|
|
|
setMessage( |
|
|
|
|
|
(prevMessage) => prevMessage + emojiObject.emoji, |
|
|
|
|
|
); |
|
|
|
|
|
messageInputRef.current?.focus(); |
|
|
}} |
|
|
}} |
|
|
className="text-white px-3 py-2 rounded-md transition flex items-center hover:bg-slate-100" |
|
|
|
|
|
aria-label="Start recording" |
|
|
|
|
|
> |
|
|
|
|
|
<Image |
|
|
|
|
|
src={MicIcon} |
|
|
|
|
|
width={23} |
|
|
|
|
|
height={23} |
|
|
|
|
|
alt="Microphone" |
|
|
|
|
|
className="object-contain" |
|
|
|
|
|
/> |
|
|
|
|
|
</button> |
|
|
|
|
|
</> |
|
|
|
|
|
|
|
|
emojiStyle="native" |
|
|
|
|
|
skinTonesDisabled |
|
|
|
|
|
previewConfig={{ |
|
|
|
|
|
showPreview: false, |
|
|
|
|
|
}} |
|
|
|
|
|
/> |
|
|
|
|
|
</div> |
|
|
)} |
|
|
)} |
|
|
|
|
|
|
|
|
|
|
|
{/* Textarea Input */} |
|
|
|
|
|
<textarea |
|
|
|
|
|
className="flex-1 h-full rounded-md p-2 text-base focus:outline-none resize-none overflow-hidden" |
|
|
|
|
|
ref={messageInputRef} |
|
|
|
|
|
placeholder="Type your message..." |
|
|
|
|
|
value={message} |
|
|
|
|
|
onChange={(e) => setMessage(e.target.value)} |
|
|
|
|
|
rows={1} |
|
|
|
|
|
onKeyDown={(e) => { |
|
|
|
|
|
if (e.key === 'Enter' && !e.shiftKey) { |
|
|
|
|
|
handleSendMessage(); |
|
|
|
|
|
e.preventDefault(); |
|
|
|
|
|
} |
|
|
|
|
|
}} |
|
|
|
|
|
aria-label="Message input" |
|
|
|
|
|
/> |
|
|
|
|
|
|
|
|
|
|
|
{/* File Input and Mic Button */} |
|
|
|
|
|
<FileInput /> |
|
|
|
|
|
<button |
|
|
|
|
|
onClick={() => { |
|
|
|
|
|
startRecording(); |
|
|
|
|
|
}} |
|
|
|
|
|
className="text-white px-3 py-2 rounded-md transition flex items-center hover:bg-slate-100" |
|
|
|
|
|
aria-label="Start recording" |
|
|
|
|
|
> |
|
|
|
|
|
<Image |
|
|
|
|
|
src={MicIcon} |
|
|
|
|
|
width={23} |
|
|
|
|
|
height={23} |
|
|
|
|
|
alt="Microphone" |
|
|
|
|
|
className="object-contain" |
|
|
|
|
|
/> |
|
|
|
|
|
</button> |
|
|
</> |
|
|
</> |
|
|
)} |
|
|
)} |
|
|
</div> |
|
|
</div> |
|
|