index.tsx 18 KB

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