v1.0.0-beta

This commit is contained in:
Ayushya Amitabh
2020-04-22 11:17:06 -04:00
parent 4225ad7d3e
commit 9604d8c448
13 changed files with 439 additions and 213 deletions
+61 -13
View File
@@ -5,11 +5,11 @@ const express = require('express');
const cors = require('cors'); const cors = require('cors');
const authenticator = (req, res, next) => { const authenticator = (req, res, next) => {
res.set('Access-Control-Allow-Origin', ['*']);
if (req.method === 'OPTIONS') { if (req.method === 'OPTIONS') {
res.status(200).send(); res.status(200).send();
return next(); return next();
} }
const token = req.headers.authorization.split(' ')[1]; const token = req.headers.authorization.split(' ')[1];
const reqToken = token.split(':')[0]; const reqToken = token.split(':')[0];
const reqUid = token.split(':')[1]; const reqUid = token.split(':')[1];
@@ -97,8 +97,8 @@ const joinRoom = async (req, res) => {
.doc(bodydata.uid) .doc(bodydata.uid)
.get() .get()
.then((userSnap) => userSnap.exists); .then((userSnap) => userSnap.exists);
if (userExists) return res.status(200).send({ roomId: bodydata.roomId }); if (userExists) res.status(200).send({ roomId: bodydata.roomId });
if (exists && bodydata.roomId && bodydata.userName) { else if (!userExists && exists && bodydata.roomId && bodydata.userName) {
await admin await admin
.firestore() .firestore()
.collection('rooms') .collection('rooms')
@@ -124,9 +124,9 @@ const joinRoom = async (req, res) => {
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
}); });
res.status(200).send({ roomId: bodydata.roomId }); res.status(200).send({ roomId: bodydata.roomId });
} } else if (!userExists && !bodydata.userName)
if (!bodydata.userName) res.status(400).send('Missing user name'); res.status(400).send({ message: 'Missing user name' });
else res.status(404).send('Party Not Found'); else res.status(404).send({ message: 'Party Not Found' });
}; };
const sendMessage = async (req, res) => { const sendMessage = async (req, res) => {
@@ -143,9 +143,10 @@ const sendMessage = async (req, res) => {
uid, uid,
}) })
.catch((messageErr) => res.status(500).send(messageErr)); .catch((messageErr) => res.status(500).send(messageErr));
await admin.firestore().collection('rooms').doc(bodydata.roomId).update({ await admin.firestore().collection('rooms').doc(roomId).update({
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
}); });
res.status(200).send({ message: 'Sent a message' });
}; };
const leaveRoom = async (req, res) => { const leaveRoom = async (req, res) => {
@@ -158,7 +159,7 @@ const leaveRoom = async (req, res) => {
.doc(req.params.uid) .doc(req.params.uid)
.delete() .delete()
.catch((err) => res.status(500).send(err)); .catch((err) => res.status(500).send(err));
res.status(200).send(); res.status(200).send({ message: 'Left the party' });
}; };
const deleteRoom = async (req, res) => { const deleteRoom = async (req, res) => {
@@ -179,7 +180,7 @@ const deleteRoom = async (req, res) => {
}); });
if (!isAdmin) { if (!isAdmin) {
console.log('[UNAUTHORIZED] Requested Delete - Room - ', req.params.rid); console.log('[UNAUTHORIZED] Requested Delete - Room - ', req.params.rid);
res.status(401).send(); res.status(401).send({ message: 'Not authorized to end this party.' });
return; return;
} }
await admin await admin
@@ -204,11 +205,30 @@ const deleteRoom = async (req, res) => {
.doc(req.params.rid) .doc(req.params.rid)
.delete() .delete()
.catch((err) => res.status(500).send(err)); .catch((err) => res.status(500).send(err));
res.status(200).send(); res.status(200).send({ message: 'Ended the party' });
}; };
const updatePlayer = async (req, res) => { const updatePlayer = async (req, res) => {
const { player, roomId, uid } = JSON.parse(req.body); const { player, roomId, uid, showChanged, userName } = JSON.parse(req.body);
const statusString =
player.status === 'PLAYING'
? 'started playing from'
: player.status === 'PAUSED'
? 'paused at'
: player.status === 'NOT_PLAYING'
? 'stopped at'
: 'is at';
const timeString =
player.time > 3600
? new Date(player.time * 1000).toISOString().substr(11, 8)
: new Date(player.time * 1000).toISOString().substr(14, 5);
const [roomExists, roomData] = await admin
.firestore()
.collection('rooms')
.doc(roomId)
.get()
.then((docSnap) => [docSnap.exists, docSnap.data()]);
if (!roomExists) return res.status(404).send({ message: 'Party not found.' });
const userExists = await admin const userExists = await admin
.firestore() .firestore()
.collection('rooms') .collection('rooms')
@@ -218,6 +238,20 @@ const updatePlayer = async (req, res) => {
.get() .get()
.then((userSnap) => userSnap.exists); .then((userSnap) => userSnap.exists);
if (userExists) { if (userExists) {
if (showChanged === true) {
await admin
.firestore()
.collection('rooms')
.doc(roomId)
.collection('messages')
.add({
uid: '<system>',
name: '',
text: `${userName} changed show to \n${player.showName}`,
time: new Date().toISOString(),
});
}
if (roomData.status !== player.status) {
await admin await admin
.firestore() .firestore()
.collection('rooms') .collection('rooms')
@@ -226,9 +260,23 @@ const updatePlayer = async (req, res) => {
player, player,
}) })
.catch((err) => res.status(500).send(err)); .catch((err) => res.status(500).send(err));
res.status(200).send(); await admin
.firestore()
.collection('rooms')
.doc(roomId)
.collection('messages')
.add({
uid: '<system>',
name: '',
text: `${userName} ${statusString} ${timeString}`,
time: new Date().toISOString(),
});
return res.status(200).send({ message: 'Synced party player' });
} else { } else {
res.status(401).send('Not in room to sync'); return res.status(208).send({ message: 'Player already up to date' });
}
} else {
return res.status(401).send('Not in room to sync');
} }
}; };
+3 -2
View File
@@ -1,6 +1,7 @@
{ {
"name": "functions", "name": "watchparty",
"description": "Cloud Functions for Firebase", "version": "1.0.0-beta",
"description": "Cloud Functions for Watch Party",
"scripts": { "scripts": {
"lint": "eslint .", "lint": "eslint .",
"serve": "firebase emulators:start --only functions", "serve": "firebase emulators:start --only functions",
+115 -6
View File
@@ -1,8 +1,117 @@
// Called when the user clicks on the browser action /* global chrome */
chrome.browserAction.onClicked.addListener(function(tab) { /*global firebase*/
// Send a message to the active tab
chrome.tabs.query({ active: true, currentWindow: true }, function(tabs) { const firebaseConfig = {
var activeTab = tabs[0]; apiKey: 'AIzaSyCJLuYpzXLDRmu2ttzFuSGmYcpFxqToWFU',
chrome.tabs.sendMessage(activeTab.id, { message: 'clicked_browser_action' }); authDomain: 'watch-party-crx.firebaseapp.com',
databaseURL: 'https://watch-party-crx.firebaseio.com',
projectId: 'watch-party-crx',
storageBucket: 'watch-party-crx.appspot.com',
messagingSenderId: '712242707190',
appId: '1:712242707190:web:65fa7f772e30da364b0ad7',
measurementId: 'G-EZS4PEVG4T',
};
firebase.initializeApp(firebaseConfig);
firebase.auth().setPersistence(firebase.auth.Auth.Persistence.LOCAL);
const isAuthed = () => true && firebase.auth().currentUser;
const doAuth = () => firebase.auth().signInAnonymously();
const fetcher = async (request, sendResponse) => {
if (!isAuthed()) await doAuth();
const { url, method } = request;
delete request.type;
delete request.url;
delete request.method;
const idToken = await firebase.auth().currentUser.getIdToken();
let resStatus = {};
fetch(url, {
method,
headers: {
Authorization: `Token ${idToken}:${firebase.auth().currentUser.uid}`,
'Access-Control-Allow-Origin': '*',
},
...request,
})
.then((res) => {
resStatus = {
status: res.status,
statusText: res.statusText,
};
return res.json();
})
.then((res) => sendResponse({ ...res, ...resStatus }))
.catch((err) => sendResponse({ ...err, status: 503, statusText: "Couldn't talk to server" }));
};
const sendAuth = async (sendResponse) => {
if (!isAuthed()) await doAuth();
sendResponse(firebase.auth().currentUser);
};
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
const { type } = request;
switch (type) {
case 'API_CALL':
fetcher(request, sendResponse);
break;
case 'GET_AUTH':
sendAuth(sendResponse);
break;
default:
console.warn('Unknown message type: <', type, '>');
break;
}
return true;
});
chrome.runtime.onConnect.addListener((port) => {
port.onDisconnect.addListener(() => {});
port.onMessage.addListener((request) => {
const { type, roomId } = request;
if (type === 'INIT') {
firebase
.firestore()
.collection('rooms')
.doc(roomId)
.onSnapshot(
(snap) => {
if (snap) {
port.postMessage(snap.data());
}
},
(err) => {}
);
firebase
.firestore()
.collection('rooms')
.doc(roomId)
.collection('users')
.onSnapshot(
(snap) => {
if (snap) {
const isAdmin = snap.docs
.filter((user) => user.id === firebase.auth().currentUser.uid)[0]
.data().admin;
port.postMessage({ admin: isAdmin });
}
},
(err) => {}
);
firebase
.firestore()
.collection('rooms')
.doc(roomId)
.collection('messages')
.orderBy('time')
.onSnapshot(
(snap) => {
if (snap)
port.postMessage({ messages: snap.docs.map((v) => ({ ...v.data(), id: v.id })) });
},
(err) => {}
);
}
}); });
}); });
+3 -5
View File
@@ -1,15 +1,13 @@
{ {
"name": "WatchParty", "name": "WatchParty",
"short_name": "WP", "short_name": "WP",
"version": "1.0", "version": "1.0.0",
"description": "Watch streamed content together.", "description": "Watch streamed content together.",
"background": { "background": {
"scripts": ["background.js"] "page": "popup.html"
},
"browser_action": {
"default_popup": "popup.html"
}, },
"permissions": ["https://*.hulu.com/*", "https://*.netflix.com/*", "https://*.amazon.com/*"], "permissions": ["https://*.hulu.com/*", "https://*.netflix.com/*", "https://*.amazon.com/*"],
"content_security_policy": "script-src 'self' https://www.gstatic.com/ https://*.firebaseio.com https://www.googleapis.com; object-src 'self'",
"content_scripts": [ "content_scripts": [
{ {
"matches": ["https://*.hulu.com/*"], "matches": ["https://*.hulu.com/*"],
+8
View File
@@ -1,6 +1,10 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta
http-equiv="Content-Security-Policy"
content="default-src *; style-src 'self' https://* 'unsafe-inline'; script-src 'self' http://* 'unsafe-inline' 'unsafe-eval'"
/>
<style> <style>
html { html {
height: fit-content; height: fit-content;
@@ -39,6 +43,10 @@
</style> </style>
</head> </head>
<body> <body>
<script src="https://www.gstatic.com/firebasejs/7.14.1/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/7.14.1/firebase-auth.js"></script>
<script src="https://www.gstatic.com/firebasejs/7.14.1/firebase-firestore.js"></script>
<script src="background.js"></script>
<a target="_blank" href="https://www.hulu.com/">Hulu</a> <a target="_blank" href="https://www.hulu.com/">Hulu</a>
<a target="_blank" href="https://www.netflix.com/browse">Netflix</a> <a target="_blank" href="https://www.netflix.com/browse">Netflix</a>
<a target="_blank" href="https://www.amazon.com/gp/video/storefront">Prime Video</a> <a target="_blank" href="https://www.amazon.com/gp/video/storefront">Prime Video</a>
+41 -32
View File
@@ -1,5 +1,4 @@
import firebase from 'firebase/app'; /*global chrome*/
import 'firebase/auth';
const API_HOST = window.location.href.startsWith('http://localhost') const API_HOST = window.location.href.startsWith('http://localhost')
? 'http://localhost:5001/watch-party-crx/us-central1/widgets' ? 'http://localhost:5001/watch-party-crx/us-central1/widgets'
@@ -26,70 +25,80 @@ export const getPlayerSource = () => {
if (HREF.startsWith(PLAYER_SOURCE.LOCAL)) return PLAYER_SOURCE.LOCAL; if (HREF.startsWith(PLAYER_SOURCE.LOCAL)) return PLAYER_SOURCE.LOCAL;
}; };
export const isAuthed = () => true && firebase.auth().currentUser; export const getUserUid = async () =>
new Promise((resolve, reject) => {
export const doAuth = () => firebase.auth().signInAnonymously(); chrome.runtime.sendMessage(
{
export const fetcher = async ({ ENDPOINT, METHOD, ...args }) => { type: 'GET_AUTH',
const idToken = await firebase.auth().currentUser.getIdToken();
return fetch(`${API_HOST}/${ENDPOINT}`, {
method: METHOD,
headers: {
Authorization: `Token ${idToken}:${firebase.auth().currentUser.uid}`,
}, },
...args, (response) => {
const { uid } = response;
window.localStorage.setItem('watchPartyUserUid', uid);
resolve(uid);
}
);
}); });
export const fetcher = async ({ endpoint, method, ...args }) => {
const url = `${API_HOST}/${endpoint}`;
return new Promise((resolve, reject) =>
chrome.runtime.sendMessage(
{
type: 'API_CALL',
url,
method,
...args,
},
(response) => {
if (response.status > 299) reject(response);
else resolve({ json: () => response });
}
)
);
}; };
export const CreateRoomApi = async (data) => { export const CreateRoomApi = async (data) => {
if (!isAuthed()) await doAuth();
return fetcher({ return fetcher({
ENDPOINT: 'create', endpoint: 'create',
METHOD: 'POST', method: 'POST',
body: JSON.stringify(data), body: JSON.stringify(data),
}); });
}; };
export const JoinRoomApi = async (data) => { export const JoinRoomApi = async (data) => {
if (!isAuthed()) await doAuth();
return fetcher({ return fetcher({
ENDPOINT: 'join', endpoint: 'join',
METHOD: 'POST', method: 'POST',
body: JSON.stringify(data), body: JSON.stringify(data),
}); });
}; };
export const SendMessageApi = async (data) => { export const SendMessageApi = async (data) => {
if (!isAuthed()) await doAuth();
return fetcher({ return fetcher({
ENDPOINT: 'message', endpoint: 'message',
METHOD: 'POST', method: 'POST',
body: JSON.stringify(data), body: JSON.stringify(data),
}); });
}; };
export const LeaveRoomApi = async (data) => { export const LeaveRoomApi = async (data) => {
if (!isAuthed()) await doAuth();
return fetcher({ return fetcher({
ENDPOINT: `leave/${data.roomId}/users/${data.uid}`, endpoint: `leave/${data.roomId}/users/${data.uid}`,
METHOD: 'DELETE', method: 'DELETE',
}); });
}; };
export const DeleteRoomApi = async (data) => { export const DeleteRoomApi = async (data) => {
if (!isAuthed()) await doAuth();
return fetcher({ return fetcher({
ENDPOINT: `delete/${data.roomId}`, endpoint: `delete/${data.roomId}`,
METHOD: 'DELETE', method: 'DELETE',
}); });
}; };
export const UpdatePlayerApi = async (data) => { export const UpdatePlayerApi = async (data) => {
if (isAuthed()) {
return fetcher({ return fetcher({
ENDPOINT: 'updatePlayer', endpoint: 'updatePlayer',
METHOD: 'POST', method: 'POST',
body: JSON.stringify(data), body: JSON.stringify(data),
}); });
}
}; };
+2
View File
@@ -6,6 +6,7 @@ import Loading from './Loading';
import firebase from 'firebase/app'; import firebase from 'firebase/app';
import 'firebase/firestore'; import 'firebase/firestore';
import Inactive from './Inactive'; import Inactive from './Inactive';
import { getUserUid } from './Api';
const darkTheme = createMuiTheme({ const darkTheme = createMuiTheme({
palette: { palette: {
@@ -32,6 +33,7 @@ const App = () => {
useEffect(() => { useEffect(() => {
setLoading(true); setLoading(true);
getUserUid();
firebase firebase
.firestore() .firestore()
.collection('server') .collection('server')
+53 -52
View File
@@ -1,3 +1,4 @@
/*global chrome */
import React, { Component } from 'react'; import React, { Component } from 'react';
import { import {
Typography, Typography,
@@ -7,16 +8,19 @@ import {
ButtonGroup, ButtonGroup,
Button, Button,
} from '@material-ui/core'; } from '@material-ui/core';
import firebase from 'firebase/app';
import 'firebase/firestore';
import 'firebase/auth';
import Thread from './Thread'; import Thread from './Thread';
import Send from '@material-ui/icons/SendRounded'; import Send from '@material-ui/icons/SendRounded';
import Sync from '@material-ui/icons/SyncOutlined';
import Exit from '@material-ui/icons/ExitToAppRounded'; import Exit from '@material-ui/icons/ExitToAppRounded';
import Share from '@material-ui/icons/ShareRounded'; import Share from '@material-ui/icons/ShareRounded';
import Delete from '@material-ui/icons/Delete'; import Delete from '@material-ui/icons/Delete';
import { SendMessageApi, LeaveRoomApi, DeleteRoomApi, UpdatePlayerApi } from './Api'; import {
SendMessageApi,
LeaveRoomApi,
DeleteRoomApi,
UpdatePlayerApi,
getPlayerSource,
PLAYER_STATUS,
} from './Api';
import { goTo } from 'route-lite'; import { goTo } from 'route-lite';
import Create from './Create'; import Create from './Create';
import VideoListener from './VideoListener'; import VideoListener from './VideoListener';
@@ -28,7 +32,7 @@ export default class Chat extends Component {
name: '', name: '',
messages: [], messages: [],
newMessage: '', newMessage: '',
admin: false, admin: null,
player: {}, player: {},
localPlayer: {}, localPlayer: {},
}; };
@@ -36,53 +40,42 @@ export default class Chat extends Component {
this.endParty = this.endParty.bind(this); this.endParty = this.endParty.bind(this);
this.leaveParty = this.leaveParty.bind(this); this.leaveParty = this.leaveParty.bind(this);
this.shareParty = this.shareParty.bind(this); this.shareParty = this.shareParty.bind(this);
this.syncPlayer = this.syncPlayer.bind(this);
this.videoEventListener = this.videoEventListener.bind(this); this.videoEventListener = this.videoEventListener.bind(this);
this.mVideoListener = new VideoListener(this.videoEventListener); this.mVideoListener = new VideoListener(this.videoEventListener);
this.port = null;
} }
componentDidMount() { componentDidMount() {
window.addEventListener('keyup', (ev) => { window.onkeypress = (ev) => {
if (ev.code === 'Enter') { if (ev.code === 'Enter') {
if (ev.target === document.getElementById('chat-message-box-input')) { if (ev.target === document.getElementById('chat-message-box-input')) {
if (ev.shiftKey === false) { if (ev.shiftKey === false) {
ev.preventDefault();
const trim = this.state.newMessage.replace(/^\s+|\s+$/g, ''); const trim = this.state.newMessage.replace(/^\s+|\s+$/g, '');
if (trim !== '') this.sendMessage(trim); if (trim !== '') this.sendMessage(trim);
this.setState({ newMessage: '' }); this.setState({ newMessage: '' });
} }
} }
} }
};
this.port = chrome.runtime.connect({ name: window.localStorage.getItem('watchPartyRoomId') });
this.port.postMessage({
type: 'INIT',
roomId: window.localStorage.getItem('watchPartyRoomId'),
}); });
firebase this.port.onMessage.addListener((response) => {
.firestore() this.setState(response);
.collection('rooms') if (response.player && this.state.localPlayer.status !== response.player.status) {
.doc(window.localStorage.getItem('watchPartyRoomId')) if (response.player.source !== getPlayerSource()) {
.onSnapshot( this.props.updateLoading(true);
(snap) => { this.props.notify('This party is for a different website');
if (snap.data().player.status !== this.state.localPlayer.status) { goTo(Create, this.props);
this.mVideoListener.doUpdate(snap.data().player); this.props.updateLoading(false);
} else {
if (this.mVideoListener.isUpdating === false)
this.mVideoListener.doUpdate(response.player);
}
} }
this.setState(snap.data());
},
(err) => {}
);
firebase
.firestore()
.collection('rooms')
.doc(window.localStorage.getItem('watchPartyRoomId'))
.collection('users')
.onSnapshot((snap) => {
const isAdmin = snap.docs
.filter((user) => user.id === firebase.auth().currentUser.uid)[0]
.data().admin;
this.setState({ admin: isAdmin });
});
firebase
.firestore()
.collection('rooms')
.doc(window.localStorage.getItem('watchPartyRoomId'))
.collection('messages')
.orderBy('time')
.onSnapshot((snap) => {
this.setState({ messages: snap.docs.map((v) => ({ ...v.data(), id: v.id })) });
const pcc = document.getElementsByClassName('thread-container')[0]; const pcc = document.getElementsByClassName('thread-container')[0];
if (pcc) pcc.scrollTop = pcc.scrollHeight; if (pcc) pcc.scrollTop = pcc.scrollHeight;
}); });
@@ -90,19 +83,31 @@ export default class Chat extends Component {
this.props.updateLoading(false); this.props.updateLoading(false);
} }
videoEventListener(video) { videoEventListener(video) {
const { status, url, time, source } = video; this.setState({ localPlayer: video });
if (this.state.player.status !== status) { if (
this.state.player.status !== video.status &&
video.showName &&
video.status !== PLAYER_STATUS.NOT_PLAYING
)
this.syncPlayer(video);
}
syncPlayer(video) {
const { status, showName, time, source, element } = video;
if (element) {
UpdatePlayerApi({ UpdatePlayerApi({
roomId: window.localStorage.getItem('watchPartyRoomId'), roomId: window.localStorage.getItem('watchPartyRoomId'),
showChanged: showName !== this.state.player.showName,
userName: window.localStorage.getItem('watchPartyUserName'),
player: { player: {
status, status: showName !== this.state.player.showName ? PLAYER_STATUS.PAUSED : status,
url, showName,
time, time,
source, source,
}, },
}); });
} else {
this.mVideoListener.start();
} }
this.setState({ localPlayer: video });
} }
endParty() { endParty() {
this.props.notify('Ending party...'); this.props.notify('Ending party...');
@@ -111,11 +116,10 @@ export default class Chat extends Component {
roomId: window.localStorage.getItem('watchPartyRoomId'), roomId: window.localStorage.getItem('watchPartyRoomId'),
}; };
DeleteRoomApi(data).then((res) => { DeleteRoomApi(data).then((res) => {
if (res.status === 200) {
this.props.notify('Ended party'); this.props.notify('Ended party');
window.localStorage.removeItem('watchPartyRoomId'); window.localStorage.removeItem('watchPartyRoomId');
goTo(Create, this.props); goTo(Create, this.props);
} this.props.updateLoading(false);
}); });
} }
leaveParty() { leaveParty() {
@@ -123,14 +127,13 @@ export default class Chat extends Component {
this.props.updateLoading(true); this.props.updateLoading(true);
const data = { const data = {
roomId: window.localStorage.getItem('watchPartyRoomId'), roomId: window.localStorage.getItem('watchPartyRoomId'),
uid: firebase.auth().currentUser.uid, uid: window.localStorage.getItem('watchPartyUserUid'),
}; };
LeaveRoomApi(data).then((res) => { LeaveRoomApi(data).then((res) => {
if (res.status === 200) {
this.props.notify('Left party'); this.props.notify('Left party');
window.localStorage.removeItem('watchPartyRoomId'); window.localStorage.removeItem('watchPartyRoomId');
goTo(Create, this.props); goTo(Create, this.props);
} this.props.updateLoading(false);
}); });
} }
shareParty() { shareParty() {
@@ -146,7 +149,7 @@ export default class Chat extends Component {
const data = { const data = {
roomId: window.localStorage.getItem('watchPartyRoomId'), roomId: window.localStorage.getItem('watchPartyRoomId'),
name: window.localStorage.getItem('watchPartyUserName'), name: window.localStorage.getItem('watchPartyUserName'),
uid: firebase.auth().currentUser.uid, uid: window.localStorage.getItem('watchPartyUserUid'),
text: message, text: message,
time: new Date().toISOString(), time: new Date().toISOString(),
}; };
@@ -166,7 +169,6 @@ export default class Chat extends Component {
<Button onClick={this.shareParty} startIcon={<Share />}> <Button onClick={this.shareParty} startIcon={<Share />}>
Share Share
</Button> </Button>
<Button startIcon={<Sync />}>Sync</Button>
<Button onClick={this.leaveParty} startIcon={<Exit />}> <Button onClick={this.leaveParty} startIcon={<Exit />}>
Leave Leave
</Button> </Button>
@@ -180,13 +182,12 @@ export default class Chat extends Component {
<Thread messages={messages} /> <Thread messages={messages} />
</div> </div>
<TextField <TextField
onSubmit={(e) => console.log(e)}
multiline multiline
rowsMax="3" rowsMax="3"
size="small" size="small"
placeholder="New message... (<Enter> to send)" placeholder="New message... (<Enter> to send)"
variant="outlined" variant="outlined"
className="chat-message-box" className="chat-message-box addFocus"
value={newMessage} value={newMessage}
onChange={(e) => this.setState({ newMessage: e.target.value })} onChange={(e) => this.setState({ newMessage: e.target.value })}
InputProps={{ InputProps={{
+13 -21
View File
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react'; import React, { useState } from 'react';
import { import {
Button, Button,
InputAdornment, InputAdornment,
@@ -38,10 +38,10 @@ const joinRoom = async (data) => {
.then((res) => res.roomId) .then((res) => res.roomId)
.catch((err) => { .catch((err) => {
data.props.updateLoading(false); data.props.updateLoading(false);
data.props.notify('Something went wrong'); data.props.notify(err.statusText);
console.error('Join Room API error', err); console.error('Join Room API error', err);
}); });
if (confirmedRoomId === apidata.roomId) { if (confirmedRoomId && confirmedRoomId === apidata.roomId) {
saveName({ saveName({
name: apidata.userName, name: apidata.userName,
notify: data.props.notify, notify: data.props.notify,
@@ -73,6 +73,7 @@ const createRoom = async (data) => {
CreateRoomApi(apidata) CreateRoomApi(apidata)
.then((res) => res.json()) .then((res) => res.json())
.then((res) => { .then((res) => {
if (res.status <= 299) {
saveName({ saveName({
name: data.name, name: data.name,
notify: data.props.notify, notify: data.props.notify,
@@ -83,11 +84,15 @@ const createRoom = async (data) => {
}); });
data.props.notify('Started party'); data.props.notify('Started party');
goTo(Chat, data.props); goTo(Chat, data.props);
} else {
data.props.updateLoading(false);
data.props.notify(res.statusText);
}
}) })
.catch((err) => { .catch((err) => {
data.props.updateLoading(false); data.props.updateLoading(false);
data.props.notify('Something went wrong'); data.props.notify(err.statusText);
console.error('Join Room API error', err); console.error('Create Room API error', err);
}); });
}; };
@@ -95,20 +100,7 @@ const Create = (props) => {
const [name, setName] = useState(getName() || ''); const [name, setName] = useState(getName() || '');
const [createName, setCreateName] = useState(''); const [createName, setCreateName] = useState('');
const [joinName, setJoinName] = useState(getRoomId() || ''); const [joinName, setJoinName] = useState(getRoomId() || '');
const { notify, updateLoading } = props; const { notify } = props;
const joinOnMount = () => {
updateLoading(false);
// NEEDS FIX - REPETITIVE API CALLS
// if (getName() && getRoomId()) {
// joinRoom({
// joinName,
// name,
// props,
// });
// }
};
useEffect(joinOnMount, []);
return ( return (
<div className="party-create-container"> <div className="party-create-container">
@@ -140,7 +132,7 @@ const Create = (props) => {
<TextField <TextField
fullWidth fullWidth
value={createName} value={createName}
className="card-field" className="card-field addFocus"
onChange={(e) => setCreateName(e.target.value)} onChange={(e) => setCreateName(e.target.value)}
label="Party Name" label="Party Name"
/> />
@@ -162,7 +154,7 @@ const Create = (props) => {
color="secondary" color="secondary"
fullWidth fullWidth
value={joinName} value={joinName}
className="card-field" className="card-field addFocus"
onChange={(e) => setJoinName(e.target.value)} onChange={(e) => setJoinName(e.target.value)}
label="Party Name" label="Party Name"
/> />
+2 -3
View File
@@ -1,18 +1,17 @@
import React from 'react'; import React from 'react';
import { Typography } from '@material-ui/core'; import { Typography } from '@material-ui/core';
import './Thread.css'; import './Thread.css';
import firebase from 'firebase/app';
import 'firebase/auth';
const Thread = (props) => const Thread = (props) =>
props.messages.map((m, midx) => { props.messages.map((m, midx) => {
const userUid = window.localStorage.getItem('watchPartyUserUid');
if (m.uid === '<system>') { if (m.uid === '<system>') {
return ( return (
<Typography variant="body2" className="thread-item system-message" key={m.id} disabled> <Typography variant="body2" className="thread-item system-message" key={m.id} disabled>
{m.text} {m.text}
</Typography> </Typography>
); );
} else if (m.uid === firebase.auth().currentUser.uid) { } else if (m.uid === userUid) {
return ( return (
<div <div
className={`thread-item ${ className={`thread-item ${
+67 -36
View File
@@ -7,7 +7,7 @@ export default class VideoListener {
this.video = { this.video = {
status: PLAYER_STATUS.NOT_PLAYING, status: PLAYER_STATUS.NOT_PLAYING,
time: 0, time: 0,
url: '', showName: '',
source: '', source: '',
element: null, element: null,
}; };
@@ -16,30 +16,65 @@ export default class VideoListener {
this.firstTimeSync = this.firstTimeSync.bind(this); this.firstTimeSync = this.firstTimeSync.bind(this);
this.doUpdate = this.doUpdate.bind(this); this.doUpdate = this.doUpdate.bind(this);
this.start = this.start.bind(this); this.start = this.start.bind(this);
this.isUpdating = false;
} }
updateStatus(newStatus) { updateStatus(newStatus) {
if (this.isUpdating === false && newStatus.status !== this.video.status) {
this.isUpdating = true;
this.video = Object.assign({}, this.video, newStatus); this.video = Object.assign({}, this.video, newStatus);
this.listener(this.video); this.listener(this.video);
this.isUpdating = false;
} else return;
} }
doUpdate(serverPlayer) { doUpdate(serverPlayer) {
if (this.video.element) this.video.element.currentTime = serverPlayer.time; if (this.video.element && this.video.status !== serverPlayer.status) {
switch (serverPlayer.status) { switch (serverPlayer.status) {
case PLAYER_STATUS.PAUSED: case PLAYER_STATUS.PAUSED:
if (this.video.element) this.video.element.pause(); this.video.element.pause();
break; break;
case PLAYER_STATUS.NOT_PLAYING: case PLAYER_STATUS.NOT_PLAYING:
if (this.video.element) this.video.element.pause(); this.video.element.pause();
break; break;
case PLAYER_STATUS.PLAYING: case PLAYER_STATUS.PLAYING:
if (this.video.element) this.video.element.play(); this.video.element.play();
break; break;
default: default:
break; break;
} }
} }
}
getShowName() {
switch (this.video.source) {
case PLAYER_SOURCE.NETFLIX:
const titleContainerParent = document.querySelector(
'.PlayerControls--control-element.text-control.video-title'
);
if (titleContainerParent) {
const titleContainer = titleContainerParent.firstChild;
const nshow = titleContainer.querySelector('h4').innerHTML;
let nepisode = '';
titleContainer.querySelectorAll('span').forEach((e) => {
if (e.innerHTML) nepisode += e.innerHTML + ' ';
});
return nshow + '\n' + nepisode;
}
break;
case PLAYER_SOURCE.AMAZON:
const ashow = document.querySelector('.f3w9jrr.ff48h02').firstChild.innerHTML;
const aepisode = document.querySelector('.f3w9jrr.ff48h02').lastChild.innerHTML;
return ashow !== '' && aepisode !== '' ? ashow + '\n' + aepisode : null;
case PLAYER_SOURCE.HULU:
const hshow = document.querySelector('.metadata-area__second-line').innerHTML;
const hepisode = document.querySelector('.metadata-area__third-line').innerHTML;
return hshow !== '' && hepisode !== '' ? hshow + '\n' + hepisode : null;
default:
break;
}
}
firstTimeSync(videoElement) { firstTimeSync(videoElement) {
this.updateStatus({ this.updateStatus({
element: videoElement, element: videoElement,
showName: this.getShowName(),
}); });
if (videoElement.paused) { if (videoElement.paused) {
this.updateStatus({ this.updateStatus({
@@ -54,17 +89,24 @@ export default class VideoListener {
}); });
} }
videoElement.onplaying = (ev) => { videoElement.onplaying = (ev) => {
this.updateStatus({ status: PLAYER_STATUS.PLAYING, time: ev.timeStamp }); this.updateStatus({
if (this.video.url === '') this.updateStatus({ url: window.location.href }); status: PLAYER_STATUS.PLAYING,
time: videoElement.currentTime,
showName: this.getShowName(),
});
}; };
videoElement.onpause = (ev) => videoElement.onpause = (ev) =>
this.updateStatus({ status: PLAYER_STATUS.PAUSED, time: ev.timeStamp }); this.updateStatus({
status: PLAYER_STATUS.PAUSED,
time: videoElement.currentTime,
showName: this.getShowName(),
});
videoElement.onended = (ev) => { videoElement.onended = (ev) => {
this.updateStatus({ status: PLAYER_STATUS.NOT_PLAYING, url: '', time: 0 }); this.updateStatus({ status: PLAYER_STATUS.NOT_PLAYING, showName: '', time: 0 });
}; };
} }
start() { start() {
this.updateStatus({ source: getPlayerSource(), url: window.location.href }); this.updateStatus({ source: getPlayerSource(), showName: this.getShowName() });
switch (this.video.source) { switch (this.video.source) {
case PLAYER_SOURCE.NETFLIX: case PLAYER_SOURCE.NETFLIX:
if (document.getElementsByClassName('AkiraPlayer')[0]) { if (document.getElementsByClassName('AkiraPlayer')[0]) {
@@ -75,25 +117,28 @@ export default class VideoListener {
this.firstTimeSync(videoElement); this.firstTimeSync(videoElement);
} }
} else { } else {
document.body.addEventListener('DOMNodeInserted', (ev) => { document.addEventListener('DOMNodeInserted', (e) => {
if (ev.target.tagName === 'VIDEO') { if (e.target) {
document.body.removeEventListener('DOMNodeInserted'); if (e.target.classList) {
this.start(); if (
e.target.classList.contains('nfp') &&
e.target.classList.contains('AkiraPlayer')
) {
e.target.addEventListener('DOMNodeInserted', (ev) => {
if (ev.target.children[0].id) {
this.firstTimeSync(ev.target.children[0].firstChild);
}
});
}
}
} }
}); });
} }
break; break;
case PLAYER_SOURCE.HULU: case PLAYER_SOURCE.HULU:
const videoElement = document.getElementsByClassName('video-player')[0]; const videoElement = document.getElementsByClassName('content-video-player')[0];
if (videoElement) { if (videoElement) {
this.firstTimeSync(videoElement); this.firstTimeSync(videoElement);
} else {
document.body.addEventListener('DOMNodeInserted', (ev) => {
if (ev.target.tagName === 'VIDEO') {
document.body.removeEventListener('DOMNodeInserted');
this.start();
}
});
} }
break; break;
case PLAYER_SOURCE.AMAZON: case PLAYER_SOURCE.AMAZON:
@@ -104,26 +149,12 @@ export default class VideoListener {
if (videoElement) { if (videoElement) {
this.firstTimeSync(videoElement); this.firstTimeSync(videoElement);
} }
} else {
document.body.addEventListener('DOMNodeInserted', (ev) => {
if (ev.target.tagName === 'VIDEO') {
document.body.removeEventListener('DOMNodeInserted');
this.start();
}
});
} }
break; break;
case PLAYER_SOURCE.LOCAL: case PLAYER_SOURCE.LOCAL:
const localVideoElement = document.getElementsByTagName('video')[0]; const localVideoElement = document.getElementsByTagName('video')[0];
if (localVideoElement) { if (localVideoElement) {
this.firstTimeSync(localVideoElement); this.firstTimeSync(localVideoElement);
} else {
document.body.addEventListener('DOMNodeInserted', (ev) => {
if (ev.target.tagName === 'VIDEO') {
document.body.removeEventListener('DOMNodeInserted');
this.start();
}
});
} }
break; break;
default: default:
+8 -3
View File
@@ -7,7 +7,8 @@ body.visible {
width: 65%; width: 65%;
} }
body.visible .watch-party-fab { body.visible .watch-party-fab {
right: calc(35% - 4rem) !important; right: 35% !important;
transform: translateX(50%);
top: 1.5rem; top: 1.5rem;
} }
body.visible .party-notification-container { body.visible .party-notification-container {
@@ -15,13 +16,14 @@ body.visible .party-notification-container {
} }
body.netflix.visible .AkiraPlayer, /* NETFLIX */ body.netflix.visible .AkiraPlayer, /* NETFLIX */
body.hulu.visible .Player__container.addFocus,/* HULU */ body.hulu.visible .Player__container,/* HULU */
body.amazon-prime.visible #dv-web-player.dv-player-fullscreen /* AMAZON PRIME */ { body.amazon-prime.visible #dv-web-player.dv-player-fullscreen /* AMAZON PRIME */ {
max-width: 65% !important; max-width: 65% !important;
} }
/* HULU MINIMIZED VIDEO */ /* HULU MINIMIZED VIDEO */
body.hulu.visible #__player__.Player__container.Player__container--modal { body.hulu.visible #__player__ .Player__container.Player__container--modal {
right: calc(35% + 2rem) !important; right: calc(35% + 2rem) !important;
z-index: 99999 !important;
} }
/* HULU & AMAZON PRIME FIX */ /* HULU & AMAZON PRIME FIX */
body.hulu #party-routes-wrapper, body.hulu #party-routes-wrapper,
@@ -77,11 +79,13 @@ body.amazon-prime #party-routes-wrapper {
color: white; color: white;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
z-index: 9999;
} }
#party-routes-wrapper.visible { #party-routes-wrapper.visible {
right: 0; right: 0;
} }
.party-notification-container { .party-notification-container {
z-index: 99999 !important;
display: none !important; display: none !important;
bottom: 0.8rem !important; bottom: 0.8rem !important;
right: 1rem !important; right: 1rem !important;
@@ -97,6 +101,7 @@ body.amazon-prime #party-routes-wrapper {
position: fixed !important; position: fixed !important;
bottom: 1rem !important; bottom: 1rem !important;
right: -2rem !important; right: -2rem !important;
border: 1px solid white;
background-color: black !important; background-color: black !important;
/* background-image: url('/logo192.png'); */ /* background-image: url('/logo192.png'); */
background-image: url('chrome-extension://__MSG_@@extension_id__/logo192.png'); background-image: url('chrome-extension://__MSG_@@extension_id__/logo192.png');
+23
View File
@@ -27,11 +27,34 @@ switch (getPlayerSource()) {
document.body.classList.add('netflix'); document.body.classList.add('netflix');
break; break;
case PLAYER_SOURCE.HULU: case PLAYER_SOURCE.HULU:
document.addEventListener(
'keydown',
(ev) => {
if (ev.target.tagName !== 'INPUT' || ev.target.tagName !== 'TEXTAREA') {
ev.stopImmediatePropagation();
ev.stopPropagation();
} else {
ev.target.focus();
}
},
{ capture: true }
);
document.body.classList.add('hulu'); document.body.classList.add('hulu');
break; break;
case PLAYER_SOURCE.AMAZON: case PLAYER_SOURCE.AMAZON:
document.body.classList.add('amazon-prime'); document.body.classList.add('amazon-prime');
break; break;
case PLAYER_SOURCE.LOCAL:
const VideoPlayer = document.createElement('video');
VideoPlayer.autoplay = true;
VideoPlayer.muted = true;
VideoPlayer.controls = true;
VideoPlayer.style.width = '300px';
VideoPlayer.src =
'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4';
VideoPlayer.addEventListener('change', (e) => console.log(e));
document.body.appendChild(VideoPlayer);
break;
default: default:
break; break;
} }