diff --git a/app/src/Api.js b/app/src/Api.js index 3d427f8..ba48afb 100644 --- a/app/src/Api.js +++ b/app/src/Api.js @@ -33,14 +33,16 @@ export const getPlayerSource = () => { }; export const LocalData = () => { - return JSON.parse(window.localStorage.getItem('WatchParty')) || {}; + return window.localStorage.getItem('WatchParty') + ? JSON.parse(window.localStorage.getItem('WatchParty')) + : {}; }; export const updateLocalData = (data) => { window.localStorage.setItem('WatchParty', JSON.stringify(Object.assign({}, LocalData(), data))); }; -export const emptyLocalData = () => window.localStorage.setItem('WatchParty', undefined); +export const emptyLocalData = () => window.localStorage.reject('WatchParty'); export const getUserUid = async () => new Promise((resolve, reject) => { @@ -100,14 +102,14 @@ export const SendMessageApi = async (data) => { export const LeaveRoomApi = async (data) => { return fetcher({ - endpoint: `leave/${data.roomId}/users/${data.uid}`, + endpoint: `leave/${data.partyName}/users/${data.userUid}`, method: 'DELETE', }); }; export const DeleteRoomApi = async (data) => { return fetcher({ - endpoint: `delete/${data.roomId}`, + endpoint: `delete/${data.partyName}`, method: 'DELETE', }); }; @@ -133,3 +135,11 @@ export const GetRefreshedVoxeetTokenApi = async (data) => { method: 'GET', }); }; + +export const UpdateVoxeetApi = async (data) => { + return fetcher({ + endpoint: 'updateVoxeet', + method: 'POST', + body: JSON.stringify(data), + }); +}; diff --git a/app/src/CallManager.js b/app/src/CallManager.js index 55ec8f5..effcb4a 100644 --- a/app/src/CallManager.js +++ b/app/src/CallManager.js @@ -1,65 +1,148 @@ -import { VoxeetSDK } from '@voxeet/voxeet-web-sdk'; -import { GetVoxeetTokenApi, GetRefreshedVoxeetTokenApi } from './Api'; +import VoxeetSDK from '@voxeet/voxeet-web-sdk'; +import { + GetVoxeetTokenApi, + GetRefreshedVoxeetTokenApi, + LocalData, + UpdateVoxeetApi, + updateLocalData, +} from './Api'; export default class CallManager { - constructor(listener) { + constructor(listener, notify) { this.listener = listener; - this.__refreshToken = null; - this.__accessToken = null; + this.notify = notify; + this.__refreshToken = LocalData().vaT; + this.__accessToken = LocalData().vrT; this.__sessionInitialized = false; + this.__admin = false; } - toggleCall(onCall, conferenceRoomId) { + toggleCall(onCall, conference, admin) { + this.__admin = admin; if (onCall === true) this.leaveCall(); - else this.boot(conferenceRoomId); + else this.boot(conference); } - async boot(conferenceRoomId) { - if ( - this.__accessToken !== undefined || - this.__refreshToken !== undefined || - this.__accessToken !== null || - this.__refreshToken !== null - ) - await this.initialize(); + toggleMute(muted) { + VoxeetSDK.conference.mute(VoxeetSDK.session.participant, !muted); + this.listener({ muted: !muted }); + } + async boot(conference) { + await this.initialize(); const opened = await this.openSession(); - if (opened && conferenceRoomId) this.joinCall(conferenceRoomId); - else if (opened && !conferenceRoomId) this.startCall(conferenceRoomId); - else console.log('Unable to open Voxeet Session'); + if (opened && conference) { + await VoxeetSDK.conference + .fetch(conference._id) + .then(this.joinCall) + .catch((err) => { + console.error('Failed to fetch conference', err); + this.startCall(); + }); + } else if (opened && !conference) this.startCall(); + else console.error('Unable to open Voxeet Session'); } - async startCall(conferenceRoomId) { - VoxeetSDK.conference.create({ alias: window.localStorage.getItem('watchPartyRoomId') }); + async startCall() { + if (this.__admin === true) { + VoxeetSDK.conference + .create({ alias: LocalData().partyName, pin: LocalData().partyPin }) + .then(async (conference) => { + const updatedVoxeet = await UpdateVoxeetApi({ + conference, + partyName: LocalData().partyName, + partyPin: LocalData().partyPin, + userName: LocalData().userName, + }) + .then((res) => res.json()) + .then((res) => res.status <= 299) + .catch((err) => console.error('Update Voxeet API Error', err)); + if (updatedVoxeet === true) { + this.joinCall(conference); + } else { + console.error('Failed to update party Voxeet'); + } + }); + } else { + this.notify('Ask your party admin to start the call.'); + this.leaveCall(); + } + } + async joinCall(conference) { + const joined = await VoxeetSDK.conference + .join(conference, { constraints: { audio: true, video: false } }) + .then(() => true); + if (!joined) { + this.startCall(); + } + } + async leaveCall() { + VoxeetSDK.conference.leave().then(() => { + VoxeetSDK.session.close(); + this.listener({ onCall: false }); + }); } - async joinCall(conferenceRoomId) {} - async leaveCall() {} async openSession() { if (!this.__sessionInitialized) { try { await VoxeetSDK.session.open({ - externalId: window.localStorage.getItem('watchPartyUserUid'), - name: window.localStorage.getItem('watchPartyUserName'), + externalId: LocalData().userUid, + name: LocalData().userName, }); return true; } catch (error) { console.error('Error Opening Voxeet Session', error); } + console.error('Session not initialized'); return false; } return true; } async initialize() { - GetVoxeetTokenApi() - .then((res) => res.json()) - .then((tokenResult) => { - if (tokenResult.status <= 299) { - const { access_token, refresh_token } = tokenResult; - this.updateTokens(access_token, refresh_token); - VoxeetSDK.initializeToken(access_token, this.refreshToken); - } else { - console.error('Get Voxeet Token API Result Error', tokenResult); - } - }) - .catch((err) => { - console.error('Get Voxeet Token API error', err); - }); + VoxeetSDK.conference.on('joined', () => { + this.listener({ onCall: true }); + }); + + VoxeetSDK.conference.on('left', () => { + this.listener({ onCall: false }); + }); + + VoxeetSDK.conference.on('streamAdded', (participant, stream) => { + if (participant !== VoxeetSDK.session.participant) { + const voiceNode = document.createElement('video'); + voiceNode.srcObject = stream; + voiceNode.id = participant.id; + voiceNode.width = 0; + voiceNode.height = 0; + voiceNode.autoplay = true; + voiceNode.classList.add('voxeetAudioOut'); + document.body.appendChild(voiceNode); + } + }); + + VoxeetSDK.conference.on('streamRemoved', (participant, stream) => { + document.getElementById(participant.id).remove(); + }); + + if ( + this.__accessToken !== undefined || + this.__refreshToken !== undefined || + this.__accessToken !== null || + this.__refreshToken !== null + ) { + await GetVoxeetTokenApi() + .then((res) => res.json()) + .then((tokenResult) => { + if (tokenResult.status <= 299) { + const { access_token, refresh_token } = tokenResult; + this.updateTokens(access_token, refresh_token); + VoxeetSDK.initializeToken(access_token, refresh_token); + } else { + console.error('Get Voxeet Token API Result Error', tokenResult); + } + }) + .catch((err) => { + console.error('Get Voxeet Token API error', err); + }); + } else { + VoxeetSDK.initializeToken(this.__accessToken, this.__refreshToken); + } } async refreshToken() { return new Promise((resolve, reject) => { @@ -79,5 +162,6 @@ export default class CallManager { updateTokens(aT, rT) { this.__accessToken = aT; this.__refreshToken = rT; + updateLocalData({ vaT: aT, vrT: rT }); } } diff --git a/app/src/Chat.js b/app/src/Chat.js index 15f0395..f9e7b33 100644 --- a/app/src/Chat.js +++ b/app/src/Chat.js @@ -12,6 +12,7 @@ import { ExpansionPanelDetails, Card, CardContent, + Tooltip, } from '@material-ui/core'; import Thread from './Thread'; import Send from '@material-ui/icons/Send'; @@ -34,18 +35,13 @@ import { PLAYER_SOURCE, LocalData, PLAYER_STATUS, + emptyLocalData, } from './Api'; import { goTo } from 'route-lite'; import Create from './Create'; import VideoListener from './VideoListener'; import CallManager from './CallManager'; -import { - MoreVert, - FileCopyTwoTone, - PauseCircleFilled, - LinkOutlined, - PlayCircleFilled, -} from '@material-ui/icons'; +import { MoreVert, PauseCircleFilled, LinkOutlined, PlayCircleFilled } from '@material-ui/icons'; const giphyFetch = new GiphyFetch('ojJbIMUN6XRMn4mHiFFav3rMCpXUg8sq'); const alertMessage = { @@ -78,7 +74,7 @@ export default class Chat extends Component { this.getCarousel = this.getCarousel.bind(this); this.handlePhoneCall = this.handlePhoneCall.bind(this); this.callEventListener = this.callEventListener.bind(this); - this.mCallManager = new CallManager(this.callEventListener); + this.mCallManager = new CallManager(this.callEventListener, this.props.notify); this.sendMessageInvoker = this.sendMessageInvoker.bind(this); this.videoEventListener = this.videoEventListener.bind(this); this.mVideoListener = new VideoListener(this.videoEventListener); @@ -129,7 +125,9 @@ export default class Chat extends Component { videoEventListener(video) { this.syncPlayer(video); } - callEventListener(event) {} + callEventListener(event) { + this.setState(event); + } syncPlayer(video) { const { status, showName, time, source, element, showChanged } = video; if (element) { @@ -158,12 +156,12 @@ export default class Chat extends Component { this.props.notify('Ending party...'); this.props.updateLoading(true); const data = { - roomId: LocalData().partyName, + partyName: LocalData().partyName, }; DeleteRoomApi(data) .then((res) => { this.props.notify('Ended party'); - // window.localStorage.removeItem('watchPartyRoomId'); + emptyLocalData(); goTo(Create, this.props); this.props.updateLoading(false); }) @@ -177,13 +175,13 @@ export default class Chat extends Component { this.props.notify('Leaving party...'); this.props.updateLoading(true); const data = { - roomId: LocalData().partyName, - uid: LocalData().userUid, + partyName: LocalData().partyName, + userUid: LocalData().userUid, }; LeaveRoomApi(data) .then((res) => { this.props.notify('Left party'); - // window.localStorage.removeItem('watchPartyRoomId'); + emptyLocalData(); goTo(Create, this.props); this.props.updateLoading(false); }) @@ -206,9 +204,9 @@ export default class Chat extends Component { } sendMessage(message) { const data = { - roomId: window.localStorage.getItem('watchPartyRoomId'), - name: window.localStorage.getItem('watchPartyUserName'), - uid: window.localStorage.getItem('watchPartyUserUid'), + roomId: LocalData().partyName, + name: LocalData().userName, + uid: LocalData().userUid, text: message, time: new Date(), type: '', @@ -236,12 +234,12 @@ export default class Chat extends Component { ? giphyFetch.trending({ offset, limit: 1 }) : giphyFetch.search(this.state.newMessage, { offset, limit: 1 }); } - async handlePhoneCall() { - this.setState({ onCall: !this.state.onCall }); + handlePhoneCall() { + const { onCall, conference, admin } = this.state; + this.mCallManager.toggleCall(onCall, conference, admin); } handleMute() { - this.mCallManager.toggleMute(); - this.setState({ muted: !this.state.muted }); + this.mCallManager.toggleMute(this.state.muted); } getPrettyTime(t) { return t > 3600 @@ -258,10 +256,13 @@ export default class Chat extends Component { + > + + } >
@@ -290,11 +291,13 @@ export default class Chat extends Component { fullWidth InputProps={{ endAdornment: ( - - - - - + + + + + + + ), }} /> @@ -359,17 +362,29 @@ export default class Chat extends Component {
{gif || newMessage ? null : ( - {onCall ? : } + {onCall ? ( + + + + ) : ( + + + + )} )} {!gif && !newMessage && onCall && !muted ? ( - - - + + + + + ) : !gif && !newMessage && onCall && muted ? ( - - - + + + + + ) : null} props.messages.map((m, midx) => { - const userUid = window.localStorage.getItem('watchPartyUserUid'); + const { userUid } = LocalData(); if (m.uid === '') { - if (m.name === '') { - return ( - - {m.text} - - ); - } + // SYSTEM MESSAGES return ( {m.text} ); } else if (m.uid === userUid) { + // OWN MESSAGE return (
); } else { + // OTHER USER MESSAGE return (
{ const { key, secret } = VoxeetSecret; - const credentials = btoa(encodeURI(key) + ':' + encodeURI(secret)); + const credentials = Buffer.from(encodeURI(key) + ':' + encodeURI(secret)).toString('base64'); const headers = { 'Content-Type': 'application/json', 'Cache-Control': 'no-cache', diff --git a/functions/modules/updateVoxeet.js b/functions/modules/updateVoxeet.js new file mode 100644 index 0000000..73ae0c0 --- /dev/null +++ b/functions/modules/updateVoxeet.js @@ -0,0 +1,47 @@ +const admin = require('firebase-admin'); + +module.exports = async (req, res) => { + const { conference, partyName, partyPin, userName } = JSON.parse(req.body); + // GET ROOM PIN + const roomPin = await admin + .firestore() + .collection('rooms') + .doc(partyName) + .get() + .then((roomSnap) => (roomSnap.exists ? roomSnap.data().pin : null)); + // CHECK IF ROOM EXISTS + if (roomPin) { + // CHECK FOR CORRECT PIN + if (roomPin === partyPin) { + // UPDATE CONFERENCE DETAILS + await admin + .firestore() + .collection('rooms') + .doc(partyName) + .update({ conference }) + .catch((err) => + res.status(500).send({ message: 'Unable to update the call details', error: err }) + ); + // ADD SYSTEM MESSAGE + await admin + .firestore() + .collection('rooms') + .doc(partyName) + .collection('messages') + .add({ + uid: '', + name: '', + text: `${userName} started the call.`, + time: new Date(), + }) + .catch((err) => console.error('Failed to add update conference message', err)); + res.status(200).send({ message: 'Started the conference' }); + } else { + // WRONG ROOM PIN + return res.status(401).send({ message: 'Wrong party pin, try rejoining' }); + } + } else { + // ROOM DOESN'T EXIST + return res.status(404).send({ message: 'Party Not Found' }); + } +}; diff --git a/functions/package-lock.json b/functions/package-lock.json index f58cb2e..069b38c 100644 --- a/functions/package-lock.json +++ b/functions/package-lock.json @@ -2085,8 +2085,7 @@ "node-fetch": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", - "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==", - "optional": true + "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==" }, "node-forge": { "version": "0.7.4", diff --git a/functions/package.json b/functions/package.json index c300772..fdcdc04 100644 --- a/functions/package.json +++ b/functions/package.json @@ -17,7 +17,8 @@ "cors": "^2.8.5", "express": "^4.17.1", "firebase-admin": "^8.6.0", - "firebase-functions": "^3.3.0" + "firebase-functions": "^3.3.0", + "node-fetch": "^2.6.0" }, "devDependencies": { "eslint": "^5.12.0",