index.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513
  1. import React, { useState, useEffect, useContext, useRef } from "react";
  2. import "emoji-mart/css/emoji-mart.css";
  3. import { useParams } from "react-router-dom";
  4. import { Picker } from "emoji-mart";
  5. import MicRecorder from "mic-recorder-to-mp3";
  6. import clsx from "clsx";
  7. import { makeStyles } from "@material-ui/core/styles";
  8. import Paper from "@material-ui/core/Paper";
  9. import InputBase from "@material-ui/core/InputBase";
  10. import CircularProgress from "@material-ui/core/CircularProgress";
  11. import { green } from "@material-ui/core/colors";
  12. import AttachFileIcon from "@material-ui/icons/AttachFile";
  13. import IconButton from "@material-ui/core/IconButton";
  14. import MoodIcon from "@material-ui/icons/Mood";
  15. import SendIcon from "@material-ui/icons/Send";
  16. import CancelIcon from "@material-ui/icons/Cancel";
  17. import ClearIcon from "@material-ui/icons/Clear";
  18. import MicIcon from "@material-ui/icons/Mic";
  19. import CheckCircleOutlineIcon from "@material-ui/icons/CheckCircleOutline";
  20. import HighlightOffIcon from "@material-ui/icons/HighlightOff";
  21. import { FormControlLabel, Switch } from "@material-ui/core";
  22. import { i18n } from "../../translate/i18n";
  23. import api from "../../services/api";
  24. import RecordingTimer from "./RecordingTimer";
  25. import { ReplyMessageContext } from "../../context/ReplyingMessage/ReplyingMessageContext";
  26. import { AuthContext } from "../../context/Auth/AuthContext";
  27. import { useLocalStorage } from "../../hooks/useLocalStorage";
  28. import toastError from "../../errors/toastError";
  29. const Mp3Recorder = new MicRecorder({ bitRate: 128 });
  30. const useStyles = makeStyles(theme => ({
  31. mainWrapper: {
  32. backgroundColor: theme.palette.bordabox, //DARK MODE PLW DESIGN//
  33. display: "flex",
  34. flexDirection: "column",
  35. alignItems: "center",
  36. borderTop: "1px solid rgba(0, 0, 0, 0.12)",
  37. },
  38. newMessageBox: {
  39. background: "#eee",
  40. width: "100%",
  41. display: "flex",
  42. padding: "7px",
  43. alignItems: "center",
  44. },
  45. messageInputWrapper: {
  46. padding: 6,
  47. marginRight: 7,
  48. background: "#fff",
  49. display: "flex",
  50. borderRadius: 20,
  51. flex: 1,
  52. },
  53. messageInput: {
  54. paddingLeft: 10,
  55. flex: 1,
  56. border: "none",
  57. },
  58. sendMessageIcons: {
  59. color: "grey",
  60. },
  61. uploadInput: {
  62. display: "none",
  63. },
  64. viewMediaInputWrapper: {
  65. display: "flex",
  66. padding: "10px 13px",
  67. position: "relative",
  68. justifyContent: "space-between",
  69. alignItems: "center",
  70. backgroundColor: "#eee",
  71. borderTop: "1px solid rgba(0, 0, 0, 0.12)",
  72. },
  73. emojiBox: {
  74. position: "absolute",
  75. bottom: 63,
  76. width: 40,
  77. borderTop: "1px solid #e8e8e8",
  78. },
  79. circleLoading: {
  80. color: green[500],
  81. opacity: "70%",
  82. position: "absolute",
  83. top: "20%",
  84. left: "50%",
  85. marginLeft: -12,
  86. },
  87. audioLoading: {
  88. color: green[500],
  89. opacity: "70%",
  90. },
  91. recorderWrapper: {
  92. display: "flex",
  93. alignItems: "center",
  94. alignContent: "middle",
  95. },
  96. cancelAudioIcon: {
  97. color: "red",
  98. },
  99. sendAudioIcon: {
  100. color: "green",
  101. },
  102. replyginMsgWrapper: {
  103. display: "flex",
  104. width: "100%",
  105. alignItems: "center",
  106. justifyContent: "center",
  107. paddingTop: 8,
  108. paddingLeft: 73,
  109. paddingRight: 7,
  110. },
  111. replyginMsgContainer: {
  112. flex: 1,
  113. marginRight: 5,
  114. overflowY: "hidden",
  115. backgroundColor: "rgba(0, 0, 0, 0.05)",
  116. borderRadius: "7.5px",
  117. display: "flex",
  118. position: "relative",
  119. },
  120. replyginMsgBody: {
  121. padding: 10,
  122. height: "auto",
  123. display: "block",
  124. whiteSpace: "pre-wrap",
  125. overflow: "hidden",
  126. },
  127. replyginContactMsgSideColor: {
  128. flex: "none",
  129. width: "4px",
  130. backgroundColor: "#35cd96",
  131. },
  132. replyginSelfMsgSideColor: {
  133. flex: "none",
  134. width: "4px",
  135. backgroundColor: "#6bcbef",
  136. },
  137. messageContactName: {
  138. display: "flex",
  139. color: "#6bcbef",
  140. fontWeight: 500,
  141. },
  142. }));
  143. const MessageInput = ({ ticketStatus }) => {
  144. const classes = useStyles();
  145. const { ticketId } = useParams();
  146. const [medias, setMedias] = useState([]);
  147. const [inputMessage, setInputMessage] = useState("");
  148. const [showEmoji, setShowEmoji] = useState(false);
  149. const [loading, setLoading] = useState(false);
  150. const [recording, setRecording] = useState(false);
  151. const inputRef = useRef();
  152. const { setReplyingMessage, replyingMessage } = useContext(
  153. ReplyMessageContext
  154. );
  155. const { user } = useContext(AuthContext);
  156. const [signMessage, setSignMessage] = useLocalStorage("signOption", true);
  157. useEffect(() => {
  158. inputRef.current.focus();
  159. }, [replyingMessage]);
  160. useEffect(() => {
  161. inputRef.current.focus();
  162. return () => {
  163. setInputMessage("");
  164. setShowEmoji(false);
  165. setMedias([]);
  166. setReplyingMessage(null);
  167. };
  168. }, [ticketId, setReplyingMessage]);
  169. const handleChangeInput = e => {
  170. setInputMessage(e.target.value);
  171. };
  172. const handleAddEmoji = e => {
  173. let emoji = e.native;
  174. setInputMessage(prevState => prevState + emoji);
  175. };
  176. const handleChangeMedias = e => {
  177. if (!e.target.files) {
  178. return;
  179. }
  180. const selectedMedias = Array.from(e.target.files);
  181. setMedias(selectedMedias);
  182. };
  183. const handleInputPaste = e => {
  184. if (e.clipboardData.files[0]) {
  185. setMedias([e.clipboardData.files[0]]);
  186. }
  187. };
  188. const handleUploadMedia = async e => {
  189. setLoading(true);
  190. e.preventDefault();
  191. const formData = new FormData();
  192. formData.append("fromMe", true);
  193. medias.forEach(media => {
  194. formData.append("medias", media);
  195. formData.append("body", media.name);
  196. });
  197. try {
  198. await api.post(`/messages/${ticketId}`, formData);
  199. } catch (err) {
  200. toastError(err);
  201. }
  202. setLoading(false);
  203. setMedias([]);
  204. };
  205. const handleSendMessage = async () => {
  206. if (inputMessage.trim() === "") return;
  207. setLoading(true);
  208. const message = {
  209. read: 1,
  210. fromMe: true,
  211. mediaUrl: "",
  212. body: signMessage
  213. ? `*${user?.name}:*\n${inputMessage.trim()}`
  214. : inputMessage.trim(),
  215. quotedMsg: replyingMessage,
  216. };
  217. try {
  218. await api.post(`/messages/${ticketId}`, message);
  219. } catch (err) {
  220. toastError(err);
  221. }
  222. setInputMessage("");
  223. setShowEmoji(false);
  224. setLoading(false);
  225. setReplyingMessage(null);
  226. };
  227. const handleStartRecording = async () => {
  228. setLoading(true);
  229. try {
  230. await navigator.mediaDevices.getUserMedia({ audio: true });
  231. await Mp3Recorder.start();
  232. setRecording(true);
  233. setLoading(false);
  234. } catch (err) {
  235. toastError(err);
  236. setLoading(false);
  237. }
  238. };
  239. const handleUploadAudio = async () => {
  240. setLoading(true);
  241. try {
  242. const [, blob] = await Mp3Recorder.stop().getMp3();
  243. if (blob.size < 10000) {
  244. setLoading(false);
  245. setRecording(false);
  246. return;
  247. }
  248. const formData = new FormData();
  249. const filename = `${new Date().getTime()}.mp3`;
  250. formData.append("medias", blob, filename);
  251. formData.append("body", filename);
  252. formData.append("fromMe", true);
  253. await api.post(`/messages/${ticketId}`, formData);
  254. } catch (err) {
  255. toastError(err);
  256. }
  257. setRecording(false);
  258. setLoading(false);
  259. };
  260. const handleCancelAudio = async () => {
  261. try {
  262. await Mp3Recorder.stop().getMp3();
  263. setRecording(false);
  264. } catch (err) {
  265. toastError(err);
  266. }
  267. };
  268. const renderReplyingMessage = message => {
  269. return (
  270. <div className={classes.replyginMsgWrapper}>
  271. <div className={classes.replyginMsgContainer}>
  272. <span
  273. className={clsx(classes.replyginContactMsgSideColor, {
  274. [classes.replyginSelfMsgSideColor]: !message.fromMe,
  275. })}
  276. ></span>
  277. <div className={classes.replyginMsgBody}>
  278. {!message.fromMe && (
  279. <span className={classes.messageContactName}>
  280. {message.contact?.name}
  281. </span>
  282. )}
  283. {message.body}
  284. </div>
  285. </div>
  286. <IconButton
  287. aria-label="showRecorder"
  288. component="span"
  289. disabled={loading || ticketStatus !== "open"}
  290. onClick={() => setReplyingMessage(null)}
  291. >
  292. <ClearIcon className={classes.sendMessageIcons} />
  293. </IconButton>
  294. </div>
  295. );
  296. };
  297. if (medias.length > 0)
  298. return (
  299. <Paper elevation={0} square className={classes.viewMediaInputWrapper}>
  300. <IconButton
  301. aria-label="cancel-upload"
  302. component="span"
  303. onClick={e => setMedias([])}
  304. >
  305. <CancelIcon className={classes.sendMessageIcons} />
  306. </IconButton>
  307. {loading ? (
  308. <div>
  309. <CircularProgress className={classes.circleLoading} />
  310. </div>
  311. ) : (
  312. <span>
  313. {medias[0]?.name}
  314. {/* <img src={media.preview} alt=""></img> */}
  315. </span>
  316. )}
  317. <IconButton
  318. aria-label="send-upload"
  319. component="span"
  320. onClick={handleUploadMedia}
  321. disabled={loading}
  322. >
  323. <SendIcon className={classes.sendMessageIcons} />
  324. </IconButton>
  325. </Paper>
  326. );
  327. else {
  328. return (
  329. <Paper square elevation={0} className={classes.mainWrapper}>
  330. {replyingMessage && renderReplyingMessage(replyingMessage)}
  331. <div className={classes.newMessageBox}>
  332. <IconButton
  333. aria-label="emojiPicker"
  334. component="span"
  335. disabled={loading || recording || ticketStatus !== "open"}
  336. onClick={e => setShowEmoji(prevState => !prevState)}
  337. >
  338. <MoodIcon className={classes.sendMessageIcons} />
  339. </IconButton>
  340. {showEmoji ? (
  341. <div className={classes.emojiBox}>
  342. <Picker
  343. perLine={16}
  344. showPreview={false}
  345. showSkinTones={false}
  346. onSelect={handleAddEmoji}
  347. />
  348. </div>
  349. ) : null}
  350. <input
  351. multiple
  352. type="file"
  353. id="upload-button"
  354. disabled={loading || recording || ticketStatus !== "open"}
  355. className={classes.uploadInput}
  356. onChange={handleChangeMedias}
  357. />
  358. <label htmlFor="upload-button">
  359. <IconButton
  360. aria-label="upload"
  361. component="span"
  362. disabled={loading || recording || ticketStatus !== "open"}
  363. >
  364. <AttachFileIcon className={classes.sendMessageIcons} />
  365. </IconButton>
  366. </label>
  367. <FormControlLabel
  368. style={{ marginRight: 7, color: "gray" }}
  369. label={i18n.t("messagesInput.signMessage")}
  370. labelPlacement="start"
  371. control={
  372. <Switch
  373. size="small"
  374. checked={signMessage}
  375. onChange={e => {
  376. setSignMessage(e.target.checked);
  377. }}
  378. name="showAllTickets"
  379. color="primary"
  380. />
  381. }
  382. />
  383. <div className={classes.messageInputWrapper}>
  384. <InputBase
  385. inputRef={input => {
  386. input && input.focus();
  387. input && (inputRef.current = input);
  388. }}
  389. className={classes.messageInput}
  390. placeholder={
  391. ticketStatus === "open"
  392. ? i18n.t("messagesInput.placeholderOpen")
  393. : i18n.t("messagesInput.placeholderClosed")
  394. }
  395. multiline
  396. maxRows={5}
  397. value={inputMessage}
  398. onChange={handleChangeInput}
  399. disabled={recording || loading || ticketStatus !== "open"}
  400. onPaste={e => {
  401. ticketStatus === "open" && handleInputPaste(e);
  402. }}
  403. onKeyPress={e => {
  404. if (loading || e.shiftKey) return;
  405. else if (e.key === "Enter") {
  406. handleSendMessage();
  407. }
  408. }}
  409. />
  410. </div>
  411. {inputMessage ? (
  412. <IconButton
  413. aria-label="sendMessage"
  414. component="span"
  415. onClick={handleSendMessage}
  416. disabled={loading}
  417. >
  418. <SendIcon className={classes.sendMessageIcons} />
  419. </IconButton>
  420. ) : recording ? (
  421. <div className={classes.recorderWrapper}>
  422. <IconButton
  423. aria-label="cancelRecording"
  424. component="span"
  425. fontSize="large"
  426. disabled={loading}
  427. onClick={handleCancelAudio}
  428. >
  429. <HighlightOffIcon className={classes.cancelAudioIcon} />
  430. </IconButton>
  431. {loading ? (
  432. <div>
  433. <CircularProgress className={classes.audioLoading} />
  434. </div>
  435. ) : (
  436. <RecordingTimer />
  437. )}
  438. <IconButton
  439. aria-label="sendRecordedAudio"
  440. component="span"
  441. onClick={handleUploadAudio}
  442. disabled={loading}
  443. >
  444. <CheckCircleOutlineIcon className={classes.sendAudioIcon} />
  445. </IconButton>
  446. </div>
  447. ) : (
  448. <IconButton
  449. aria-label="showRecorder"
  450. component="span"
  451. disabled={loading || ticketStatus !== "open"}
  452. onClick={handleStartRecording}
  453. >
  454. <MicIcon className={classes.sendMessageIcons} />
  455. </IconButton>
  456. )}
  457. </div>
  458. </Paper>
  459. );
  460. }
  461. };
  462. export default MessageInput;