feat: start replying messages on frontend

This commit is contained in:
canove
2020-10-28 20:26:39 -03:00
parent de86208a36
commit b47c1a83b4
5 changed files with 253 additions and 118 deletions

View File

@@ -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>
);
}

View File

@@ -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>
</>
);

View File

@@ -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}

View File

@@ -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}

View File

@@ -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 };