From 15679cd0010b59dfb3ec4ff5c9ecc2c3b18a9bce Mon Sep 17 00:00:00 2001 From: Ayushya Amitabh Date: Fri, 17 Apr 2020 14:37:39 -0400 Subject: [PATCH] v0.9 --- functions/index.js | 27 +++++++++ src/Api.js | 33 ++++++++++- src/App.js | 49 ++++++++++++++-- src/Chat.js | 43 ++++++++++++-- src/Create.js | 91 ++++++++++++++++++++++------- src/Inactive.js | 13 +++++ src/Loading.js | 8 +-- src/VideoListener.js | 133 +++++++++++++++++++++++++++++++++++++++++++ src/data_model.js | 38 ------------- src/index.css | 52 +++++++++++++++-- src/index.js | 34 ++++++++--- src/netflix.css | 108 ----------------------------------- 12 files changed, 439 insertions(+), 190 deletions(-) create mode 100644 src/Inactive.js create mode 100644 src/VideoListener.js delete mode 100644 src/data_model.js delete mode 100644 src/netflix.css diff --git a/functions/index.js b/functions/index.js index 8bc0b65..267e026 100644 --- a/functions/index.js +++ b/functions/index.js @@ -48,6 +48,7 @@ const createRoom = async (req, res) => { .collection('rooms') .add({ name: bodydata.roomName, + player: bodydata.player, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }) @@ -206,6 +207,31 @@ const deleteRoom = async (req, res) => { res.status(200).send(); }; +const updatePlayer = async (req, res) => { + const { player, roomId, uid } = JSON.parse(req.body); + const userExists = await admin + .firestore() + .collection('rooms') + .doc(roomId) + .collection('users') + .doc(uid) + .get() + .then((userSnap) => userSnap.exists); + if (userExists) { + await admin + .firestore() + .collection('rooms') + .doc(roomId) + .update({ + player, + }) + .catch((err) => res.status(500).send(err)); + res.status(200).send(); + } else { + res.status(401).send('Not in room to sync'); + } +}; + const app = express(); app.use(cors({ origin: true })); app.use(authenticator); @@ -220,5 +246,6 @@ app.post('/join', joinRoom); app.post('/message', sendMessage); app.delete('/leave/:rid/users/:uid', leaveRoom); app.delete('/delete/:rid', deleteRoom); +app.post('/updatePlayer', updatePlayer); exports.widgets = functions.https.onRequest(app); diff --git a/src/Api.js b/src/Api.js index b891ab8..f583b2d 100644 --- a/src/Api.js +++ b/src/Api.js @@ -5,6 +5,27 @@ const API_HOST = window.location.href.startsWith('http://localhost') ? 'http://localhost:5001/watch-party-crx/us-central1/widgets' : 'https://us-central1-watch-party-crx.cloudfunctions.net/widgets'; +export const PLAYER_STATUS = { + PLAYING: 'PLAYING', + PAUSED: 'PAUSED', + NOT_PLAYING: 'NOT_PLAYING', +}; + +export const PLAYER_SOURCE = { + NETFLIX: 'https://www.netflix.', + AMAZON: 'https://www.amazon.', + HULU: 'https://www.hulu.', + LOCAL: 'http://localhost', +}; + +export const getPlayerSource = () => { + const HREF = window.location.href; + if (HREF.startsWith(PLAYER_SOURCE.NETFLIX)) return PLAYER_SOURCE.NETFLIX; + if (HREF.startsWith(PLAYER_SOURCE.HULU)) return PLAYER_SOURCE.HULU; + if (HREF.startsWith(PLAYER_SOURCE.AMAZON)) return PLAYER_SOURCE.AMAZON; + if (HREF.startsWith(PLAYER_SOURCE.LOCAL)) return PLAYER_SOURCE.LOCAL; +}; + export const isAuthed = () => true && firebase.auth().currentUser; export const doAuth = () => firebase.auth().signInAnonymously(); @@ -42,7 +63,7 @@ export const SendMessageApi = async (data) => { if (!isAuthed()) await doAuth(); return fetcher({ ENDPOINT: 'message', - METHOD: 'post', + METHOD: 'POST', body: JSON.stringify(data), }); }; @@ -62,3 +83,13 @@ export const DeleteRoomApi = async (data) => { METHOD: 'DELETE', }); }; + +export const UpdatePlayerApi = async (data) => { + if (isAuthed()) { + return fetcher({ + ENDPOINT: 'updatePlayer', + METHOD: 'POST', + body: JSON.stringify(data), + }); + } +}; diff --git a/src/App.js b/src/App.js index 1de1870..8ca7172 100644 --- a/src/App.js +++ b/src/App.js @@ -1,7 +1,11 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import Router from 'route-lite'; -import { Fab, createMuiTheme, ThemeProvider } from '@material-ui/core'; +import { Fab, createMuiTheme, ThemeProvider, Snackbar } from '@material-ui/core'; import Create from './Create'; +import Loading from './Loading'; +import firebase from 'firebase/app'; +import 'firebase/firestore'; +import Inactive from './Inactive'; const darkTheme = createMuiTheme({ palette: { @@ -14,12 +18,31 @@ const darkTheme = createMuiTheme({ const Routes = (props) => ( - + ); const App = () => { + const closedNotification = { open: false, message: '' }; + + const [status, setStatus] = useState({ active: false, message: '' }); const [visible, setVisible] = useState(false); + const [loading, setLoading] = useState(false); + const [notification, setNotification] = useState(closedNotification); + + useEffect(() => { + setLoading(true); + firebase + .firestore() + .collection('server') + .doc('status') + .onSnapshot((statusSnap) => { + setStatus(statusSnap.data()); + setLoading(false); + }); + }, []); + + // HANDLE VISIBILITY STYLING const PartyRoutesWrapper = document.getElementById('party-routes-wrapper'); if (visible) { document.body.classList.add('visible'); @@ -28,11 +51,29 @@ const App = () => { document.body.classList.remove('visible'); PartyRoutesWrapper && PartyRoutesWrapper.classList.remove('visible'); } + return ( + setNotification(closedNotification)} + message={notification.message} + resumeHideDuration={500} + autoHideDuration={2000} + />
- + {loading && } + {status.active ? ( + setNotification({ open: true, message })} + /> + ) : ( + + )}
setVisible(!visible)} className="watch-party-fab"> diff --git a/src/Chat.js b/src/Chat.js index 35081f0..6e102d0 100644 --- a/src/Chat.js +++ b/src/Chat.js @@ -16,9 +16,10 @@ import Sync from '@material-ui/icons/SyncOutlined'; import Exit from '@material-ui/icons/ExitToAppRounded'; import Share from '@material-ui/icons/ShareRounded'; import Delete from '@material-ui/icons/Delete'; -import { SendMessageApi, LeaveRoomApi, DeleteRoomApi } from './Api'; +import { SendMessageApi, LeaveRoomApi, DeleteRoomApi, UpdatePlayerApi } from './Api'; import { goTo } from 'route-lite'; import Create from './Create'; +import VideoListener from './VideoListener'; export default class Chat extends Component { constructor(props) { @@ -28,8 +29,15 @@ export default class Chat extends Component { messages: [], newMessage: '', admin: false, + player: {}, + localPlayer: {}, }; this.sendMessage = this.sendMessage.bind(this); + this.endParty = this.endParty.bind(this); + this.leaveParty = this.leaveParty.bind(this); + this.shareParty = this.shareParty.bind(this); + this.videoEventListener = this.videoEventListener.bind(this); + this.mVideoListener = new VideoListener(this.videoEventListener); } componentDidMount() { window.addEventListener('keyup', (ev) => { @@ -49,9 +57,12 @@ export default class Chat extends Component { .doc(window.localStorage.getItem('watchPartyRoomId')) .onSnapshot( (snap) => { + if (snap.data().player.status !== this.state.localPlayer.status) { + this.mVideoListener.doUpdate(snap.data().player); + } this.setState(snap.data()); }, - (err) => console.error(err) + (err) => {} ); firebase .firestore() @@ -75,27 +86,50 @@ export default class Chat extends Component { const pcc = document.getElementsByClassName('thread-container')[0]; if (pcc) pcc.scrollTop = pcc.scrollHeight; }); + this.mVideoListener.start(); + this.props.updateLoading(false); + } + videoEventListener(video) { + const { status, url, time, source } = video; + if (this.state.player.status !== status) { + UpdatePlayerApi({ + roomId: window.localStorage.getItem('watchPartyRoomId'), + player: { + status, + url, + time, + source, + }, + }); + } + this.setState({ localPlayer: video }); } endParty() { + this.props.notify('Ending party...'); + this.props.updateLoading(true); const data = { roomId: window.localStorage.getItem('watchPartyRoomId'), }; DeleteRoomApi(data).then((res) => { if (res.status === 200) { + this.props.notify('Ended party'); window.localStorage.removeItem('watchPartyRoomId'); - goTo(Create); + goTo(Create, this.props); } }); } leaveParty() { + this.props.notify('Leaving party...'); + this.props.updateLoading(true); const data = { roomId: window.localStorage.getItem('watchPartyRoomId'), uid: firebase.auth().currentUser.uid, }; LeaveRoomApi(data).then((res) => { if (res.status === 200) { + this.props.notify('Left party'); window.localStorage.removeItem('watchPartyRoomId'); - goTo(Create); + goTo(Create, this.props); } }); } @@ -106,6 +140,7 @@ export default class Chat extends Component { el.select(); document.execCommand('copy'); document.body.removeChild(el); + this.props.notify('Copied room code'); } sendMessage(message) { const data = { diff --git a/src/Create.js b/src/Create.js index 75b0e3f..18b29af 100644 --- a/src/Create.js +++ b/src/Create.js @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { Button, InputAdornment, @@ -8,17 +8,27 @@ import { Paper, } from '@material-ui/core'; import Save from '@material-ui/icons/Save'; -import { JoinRoomApi, CreateRoomApi } from './Api'; +import { JoinRoomApi, CreateRoomApi, PLAYER_STATUS, getPlayerSource } from './Api'; import { goTo } from 'route-lite'; import Chat from './Chat'; -const saveName = (name) => window.localStorage.setItem('watchPartyUserName', name); +const saveName = (data) => { + const { name, notify } = data; + window.localStorage.setItem('watchPartyUserName', name); + notify('Saved name'); +}; const getName = () => window.localStorage.getItem('watchPartyUserName'); -const saveRoomId = (rid) => window.localStorage.setItem('watchPartyRoomId', rid); +const saveRoomId = (data) => { + const { rid, notify } = data; + window.localStorage.setItem('watchPartyRoomId', rid); + notify('Saved room'); +}; const getRoomId = () => window.localStorage.getItem('watchPartyRoomId'); const joinRoom = async (data) => { + data.props.updateLoading(true); + data.props.notify('Joining party...'); const apidata = { roomId: data.joinName.trim(), userName: data.name.trim(), @@ -26,25 +36,58 @@ const joinRoom = async (data) => { const confirmedRoomId = await JoinRoomApi(apidata) .then((res) => res.json()) .then((res) => res.roomId) - .catch((err) => console.error('Join Room API error', err)); + .catch((err) => { + data.props.updateLoading(false); + data.props.notify('Something went wrong'); + console.error('Join Room API error', err); + }); if (confirmedRoomId === apidata.roomId) { - saveRoomId(confirmedRoomId); - saveName(apidata.userName); - goTo(Chat); + saveName({ + name: apidata.userName, + notify: data.props.notify, + }); + saveRoomId({ + rid: confirmedRoomId, + notify: data.props.notify, + }); + data.props.notify('Joined party'); + goTo(Chat, data.props); } }; const createRoom = async (data) => { + data.props.updateLoading(true); + data.props.notify('Starting party...'); + const apidata = { roomName: data.createName, userName: data.name, + player: { + source: getPlayerSource(), + status: PLAYER_STATUS.NOT_PLAYING, + time: 0, + url: '', + }, }; + CreateRoomApi(apidata) .then((res) => res.json()) .then((res) => { - saveRoomId(res.roomId); - saveName(data.name); - goTo(Chat); + saveName({ + name: data.name, + notify: data.props.notify, + }); + saveRoomId({ + rid: res.roomId, + notify: data.props.notify, + }); + data.props.notify('Started party'); + goTo(Chat, data.props); + }) + .catch((err) => { + data.props.updateLoading(false); + data.props.notify('Something went wrong'); + console.error('Join Room API error', err); }); }; @@ -52,11 +95,21 @@ const Create = (props) => { const [name, setName] = useState(getName() || ''); const [createName, setCreateName] = useState(''); const [joinName, setJoinName] = useState(getRoomId() || ''); - if (getName() && getRoomId()) - joinRoom({ - joinName: getRoomId(), - name: getName(), - }); + const { notify, updateLoading } = props; + + const joinOnMount = () => { + updateLoading(false); + // NEEDS FIX - REPETITIVE API CALLS + // if (getName() && getRoomId()) { + // joinRoom({ + // joinName, + // name, + // props, + // }); + // } + }; + useEffect(joinOnMount, []); + return (
@@ -73,7 +126,7 @@ const Create = (props) => { InputProps={{ endAdornment: ( - saveName(name)}> + saveName({ name, notify })}> @@ -92,7 +145,7 @@ const Create = (props) => { label="Party Name" />