index.tsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. import { makeStyles } from "@material-ui/core/styles";
  2. import SendIcon from '@mui/icons-material/Send';
  3. import MicNoneIcon from '@mui/icons-material/MicNone';
  4. import VideocamIcon from '@mui/icons-material/Videocam';
  5. import PauseIcon from '@mui/icons-material/Pause';
  6. import AttachFileIcon from '@mui/icons-material/AttachFile';
  7. import SentimentSatisfiedAltIcon from '@mui/icons-material/SentimentSatisfiedAlt';
  8. import CloseIcon from '@mui/icons-material/Close';
  9. import Avatar from '@mui/material/Avatar';
  10. import Picker from 'emoji-picker-react';
  11. import LinearProgress from '@mui/material/LinearProgress';
  12. import { useReactMediaRecorder } from "react-media-recorder";
  13. import { useState } from "react";
  14. import { useSelector } from "react-redux";
  15. import FilesMenu from "../FilesMenu";
  16. import {
  17. sentMessageById, sentImgMessageById, sentAudioMessageById,
  18. sentVideoMessageById,sentFileMessageById
  19. } from '../../../../../api-data'
  20. import { getChat } from '../../../../../redux/chat/selector'
  21. import { getIsOpen } from '../../../../../redux/control/selector'
  22. import { playNotification,prodBaseURL } from "../../../../../helpers";
  23. import { typingChat } from "../../../../../api-data";
  24. const useStyles = makeStyles({
  25. container: {
  26. width: '35vw',
  27. height:'6vh',
  28. position: 'fixed',
  29. bottom: '2vh',
  30. borderRadius: 8,
  31. padding: 10,
  32. display: 'flex',
  33. flexWrap: 'nowrap',
  34. alignContent: 'start',
  35. alignItems: 'start',
  36. color: '#6b6b6b',
  37. backgroundColor: '#ffffff',
  38. },
  39. textarea: {
  40. width: '100%',
  41. height: '100%',
  42. outline: 'none',
  43. border:'none',
  44. padding: '0px 10px',
  45. marginLeft: 8,
  46. marginRight: 8,
  47. overflowY:'auto',
  48. resize: 'none',
  49. '&::placeholder': {
  50. color: 'rgb(82, 82, 82)',
  51. fontWeight: 600
  52. }
  53. },
  54. attachIcon: {
  55. transform:'rotate(30deg)',
  56. },
  57. borderTop: {
  58. position: 'absolute',
  59. left: 0,
  60. top: '-2vh',
  61. width: '100%',
  62. height: 1,
  63. background:'#ffffff',
  64. },
  65. visualizerTop: {
  66. position: 'absolute',
  67. left: 0,
  68. top: '-2vh',
  69. width: '100%',
  70. color:'rgb(41, 139, 231)'
  71. },
  72. filesMenu: {
  73. background: '#fdfdfd',
  74. position: 'absolute',
  75. width: '15vw',
  76. maxWidth: '100%',
  77. left: '61%',
  78. bottom:'10vh',
  79. zIndex: 10,
  80. visibility: 'visible',
  81. borderRadius: 10,
  82. padding: '4px 6px',
  83. },
  84. emoji: {
  85. position: 'absolute',
  86. bottom:'10vh',
  87. zIndex: 10,
  88. visibility: 'visible',
  89. },
  90. iconCancel: {
  91. position: 'absolute',
  92. left: -72,
  93. bottom:-1,
  94. display:'flex',
  95. backgroundColor: '#ffffff',
  96. color: 'rgb(243, 69, 69)',
  97. border:'solid 4px rgb(243, 69, 69)',
  98. borderRadius: '50%',
  99. '&:hover': {
  100. backgroundColor: 'rgb(243, 69, 69)',
  101. color: '#ffffff',
  102. }
  103. },
  104. avatarCamera: {
  105. position: 'absolute',
  106. left: -72,
  107. bottom:-1,
  108. display: 'flex',
  109. borderRadius: '50%',
  110. zIndex: 10,
  111. border: 'solid 14px #ffffff',
  112. '&:hover': {
  113. backgroundColor: 'rgb(41, 139, 231)',
  114. border:'solid 14px rgb(41, 139, 231)',
  115. color: '#ffffff',
  116. }
  117. },
  118. avatarRight: {
  119. position: 'absolute',
  120. right: -72,
  121. bottom:-1,
  122. display: 'flex',
  123. borderRadius: '50%',
  124. zIndex: 10,
  125. border: 'solid 14px #ffffff',
  126. '&:hover': {
  127. backgroundColor: 'rgb(41, 139, 231)',
  128. border:'solid 14px rgb(41, 139, 231)',
  129. color: '#ffffff'
  130. }
  131. },
  132. pauseLeft: {
  133. position: 'absolute',
  134. left: -72,
  135. bottom:-1,
  136. zIndex: 10,
  137. },
  138. pauseRight: {
  139. position: 'absolute',
  140. right: -72,
  141. bottom:-1,
  142. zIndex: 10,
  143. },
  144. avatarPause: {
  145. backgroundColor: '#ffffff',
  146. cursor: 'pointer',
  147. animation: `$shake 1s`,
  148. animationIterationCount:'infinite',
  149. '&:hover': {
  150. backgroundColor: 'rgb(41, 139, 231)',
  151. color: '#ffffff',
  152. }
  153. },
  154. overlay: {
  155. position: 'fixed',
  156. top: 0,
  157. left: 0,
  158. width: '100vw',
  159. height: '100vh',
  160. zIndex:100
  161. },
  162. ringContainerLeft: {
  163. position: 'absolute',
  164. left: -25,
  165. top: -25,
  166. zIndex: 10,
  167. },
  168. ringContainerRight: {
  169. position: 'absolute',
  170. right: -25,
  171. top: -25,
  172. zIndex: 10,
  173. },
  174. circle: {
  175. width: 15,
  176. height: 15,
  177. backgroundColor: 'rgb(255, 4, 4)',
  178. borderRadius: '50%',
  179. position: 'relative'
  180. },
  181. ringRing: {
  182. border: '3px solid rgb(255, 4, 4)',
  183. borderRadius: '50%',
  184. height: 25,
  185. width: 25,
  186. position: 'absolute',
  187. right: -5,
  188. top: -5,
  189. animation: `$pulsate 1s ease-out`,
  190. animationIterationCount: 'infinite',
  191. opacity: 0
  192. },
  193. '@keyframes pulsate': {
  194. '0%': {transform: 'scale(0.1, 0.1)', opacity: 0},
  195. '50%': { opacity: 1},
  196. '100%': {transform: 'scale(1.2, 1.2)', opacity: 0},
  197. },
  198. '@keyframes shake': {
  199. '0%': { transform: 'translate(0.5px, 0.5px) rotate(0deg)'},
  200. '10%': { transform: 'translate(-0.5px, -1px) rotate(-1deg)'},
  201. '20%': { transform: 'translate(-1.5px, 0px) rotate(1deg)'},
  202. '30%': { transform: 'translate(1.5px, 1px) rotate(0deg)'},
  203. '40%': { transform: 'translate(0.5px, -0.5px) rotate(1deg)'},
  204. '50%': { transform: 'translate(-0.5px, 1px) rotate(-1deg)'},
  205. '60%': { transform: 'translate(-1.5px, 0.5px) rotate(0deg)'},
  206. '70%': { transform: 'translate(1.5px, 0.5px) rotate(-1deg)'},
  207. '80%': { transform: 'translate(-0.5px, -0.5px) rotate(1deg)'},
  208. '90%': { transform: 'translate(0.5px, 1px) rotate(0deg)'},
  209. '100%': { transform: 'translate(0.5px, -1px) rotate(-1deg)'},
  210. },
  211. });
  212. interface ISendMessage{
  213. isArrow: boolean,
  214. }
  215. const SendMessage = ({isArrow }:ISendMessage) => {
  216. const classes = useStyles();
  217. const { companionId } = useSelector(getChat)
  218. const isOpen = useSelector(getIsOpen)
  219. const [value, setValue] = useState<string>('')
  220. const [file, setFile] = useState<any>(false)
  221. const [isOpenMenu, setIsOpenMenu] = useState<boolean>(false)
  222. const [isOpenEmoji, setIsOpenEmoji] = useState<boolean>(false)
  223. const [isRecording, setIsRecording] = useState<boolean>(false)
  224. const [isFilming, setIsFilming] = useState<boolean>(false)
  225. const [type, setType] = useState<string>('')
  226. const { status, startRecording, stopRecording, mediaBlobUrl, clearBlobUrl } = useReactMediaRecorder({ audio: true });
  227. const { status: _status, startRecording: _startRecording, stopRecording: _stopRecording,
  228. mediaBlobUrl: _mediaBlobUrl, clearBlobUrl: _clearBlobUrl } = useReactMediaRecorder({ video: true });
  229. const onEmojiClick = (_e:any, emojiObject:any) => {
  230. setValue(prevValue => prevValue + emojiObject.emoji)
  231. setIsOpenEmoji(false)
  232. };
  233. const clearMessage = () => {
  234. file &&setFile(false)
  235. isRecording && setIsRecording(false)
  236. isFilming && setIsFilming(false)
  237. value && setValue('')
  238. type && setType('')
  239. mediaBlobUrl && clearBlobUrl()
  240. _mediaBlobUrl && _clearBlobUrl()
  241. isOpenMenu && setIsOpenMenu(false)
  242. isOpenEmoji && setIsOpenEmoji(false)
  243. }
  244. const sentMessage = async () => {
  245. if (value) sentMessageById(companionId, value)
  246. if (mediaBlobUrl && type === 'recording') {
  247. const audio = new XMLHttpRequest();
  248. audio.open('GET', mediaBlobUrl, true);
  249. audio.responseType = 'blob';
  250. audio.onload = () => {
  251. if (audio.status === 200) {
  252. const blob = audio.response
  253. const file = new File([blob], 'audio.mp3', {
  254. type: 'audio/mpeg'
  255. })
  256. const formData: any = new FormData()
  257. formData.append("audio", file)
  258. sentAudioMessageById(companionId, formData)
  259. clearBlobUrl()
  260. }
  261. }
  262. audio.send();
  263. }
  264. if (_mediaBlobUrl && type === 'filming') {
  265. const video = new XMLHttpRequest();
  266. video.open('GET', _mediaBlobUrl, true);
  267. video.responseType = 'blob';
  268. video.onload = () => {
  269. if (video.status === 200) {
  270. const blob = video.response
  271. const file = new File([blob], 'video.mp4', {
  272. type: 'video/mp4'
  273. })
  274. const formData: any = new FormData()
  275. formData.append("video", file)
  276. sentVideoMessageById(companionId, formData)
  277. _clearBlobUrl()
  278. }
  279. }
  280. video.send();
  281. }
  282. if (file && type) {
  283. if (file.type.includes('image') && type === 'content') {
  284. const formData: any = new FormData()
  285. formData.append("image", file);
  286. await sentImgMessageById(companionId, formData)
  287. }
  288. if (file.type.includes('audio') && type === 'content') {
  289. const formData: any = new FormData()
  290. formData.append("audio", file);
  291. sentAudioMessageById(companionId, formData)
  292. }
  293. if (file.type.includes('video') && type === 'content') {
  294. const formData: any = new FormData()
  295. formData.append("video", file);
  296. sentVideoMessageById(companionId, formData)
  297. }
  298. if (file.type.includes('application') && type === 'application') {
  299. const formData: any = new FormData()
  300. formData.append("file", file);
  301. sentFileMessageById(companionId, formData)
  302. }
  303. }
  304. clearMessage()
  305. playNotification(`${prodBaseURL}/send.mp3`)
  306. }
  307. const handleTextarea = (e: React.ChangeEvent<HTMLTextAreaElement>) => setValue(e.target.value)
  308. const handleFocusTextarea = async () => await typingChat(companionId,true)
  309. const handleBlurTextarea = async () => await typingChat(companionId,false)
  310. const handleOpenFileMenu = () => !isOpenMenu&&setIsOpenMenu(true)
  311. const handleCloseFileMenu = (e:any) => e.target.id === 'overlay'&&isOpenMenu&&setIsOpenMenu(false)
  312. const handleOpenEmoji = () => !isOpenEmoji&&setIsOpenEmoji(true)
  313. const handleCloseEmoji = (e: any) => e.target.id === 'overlay'&&isOpenEmoji&&setIsOpenEmoji(false)
  314. const handleRecording = () => {
  315. if (isRecording) return stopRecording()
  316. startRecording()
  317. setType('recording')
  318. setIsRecording(true)
  319. }
  320. const handleFilming = () => {
  321. if (isFilming) return _stopRecording()
  322. _startRecording()
  323. setType('filming')
  324. setIsFilming(true)
  325. }
  326. return (
  327. <div className={classes.container}>
  328. {isArrow && <div className={classes.borderTop}></div>}
  329. {isFilming && _status !== 'stopped' &&
  330. <>
  331. <div className={classes.pauseLeft}>
  332. <Avatar onClick={handleFilming } className={classes.avatarPause}
  333. sx={{backgroundColor: '#ffffff',color:'#6b6b6b',width: 56, height: 56}}>
  334. <PauseIcon fontSize="large"/>
  335. </Avatar>
  336. </div>
  337. <div className={classes.ringContainerLeft}>
  338. <div className={classes.ringRing}></div>
  339. <div className={classes.circle}></div>
  340. </div>
  341. </>}
  342. {isRecording && status !== 'stopped' &&
  343. <>
  344. <div className={classes.pauseRight}>
  345. <Avatar onClick={handleRecording} className={classes.avatarPause}
  346. sx={{backgroundColor: '#ffffff',color:'#6b6b6b',width: 56, height: 56}}>
  347. <PauseIcon fontSize="large"/>
  348. </Avatar>
  349. </div>
  350. <div className={classes.ringContainerRight}>
  351. <div className={classes.ringRing}></div>
  352. <div className={classes.circle}></div>
  353. </div>
  354. </>}
  355. <div className={classes.visualizerTop}
  356. style={{display: value || file || status === 'stopped' || _status === 'stopped' ? 'block':'none' }}>
  357. <LinearProgress color="inherit" variant="determinate" value={100}/>
  358. </div>
  359. <CloseIcon onClick={clearMessage} fontSize="small" className={classes.iconCancel}
  360. sx={{width: 56, height: 56, display: file || value || status === 'stopped'
  361. || _status === 'stopped' ? 'inline-block' : 'none'}} />
  362. <VideocamIcon onClick={handleFilming} className={classes.avatarCamera}
  363. sx={{backgroundColor: '#ffffff', color: '#6b6b6b', width: 56, height: 56}}
  364. style={{display: status !== 'idle' || _status === 'stopped' || file || value || isFilming ? 'none' : 'block'}} />
  365. <SendIcon onClick={sentMessage} className={classes.avatarRight}
  366. sx={{backgroundColor: '#ffffff',color: 'rgb(41, 139, 231)', width: 56, height: 56}}
  367. style={{display: value || file || status === 'stopped' || _status === 'stopped' ? 'block':'none' }}/>
  368. <MicNoneIcon onClick={handleRecording} className={classes.avatarRight}
  369. sx={{backgroundColor:'#ffffff',color: '#6b6b6b', width: 56, height: 56}}
  370. style={{display: !value && !file && status !== 'stopped' && _status === 'idle'&&!isRecording ? 'block' : 'none'}}/>
  371. <SentimentSatisfiedAltIcon onClick={handleOpenEmoji}
  372. fontSize='medium' sx={{color: isOpenEmoji ? 'rgb(41, 139, 231)' : '#6b6b6b', cursor: 'pointer',
  373. pointerEvents: file || status !== 'idle' || _status !== 'idle' ? 'none' : "auto",
  374. '&:hover': { color: 'rgb(41, 139, 231)'}}} />
  375. <div onClick={handleCloseEmoji} className={classes.overlay} id='overlay'
  376. style={{ display: isOpenEmoji ? 'block':'none'}}>
  377. <div className={classes.emoji} style={{left: isOpen&&isOpen !== 'menu'?'32.5vw':'45vw'}}>
  378. <Picker onEmojiClick={onEmojiClick} />
  379. </div>
  380. </div>
  381. <textarea disabled={file || status !== 'idle' || _status !== 'idle' ? true : false} value={value} onBlur={handleBlurTextarea}
  382. onFocus={handleFocusTextarea} onChange={handleTextarea} className={classes.textarea}
  383. placeholder={file ? 'The File is ready to send' : status === 'idle' && _status === 'idle' ? 'Message ' :
  384. `${status === 'stopped' || _status === 'stopped' ?'Recorded':'Recording in progress'}`} rows={1}>
  385. </textarea>
  386. <AttachFileIcon onClick={handleOpenFileMenu} className={classes.attachIcon}
  387. fontSize='medium' sx={{color: isOpenMenu ? 'rgb(41, 139, 231)' : '#6b6b6b', cursor: 'pointer',
  388. pointerEvents: value || status !== 'idle' || _status !== 'idle' ? 'none' : "auto",'&:hover': { color: 'rgb(41, 139, 231)'}}} />
  389. <div onClick={handleCloseFileMenu} className={classes.overlay} id='overlay'
  390. style={{ display: isOpenMenu ? 'block':'none'}}>
  391. <div className={classes.filesMenu} style={{left: isOpen&&isOpen !== 'menu'?'52.5vw':'65vw'}}>
  392. <FilesMenu setFile={setFile} setValue={setValue} setIsOpenMenu={setIsOpenMenu} setType={setType}/>
  393. </div>
  394. </div>
  395. </div>
  396. )
  397. }
  398. export default SendMessage