diff --git a/backend/src/controllers/QuickAnswerController.ts b/backend/src/controllers/QuickAnswerController.ts new file mode 100644 index 0000000..3828e91 --- /dev/null +++ b/backend/src/controllers/QuickAnswerController.ts @@ -0,0 +1,117 @@ +import * as Yup from "yup"; +import { Request, Response } from "express"; +import { getIO } from "../libs/socket"; + +import ListQuickAnswerService from "../services/QuickAnswerService/ListQuickAnswerService"; +import CreateQuickAnswerService from "../services/QuickAnswerService/CreateQuickAnswerService"; +import ShowQuickAnswerService from "../services/QuickAnswerService/ShowQuickAnswerService"; +import UpdateQuickAnswerService from "../services/QuickAnswerService/UpdateQuickAnswerService"; +import DeleteQuickAnswerService from "../services/QuickAnswerService/DeleteQuickAnswerService"; + +import AppError from "../errors/AppError"; + +type IndexQuery = { + searchParam: string; + pageNumber: string; +}; + +interface QuickAnswerData { + shortcut: string; + message: string; +} + +export const index = async (req: Request, res: Response): Promise => { + const { searchParam, pageNumber } = req.query as IndexQuery; + + const { quickAnswers, count, hasMore } = await ListQuickAnswerService({ + searchParam, + pageNumber + }); + + return res.json({ quickAnswers, count, hasMore }); +}; + +export const store = async (req: Request, res: Response): Promise => { + const newQuickAnswer: QuickAnswerData = req.body; + + const QuickAnswerSchema = Yup.object().shape({ + shortcut: Yup.string().required(), + message: Yup.string().required() + }); + + try { + await QuickAnswerSchema.validate(newQuickAnswer); + } catch (err) { + throw new AppError(err.message); + } + + const quickAnswer = await CreateQuickAnswerService({ + ...newQuickAnswer + }); + + const io = getIO(); + io.emit("quickAnswer", { + action: "create", + quickAnswer + }); + + return res.status(200).json(quickAnswer); +}; + +export const show = async (req: Request, res: Response): Promise => { + const { quickAnswerId } = req.params; + + const quickAnswer = await ShowQuickAnswerService(quickAnswerId); + + return res.status(200).json(quickAnswer); +}; + +export const update = async ( + req: Request, + res: Response +): Promise => { + const quickAnswerData: QuickAnswerData = req.body; + + const schema = Yup.object().shape({ + shortcut: Yup.string(), + message: Yup.string() + }); + + try { + await schema.validate(quickAnswerData); + } catch (err) { + throw new AppError(err.message); + } + + const { quickAnswerId } = req.params; + + const quickAnswer = await UpdateQuickAnswerService({ + quickAnswerData, + quickAnswerId + }); + + const io = getIO(); + io.emit("quickAnswer", { + action: "update", + quickAnswer + }); + + return res.status(200).json(quickAnswer); +}; + +export const remove = async ( + req: Request, + res: Response +): Promise => { + const { quickAnswerId } = req.params; + + await DeleteQuickAnswerService(quickAnswerId); + + const io = getIO(); + io.emit("quickAnswer", { + action: "delete", + quickAnswerId + }); + + return res.status(200).json({ message: "Quick Answer deleted" }); +}; diff --git a/backend/src/database/index.ts b/backend/src/database/index.ts index b62e840..4c230ac 100644 --- a/backend/src/database/index.ts +++ b/backend/src/database/index.ts @@ -9,6 +9,7 @@ import Message from "../models/Message"; import Queue from "../models/Queue"; import WhatsappQueue from "../models/WhatsappQueue"; import UserQueue from "../models/UserQueue"; +import QuickAnswer from "../models/QuickAnswer"; // eslint-disable-next-line const dbConfig = require("../config/database"); @@ -26,7 +27,8 @@ const models = [ Setting, Queue, WhatsappQueue, - UserQueue + UserQueue, + QuickAnswer ]; sequelize.addModels(models); diff --git a/backend/src/database/migrations/20210818102605-create-quickAnswers.ts b/backend/src/database/migrations/20210818102605-create-quickAnswers.ts new file mode 100644 index 0000000..e7e81ee --- /dev/null +++ b/backend/src/database/migrations/20210818102605-create-quickAnswers.ts @@ -0,0 +1,34 @@ +import { QueryInterface, DataTypes } from "sequelize"; + +module.exports = { + up: (queryInterface: QueryInterface) => { + return queryInterface.createTable("QuickAnswers", { + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true, + allowNull: false + }, + shortcut: { + type: DataTypes.TEXT, + allowNull: false + }, + message: { + type: DataTypes.TEXT, + allowNull: false + }, + createdAt: { + type: DataTypes.DATE, + allowNull: false + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: false + } + }); + }, + + down: (queryInterface: QueryInterface) => { + return queryInterface.dropTable("QuickAnswers"); + } +}; diff --git a/backend/src/models/QuickAnswer.ts b/backend/src/models/QuickAnswer.ts new file mode 100644 index 0000000..3549734 --- /dev/null +++ b/backend/src/models/QuickAnswer.ts @@ -0,0 +1,32 @@ +import { + Table, + Column, + DataType, + CreatedAt, + UpdatedAt, + Model, + PrimaryKey, + AutoIncrement +} from "sequelize-typescript"; + +@Table +class QuickAnswer extends Model { + @PrimaryKey + @AutoIncrement + @Column + id: number; + + @Column(DataType.TEXT) + shortcut: string; + + @Column(DataType.TEXT) + message: string; + + @CreatedAt + createdAt: Date; + + @UpdatedAt + updatedAt: Date; +} + +export default QuickAnswer; diff --git a/backend/src/routes/index.ts b/backend/src/routes/index.ts index 7043f61..4109c7b 100644 --- a/backend/src/routes/index.ts +++ b/backend/src/routes/index.ts @@ -9,6 +9,7 @@ import whatsappRoutes from "./whatsappRoutes"; import messageRoutes from "./messageRoutes"; import whatsappSessionRoutes from "./whatsappSessionRoutes"; import queueRoutes from "./queueRoutes"; +import quickAnswerRoutes from "./quickAnswerRoutes"; const routes = Router(); @@ -22,5 +23,6 @@ routes.use(messageRoutes); routes.use(messageRoutes); routes.use(whatsappSessionRoutes); routes.use(queueRoutes); +routes.use(quickAnswerRoutes); export default routes; diff --git a/backend/src/routes/quickAnswerRoutes.ts b/backend/src/routes/quickAnswerRoutes.ts new file mode 100644 index 0000000..eab4557 --- /dev/null +++ b/backend/src/routes/quickAnswerRoutes.ts @@ -0,0 +1,30 @@ +import express from "express"; +import isAuth from "../middleware/isAuth"; + +import * as QuickAnswerController from "../controllers/QuickAnswerController"; + +const quickAnswerRoutes = express.Router(); + +quickAnswerRoutes.get("/quickAnswers", isAuth, QuickAnswerController.index); + +quickAnswerRoutes.get( + "/quickAnswers/:quickAnswerId", + isAuth, + QuickAnswerController.show +); + +quickAnswerRoutes.post("/quickAnswers", isAuth, QuickAnswerController.store); + +quickAnswerRoutes.put( + "/quickAnswers/:quickAnswerId", + isAuth, + QuickAnswerController.update +); + +quickAnswerRoutes.delete( + "/quickAnswers/:quickAnswerId", + isAuth, + QuickAnswerController.remove +); + +export default quickAnswerRoutes; diff --git a/backend/src/services/QuickAnswerService/CreateQuickAnswerService.ts b/backend/src/services/QuickAnswerService/CreateQuickAnswerService.ts new file mode 100644 index 0000000..80668e1 --- /dev/null +++ b/backend/src/services/QuickAnswerService/CreateQuickAnswerService.ts @@ -0,0 +1,26 @@ +import AppError from "../../errors/AppError"; +import QuickAnswer from "../../models/QuickAnswer"; + +interface Request { + shortcut: string; + message: string; +} + +const CreateQuickAnswerService = async ({ + shortcut, + message +}: Request): Promise => { + const nameExists = await QuickAnswer.findOne({ + where: { shortcut } + }); + + if (nameExists) { + throw new AppError("ERR__SHORTCUT_DUPLICATED"); + } + + const quickAnswer = await QuickAnswer.create({ shortcut, message }); + + return quickAnswer; +}; + +export default CreateQuickAnswerService; diff --git a/backend/src/services/QuickAnswerService/DeleteQuickAnswerService.ts b/backend/src/services/QuickAnswerService/DeleteQuickAnswerService.ts new file mode 100644 index 0000000..1cc21b2 --- /dev/null +++ b/backend/src/services/QuickAnswerService/DeleteQuickAnswerService.ts @@ -0,0 +1,16 @@ +import QuickAnswer from "../../models/QuickAnswer"; +import AppError from "../../errors/AppError"; + +const DeleteQuickAnswerService = async (id: string): Promise => { + const quickAnswer = await QuickAnswer.findOne({ + where: { id } + }); + + if (!quickAnswer) { + throw new AppError("ERR_NO_QUICK_ANSWER_FOUND", 404); + } + + await quickAnswer.destroy(); +}; + +export default DeleteQuickAnswerService; diff --git a/backend/src/services/QuickAnswerService/ListQuickAnswerService.ts b/backend/src/services/QuickAnswerService/ListQuickAnswerService.ts new file mode 100644 index 0000000..0ddcbcc --- /dev/null +++ b/backend/src/services/QuickAnswerService/ListQuickAnswerService.ts @@ -0,0 +1,45 @@ +import { Sequelize } from "sequelize"; +import QuickAnswer from "../../models/QuickAnswer"; + +interface Request { + searchParam?: string; + pageNumber?: string; +} + +interface Response { + quickAnswers: QuickAnswer[]; + count: number; + hasMore: boolean; +} + +const ListQuickAnswerService = async ({ + searchParam = "", + pageNumber = "1" +}: Request): Promise => { + const whereCondition = { + message: Sequelize.where( + Sequelize.fn("LOWER", Sequelize.col("message")), + "LIKE", + `%${searchParam.toLowerCase().trim()}%` + ) + }; + const limit = 20; + const offset = limit * (+pageNumber - 1); + + const { count, rows: quickAnswers } = await QuickAnswer.findAndCountAll({ + where: whereCondition, + limit, + offset, + order: [["message", "ASC"]] + }); + + const hasMore = count > offset + quickAnswers.length; + + return { + quickAnswers, + count, + hasMore + }; +}; + +export default ListQuickAnswerService; diff --git a/backend/src/services/QuickAnswerService/ShowQuickAnswerService.ts b/backend/src/services/QuickAnswerService/ShowQuickAnswerService.ts new file mode 100644 index 0000000..1ed3d2e --- /dev/null +++ b/backend/src/services/QuickAnswerService/ShowQuickAnswerService.ts @@ -0,0 +1,14 @@ +import QuickAnswer from "../../models/QuickAnswer"; +import AppError from "../../errors/AppError"; + +const ShowQuickAnswerService = async (id: string): Promise => { + const quickAnswer = await QuickAnswer.findByPk(id); + + if (!quickAnswer) { + throw new AppError("ERR_NO_QUICK_ANSWERS_FOUND", 404); + } + + return quickAnswer; +}; + +export default ShowQuickAnswerService; diff --git a/backend/src/services/QuickAnswerService/UpdateQuickAnswerService.ts b/backend/src/services/QuickAnswerService/UpdateQuickAnswerService.ts new file mode 100644 index 0000000..e50351b --- /dev/null +++ b/backend/src/services/QuickAnswerService/UpdateQuickAnswerService.ts @@ -0,0 +1,40 @@ +import QuickAnswer from "../../models/QuickAnswer"; +import AppError from "../../errors/AppError"; + +interface QuickAnswerData { + shortcut?: string; + message?: string; +} + +interface Request { + quickAnswerData: QuickAnswerData; + quickAnswerId: string; +} + +const UpdateQuickAnswerService = async ({ + quickAnswerData, + quickAnswerId +}: Request): Promise => { + const { shortcut, message } = quickAnswerData; + + const quickAnswer = await QuickAnswer.findOne({ + where: { id: quickAnswerId }, + attributes: ["id", "shortcut", "message"] + }); + + if (!quickAnswer) { + throw new AppError("ERR_NO_QUICK_ANSWERS_FOUND", 404); + } + await quickAnswer.update({ + shortcut, + message + }); + + await quickAnswer.reload({ + attributes: ["id", "shortcut", "message"] + }); + + return quickAnswer; +}; + +export default UpdateQuickAnswerService; diff --git a/frontend/src/App.js b/frontend/src/App.js index f1dfbde..ff22dac 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -2,46 +2,46 @@ import React, { useState, useEffect } from "react"; import Routes from "./routes"; import "react-toastify/dist/ReactToastify.css"; -import { createMuiTheme, ThemeProvider } from "@material-ui/core/styles"; +import { createTheme, ThemeProvider } from "@material-ui/core/styles"; import { ptBR } from "@material-ui/core/locale"; const App = () => { - const [locale, setLocale] = useState(); + const [locale, setLocale] = useState(); - const theme = createMuiTheme( - { - scrollbarStyles: { - "&::-webkit-scrollbar": { - width: "8px", - height: "8px", - }, - "&::-webkit-scrollbar-thumb": { - boxShadow: "inset 0 0 6px rgba(0, 0, 0, 0.3)", - backgroundColor: "#e8e8e8", - }, - }, - palette: { - primary: { main: "#2576d2" }, - }, - }, - locale - ); + const theme = createTheme( + { + scrollbarStyles: { + "&::-webkit-scrollbar": { + width: "8px", + height: "8px", + }, + "&::-webkit-scrollbar-thumb": { + boxShadow: "inset 0 0 6px rgba(0, 0, 0, 0.3)", + backgroundColor: "#e8e8e8", + }, + }, + palette: { + primary: { main: "#2576d2" }, + }, + }, + locale + ); - useEffect(() => { - const i18nlocale = localStorage.getItem("i18nextLng"); - const browserLocale = - i18nlocale.substring(0, 2) + i18nlocale.substring(3, 5); + useEffect(() => { + const i18nlocale = localStorage.getItem("i18nextLng"); + const browserLocale = + i18nlocale.substring(0, 2) + i18nlocale.substring(3, 5); - if (browserLocale === "ptBR") { - setLocale(ptBR); - } - }, []); + if (browserLocale === "ptBR") { + setLocale(ptBR); + } + }, []); - return ( - - - - ); + return ( + + + + ); }; export default App; diff --git a/frontend/src/components/ColorPicker/index.js b/frontend/src/components/ColorPicker/index.js index b89b233..c10360e 100644 --- a/frontend/src/components/ColorPicker/index.js +++ b/frontend/src/components/ColorPicker/index.js @@ -4,28 +4,28 @@ import React, { useState } from "react"; import { GithubPicker } from "react-color"; const ColorPicker = ({ onChange, currentColor, handleClose, open }) => { - const [selectedColor, setSelectedColor] = useState(currentColor); + const [selectedColor, setSelectedColor] = useState(currentColor); - const handleChange = color => { - setSelectedColor(color.hex); - handleClose(); - }; + const handleChange = (color) => { + setSelectedColor(color.hex); + handleClose(); + }; - return ( - - onChange(color.hex)} - /> - - ); + return ( + + onChange(color.hex)} + /> + + ); }; export default ColorPicker; diff --git a/frontend/src/components/MainContainer/index.js b/frontend/src/components/MainContainer/index.js index 24b7b9a..0045703 100644 --- a/frontend/src/components/MainContainer/index.js +++ b/frontend/src/components/MainContainer/index.js @@ -3,29 +3,31 @@ import React from "react"; import { makeStyles } from "@material-ui/core/styles"; import Container from "@material-ui/core/Container"; -const useStyles = makeStyles(theme => ({ - mainContainer: { - flex: 1, - padding: theme.spacing(2), - height: `calc(100% - 48px)`, - }, +const useStyles = makeStyles((theme) => ({ + mainContainer: { + flex: 1, + // padding: theme.spacing(2), + // height: `calc(100% - 48px)`, + padding: 0, + height: "100%", + }, - contentWrapper: { - height: "100%", - overflowY: "hidden", - display: "flex", - flexDirection: "column", - }, + contentWrapper: { + height: "100%", + overflowY: "hidden", + display: "flex", + flexDirection: "column", + }, })); const MainContainer = ({ children }) => { - const classes = useStyles(); + const classes = useStyles(); - return ( - -
{children}
-
- ); + return ( + +
{children}
+
+ ); }; export default MainContainer; diff --git a/frontend/src/components/MessageInput/index.js b/frontend/src/components/MessageInput/index.js index a6ce895..3a33829 100644 --- a/frontend/src/components/MessageInput/index.js +++ b/frontend/src/components/MessageInput/index.js @@ -12,6 +12,7 @@ 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"; @@ -19,7 +20,14 @@ 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, Switch } from "@material-ui/core"; +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"; @@ -31,483 +39,636 @@ import toastError from "../../errors/toastError"; 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)", - }, +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", - }, + newMessageBox: { + background: "#eee", + width: "100%", + display: "flex", + padding: "7px", + alignItems: "center", + }, - messageInputWrapper: { - padding: 6, - marginRight: 7, - background: "#fff", - display: "flex", - borderRadius: 20, - flex: 1, - }, + messageInputWrapper: { + padding: 6, + marginRight: 7, + background: "#fff", + display: "flex", + borderRadius: 20, + flex: 1, + position: "relative", + }, - messageInput: { - paddingLeft: 10, - flex: 1, - border: "none", - }, + messageInput: { + paddingLeft: 10, + flex: 1, + border: "none", + }, - sendMessageIcons: { - color: "grey", - }, + sendMessageIcons: { + color: "grey", + }, - uploadInput: { - display: "none", - }, + 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)", - }, + 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", - }, + 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, - }, + circleLoading: { + color: green[500], + opacity: "70%", + position: "absolute", + top: "20%", + left: "50%", + marginLeft: -12, + }, - audioLoading: { - color: green[500], - opacity: "70%", - }, + audioLoading: { + color: green[500], + opacity: "70%", + }, - recorderWrapper: { - display: "flex", - alignItems: "center", - alignContent: "middle", - }, + recorderWrapper: { + display: "flex", + alignItems: "center", + alignContent: "middle", + }, - cancelAudioIcon: { - color: "red", - }, + cancelAudioIcon: { + color: "red", + }, - sendAudioIcon: { - color: "green", - }, + sendAudioIcon: { + color: "green", + }, - replyginMsgWrapper: { - display: "flex", - width: "100%", - alignItems: "center", - justifyContent: "center", - paddingTop: 8, - paddingLeft: 73, - paddingRight: 7, - }, + 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", - }, + 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", - }, + replyginMsgBody: { + padding: 10, + height: "auto", + display: "block", + whiteSpace: "pre-wrap", + overflow: "hidden", + }, - replyginContactMsgSideColor: { - flex: "none", - width: "4px", - backgroundColor: "#35cd96", - }, + replyginContactMsgSideColor: { + flex: "none", + width: "4px", + backgroundColor: "#35cd96", + }, - replyginSelfMsgSideColor: { - flex: "none", - width: "4px", - backgroundColor: "#6bcbef", - }, + replyginSelfMsgSideColor: { + flex: "none", + width: "4px", + backgroundColor: "#6bcbef", + }, - messageContactName: { - display: "flex", - color: "#6bcbef", - fontWeight: 500, - }, + 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 }) => { - const classes = useStyles(); - const { ticketId } = useParams(); + 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 inputRef = useRef(); - const { setReplyingMessage, replyingMessage } = useContext( - ReplyMessageContext - ); - const { user } = useContext(AuthContext); + 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 [signMessage, setSignMessage] = useLocalStorage("signOption", true); + const [signMessage, setSignMessage] = useLocalStorage("signOption", true); - useEffect(() => { - inputRef.current.focus(); - }, [replyingMessage]); + useEffect(() => { + inputRef.current.focus(); + }, [replyingMessage]); - useEffect(() => { - inputRef.current.focus(); - return () => { - setInputMessage(""); - setShowEmoji(false); - setMedias([]); - setReplyingMessage(null); - }; - }, [ticketId, setReplyingMessage]); + useEffect(() => { + inputRef.current.focus(); + return () => { + setInputMessage(""); + setShowEmoji(false); + setMedias([]); + setReplyingMessage(null); + }; + }, [ticketId, setReplyingMessage]); - const handleChangeInput = e => { - setInputMessage(e.target.value); - }; + const handleChangeInput = (e) => { + setInputMessage(e.target.value); + handleLoadQuickAnswer(e.target.value); + }; - const handleAddEmoji = e => { - let emoji = e.native; - setInputMessage(prevState => prevState + emoji); - }; + const handleQuickAnswersClick = (value) => { + setInputMessage(value); + setTypeBar(false); + }; - const handleChangeMedias = e => { - if (!e.target.files) { - return; - } + const handleAddEmoji = (e) => { + let emoji = e.native; + setInputMessage((prevState) => prevState + emoji); + }; - const selectedMedias = Array.from(e.target.files); - setMedias(selectedMedias); - }; + const handleChangeMedias = (e) => { + if (!e.target.files) { + return; + } - const handleInputPaste = e => { - if (e.clipboardData.files[0]) { - setMedias([e.clipboardData.files[0]]); - } - }; + const selectedMedias = Array.from(e.target.files); + setMedias(selectedMedias); + }; - const handleUploadMedia = async e => { - setLoading(true); - e.preventDefault(); + const handleInputPaste = (e) => { + if (e.clipboardData.files[0]) { + setMedias([e.clipboardData.files[0]]); + } + }; - const formData = new FormData(); - formData.append("fromMe", true); - medias.forEach(media => { - formData.append("medias", media); - formData.append("body", media.name); - }); + const handleUploadMedia = async (e) => { + setLoading(true); + e.preventDefault(); - try { - await api.post(`/messages/${ticketId}`, formData); - } catch (err) { - toastError(err); - } + const formData = new FormData(); + formData.append("fromMe", true); + medias.forEach((media) => { + formData.append("medias", media); + formData.append("body", media.name); + }); - setLoading(false); - setMedias([]); - }; + try { + await api.post(`/messages/${ticketId}`, formData); + } catch (err) { + toastError(err); + } - const handleSendMessage = async () => { - if (inputMessage.trim() === "") return; - setLoading(true); + setLoading(false); + setMedias([]); + }; - const message = { - read: 1, - fromMe: true, - mediaUrl: "", - body: signMessage - ? `*${user?.name}:*\n${inputMessage.trim()}` - : inputMessage.trim(), - quotedMsg: replyingMessage, - }; - try { - await api.post(`/messages/${ticketId}`, message); - } catch (err) { - toastError(err); - } + const handleSendMessage = async () => { + if (inputMessage.trim() === "") return; + setLoading(true); - setInputMessage(""); - setShowEmoji(false); - setLoading(false); - setReplyingMessage(null); - }; + const message = { + read: 1, + fromMe: true, + mediaUrl: "", + body: signMessage + ? `*${user?.name}:*\n${inputMessage.trim()}` + : inputMessage.trim(), + quotedMsg: replyingMessage, + }; + try { + await api.post(`/messages/${ticketId}`, message); + } catch (err) { + toastError(err); + } - 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); - } - }; + setInputMessage(""); + setShowEmoji(false); + setLoading(false); + setReplyingMessage(null); + }; - const handleUploadAudio = async () => { - setLoading(true); - try { - const [, blob] = await Mp3Recorder.stop().getMp3(); - if (blob.size < 10000) { - setLoading(false); - setRecording(false); - return; - } + 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 formData = new FormData(); - const filename = `${new Date().getTime()}.mp3`; - formData.append("medias", blob, filename); - formData.append("body", filename); - formData.append("fromMe", true); + const handleLoadQuickAnswer = async (value) => { + if (value && value.indexOf("/") === 0) { + try { + const { data } = await api.get("/quickAnswers/", { + params: { searchParam: inputMessage.substring(1) }, + }); + setQuickAnswer(data.quickAnswers); + if (data.quickAnswers.length > 0) { + setTypeBar(true); + } else { + setTypeBar(false); + } + } catch (err) { + setTypeBar(false); + } + } else { + setTypeBar(false); + } + }; - await api.post(`/messages/${ticketId}`, formData); - } catch (err) { - toastError(err); - } + const handleUploadAudio = async () => { + setLoading(true); + try { + const [, blob] = await Mp3Recorder.stop().getMp3(); + if (blob.size < 10000) { + setLoading(false); + setRecording(false); + return; + } - setRecording(false); - setLoading(false); - }; + const formData = new FormData(); + const filename = `${new Date().getTime()}.mp3`; + formData.append("medias", blob, filename); + formData.append("body", filename); + formData.append("fromMe", true); - const handleCancelAudio = async () => { - try { - await Mp3Recorder.stop().getMp3(); - setRecording(false); - } catch (err) { - toastError(err); - } - }; + await api.post(`/messages/${ticketId}`, formData); + } catch (err) { + toastError(err); + } - const renderReplyingMessage = message => { - return ( -
-
- -
- {!message.fromMe && ( - - {message.contact?.name} - - )} - {message.body} -
-
- setReplyingMessage(null)} - > - - -
- ); - }; + setRecording(false); + setLoading(false); + }; - if (medias.length > 0) - return ( - - setMedias([])} - > - - + const handleCancelAudio = async () => { + try { + await Mp3Recorder.stop().getMp3(); + setRecording(false); + } catch (err) { + toastError(err); + } + }; - {loading ? ( -
- -
- ) : ( - - {medias[0]?.name} - {/* */} - - )} - - - -
- ); - else { - return ( - - {replyingMessage && renderReplyingMessage(replyingMessage)} -
- setShowEmoji(prevState => !prevState)} - > - - - {showEmoji ? ( -
- -
- ) : null} + const handleOpenMenuClick = (event) => { + setAnchorEl(event.currentTarget); + }; - - - { - setSignMessage(e.target.checked); - }} - name="showAllTickets" - color="primary" - /> - } - /> -
- { - input && input.focus(); - input && (inputRef.current = input); - }} - 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(); - } - }} - /> -
- {inputMessage ? ( - - - - ) : recording ? ( -
- - - - {loading ? ( -
- -
- ) : ( - - )} + const handleMenuItemClick = (event) => { + setAnchorEl(null); + }; - - - -
- ) : ( - - - - )} -
-
- ); - } + const renderReplyingMessage = (message) => { + return ( +
+
+ +
+ {!message.fromMe && ( + + {message.contact?.name} + + )} + {message.body} +
+
+ setReplyingMessage(null)} + > + + +
+ ); + }; + + if (medias.length > 0) + return ( + + setMedias([])} + > + + + + {loading ? ( +
+ +
+ ) : ( + + {medias[0]?.name} + {/* */} + + )} + + + +
+ ); + else { + return ( + + {replyingMessage && renderReplyingMessage(replyingMessage)} +
+ + setShowEmoji((prevState) => !prevState)} + > + + + {showEmoji ? ( +
+ setShowEmoji(false)}> + + +
+ ) : null} + + + + { + setSignMessage(e.target.checked); + }} + name="showAllTickets" + color="primary" + /> + } + /> +
+ + + + + + + setShowEmoji((prevState) => !prevState)} + > + + + + + + + + + { + setSignMessage(e.target.checked); + }} + name="showAllTickets" + color="primary" + /> + } + /> + + + +
+ { + input && input.focus(); + input && (inputRef.current = input); + }} + 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(); + } + }} + /> + {typeBar ? ( + + ) : ( +
+ )} +
+ {inputMessage ? ( + + + + ) : recording ? ( +
+ + + + {loading ? ( +
+ +
+ ) : ( + + )} + + + + +
+ ) : ( + + + + )} +
+
+ ); + } }; export default MessageInput; diff --git a/frontend/src/components/MessageOptionsMenu/index.js b/frontend/src/components/MessageOptionsMenu/index.js index 83a0b6e..ea0dd0b 100644 --- a/frontend/src/components/MessageOptionsMenu/index.js +++ b/frontend/src/components/MessageOptionsMenu/index.js @@ -10,62 +10,62 @@ import { ReplyMessageContext } from "../../context/ReplyingMessage/ReplyingMessa import toastError from "../../errors/toastError"; const MessageOptionsMenu = ({ message, menuOpen, handleClose, anchorEl }) => { - const { setReplyingMessage } = useContext(ReplyMessageContext); - const [confirmationOpen, setConfirmationOpen] = useState(false); + const { setReplyingMessage } = useContext(ReplyMessageContext); + const [confirmationOpen, setConfirmationOpen] = useState(false); - const handleDeleteMessage = async () => { - try { - await api.delete(`/messages/${message.id}`); - } catch (err) { - toastError(err); - } - }; + const handleDeleteMessage = async () => { + try { + await api.delete(`/messages/${message.id}`); + } catch (err) { + toastError(err); + } + }; - const hanldeReplyMessage = () => { - setReplyingMessage(message); - handleClose(); - }; + const hanldeReplyMessage = () => { + setReplyingMessage(message); + handleClose(); + }; - const handleOpenConfirmationModal = e => { - setConfirmationOpen(true); - handleClose(); - }; + const handleOpenConfirmationModal = (e) => { + setConfirmationOpen(true); + handleClose(); + }; - return ( - <> - - {i18n.t("messageOptionsMenu.confirmationModal.message")} - - - {message.fromMe && ( - - {i18n.t("messageOptionsMenu.delete")} - - )} - - {i18n.t("messageOptionsMenu.reply")} - - - - ); + return ( + <> + + {i18n.t("messageOptionsMenu.confirmationModal.message")} + + + {message.fromMe && ( + + {i18n.t("messageOptionsMenu.delete")} + + )} + + {i18n.t("messageOptionsMenu.reply")} + + + + ); }; export default MessageOptionsMenu; diff --git a/frontend/src/components/MessagesList/index.js b/frontend/src/components/MessagesList/index.js index e22af75..ef91b81 100644 --- a/frontend/src/components/MessagesList/index.js +++ b/frontend/src/components/MessagesList/index.js @@ -6,19 +6,19 @@ import clsx from "clsx"; import { green } from "@material-ui/core/colors"; import { - Button, - CircularProgress, - Divider, - IconButton, - makeStyles, + Button, + CircularProgress, + Divider, + IconButton, + makeStyles, } from "@material-ui/core"; import { - AccessTime, - Block, - Done, - DoneAll, - ExpandMore, - GetApp, + AccessTime, + Block, + Done, + DoneAll, + ExpandMore, + GetApp, } from "@material-ui/icons"; import MarkdownWrapper from "../MarkdownWrapper"; @@ -29,623 +29,626 @@ import whatsBackground from "../../assets/wa-background.png"; import api from "../../services/api"; import toastError from "../../errors/toastError"; -const useStyles = makeStyles(theme => ({ - messagesListWrapper: { - overflow: "hidden", - position: "relative", - display: "flex", - flexDirection: "column", - flexGrow: 1, - }, +const useStyles = makeStyles((theme) => ({ + messagesListWrapper: { + overflow: "hidden", + position: "relative", + display: "flex", + flexDirection: "column", + flexGrow: 1, + }, - messagesList: { - backgroundImage: `url(${whatsBackground})`, - display: "flex", - flexDirection: "column", - flexGrow: 1, - padding: "20px 20px 20px 20px", - overflowY: "scroll", - ...theme.scrollbarStyles, - }, + messagesList: { + backgroundImage: `url(${whatsBackground})`, + display: "flex", + flexDirection: "column", + flexGrow: 1, + padding: "20px 20px 20px 20px", + overflowY: "scroll", + [theme.breakpoints.down("sm")]: { + paddingBottom: "90px", + }, + ...theme.scrollbarStyles, + }, - circleLoading: { - color: green[500], - position: "absolute", - opacity: "70%", - top: 0, - left: "50%", - marginTop: 12, - }, + circleLoading: { + color: green[500], + position: "absolute", + opacity: "70%", + top: 0, + left: "50%", + marginTop: 12, + }, - messageLeft: { - marginRight: 20, - marginTop: 2, - minWidth: 100, - maxWidth: 600, - height: "auto", - display: "block", - position: "relative", - "&:hover #messageActionsButton": { - display: "flex", - position: "absolute", - top: 0, - right: 0, - }, + messageLeft: { + marginRight: 20, + marginTop: 2, + minWidth: 100, + maxWidth: 600, + height: "auto", + display: "block", + position: "relative", + "&:hover #messageActionsButton": { + display: "flex", + position: "absolute", + top: 0, + right: 0, + }, - whiteSpace: "pre-wrap", - backgroundColor: "#ffffff", - color: "#303030", - alignSelf: "flex-start", - borderTopLeftRadius: 0, - borderTopRightRadius: 8, - borderBottomLeftRadius: 8, - borderBottomRightRadius: 8, - paddingLeft: 5, - paddingRight: 5, - paddingTop: 5, - paddingBottom: 0, - boxShadow: "0 1px 1px #b3b3b3", - }, + whiteSpace: "pre-wrap", + backgroundColor: "#ffffff", + color: "#303030", + alignSelf: "flex-start", + borderTopLeftRadius: 0, + borderTopRightRadius: 8, + borderBottomLeftRadius: 8, + borderBottomRightRadius: 8, + paddingLeft: 5, + paddingRight: 5, + paddingTop: 5, + paddingBottom: 0, + boxShadow: "0 1px 1px #b3b3b3", + }, - quotedContainerLeft: { - margin: "-3px -80px 6px -6px", - overflow: "hidden", - backgroundColor: "#f0f0f0", - borderRadius: "7.5px", - display: "flex", - position: "relative", - }, + quotedContainerLeft: { + margin: "-3px -80px 6px -6px", + overflow: "hidden", + backgroundColor: "#f0f0f0", + borderRadius: "7.5px", + display: "flex", + position: "relative", + }, - quotedMsg: { - padding: 10, - maxWidth: 300, - height: "auto", - display: "block", - whiteSpace: "pre-wrap", - overflow: "hidden", - }, + quotedMsg: { + padding: 10, + maxWidth: 300, + height: "auto", + display: "block", + whiteSpace: "pre-wrap", + overflow: "hidden", + }, - quotedSideColorLeft: { - flex: "none", - width: "4px", - backgroundColor: "#6bcbef", - }, + quotedSideColorLeft: { + flex: "none", + width: "4px", + backgroundColor: "#6bcbef", + }, - messageRight: { - marginLeft: 20, - marginTop: 2, - minWidth: 100, - maxWidth: 600, - height: "auto", - display: "block", - position: "relative", - "&:hover #messageActionsButton": { - display: "flex", - position: "absolute", - top: 0, - right: 0, - }, + messageRight: { + marginLeft: 20, + marginTop: 2, + minWidth: 100, + maxWidth: 600, + height: "auto", + display: "block", + position: "relative", + "&:hover #messageActionsButton": { + display: "flex", + position: "absolute", + top: 0, + right: 0, + }, - whiteSpace: "pre-wrap", - backgroundColor: "#dcf8c6", - color: "#303030", - alignSelf: "flex-end", - borderTopLeftRadius: 8, - borderTopRightRadius: 8, - borderBottomLeftRadius: 8, - borderBottomRightRadius: 0, - paddingLeft: 5, - paddingRight: 5, - paddingTop: 5, - paddingBottom: 0, - boxShadow: "0 1px 1px #b3b3b3", - }, + whiteSpace: "pre-wrap", + backgroundColor: "#dcf8c6", + color: "#303030", + alignSelf: "flex-end", + borderTopLeftRadius: 8, + borderTopRightRadius: 8, + borderBottomLeftRadius: 8, + borderBottomRightRadius: 0, + paddingLeft: 5, + paddingRight: 5, + paddingTop: 5, + paddingBottom: 0, + boxShadow: "0 1px 1px #b3b3b3", + }, - quotedContainerRight: { - margin: "-3px -80px 6px -6px", - overflowY: "hidden", - backgroundColor: "#cfe9ba", - borderRadius: "7.5px", - display: "flex", - position: "relative", - }, + quotedContainerRight: { + margin: "-3px -80px 6px -6px", + overflowY: "hidden", + backgroundColor: "#cfe9ba", + borderRadius: "7.5px", + display: "flex", + position: "relative", + }, - quotedMsgRight: { - padding: 10, - maxWidth: 300, - height: "auto", - whiteSpace: "pre-wrap", - }, + quotedMsgRight: { + padding: 10, + maxWidth: 300, + height: "auto", + whiteSpace: "pre-wrap", + }, - quotedSideColorRight: { - flex: "none", - width: "4px", - backgroundColor: "#35cd96", - }, + quotedSideColorRight: { + flex: "none", + width: "4px", + backgroundColor: "#35cd96", + }, - messageActionsButton: { - display: "none", - position: "relative", - color: "#999", - zIndex: 1, - backgroundColor: "inherit", - opacity: "90%", - "&:hover, &.Mui-focusVisible": { backgroundColor: "inherit" }, - }, + messageActionsButton: { + display: "none", + position: "relative", + color: "#999", + zIndex: 1, + backgroundColor: "inherit", + opacity: "90%", + "&:hover, &.Mui-focusVisible": { backgroundColor: "inherit" }, + }, - messageContactName: { - display: "flex", - color: "#6bcbef", - fontWeight: 500, - }, + messageContactName: { + display: "flex", + color: "#6bcbef", + fontWeight: 500, + }, - textContentItem: { - overflowWrap: "break-word", - padding: "3px 80px 6px 6px", - }, + textContentItem: { + overflowWrap: "break-word", + padding: "3px 80px 6px 6px", + }, - textContentItemDeleted: { - fontStyle: "italic", - color: "rgba(0, 0, 0, 0.36)", - overflowWrap: "break-word", - padding: "3px 80px 6px 6px", - }, + textContentItemDeleted: { + fontStyle: "italic", + color: "rgba(0, 0, 0, 0.36)", + overflowWrap: "break-word", + padding: "3px 80px 6px 6px", + }, - messageMedia: { - objectFit: "cover", - width: 250, - height: 200, - borderTopLeftRadius: 8, - borderTopRightRadius: 8, - borderBottomLeftRadius: 8, - borderBottomRightRadius: 8, - }, + messageMedia: { + objectFit: "cover", + width: 250, + height: 200, + borderTopLeftRadius: 8, + borderTopRightRadius: 8, + borderBottomLeftRadius: 8, + borderBottomRightRadius: 8, + }, - timestamp: { - fontSize: 11, - position: "absolute", - bottom: 0, - right: 5, - color: "#999", - }, + timestamp: { + fontSize: 11, + position: "absolute", + bottom: 0, + right: 5, + color: "#999", + }, - dailyTimestamp: { - alignItems: "center", - textAlign: "center", - alignSelf: "center", - width: "110px", - backgroundColor: "#e1f3fb", - margin: "10px", - borderRadius: "10px", - boxShadow: "0 1px 1px #b3b3b3", - }, + dailyTimestamp: { + alignItems: "center", + textAlign: "center", + alignSelf: "center", + width: "110px", + backgroundColor: "#e1f3fb", + margin: "10px", + borderRadius: "10px", + boxShadow: "0 1px 1px #b3b3b3", + }, - dailyTimestampText: { - color: "#808888", - padding: 8, - alignSelf: "center", - marginLeft: "0px", - }, + dailyTimestampText: { + color: "#808888", + padding: 8, + alignSelf: "center", + marginLeft: "0px", + }, - ackIcons: { - fontSize: 18, - verticalAlign: "middle", - marginLeft: 4, - }, + ackIcons: { + fontSize: 18, + verticalAlign: "middle", + marginLeft: 4, + }, - deletedIcon: { - fontSize: 18, - verticalAlign: "middle", - marginRight: 4, - }, + deletedIcon: { + fontSize: 18, + verticalAlign: "middle", + marginRight: 4, + }, - ackDoneAllIcon: { - color: green[500], - fontSize: 18, - verticalAlign: "middle", - marginLeft: 4, - }, + ackDoneAllIcon: { + color: green[500], + fontSize: 18, + verticalAlign: "middle", + marginLeft: 4, + }, - downloadMedia: { - display: "flex", - alignItems: "center", - justifyContent: "center", - backgroundColor: "inherit", - padding: 10, - }, + downloadMedia: { + display: "flex", + alignItems: "center", + justifyContent: "center", + backgroundColor: "inherit", + padding: 10, + }, })); const reducer = (state, action) => { - if (action.type === "LOAD_MESSAGES") { - const messages = action.payload; - const newMessages = []; + if (action.type === "LOAD_MESSAGES") { + const messages = action.payload; + const newMessages = []; - messages.forEach(message => { - const messageIndex = state.findIndex(m => m.id === message.id); - if (messageIndex !== -1) { - state[messageIndex] = message; - } else { - newMessages.push(message); - } - }); + messages.forEach((message) => { + const messageIndex = state.findIndex((m) => m.id === message.id); + if (messageIndex !== -1) { + state[messageIndex] = message; + } else { + newMessages.push(message); + } + }); - return [...newMessages, ...state]; - } + return [...newMessages, ...state]; + } - if (action.type === "ADD_MESSAGE") { - const newMessage = action.payload; - const messageIndex = state.findIndex(m => m.id === newMessage.id); + if (action.type === "ADD_MESSAGE") { + const newMessage = action.payload; + const messageIndex = state.findIndex((m) => m.id === newMessage.id); - if (messageIndex !== -1) { - state[messageIndex] = newMessage; - } else { - state.push(newMessage); - } + if (messageIndex !== -1) { + state[messageIndex] = newMessage; + } else { + state.push(newMessage); + } - return [...state]; - } + return [...state]; + } - if (action.type === "UPDATE_MESSAGE") { - const messageToUpdate = action.payload; - const messageIndex = state.findIndex(m => m.id === messageToUpdate.id); + if (action.type === "UPDATE_MESSAGE") { + const messageToUpdate = action.payload; + const messageIndex = state.findIndex((m) => m.id === messageToUpdate.id); - if (messageIndex !== -1) { - state[messageIndex] = messageToUpdate; - } + if (messageIndex !== -1) { + state[messageIndex] = messageToUpdate; + } - return [...state]; - } + return [...state]; + } - if (action.type === "RESET") { - return []; - } + if (action.type === "RESET") { + return []; + } }; const MessagesList = ({ ticketId, isGroup }) => { - const classes = useStyles(); + const classes = useStyles(); - const [messagesList, dispatch] = useReducer(reducer, []); - const [pageNumber, setPageNumber] = useState(1); - const [hasMore, setHasMore] = useState(false); - const [loading, setLoading] = useState(false); - const lastMessageRef = useRef(); + const [messagesList, dispatch] = useReducer(reducer, []); + const [pageNumber, setPageNumber] = useState(1); + const [hasMore, setHasMore] = useState(false); + const [loading, setLoading] = useState(false); + const lastMessageRef = useRef(); - const [selectedMessage, setSelectedMessage] = useState({}); - const [anchorEl, setAnchorEl] = useState(null); - const messageOptionsMenuOpen = Boolean(anchorEl); - const currentTicketId = useRef(ticketId); + const [selectedMessage, setSelectedMessage] = useState({}); + const [anchorEl, setAnchorEl] = useState(null); + const messageOptionsMenuOpen = Boolean(anchorEl); + const currentTicketId = useRef(ticketId); - useEffect(() => { - dispatch({ type: "RESET" }); - setPageNumber(1); + useEffect(() => { + dispatch({ type: "RESET" }); + setPageNumber(1); - currentTicketId.current = ticketId; - }, [ticketId]); + currentTicketId.current = ticketId; + }, [ticketId]); - useEffect(() => { - setLoading(true); - const delayDebounceFn = setTimeout(() => { - const fetchMessages = async () => { - try { - const { data } = await api.get("/messages/" + ticketId, { - params: { pageNumber }, - }); + useEffect(() => { + setLoading(true); + const delayDebounceFn = setTimeout(() => { + const fetchMessages = async () => { + try { + const { data } = await api.get("/messages/" + ticketId, { + params: { pageNumber }, + }); - if (currentTicketId.current === ticketId) { - dispatch({ type: "LOAD_MESSAGES", payload: data.messages }); - setHasMore(data.hasMore); - setLoading(false); - } + if (currentTicketId.current === ticketId) { + dispatch({ type: "LOAD_MESSAGES", payload: data.messages }); + setHasMore(data.hasMore); + setLoading(false); + } - if (pageNumber === 1 && data.messages.length > 1) { - scrollToBottom(); - } - } catch (err) { - setLoading(false); - toastError(err); - } - }; - fetchMessages(); - }, 500); - return () => { - clearTimeout(delayDebounceFn); - }; - }, [pageNumber, ticketId]); + if (pageNumber === 1 && data.messages.length > 1) { + scrollToBottom(); + } + } catch (err) { + setLoading(false); + toastError(err); + } + }; + fetchMessages(); + }, 500); + return () => { + clearTimeout(delayDebounceFn); + }; + }, [pageNumber, ticketId]); - useEffect(() => { - const socket = openSocket(process.env.REACT_APP_BACKEND_URL); + useEffect(() => { + const socket = openSocket(process.env.REACT_APP_BACKEND_URL); - socket.on("connect", () => socket.emit("joinChatBox", ticketId)); + socket.on("connect", () => socket.emit("joinChatBox", ticketId)); - socket.on("appMessage", data => { - if (data.action === "create") { - dispatch({ type: "ADD_MESSAGE", payload: data.message }); - scrollToBottom(); - } + socket.on("appMessage", (data) => { + if (data.action === "create") { + dispatch({ type: "ADD_MESSAGE", payload: data.message }); + scrollToBottom(); + } - if (data.action === "update") { - dispatch({ type: "UPDATE_MESSAGE", payload: data.message }); - } - }); + if (data.action === "update") { + dispatch({ type: "UPDATE_MESSAGE", payload: data.message }); + } + }); - return () => { - socket.disconnect(); - }; - }, [ticketId]); + return () => { + socket.disconnect(); + }; + }, [ticketId]); - const loadMore = () => { - setPageNumber(prevPageNumber => prevPageNumber + 1); - }; + const loadMore = () => { + setPageNumber((prevPageNumber) => prevPageNumber + 1); + }; - const scrollToBottom = () => { - if (lastMessageRef.current) { - lastMessageRef.current.scrollIntoView({}); - } - }; + const scrollToBottom = () => { + if (lastMessageRef.current) { + lastMessageRef.current.scrollIntoView({}); + } + }; - const handleScroll = e => { - if (!hasMore) return; - const { scrollTop } = e.currentTarget; + const handleScroll = (e) => { + if (!hasMore) return; + const { scrollTop } = e.currentTarget; - if (scrollTop === 0) { - document.getElementById("messagesList").scrollTop = 1; - } + if (scrollTop === 0) { + document.getElementById("messagesList").scrollTop = 1; + } - if (loading) { - return; - } + if (loading) { + return; + } - if (scrollTop < 50) { - loadMore(); - } - }; + if (scrollTop < 50) { + loadMore(); + } + }; - const handleOpenMessageOptionsMenu = (e, message) => { - setAnchorEl(e.currentTarget); - setSelectedMessage(message); - }; + const handleOpenMessageOptionsMenu = (e, message) => { + setAnchorEl(e.currentTarget); + setSelectedMessage(message); + }; - const handleCloseMessageOptionsMenu = e => { - setAnchorEl(null); - }; + const handleCloseMessageOptionsMenu = (e) => { + setAnchorEl(null); + }; - const checkMessageMedia = message => { - if (message.mediaType === "image") { - return ; - } - if (message.mediaType === "audio") { - return ( - - ); - } + const checkMessageMedia = (message) => { + if (message.mediaType === "image") { + return ; + } + if (message.mediaType === "audio") { + return ( + + ); + } - if (message.mediaType === "video") { - return ( -