mirror of
https://github.com/cheveguerra/whaticket-community.git
synced 2026-04-18 03:39:29 +00:00
feat: start replying messages on frontend
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React, { useState, useEffect, useContext } from "react";
|
||||
import "emoji-mart/css/emoji-mart.css";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { Picker } from "emoji-mart";
|
||||
import { toast } from "react-toastify";
|
||||
import MicRecorder from "mic-recorder-to-mp3";
|
||||
import clsx from "clsx";
|
||||
|
||||
import { makeStyles } from "@material-ui/core/styles";
|
||||
import Paper from "@material-ui/core/Paper";
|
||||
@@ -15,6 +16,7 @@ import IconButton from "@material-ui/core/IconButton";
|
||||
import MoodIcon from "@material-ui/icons/Mood";
|
||||
import SendIcon from "@material-ui/icons/Send";
|
||||
import CancelIcon from "@material-ui/icons/Cancel";
|
||||
import ClearIcon from "@material-ui/icons/Clear";
|
||||
import MicIcon from "@material-ui/icons/Mic";
|
||||
import CheckCircleOutlineIcon from "@material-ui/icons/CheckCircleOutline";
|
||||
import HighlightOffIcon from "@material-ui/icons/HighlightOff";
|
||||
@@ -22,20 +24,30 @@ import HighlightOffIcon from "@material-ui/icons/HighlightOff";
|
||||
import { i18n } from "../../translate/i18n";
|
||||
import api from "../../services/api";
|
||||
import RecordingTimer from "./RecordingTimer";
|
||||
import { ReplyMessageContext } from "../../context/ReplyingMessage/ReplyingMessageContext";
|
||||
|
||||
const Mp3Recorder = new MicRecorder({ bitRate: 128 });
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
newMessageBox: {
|
||||
mainWrapper: {
|
||||
background: "#eee",
|
||||
display: "flex",
|
||||
padding: "7px",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
borderTop: "1px solid rgba(0, 0, 0, 0.12)",
|
||||
},
|
||||
|
||||
newMessageBox: {
|
||||
background: "#eee",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
padding: "7px",
|
||||
alignItems: "center",
|
||||
},
|
||||
|
||||
messageInputWrapper: {
|
||||
padding: 6,
|
||||
marginRight: 7,
|
||||
background: "#fff",
|
||||
display: "flex",
|
||||
borderRadius: 20,
|
||||
@@ -79,8 +91,6 @@ const useStyles = makeStyles(theme => ({
|
||||
position: "absolute",
|
||||
top: "20%",
|
||||
left: "50%",
|
||||
// marginTop: 8,
|
||||
// marginBottom: 6,
|
||||
marginLeft: -12,
|
||||
},
|
||||
|
||||
@@ -102,6 +112,62 @@ const useStyles = makeStyles(theme => ({
|
||||
sendAudioIcon: {
|
||||
color: "green",
|
||||
},
|
||||
|
||||
quotedMsgWrapper: {
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
paddingTop: 8,
|
||||
paddingLeft: 73,
|
||||
paddingRight: 7,
|
||||
},
|
||||
|
||||
quotedContainerRight: {
|
||||
flex: 1,
|
||||
marginRight: 5,
|
||||
overflowY: "hidden",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.05)",
|
||||
borderRadius: "7.5px",
|
||||
display: "flex",
|
||||
position: "relative",
|
||||
},
|
||||
|
||||
quotedMsgRight: {
|
||||
padding: 10,
|
||||
maxWidth: 300,
|
||||
height: "auto",
|
||||
whiteSpace: "pre-wrap",
|
||||
},
|
||||
|
||||
quotedSideRight: {
|
||||
flex: "none",
|
||||
width: "4px",
|
||||
backgroundColor: "#35cd96",
|
||||
},
|
||||
|
||||
quotedContainerLeft: {
|
||||
overflow: "hidden",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.05)",
|
||||
borderRadius: "7.5px",
|
||||
display: "flex",
|
||||
position: "relative",
|
||||
},
|
||||
|
||||
quotedMsg: {
|
||||
padding: 10,
|
||||
maxWidth: 300,
|
||||
height: "auto",
|
||||
display: "block",
|
||||
whiteSpace: "pre-wrap",
|
||||
overflow: "hidden",
|
||||
},
|
||||
|
||||
quotedSideLeft: {
|
||||
flex: "none",
|
||||
width: "4px",
|
||||
backgroundColor: "#6bcbef",
|
||||
},
|
||||
}));
|
||||
|
||||
const MessageInput = ({ ticketStatus }) => {
|
||||
@@ -114,6 +180,9 @@ const MessageInput = ({ ticketStatus }) => {
|
||||
const [showEmoji, setShowEmoji] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [recording, setRecording] = useState(false);
|
||||
const { setReplyingMessage, replyingMessage } = useContext(
|
||||
ReplyMessageContext
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
@@ -263,6 +332,40 @@ const MessageInput = ({ ticketStatus }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const renderQuotedMessage = message => {
|
||||
return (
|
||||
<div className={classes.quotedMsgWrapper}>
|
||||
<div
|
||||
className={clsx(classes.quotedContainerLeft, {
|
||||
[classes.quotedContainerRight]: message.fromMe,
|
||||
})}
|
||||
>
|
||||
<span
|
||||
className={clsx(classes.quotedSideLeft, {
|
||||
[classes.quotedSideRight]: message.quotedMsg?.fromMe,
|
||||
})}
|
||||
></span>
|
||||
<div className={classes.quotedMsg}>
|
||||
{!message.fromMe && (
|
||||
<span className={classes.messageContactName}>
|
||||
{message.contact?.name}
|
||||
</span>
|
||||
)}
|
||||
{message.body}
|
||||
</div>
|
||||
</div>
|
||||
<IconButton
|
||||
aria-label="showRecorder"
|
||||
component="span"
|
||||
disabled={loading || ticketStatus !== "open"}
|
||||
onClick={() => setReplyingMessage({})}
|
||||
>
|
||||
<ClearIcon className={classes.sendMessageIcons} />
|
||||
</IconButton>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
if (medias.length > 0)
|
||||
return (
|
||||
<Paper elevation={0} square className={classes.viewMediaInputWrapper}>
|
||||
@@ -296,115 +399,118 @@ const MessageInput = ({ ticketStatus }) => {
|
||||
);
|
||||
else {
|
||||
return (
|
||||
<Paper square elevation={0} className={classes.newMessageBox}>
|
||||
<IconButton
|
||||
aria-label="emojiPicker"
|
||||
component="span"
|
||||
disabled={loading || recording || ticketStatus !== "open"}
|
||||
onClick={e => setShowEmoji(prevState => !prevState)}
|
||||
>
|
||||
<MoodIcon className={classes.sendMessageIcons} />
|
||||
</IconButton>
|
||||
{showEmoji ? (
|
||||
<div className={classes.emojiBox}>
|
||||
<Picker
|
||||
perLine={16}
|
||||
showPreview={false}
|
||||
showSkinTones={false}
|
||||
onSelect={handleAddEmoji}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<input
|
||||
multiple
|
||||
type="file"
|
||||
id="upload-button"
|
||||
disabled={loading || recording || ticketStatus !== "open"}
|
||||
className={classes.uploadInput}
|
||||
onChange={handleChangeMedias}
|
||||
/>
|
||||
<label htmlFor="upload-button">
|
||||
<Paper square elevation={0} className={classes.mainWrapper}>
|
||||
{replyingMessage.id && renderQuotedMessage(replyingMessage)}
|
||||
<div className={classes.newMessageBox}>
|
||||
<IconButton
|
||||
aria-label="upload"
|
||||
aria-label="emojiPicker"
|
||||
component="span"
|
||||
disabled={loading || recording || ticketStatus !== "open"}
|
||||
onClick={e => setShowEmoji(prevState => !prevState)}
|
||||
>
|
||||
<AttachFileIcon className={classes.sendMessageIcons} />
|
||||
<MoodIcon className={classes.sendMessageIcons} />
|
||||
</IconButton>
|
||||
</label>
|
||||
<div className={classes.messageInputWrapper}>
|
||||
<InputBase
|
||||
inputRef={input => input && input.focus()}
|
||||
className={classes.messageInput}
|
||||
placeholder={
|
||||
ticketStatus === "open"
|
||||
? i18n.t("messagesInput.placeholderOpen")
|
||||
: i18n.t("messagesInput.placeholderClosed")
|
||||
}
|
||||
multiline
|
||||
rowsMax={5}
|
||||
value={inputMessage}
|
||||
onChange={handleChangeInput}
|
||||
disabled={recording || loading || ticketStatus !== "open"}
|
||||
onPaste={e => {
|
||||
ticketStatus === "open" && handleInputPaste(e);
|
||||
}}
|
||||
onKeyPress={e => {
|
||||
if (loading || e.shiftKey) return;
|
||||
else if (e.key === "Enter") {
|
||||
handleSendMessage();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{inputMessage ? (
|
||||
<IconButton
|
||||
aria-label="sendMessage"
|
||||
component="span"
|
||||
onClick={handleSendMessage}
|
||||
disabled={loading}
|
||||
>
|
||||
<SendIcon className={classes.sendMessageIcons} />
|
||||
</IconButton>
|
||||
) : recording ? (
|
||||
<div className={classes.recorderWrapper}>
|
||||
<IconButton
|
||||
aria-label="cancelRecording"
|
||||
component="span"
|
||||
fontSize="large"
|
||||
disabled={loading}
|
||||
onClick={handleCancelAudio}
|
||||
>
|
||||
<HighlightOffIcon className={classes.cancelAudioIcon} />
|
||||
</IconButton>
|
||||
{loading ? (
|
||||
<div>
|
||||
<CircularProgress className={classes.audioLoading} />
|
||||
</div>
|
||||
) : (
|
||||
<RecordingTimer />
|
||||
)}
|
||||
{showEmoji ? (
|
||||
<div className={classes.emojiBox}>
|
||||
<Picker
|
||||
perLine={16}
|
||||
showPreview={false}
|
||||
showSkinTones={false}
|
||||
onSelect={handleAddEmoji}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<input
|
||||
multiple
|
||||
type="file"
|
||||
id="upload-button"
|
||||
disabled={loading || recording || ticketStatus !== "open"}
|
||||
className={classes.uploadInput}
|
||||
onChange={handleChangeMedias}
|
||||
/>
|
||||
<label htmlFor="upload-button">
|
||||
<IconButton
|
||||
aria-label="sendRecordedAudio"
|
||||
aria-label="upload"
|
||||
component="span"
|
||||
onClick={handleUploadAudio}
|
||||
disabled={loading || recording || ticketStatus !== "open"}
|
||||
>
|
||||
<AttachFileIcon className={classes.sendMessageIcons} />
|
||||
</IconButton>
|
||||
</label>
|
||||
<div className={classes.messageInputWrapper}>
|
||||
<InputBase
|
||||
inputRef={input => input && input.focus()}
|
||||
className={classes.messageInput}
|
||||
placeholder={
|
||||
ticketStatus === "open"
|
||||
? i18n.t("messagesInput.placeholderOpen")
|
||||
: i18n.t("messagesInput.placeholderClosed")
|
||||
}
|
||||
multiline
|
||||
rowsMax={5}
|
||||
value={inputMessage}
|
||||
onChange={handleChangeInput}
|
||||
disabled={recording || loading || ticketStatus !== "open"}
|
||||
onPaste={e => {
|
||||
ticketStatus === "open" && handleInputPaste(e);
|
||||
}}
|
||||
onKeyPress={e => {
|
||||
if (loading || e.shiftKey) return;
|
||||
else if (e.key === "Enter") {
|
||||
handleSendMessage();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{inputMessage ? (
|
||||
<IconButton
|
||||
aria-label="sendMessage"
|
||||
component="span"
|
||||
onClick={handleSendMessage}
|
||||
disabled={loading}
|
||||
>
|
||||
<CheckCircleOutlineIcon className={classes.sendAudioIcon} />
|
||||
<SendIcon className={classes.sendMessageIcons} />
|
||||
</IconButton>
|
||||
</div>
|
||||
) : (
|
||||
<IconButton
|
||||
aria-label="showRecorder"
|
||||
component="span"
|
||||
disabled={loading || ticketStatus !== "open"}
|
||||
onClick={handleStartRecording}
|
||||
>
|
||||
<MicIcon className={classes.sendMessageIcons} />
|
||||
</IconButton>
|
||||
)}
|
||||
) : recording ? (
|
||||
<div className={classes.recorderWrapper}>
|
||||
<IconButton
|
||||
aria-label="cancelRecording"
|
||||
component="span"
|
||||
fontSize="large"
|
||||
disabled={loading}
|
||||
onClick={handleCancelAudio}
|
||||
>
|
||||
<HighlightOffIcon className={classes.cancelAudioIcon} />
|
||||
</IconButton>
|
||||
{loading ? (
|
||||
<div>
|
||||
<CircularProgress className={classes.audioLoading} />
|
||||
</div>
|
||||
) : (
|
||||
<RecordingTimer />
|
||||
)}
|
||||
|
||||
<IconButton
|
||||
aria-label="sendRecordedAudio"
|
||||
component="span"
|
||||
onClick={handleUploadAudio}
|
||||
disabled={loading}
|
||||
>
|
||||
<CheckCircleOutlineIcon className={classes.sendAudioIcon} />
|
||||
</IconButton>
|
||||
</div>
|
||||
) : (
|
||||
<IconButton
|
||||
aria-label="showRecorder"
|
||||
component="span"
|
||||
disabled={loading || ticketStatus !== "open"}
|
||||
onClick={handleStartRecording}
|
||||
>
|
||||
<MicIcon className={classes.sendMessageIcons} />
|
||||
</IconButton>
|
||||
)}
|
||||
</div>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from "react";
|
||||
import React, { useState, useContext } from "react";
|
||||
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
@@ -8,13 +8,15 @@ import { i18n } from "../../translate/i18n";
|
||||
import api from "../../services/api";
|
||||
import ConfirmationModal from "../ConfirmationModal";
|
||||
import { Menu } from "@material-ui/core";
|
||||
import { ReplyMessageContext } from "../../context/ReplyingMessage/ReplyingMessageContext";
|
||||
|
||||
const MessageOptionsMenu = ({ messageId, menuOpen, handleClose, anchorEl }) => {
|
||||
const MessageOptionsMenu = ({ message, menuOpen, handleClose, anchorEl }) => {
|
||||
const { setReplyingMessage } = useContext(ReplyMessageContext);
|
||||
const [confirmationOpen, setConfirmationOpen] = useState(false);
|
||||
|
||||
const handleDeleteMessage = async () => {
|
||||
try {
|
||||
await api.delete(`/messages/${messageId}`);
|
||||
await api.delete(`/messages/${message.id}`);
|
||||
} catch (err) {
|
||||
const errorMsg = err.response?.data?.error;
|
||||
if (errorMsg) {
|
||||
@@ -29,6 +31,11 @@ const MessageOptionsMenu = ({ messageId, menuOpen, handleClose, anchorEl }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const hanldeReplyMessage = () => {
|
||||
setReplyingMessage(message);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleOpenConfirmationModal = e => {
|
||||
setConfirmationOpen(true);
|
||||
handleClose();
|
||||
@@ -61,7 +68,9 @@ const MessageOptionsMenu = ({ messageId, menuOpen, handleClose, anchorEl }) => {
|
||||
<MenuItem onClick={handleOpenConfirmationModal}>
|
||||
{i18n.t("messageOptionsMenu.delete")}
|
||||
</MenuItem>
|
||||
<MenuItem disabled> {i18n.t("messageOptionsMenu.reply")}</MenuItem>
|
||||
<MenuItem onClick={hanldeReplyMessage}>
|
||||
{i18n.t("messageOptionsMenu.reply")}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -295,7 +295,7 @@ const reducer = (state, action) => {
|
||||
}
|
||||
};
|
||||
|
||||
const MessagesList = ({ ticketId, isGroup }) => {
|
||||
const MessagesList = ({ ticketId, isGroup, setReplyingMessage }) => {
|
||||
const classes = useStyles();
|
||||
|
||||
const [messagesList, dispatch] = useReducer(reducer, []);
|
||||
@@ -304,7 +304,7 @@ const MessagesList = ({ ticketId, isGroup }) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const lastMessageRef = useRef();
|
||||
|
||||
const [selectedMessageId, setSelectedMessageId] = useState(null);
|
||||
const [selectedMessage, setSelectedMessage] = useState({});
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const messageOptionsMenuOpen = Boolean(anchorEl);
|
||||
const currentTicketId = useRef(ticketId);
|
||||
@@ -402,9 +402,9 @@ const MessagesList = ({ ticketId, isGroup }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenMessageOptionsMenu = (e, messageId) => {
|
||||
const handleOpenMessageOptionsMenu = (e, message) => {
|
||||
setAnchorEl(e.currentTarget);
|
||||
setSelectedMessageId(messageId);
|
||||
setSelectedMessage(message);
|
||||
};
|
||||
|
||||
const handleCloseMessageOptionsMenu = e => {
|
||||
@@ -581,7 +581,7 @@ const MessagesList = ({ ticketId, isGroup }) => {
|
||||
id="messageActionsButton"
|
||||
disabled={message.isDeleted}
|
||||
className={classes.messageActionsButton}
|
||||
onClick={e => handleOpenMessageOptionsMenu(e, message.id)}
|
||||
onClick={e => handleOpenMessageOptionsMenu(e, message)}
|
||||
>
|
||||
<ExpandMore />
|
||||
</IconButton>
|
||||
@@ -619,7 +619,7 @@ const MessagesList = ({ ticketId, isGroup }) => {
|
||||
return (
|
||||
<div className={classes.messagesListWrapper}>
|
||||
<MessageOptionsMenu
|
||||
messageId={selectedMessageId}
|
||||
message={selectedMessage}
|
||||
anchorEl={anchorEl}
|
||||
menuOpen={messageOptionsMenuOpen}
|
||||
handleClose={handleCloseMessageOptionsMenu}
|
||||
|
||||
@@ -15,6 +15,7 @@ import TicketActionButtons from "../TicketActionButtons";
|
||||
import MessagesList from "../MessagesList";
|
||||
import api from "../../services/api";
|
||||
import { i18n } from "../../translate/i18n";
|
||||
import { ReplyMessageProvider } from "../../context/ReplyingMessage/ReplyingMessageContext";
|
||||
|
||||
const drawerWidth = 320;
|
||||
|
||||
@@ -151,11 +152,13 @@ const Ticket = () => {
|
||||
/>
|
||||
<TicketActionButtons ticket={ticket} />
|
||||
</TicketHeader>
|
||||
<MessagesList
|
||||
ticketId={ticketId}
|
||||
isGroup={ticket.isGroup}
|
||||
></MessagesList>
|
||||
<MessageInput ticketStatus={ticket.status} />
|
||||
<ReplyMessageProvider>
|
||||
<MessagesList
|
||||
ticketId={ticketId}
|
||||
isGroup={ticket.isGroup}
|
||||
></MessagesList>
|
||||
<MessageInput ticketStatus={ticket.status} />
|
||||
</ReplyMessageProvider>
|
||||
</Paper>
|
||||
<ContactDrawer
|
||||
open={drawerOpen}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import React, { useState, createContext } from "react";
|
||||
|
||||
const ReplyMessageContext = createContext();
|
||||
|
||||
const ReplyMessageProvider = ({ children }) => {
|
||||
const [replyingMessage, setReplyingMessage] = useState({});
|
||||
|
||||
return (
|
||||
<ReplyMessageContext.Provider
|
||||
value={{ replyingMessage, setReplyingMessage }}
|
||||
>
|
||||
{children}
|
||||
</ReplyMessageContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export { ReplyMessageContext, ReplyMessageProvider };
|
||||
Reference in New Issue
Block a user