From 751d4cf09251be0411f814b8f901ce9c9ad22986 Mon Sep 17 00:00:00 2001 From: sina_sajjadi Date: Mon, 20 Jan 2025 14:45:22 +0330 Subject: [PATCH] feat: add new SVG icons and update chat components with loading states --- package.json | 3 + public/image/Frame 1000005529.svg | 49 +++ public/image/State=Send.svg | 3 + public/image/Vector.svg | 3 + public/image/VectorWhite.svg | 3 + public/image/bars.svg | 3 + public/image/clock-fast-forward.png | Bin 0 -> 650 bytes public/image/copy-06.svg | 3 + public/image/edit-04.svg | 3 + public/image/quill_checkmark-double.svg | 3 + public/image/search.svg | 3 + public/image/trash-03.svg | 3 + src/components/chat/audio-message.tsx | 99 +++++ src/components/chat/chat-input.tsx | 305 ++++++++++++++++ src/components/chat/contact-info.tsx | 32 ++ src/components/chat/contex-menu.tsx | 79 ++++ src/components/chat/file-message.tsx | 173 +++++++++ src/components/chat/image-message.tsx | 50 +++ src/components/chat/message.tsx | 267 ++++++++++++++ src/components/chat/messages-list.tsx | 71 ++++ src/components/layouts/admin/index.tsx | 1 - src/components/layouts/topbar/message-bar.tsx | 135 ------- src/contexts/WebSocket.context.tsx | 341 ++++++++++++++++++ src/pages/_app.tsx | 33 +- src/pages/chat/chat-box.tsx | 46 +++ src/pages/chat/contact-info.tsx | 59 +++ src/pages/chat/converstions.tsx | 126 +++++++ src/pages/chat/index.tsx | 32 ++ src/pages/subscriptions/active-section.tsx | 3 +- src/pages/subscriptions/history-section.tsx | 4 +- src/pages/subscriptions/plans-section.tsx | 12 +- 31 files changed, 1789 insertions(+), 158 deletions(-) create mode 100644 public/image/Frame 1000005529.svg create mode 100644 public/image/State=Send.svg create mode 100644 public/image/Vector.svg create mode 100644 public/image/VectorWhite.svg create mode 100644 public/image/bars.svg create mode 100644 public/image/clock-fast-forward.png create mode 100644 public/image/copy-06.svg create mode 100644 public/image/edit-04.svg create mode 100644 public/image/quill_checkmark-double.svg create mode 100644 public/image/search.svg create mode 100644 public/image/trash-03.svg create mode 100644 src/components/chat/audio-message.tsx create mode 100644 src/components/chat/chat-input.tsx create mode 100644 src/components/chat/contact-info.tsx create mode 100644 src/components/chat/contex-menu.tsx create mode 100644 src/components/chat/file-message.tsx create mode 100644 src/components/chat/image-message.tsx create mode 100644 src/components/chat/message.tsx create mode 100644 src/components/chat/messages-list.tsx create mode 100644 src/contexts/WebSocket.context.tsx create mode 100644 src/pages/chat/chat-box.tsx create mode 100644 src/pages/chat/contact-info.tsx create mode 100644 src/pages/chat/converstions.tsx create mode 100644 src/pages/chat/index.tsx diff --git a/package.json b/package.json index a11777d..87f05eb 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "cookie": "0.6.0", "date-fns": "^4.1.0", "dayjs": "1.11.10", + "emoji-picker-react": "^4.12.0", "framer-motion": "10.16.4", "i18next": "23.6.0", "jotai": "2.5.1", @@ -41,6 +42,7 @@ "rc-table": "7.22.2", "react": "18.2.0", "react-apexcharts": "1.4.1", + "react-audio-visualize": "^1.2.0", "react-content-loader": "6.2.1", "react-countdown": "2.3.5", "react-datepicker": "4.21.0", @@ -50,6 +52,7 @@ "react-i18next": "13.3.1", "react-icons": "^5.4.0", "react-laag": "2.0.5", + "react-media-recorder": "^1.7.1", "react-phone-input-2": "2.15.1", "react-query": "3.39.3", "react-quill": "2.0.0", diff --git a/public/image/Frame 1000005529.svg b/public/image/Frame 1000005529.svg new file mode 100644 index 0000000..4afb24e --- /dev/null +++ b/public/image/Frame 1000005529.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/image/State=Send.svg b/public/image/State=Send.svg new file mode 100644 index 0000000..9754bf8 --- /dev/null +++ b/public/image/State=Send.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/image/Vector.svg b/public/image/Vector.svg new file mode 100644 index 0000000..c21a008 --- /dev/null +++ b/public/image/Vector.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/image/VectorWhite.svg b/public/image/VectorWhite.svg new file mode 100644 index 0000000..04d405d --- /dev/null +++ b/public/image/VectorWhite.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/image/bars.svg b/public/image/bars.svg new file mode 100644 index 0000000..3d9f2eb --- /dev/null +++ b/public/image/bars.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/image/clock-fast-forward.png b/public/image/clock-fast-forward.png new file mode 100644 index 0000000000000000000000000000000000000000..b6e296c86660e997e83c01a60dc06830675571ec GIT binary patch literal 650 zcmV;50(Jd~P)9L#*Lnt zaLK#x{y!1<>oC%ozV8=ux!kQ`81`zly2O#CSnoIs@bpThQuibS30uR=DPZR{9;_gCu!bcCFa zSEb9=2WKHB`&^L24>kuJ0@~7TOCa)wGNP3`nC>8K zV4WJ&;961uZ(E#d?NAuXNK-41FgcO%Xobg@U&J$xJwUiDq;A9Y zzUKhy8DXcv5l%iE!sZ#GZiDY0WPnhcl}0Q{0XHukXktzG_VH6cnIS`0P-KDXx&*Rm k>wZ3;?-q;2sr*}f0mJbLQ8sql@Bjb+07*qoM6N<$g7)zk2LJ#7 literal 0 HcmV?d00001 diff --git a/public/image/copy-06.svg b/public/image/copy-06.svg new file mode 100644 index 0000000..5661b35 --- /dev/null +++ b/public/image/copy-06.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/image/edit-04.svg b/public/image/edit-04.svg new file mode 100644 index 0000000..e8acb5a --- /dev/null +++ b/public/image/edit-04.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/image/quill_checkmark-double.svg b/public/image/quill_checkmark-double.svg new file mode 100644 index 0000000..9b0fac8 --- /dev/null +++ b/public/image/quill_checkmark-double.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/image/search.svg b/public/image/search.svg new file mode 100644 index 0000000..a9f61f6 --- /dev/null +++ b/public/image/search.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/image/trash-03.svg b/public/image/trash-03.svg new file mode 100644 index 0000000..2a879fb --- /dev/null +++ b/public/image/trash-03.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/chat/audio-message.tsx b/src/components/chat/audio-message.tsx new file mode 100644 index 0000000..f8829dd --- /dev/null +++ b/src/components/chat/audio-message.tsx @@ -0,0 +1,99 @@ +import { useRef, useState, useEffect } from "react"; +import { FaPlay, FaPause, FaCheckDouble } from "react-icons/fa"; +import { AudioVisualizer } from "react-audio-visualize"; + +const AudioMessage = ({ audio, timestamp = "2:47 PM" }) => { + const audioRef = useRef(null); + const [isPlaying, setIsPlaying] = useState(false); + const [currentTime, setCurrentTime] = useState(0); + const [audioBlob, setAudioBlob] = useState(null); + const isUser = audio.by_user.account_type === "user" + useEffect(() => { + // If audio.content is a URL string, fetch and convert it to Blob + if (typeof audio.content === 'string') { + fetch(audio.content) + .then(res => res.blob()) + .then(blob => { + setAudioBlob(blob); + }) + .catch(err => console.error("Error fetching audio:", err)); + } else if (audio.content instanceof Blob) { + // If audio.content is already a Blob, just set it + setAudioBlob(audio.content); + } else { + console.error("audio.content must be a Blob or a URL string."); + } + }, [audio.content]); + + const togglePlay = () => { + if (!audioRef.current) return; + if (isPlaying) { + audioRef.current.pause(); + } else { + audioRef.current.play(); + } + }; + + const handlePlay = () => setIsPlaying(true); + const handlePause = () => setIsPlaying(false); + const handleEnded = () => { + setIsPlaying(false); + setCurrentTime(0); + }; + + const handleTimeUpdate = () => { + if (audioRef.current) { + setCurrentTime(audioRef.current.currentTime); + } + }; + + // Create a URL for the audio element if we have a blob + const audioUrl = audioBlob ? URL.createObjectURL(audioBlob) : null; + + return ( +
+ {/* Play/Pause Button */} + + + {/* Audio Visualization */} +
+ {audioBlob && ( + + )} +
+ + {/* Timestamp and Status */} + + + {/* Hidden Audio Element */} + {audioUrl && ( +
+ ); +}; + +export default AudioMessage; diff --git a/src/components/chat/chat-input.tsx b/src/components/chat/chat-input.tsx new file mode 100644 index 0000000..5062d42 --- /dev/null +++ b/src/components/chat/chat-input.tsx @@ -0,0 +1,305 @@ +import { PiTrash } from "react-icons/pi"; +import Image from "next/image"; +import SendIcon from "public/assets/images/send-01.svg"; +import MicIcon from "public/assets/images/microphone-01.svg"; +import LinkIcon from "public/assets/images/link-simple.svg"; +import SmileIcon from "public/assets/images/face-smile.svg"; +import { MdClose } from "react-icons/md"; +import { useEffect, useState, useRef } from "react"; +import { useWebSocket } from "@/contexts/WebSocket.context"; +import FileInput from "../ui/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 +const ReactMediaRecorder = dynamic( + () => import("react-media-recorder").then((mod) => mod.ReactMediaRecorder), + { ssr: false } +); + +// Helper function to format recording time +const formatTime = (seconds) => { + const mins = Math.floor(seconds / 60) + .toString() + .padStart(2, "0"); + const secs = (seconds % 60).toString().padStart(2, "0"); + return `${mins}:${secs}`; +}; + +const Input = ({ product = {} }) => { + const { + sendMessage, + setLoadingMessage, + editingMessage, + setEditingMessage, + editMessage, + } = useWebSocket(); + + // State variables + const [showProduct, setShowProduct] = useState(!!product.id); + const [isRecording, setIsRecording] = useState(false); + const [recordingTime, setRecordingTime] = useState(0); + const [message, setMessage] = useState(""); + const [showEmojiPicker, setShowEmojiPicker] = useState(false); // Emoji picker state + + // Ref for the timer interval + const timerRef = useRef(null); + const messageInputRef = useRef(null); + const emojiPickerRef = useRef(null); + const emojiButtonRef = useRef(null); // Ref for the emoji toggle button + + // Ref for sendRecording flag + const sendRecordingRef = useRef(false); + + // Handle sending text messages + const handleSendMessage = () => { + if (message.trim()) { + if (Object.keys(editingMessage).length) { + const msg = { + content: message, + id: editingMessage.id, + }; + editMessage(msg); + setEditingMessage({}) + setMessage(""); + } else { + sendMessage(message, product?.id); + setMessage(""); + setShowProduct(false); + } + } + }; + + // Start the recording timer + const startTimer = () => { + setRecordingTime(0); + timerRef.current = setInterval(() => { + setRecordingTime((prevTime) => prevTime + 1); + }, 1000); + }; + + // Stop the recording timer + const stopTimer = () => { + if (timerRef.current) { + clearInterval(timerRef.current); + timerRef.current = null; + } + }; + + // Cleanup on component unmount + useEffect(() => { + return () => { + stopTimer(); + }; + }, []); + + // Handle click outside to close the emoji picker + useEffect(() => { + const handleClickOutside = (event) => { + if ( + emojiPickerRef.current && + !emojiPickerRef.current.contains(event.target) && + messageInputRef.current && + !messageInputRef.current.contains(event.target) && + emojiButtonRef.current && + !emojiButtonRef.current.contains(event.target) // Ignore clicks on the emoji button + ) { + setShowEmojiPicker(false); + } + }; + + document.addEventListener("mousedown", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, []); + useEffect(() => { + if (Object.keys(editingMessage).length) { + setMessage(editingMessage.content); + messageInputRef.current?.focus(); // Set focus on the input + } + }, [editingMessage]); + + return ( + { + setIsRecording(true); + sendRecordingRef.current = false; // Reset the flag on start + startTimer(); + }} + onStop={(blobUrl, blob) => { + setIsRecording(false); + stopTimer(); + if (sendRecordingRef.current) { + let selectedFile = blob; + selectedFile.status = "loading"; + selectedFile.name = `audio ${new Date()}.wav`; + setLoadingMessage((prev) => [...prev, selectedFile]); + } else { + console.log("Recording discarded:", blobUrl); + } + sendRecordingRef.current = false; // Reset the flag after handling + }} + render={({ startRecording, stopRecording }) => ( +
+ {/* Product Preview */} + {product.id && showProduct && ( +
+ Link Icon +
+
+ {product?.images && product.images.length > 0 && ( + {product.name + )} +
+

+ {product.name || "Unnamed Product"} +

+

{product.price}$

+
+
+ {setShowProduct(false)}} + size={20} + aria-label="Close product preview" + className="cursor-pointer" + /> +
+
+ )} + + {!!Object.keys(editingMessage).length && ( +
+ + +
+
+
+

+ {editingMessage.mime_type === "text" + ? editingMessage.content + : editingMessage.mime_type} +

+
+
+ {setEditingMessage({}); setMessage("")}} + size={20} + aria-label="Close product preview" + className="cursor-pointer" + /> +
+
+ )} + {/* Message Input Area */} +
+ {/* Emoji Button */} + + + {/* Emoji Picker */} + {showEmojiPicker && ( +
+ { + setMessage( + (prevMessage) => prevMessage + emojiObject.emoji + ); + messageInputRef.current?.focus(); + }} + emojiStyle="native" + skinTonesDisabled + previewConfig={{ + showPreview: false, + }} + /> +
+ )} + + {/* Textarea Input */} +