index.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. import { makeStyles, Typography } from '@material-ui/core'
  2. import { useState,useEffect,useCallback,useRef } from 'react';
  3. import { useSelector,useDispatch } from 'react-redux';
  4. import io from 'socket.io-client';
  5. import Moveable from "react-moveable";
  6. import { OnDrag } from "react-moveable";
  7. import ListItemText from '@mui/material/ListItemText';
  8. import ListItemAvatar from '@mui/material/ListItemAvatar';
  9. import Avatar from '@mui/material/Avatar';
  10. import PhoneIcon from '@mui/icons-material/Phone';
  11. import MinimizeIcon from '@mui/icons-material/Minimize';
  12. import CropLandscapeIcon from '@mui/icons-material/CropLandscape';
  13. import CloseIcon from '@mui/icons-material/Close';
  14. import ScreenShareIcon from '@mui/icons-material/ScreenShare';
  15. import StopScreenShareIcon from '@mui/icons-material/StopScreenShare';
  16. import VideocamIcon from '@mui/icons-material/Videocam';
  17. import VideocamOffIcon from '@mui/icons-material/VideocamOff';
  18. import MicIcon from '@mui/icons-material/Mic';
  19. import MicOffIcon from '@mui/icons-material/MicOff';
  20. import CallEndIcon from '@mui/icons-material/CallEnd';
  21. import { getChat } from '../../../redux/chat/selector';
  22. import { getAuthorizationState } from '../../../redux/authorization/selector';
  23. import { prodAwsS3,prodSocketURL, firstLetter, slicedWord } from '../../../helpers'
  24. import { socketIdChat, mediaControllersChat } from '../../../api-data';
  25. import { asyncGetChatById } from '../../../redux/chat/operations'
  26. import { actionRightIsOpen,actionScrollChat,actionOpenPinned } from '../../../redux/control/action'
  27. const Peer = require('simple-peer')
  28. const socket = io(prodSocketURL)
  29. const useStyles = makeStyles({
  30. container: {
  31. position: 'absolute',
  32. left: 0,
  33. top: 0,
  34. width: '100vw',
  35. height:'100vh',
  36. overflow: 'hidden',
  37. zIndex:100,
  38. display: 'flex',
  39. justifyContent: 'center',
  40. alignItems: 'center',
  41. alignContent: "center",
  42. backgroundColor: 'rgba(104, 105, 104, 0.6)',
  43. },
  44. shareScreenActive: {
  45. width: '100%',
  46. },
  47. shareScreenDisabled: {
  48. width: 0,
  49. height:0,
  50. },
  51. modalCall: {
  52. background: 'rgb(36, 36, 36)',
  53. display: 'flex',
  54. flexDirection: 'column',
  55. justifyContent: 'start',
  56. alignItems: 'center',
  57. justifyItems:"center",
  58. width: '34vw',
  59. height:'90vh',
  60. borderRadius: 7,
  61. },
  62. rightIcons: {
  63. display: 'flex',
  64. justifyContent: 'end',
  65. alignContent: 'center',
  66. alignItems: 'center',
  67. width:'100%'
  68. },
  69. rightIconWrapper: {
  70. color: '#ffffff',
  71. cursor: 'pointer',
  72. padding:'3px 10px 3px 10px',
  73. '&:hover': {
  74. backgroundColor:'rgb(80, 80, 80)'
  75. }
  76. },
  77. rightIconWrapperClose: {
  78. color: '#ffffff',
  79. cursor: 'pointer',
  80. padding:'3px 10px 3px 10px',
  81. borderTopRightRadius:7,
  82. '&:hover': {
  83. backgroundColor:'#f02a2a'
  84. }
  85. },
  86. bottomWrapper: {
  87. display: 'flex',
  88. justifyContent: 'center',
  89. padding: 5
  90. },
  91. bottomItem: {
  92. display: 'flex',
  93. flexDirection: 'column',
  94. justifyContent: 'center',
  95. alignContent: 'center',
  96. alignItems: 'center',
  97. cursor:'pointer',
  98. width: 80,
  99. },
  100. bottomIcon: {
  101. cursor:'pointer',
  102. },
  103. bottomIconEndAccept: {
  104. cursor: 'pointer',
  105. '&:hover': {
  106. animation: `$shake 1s`,
  107. animationIterationCount:'infinite',
  108. }
  109. },
  110. ringPulsate: {
  111. backgroundColor:"rgb(80, 80, 80)",
  112. borderRadius: '50%',
  113. height: 60,
  114. width: 60,
  115. position: 'absolute',
  116. left: 10,
  117. top: -8,
  118. animation: `$pulsate 1.5s ease-out`,
  119. animationIterationCount: 'infinite',
  120. opacity: 0,
  121. },
  122. titleIconBottom: {
  123. color: '#ffffff',
  124. fontSize: 13,
  125. paddingTop:7
  126. },
  127. myVideo: {
  128. width: 250,
  129. height: 'auto',
  130. cursor: 'pointer',
  131. position: 'absolute',
  132. top: 0,
  133. left: 0,
  134. zIndex: 150,
  135. },
  136. '@keyframes pulsate': {
  137. '0%': {transform: 'scale(1, 1)', opacity: 0},
  138. '50%': { opacity: 1},
  139. '100%': {transform: 'scale(1.2, 1.2)', opacity: 0},
  140. },
  141. '@keyframes shake': {
  142. '0%': { transform: 'rotate(0deg)'},
  143. '11%': { transform: 'rotate(10deg)'},
  144. '22%': { transform: 'rotate(20deg)'},
  145. '33%': { transform: 'rotate(30deg)'},
  146. '44%': { transform: 'rotate(20deg)'},
  147. '55%': { transform: 'rotate(10deg)'},
  148. '66%': { transform: 'rotate(0deg)'},
  149. '77%': { transform: 'rotate(-10deg)'},
  150. '88%': { transform: 'rotate(-20deg)'},
  151. '100%': { transform: 'rotate(-30deg)'},
  152. },
  153. })
  154. interface ICallBar {
  155. callStatus:any,
  156. setCallStatus: any,
  157. }
  158. const CallBar = ({callStatus,setCallStatus}:ICallBar) => {
  159. const classes = useStyles()
  160. const dispatch = useDispatch()
  161. const { _id } = useSelector(getAuthorizationState)
  162. const chat = useSelector(getChat)
  163. const { mutedMyVideo, mutedMyAudio, socketId, companionId } = chat
  164. const connectionRef = useRef<any>(null);
  165. const myVideoRef = useRef<any>(null);
  166. const myAudioRef = useRef<any>(null);
  167. const companionVideoRef = useRef<any>(null);
  168. const companionAudioRef = useRef<any>(null);
  169. const [formChatId, setFormChatId] = useState<string>('')
  170. const [mySocket, setMySocket] = useState<string>('')
  171. const [companionSocket, setCompanionSocket] = useState<string>('')
  172. const [companionSignal, setCompanionSignal] = useState<string>('')
  173. const [name, setName] = useState<string>('')
  174. const [lastName, setLastName] = useState<string>('')
  175. const [avatarUrl, setAvatarUrl] = useState<string>('')
  176. const [color, setColor] = useState<string>('')
  177. const [number, setNumber] = useState<string>('')
  178. const handleMuteVideo = () => {
  179. if (callStatus === 'conversation') {
  180. mediaControllersChat(chat.companionId,!mutedMyVideo,mutedMyAudio)
  181. }
  182. }
  183. const handleMuteAudio = () => {
  184. mediaControllersChat(chat.companionId,mutedMyVideo,!mutedMyAudio)
  185. }
  186. const handleLeaveCall = () => {
  187. setCallStatus('hanging up...')
  188. connectionRef.current.destroy();
  189. setCallStatus('')
  190. };
  191. const handleStartCall = useCallback(async () => {
  192. setCallStatus('waiting...')
  193. const mediaDevices: any = navigator.mediaDevices
  194. const stream = await mediaDevices.getDisplayMedia({
  195. video: true,
  196. audio: true
  197. })
  198. myVideoRef.current.srcObject = stream;
  199. myAudioRef.current.srcObject = stream;
  200. companionVideoRef.current.srcObject = stream;
  201. companionAudioRef.current.srcObject = stream;
  202. const peer = new Peer({
  203. initiator: true,
  204. trickle: false,
  205. stream
  206. });
  207. peer.on("signal", (data:any) => {
  208. socket.emit("callTo", {
  209. to: socketId,
  210. signalData: data,
  211. from: mySocket,
  212. userId: _id,
  213. companionId
  214. })
  215. setCallStatus('ringing...')
  216. });
  217. peer.on("stream", (companionStream: any) => {
  218. companionVideoRef.current.srcObject = companionStream;
  219. companionAudioRef.current.srcObject = companionStream;
  220. });
  221. socket.on("acceptedCall", ({ signal }: any) => {
  222. setCallStatus('connection')
  223. peer.signal(signal)
  224. setCallStatus('conversation')
  225. });
  226. connectionRef.current = peer;
  227. },[socketId,companionId,_id,mySocket,myVideoRef,setCallStatus])
  228. const handleAnswerCall = useCallback(async () => {
  229. setCallStatus('connection')
  230. dispatch(actionRightIsOpen(''))
  231. dispatch(actionOpenPinned(false))
  232. dispatch(asyncGetChatById(formChatId))
  233. setTimeout(() => dispatch(actionScrollChat(true)), 500)
  234. const stream = await navigator.mediaDevices.getUserMedia({
  235. video: true,
  236. audio: true
  237. })
  238. myVideoRef.current.srcObject = stream;
  239. myAudioRef.current.srcObject = stream;
  240. const peer = new Peer({
  241. initiator: false,
  242. trickle: false,
  243. stream,
  244. });
  245. peer.on("signal", (data: any) => {
  246. socket.emit("answerCall", {
  247. signal: data,
  248. to: companionSocket,
  249. });
  250. setCallStatus('conversation')
  251. });
  252. peer.on("stream", (companionStream: any) => {
  253. companionVideoRef.current.srcObject = companionStream;
  254. companionAudioRef.current.srcObject = companionStream;
  255. });
  256. peer.signal(companionSignal);
  257. connectionRef.current = peer;
  258. },[companionSocket,companionSignal,setCallStatus,formChatId,dispatch])
  259. useEffect(() => {
  260. socket.on("me", (id: string) => {
  261. setMySocket(id)
  262. socketIdChat(id)
  263. })
  264. socket.on('incomeCall', ({ name, lastName, avatarUrl, color, number, from, signal,_companionId }: any) => {
  265. if (connectionRef.current === null) {
  266. setCallStatus('is calling you')
  267. setName(name)
  268. setLastName(lastName)
  269. setAvatarUrl(avatarUrl)
  270. setColor(color)
  271. setNumber(number)
  272. setCompanionSocket(from)
  273. setCompanionSignal(signal)
  274. setFormChatId(_companionId)
  275. }
  276. })
  277. }, [setCallStatus])
  278. useEffect(() => {
  279. if(callStatus === 'requesting...') handleStartCall()
  280. }, [callStatus, handleStartCall])
  281. useEffect(() => {
  282. if (callStatus === '') {
  283. setName(chat.name)
  284. setLastName(chat.lastName)
  285. setAvatarUrl(chat.avatarUrl)
  286. setColor(chat.color)
  287. setNumber(chat.number)
  288. }
  289. }, [callStatus, chat])
  290. return (
  291. <div className={classes.container} style={{ top: callStatus ? 0 : '-100%' }}>
  292. <video className={classes.myVideo} ref={myVideoRef} playsInline autoPlay/>
  293. <Moveable
  294. target={myVideoRef.current}
  295. draggable={true}
  296. throttleDrag={0}
  297. hideDefaultLines={true}
  298. renderDirections={[]}
  299. rotationPosition="none"
  300. origin={false}
  301. onDrag={({ target, transform }: OnDrag) =>
  302. target!.style.transform = transform }
  303. />
  304. <div className={classes.modalCall}>
  305. <div className={classes.rightIcons} style={{marginBottom: true?0:40,}}>
  306. <div className={classes.rightIconWrapper}>
  307. <MinimizeIcon fontSize='small' />
  308. </div>
  309. <div className={classes.rightIconWrapper}>
  310. <CropLandscapeIcon fontSize='small' />
  311. </div>
  312. <div className={classes.rightIconWrapperClose} onClick={handleLeaveCall}>
  313. <CloseIcon fontSize='small' />
  314. </div>
  315. </div>
  316. {<ListItemAvatar style={{marginBottom:5}}>
  317. <Avatar alt={name} src={avatarUrl?`${prodAwsS3}/${avatarUrl}`:undefined}
  318. sx={{ background: color, width: 120, height: 120, marginRight: 2, fontSize:30,zIndex:0}}>
  319. {`${firstLetter(name)}${firstLetter(lastName)}`}
  320. </Avatar>
  321. </ListItemAvatar>}
  322. {<div style={{marginBottom:'auto'}}>
  323. <ListItemText primary={`${firstLetter(name)}${slicedWord(name, 15, 1)}
  324. ${firstLetter(lastName)}${slicedWord(lastName, 15, 1)}`}
  325. primaryTypographyProps={{ color: '#dfdfdf', fontSize: 20, fontWeight: 500 }}/>
  326. <ListItemText primary={number} primaryTypographyProps={{ color: '#ffffff', fontSize: 15, fontWeight: 500,textAlign:"center" }}/>
  327. <ListItemText secondary={callStatus} secondaryTypographyProps={{ color: "#dfdfdf",textAlign: "center" }} />
  328. </div>}
  329. <audio ref={companionAudioRef} playsInline controls autoPlay/>
  330. <video className={true ? classes.shareScreenActive : classes.shareScreenDisabled} ref={companionVideoRef} playsInline autoPlay />
  331. <audio ref={myAudioRef} playsInline controls autoPlay/>
  332. <div className={classes.bottomWrapper}>
  333. {!true&&<div className={classes.bottomItem}>
  334. <Avatar className={classes.bottomIcon}
  335. sx={{backgroundColor: '#ffffff',color: 'rgb(36, 36, 36)', width: 44, height: 44,zIndex:0}}>
  336. <ScreenShareIcon fontSize="medium" />
  337. </Avatar>
  338. <Typography variant="h6" className={classes.titleIconBottom}>Screencast</Typography>
  339. </div>}
  340. <div className={classes.bottomItem} onClick={handleMuteVideo}>
  341. <Avatar className={classes.bottomIcon}
  342. sx={{backgroundColor: mutedMyVideo?'#ffffff':'rgb(88, 88, 88)',color: mutedMyVideo?'rgb(36, 36, 36)':'#ffffff', width: 44, height: 44,zIndex:0}}>
  343. {mutedMyVideo?<VideocamIcon fontSize="medium" />:<VideocamOffIcon fontSize="medium" />}
  344. </Avatar>
  345. <Typography variant="h6" className={classes.titleIconBottom}>{mutedMyVideo?'Start Video':'Stop Video'}</Typography>
  346. </div>
  347. <div className={classes.bottomItem}>
  348. <Avatar className={classes.bottomIconEndAccept} onClick={handleLeaveCall}
  349. sx={{backgroundColor: '#f02a2a',color: '#ffffff', width: 44, height: 44,zIndex:0}}>
  350. <CallEndIcon fontSize="medium" />
  351. </Avatar>
  352. <Typography variant="h6" className={classes.titleIconBottom}>
  353. {callStatus === 'is calling you' ? 'Decline' : 'End Call'}
  354. </Typography>
  355. </div>
  356. {callStatus === 'is calling you' &&
  357. <div className={classes.bottomItem} style={{position:"relative"}}>
  358. <div className={classes.ringPulsate}></div>
  359. <Avatar className={classes.bottomIconEndAccept} onClick={handleAnswerCall}
  360. sx={{ backgroundColor: '#21f519', color: '#ffffff', width: 44, height: 44, zIndex: 0 }}>
  361. <PhoneIcon fontSize="medium" />
  362. </Avatar>
  363. <Typography variant="h6" className={classes.titleIconBottom}>
  364. Accept
  365. </Typography>
  366. </div>}
  367. <div className={classes.bottomItem}>
  368. <Avatar className={classes.bottomIcon} onClick={handleMuteAudio}
  369. sx={{backgroundColor: mutedMyAudio?'#ffffff':'rgb(88, 88, 88)',color: mutedMyAudio?'rgb(36, 36, 36)':'#ffffff', width: 44, height: 44,zIndex:0}}>
  370. {mutedMyAudio?<MicOffIcon fontSize="medium" />:<MicIcon fontSize="medium" />}
  371. </Avatar>
  372. <Typography variant="h6" className={classes.titleIconBottom}>{mutedMyAudio?'Unmute':'Mute'}</Typography>
  373. </div>
  374. </div>
  375. </div>
  376. </div>
  377. )
  378. }
  379. export default CallBar