index.tsx 21 KB

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