projeto-hit/frontend/src/components/MessageInput/index.js

815 lines
22 KiB
JavaScript
Raw Normal View History

import React, { useState, useEffect, useContext, useRef } from "react"
import "emoji-mart/css/emoji-mart.css"
import { useParams } from "react-router-dom"
import { Picker } from "emoji-mart"
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"
import InputBase from "@material-ui/core/InputBase"
import CircularProgress from "@material-ui/core/CircularProgress"
import { green } from "@material-ui/core/colors"
import AttachFileIcon from "@material-ui/icons/AttachFile"
import IconButton from "@material-ui/core/IconButton"
import MoreVert from "@material-ui/icons/MoreVert"
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"
import {
FormControlLabel,
Hidden,
Menu,
MenuItem,
Switch,
} from "@material-ui/core"
import ClickAwayListener from "@material-ui/core/ClickAwayListener"
import { i18n } from "../../translate/i18n"
import api from "../../services/api"
import RecordingTimer from "./RecordingTimer"
import { ReplyMessageContext } from "../../context/ReplyingMessage/ReplyingMessageContext"
import { AuthContext } from "../../context/Auth/AuthContext"
import { useLocalStorage } from "../../hooks/useLocalStorage"
import toastError from "../../errors/toastError"
// import TicketsManager from "../../components/TicketsManager/";
import { TabTicketContext } from "../../context/TabTicketHeaderOption/TabTicketHeaderOption"
import ModalTemplate from "../ModalTemplate"
import { render } from '@testing-library/react'
import { countTicketMsgContext } from "../../context/CountTicketMsgProvider/CountTicketMsgProvider"
const Mp3Recorder = new MicRecorder({ bitRate: 128 })
const useStyles = makeStyles((theme) => ({
mainWrapper: {
background: "#eee",
display: "flex",
flexDirection: "column",
alignItems: "center",
borderTop: "1px solid rgba(0, 0, 0, 0.12)",
[theme.breakpoints.down("sm")]: {
position: "fixed",
bottom: 0,
width: "100%",
},
},
newMessageBox: {
background: "#eee",
width: "100%",
display: "flex",
padding: "7px",
alignItems: "center",
},
messageInputWrapper: {
padding: 6,
marginRight: 7,
background: "#fff",
display: "flex",
borderRadius: 20,
flex: 1,
position: "relative",
},
messageInput: {
paddingLeft: 10,
flex: 1,
border: "none",
},
sendMessageIcons: {
color: "grey",
},
uploadInput: {
display: "none",
},
viewMediaInputWrapper: {
display: "flex",
padding: "10px 13px",
position: "relative",
justifyContent: "space-between",
alignItems: "center",
backgroundColor: "#eee",
borderTop: "1px solid rgba(0, 0, 0, 0.12)",
},
emojiBox: {
position: "absolute",
bottom: 63,
width: 40,
borderTop: "1px solid #e8e8e8",
},
circleLoading: {
color: green[500],
opacity: "70%",
position: "absolute",
top: "20%",
left: "50%",
marginLeft: -12,
},
audioLoading: {
color: green[500],
opacity: "70%",
},
recorderWrapper: {
display: "flex",
alignItems: "center",
alignContent: "middle",
},
cancelAudioIcon: {
color: "red",
},
sendAudioIcon: {
color: "green",
},
replyginMsgWrapper: {
display: "flex",
width: "100%",
alignItems: "center",
justifyContent: "center",
paddingTop: 8,
paddingLeft: 73,
paddingRight: 7,
},
replyginMsgContainer: {
flex: 1,
marginRight: 5,
overflowY: "hidden",
backgroundColor: "rgba(0, 0, 0, 0.05)",
borderRadius: "7.5px",
display: "flex",
position: "relative",
},
replyginMsgBody: {
padding: 10,
height: "auto",
display: "block",
whiteSpace: "pre-wrap",
overflow: "hidden",
},
replyginContactMsgSideColor: {
flex: "none",
width: "4px",
backgroundColor: "#35cd96",
},
replyginSelfMsgSideColor: {
flex: "none",
width: "4px",
backgroundColor: "#6bcbef",
},
messageContactName: {
display: "flex",
color: "#6bcbef",
fontWeight: 500,
},
messageQuickAnswersWrapper: {
margin: 0,
position: "absolute",
bottom: "50px",
background: "#ffffff",
padding: "2px",
border: "1px solid #CCC",
left: 0,
width: "100%",
"& li": {
listStyle: "none",
"& a": {
display: "block",
padding: "8px",
textOverflow: "ellipsis",
overflow: "hidden",
maxHeight: "32px",
"&:hover": {
background: "#F1F1F1",
cursor: "pointer",
},
},
},
},
}))
const MessageInput = ({ ticketStatus, ticketLastMessage, ticketIsRemote }) => {
const { tabOption, setTabOption } = useContext(TabTicketContext)
const { countTicketMsg, setCountTicketMsg } = useContext(countTicketMsgContext)
const classes = useStyles()
const { ticketId } = useParams()
const [medias, setMedias] = useState([])
const [inputMessage, setInputMessage] = useState("")
const [showEmoji, setShowEmoji] = useState(false)
const [loading, setLoading] = useState(false)
const [recording, setRecording] = useState(false)
const [quickAnswers, setQuickAnswer] = useState([])
const [typeBar, setTypeBar] = useState(false)
const inputRef = useRef()
const [anchorEl, setAnchorEl] = useState(null)
const { setReplyingMessage, replyingMessage } = useContext(ReplyMessageContext)
const { user } = useContext(AuthContext)
const [templates, setTemplates] = useState(null)
const [params, setParams] = useState(null)
const [signMessage, setSignMessage] = useLocalStorage("signOption", true)
const isRun = useRef(false)
useEffect(() => {
inputRef.current.focus()
}, [replyingMessage])
useEffect(() => {
if (ticketIsRemote && countTicketMsg === 0 && ticketLastMessage && ticketLastMessage.trim().length > 0) {
setInputMessage(ticketLastMessage)
}
else {
setInputMessage("")
}
}, [countTicketMsg, ticketIsRemote, ticketLastMessage])
useEffect(() => {
inputRef.current.focus()
return () => {
setInputMessage("")
setShowEmoji(false)
setMedias([])
setReplyingMessage(null)
}
}, [ticketId, setReplyingMessage])
const handleChangeInput = (e) => {
setInputMessage(e.target.value)
handleLoadQuickAnswer(e.target.value)
}
const handleQuickAnswersClick = (value) => {
setInputMessage(value)
setTypeBar(false)
}
const handleAddEmoji = (e) => {
let emoji = e.native
setInputMessage((prevState) => prevState + emoji)
}
const handleChangeMedias = (e) => {
if (!e.target.files) {
return
}
const selectedMedias = Array.from(e.target.files)
setMedias(selectedMedias)
}
const handleInputPaste = (e) => {
if (e.clipboardData.files[0]) {
console.log('clipboardData: ', e.clipboardData.files[0])
setMedias([e.clipboardData.files[0]])
}
}
const handleUploadMedia = async (e) => {
setLoading(true)
e.preventDefault()
if (tabOption === 'search') {
setTabOption('open')
}
const formData = new FormData()
formData.append("fromMe", true)
medias.forEach((media) => {
formData.append("medias", media)
formData.append("body", media.name)
})
try {
const { data } = await api.post(`/messages/${ticketId}`, formData)
console.log('DATA FROM SEND MESSAGE MEDIA: ', data)
} catch (err) {
toastError(err)
}
setLoading(false)
setMedias([])
}
const handleSendMessage = async (templateParams = null) => {
if (inputMessage.trim() === "") return
setLoading(true)
if (tabOption === 'search') {
setTabOption('open')
}
if (templateParams) {
for (let key in templateParams) {
if (templateParams.hasOwnProperty(key)) {
if (key === '_reactName') {
templateParams = null
break
}
}
}
}
let message = {
read: 1,
fromMe: true,
mediaUrl: "",
body: (signMessage && !templateParams) ? `*${user?.name}:*\n${inputMessage.trim()}` : inputMessage.trim(),
quotedMsg: replyingMessage
}
if (templateParams) {
message = { ...message, params: templateParams }
}
try {
const { data } = await api.post(`/messages/${ticketId}`, message)
setParams(null)
if (data && data?.data && Array.isArray(data.data)) {
setTemplates(data.data)
}
setCountTicketMsg(1)
} catch (err) {
toastError(err)
}
setInputMessage("")
setShowEmoji(false)
setLoading(false)
setReplyingMessage(null)
}
useEffect(() => {
if (!params) return
const body_params = params?.find(p => p?.type === 'BODY')
console.log('------------> body_params: ', body_params)
if (!body_params) return
let { text } = body_params
console.log('PARAMS FROM MESSAGE INPUT: ', params, ' | text: ', text)
let body = text.match(/{{\d+}}/g)
if (body && body.length > 0) {
const { parameters } = body_params
for (const key in parameters) {
if (!isNaN(key)) {
const { index, text: body_text } = parameters[key]
text = text.replace(`{{${index}}}`, body_text)
}
}
}
console.log('NEW TEXT: ', text)
setInputMessage(text)
}, [params])
useEffect(() => {
if (params) {
handleSendMessage(params)
}
}, [inputMessage, params])
useEffect(() => {
if (!templates) return
return render(<ModalTemplate
modal_header={'Escolha um template para iniciar o Atendimento'}
func={setParams}
templates={templates.map(({ id, name, components, language, }) => {
return { id, name, components, language, }
})}
ticketId={ticketId}
/>)
}, [templates])
const handleStartRecording = async () => {
setLoading(true)
try {
await navigator.mediaDevices.getUserMedia({ audio: true })
await Mp3Recorder.start()
setRecording(true)
setLoading(false)
} catch (err) {
toastError(err)
setLoading(false)
}
}
const handleLoadQuickAnswer = async (value) => {
if (value && value.indexOf("/") === 0) {
try {
console.log('{ searchParam: inputMessage.substring(1) },: ', { searchParam: inputMessage.substring(1) },)
console.log('USER ID: ', user.id)
const { data } = await api.get("/quickAnswers/", {
params: { searchParam: inputMessage.substring(1), userId: user.id },
})
setQuickAnswer(data.quickAnswers)
if (data.quickAnswers.length > 0) {
setTypeBar(true)
} else {
setTypeBar(false)
}
} catch (err) {
setTypeBar(false)
}
} else {
setTypeBar(false)
}
}
const handleUploadAudio = async () => {
setLoading(true)
if (tabOption === 'search') {
setTabOption('open')
}
try {
const [, blob] = await Mp3Recorder.stop().getMp3()
if (blob.size < 10000) {
setLoading(false)
setRecording(false)
return
}
const formData = new FormData()
const filename = `${new Date().getTime()}.mp3`
formData.append("medias", blob, filename)
formData.append("body", filename)
formData.append("fromMe", true)
2023-09-08 19:50:51 +00:00
formData.append("mic_audio", true)
await api.post(`/messages/${ticketId}`, formData)
} catch (err) {
toastError(err)
}
setRecording(false)
setLoading(false)
}
const handleCancelAudio = async () => {
try {
await Mp3Recorder.stop().getMp3()
setRecording(false)
} catch (err) {
toastError(err)
}
}
const handleOpenMenuClick = (event) => {
setAnchorEl(event.currentTarget)
}
const handleMenuItemClick = (event) => {
setAnchorEl(null)
}
const renderReplyingMessage = (message) => {
return (
<div className={classes.replyginMsgWrapper}>
<div className={classes.replyginMsgContainer}>
<span
className={clsx(classes.replyginContactMsgSideColor, {
[classes.replyginSelfMsgSideColor]: !message.fromMe,
})}
></span>
<div className={classes.replyginMsgBody}>
{!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(null)}
>
<ClearIcon className={classes.sendMessageIcons} />
</IconButton>
</div>
)
}
if (medias.length > 0)
return (
<Paper elevation={0} square className={classes.viewMediaInputWrapper}>
<IconButton
aria-label="cancel-upload"
component="span"
onClick={(e) => setMedias([])}
>
<CancelIcon className={classes.sendMessageIcons} />
</IconButton>
{loading ? (
<div>
<CircularProgress className={classes.circleLoading} />
</div>
) : (
<span>
{medias[0]?.name}
{/* <img src={media.preview} alt=""></img> */}
</span>
)}
<IconButton
aria-label="send-upload"
component="span"
onClick={handleUploadMedia}
disabled={loading}
>
<SendIcon className={classes.sendMessageIcons} />
</IconButton>
</Paper>
)
else {
return (
<Paper square elevation={0} className={classes.mainWrapper}>
{replyingMessage && renderReplyingMessage(replyingMessage)}
<div className={classes.newMessageBox}>
<Hidden only={["sm", "xs"]}>
<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}>
<ClickAwayListener onClickAway={(e) => setShowEmoji(false)}>
<Picker
perLine={16}
showPreview={false}
showSkinTones={false}
onSelect={handleAddEmoji}
/>
</ClickAwayListener>
</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="upload"
component="span"
disabled={loading || recording || ticketStatus !== "open"}
>
<AttachFileIcon className={classes.sendMessageIcons} />
</IconButton>
</label>
<FormControlLabel
style={{ marginRight: 7, color: "gray" }}
label={i18n.t("messagesInput.signMessage")}
labelPlacement="start"
control={
<Switch
size="small"
checked={signMessage}
onChange={(e) => {
setSignMessage(e.target.checked)
}}
name="showAllTickets"
color="primary"
/>
}
/>
</Hidden>
<Hidden only={["md", "lg", "xl"]}>
<IconButton
aria-controls="simple-menu"
aria-haspopup="true"
onClick={handleOpenMenuClick}
>
<MoreVert></MoreVert>
</IconButton>
<Menu
id="simple-menu"
keepMounted
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleMenuItemClick}
>
<MenuItem onClick={handleMenuItemClick}>
<IconButton
aria-label="emojiPicker"
component="span"
disabled={loading || recording || ticketStatus !== "open"}
onClick={(e) => setShowEmoji((prevState) => !prevState)}
>
<MoodIcon className={classes.sendMessageIcons} />
</IconButton>
</MenuItem>
<MenuItem onClick={handleMenuItemClick}>
<input
multiple
type="file"
id="upload-button"
disabled={loading || recording || ticketStatus !== "open"}
className={classes.uploadInput}
onChange={handleChangeMedias}
/>
<label htmlFor="upload-button">
<IconButton
aria-label="upload"
component="span"
disabled={loading || recording || ticketStatus !== "open"}
>
<AttachFileIcon className={classes.sendMessageIcons} />
</IconButton>
</label>
</MenuItem>
<MenuItem onClick={handleMenuItemClick}>
<FormControlLabel
style={{ marginRight: 7, color: "gray" }}
label={i18n.t("messagesInput.signMessage")}
labelPlacement="start"
control={
<Switch
size="small"
checked={signMessage}
onChange={(e) => {
setSignMessage(e.target.checked)
}}
name="showAllTickets"
color="primary"
/>
}
/>
</MenuItem>
</Menu>
</Hidden>
<div className={classes.messageInputWrapper}>
<InputBase
inputRef={(input) => {
input && input.focus()
input && (inputRef.current = input)
}}
className={classes.messageInput}
placeholder={
ticketStatus === "open"
? i18n.t("messagesInput.placeholderOpen")
: i18n.t("messagesInput.placeholderClosed")
}
multiline
// rowsMax={5}
maxRows={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()
}
}}
/>
{typeBar ? (
<ul className={classes.messageQuickAnswersWrapper}>
{quickAnswers.map((value, index) => {
return (
<li
className={classes.messageQuickAnswersWrapperItem}
key={index}
>
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
<a onClick={() => handleQuickAnswersClick(value.message)}>
{`${value.shortcut} - ${value.message}`}
</a>
</li>
)
})}
</ul>
) : (
<div></div>
)}
</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 />
)}
<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>
)
}
}
export default MessageInput