/************************************************************************ Copyright (C) Ayushya Amitabh - All Rights Reserved Unauthorized copying of this file, via any medium is strictly prohibited Proprietary and confidential Written by Ayushya Amitabh , 2020 https://www.chromewatchparty.com/copyright ========================================================================= ALL CONTENT ON WATCH PARTY'S WEBSITE OR IN WATCH PARTY'S CHROME EXTENSION APP, UNLESS OTHERWISE CITED ARE THE SOLE INTELECTUAL PROPERTY OF AYUSHYA AMITABH, AND MAY NOT BE COPIED, EDITED, OR REPRODUCED WITHOUT PERMISSION. ************************************************************************/ /*global chrome */ import React, { Component } from 'react'; import { Typography, TextField, InputAdornment, IconButton, ButtonGroup, Button, ExpansionPanel, ExpansionPanelSummary, ExpansionPanelDetails, Card, CardContent, Tooltip, Switch, FormControlLabel, Chip, CircularProgress, Paper, Grid, Slider, } from '@material-ui/core'; import Thread from './Thread'; import { SendMessageApi, LeaveRoomApi, DeleteRoomApi, UpdatePlayerApi, getPlayerSource, PLAYER_SOURCE, LocalData, PLAYER_STATUS, updateLocalData, UpdateAdminOnly, scrollThread, removeLocalData, } from './Api'; import { Send, ExitToApp as Exit, Share, Delete, CallRounded as PhoneEnable, CallEndRounded as PhoneDisable, MicOff, Mic as MicOn, Gif, KeyboardArrowDown, PauseCircleFilled, LinkOutlined, PlayCircleFilled, Cancel, Error, FullscreenExit, Fullscreen, VolumeDown, VolumeUp, CancelRounded, } from '@material-ui/icons'; import Create from './Create'; import { goTo } from 'route-lite'; import CallManager from './CallManager'; import VideoListener from './VideoListener'; import ReactGiphySearchbox from 'react-giphy-searchbox'; // const giphyFetch = new GiphyFetch('ojJbIMUN6XRMn4mHiFFav3rMCpXUg8sq'); export default class Chat extends Component { constructor(props) { super(props); this.state = { name: '', messages: [], newMessage: '', admin: null, adminOnly: false, adminModeUpdate: false, users: [], usersOnCall: {}, gif: false, onCall: false, muted: false, callActionPending: false, menuPopover: true, allowChatNotification: LocalData().allowChatNotification !== undefined ? LocalData().allowChatNotification : true, isFullscreen: document.fullscreen, allowAutoRejoin: LocalData().allowAutoRejoin !== undefined ? LocalData().allowAutoRejoin : true, replyTo: null, sendingMessages: [], volumes: {}, }; this.port = null; this.gifClick = this.gifClick.bind(this); this.endParty = this.endParty.bind(this); this.handleMute = this.handleMute.bind(this); this.leaveParty = this.leaveParty.bind(this); this.shareParty = this.shareParty.bind(this); this.syncPlayer = this.syncPlayer.bind(this); this.setReplyTo = this.setReplyTo.bind(this); this.sendMessage = this.sendMessage.bind(this); this.updateAdminOnly = this.updateAdminOnly.bind(this); this.handlePhoneCall = this.handlePhoneCall.bind(this); this.getReplyToContent = this.getReplyToContent.bind(this); this.callEventListener = this.callEventListener.bind(this); this.sendMessageInvoker = this.sendMessageInvoker.bind(this); this.videoEventListener = this.videoEventListener.bind(this); this.mVideoListener = new VideoListener(this.videoEventListener); this.updateAllowAutoRejoin = this.updateAllowAutoRejoin.bind(this); this.updateAllowChatNotification = this.updateAllowChatNotification.bind(this); this.mCallManager = new CallManager(this.callEventListener, this.props.notify); } componentDidMount() { // EVENT CAPTURE FOR ENTER TO SEND window.onkeypress = (ev) => { if (ev.code === 'Enter') { if (ev.target === document.getElementById('chat-message-box-input')) { if (ev.shiftKey === false && this.state.gif === true) ev.preventDefault(); if (ev.shiftKey === false && this.state.gif === false) { ev.preventDefault(); this.sendMessageInvoker(); } } } }; // FULLSCREEN LISTENER document.addEventListener('fullscreenchange', (ev) => { this.setState({ isFullscreen: document.fullscreen }); }); // ACTIVE TRACKER if (LocalData().allowAutoRejoin === undefined || LocalData().allowAutoRejoin === null) { updateLocalData({ allowAutoRejoin: this.state.allowAutoRejoin }); } if (this.state.allowAutoRejoin === true) { updateLocalData({ lastActive: new Date().getTime() }); } setInterval(() => { if (this.state.allowAutoRejoin === true) { updateLocalData({ lastActive: new Date().getTime() }); } }, 60000); // VIDEO LISTENER START this.mVideoListener.start(); // CHROME FIREBASE FIRESTORE LISTENER this.port = chrome.runtime.connect({ name: LocalData().partyName }); // Initialize listener this.port.postMessage({ type: 'INIT', roomId: LocalData().partyName, roomPin: LocalData().partyPin, }); // Listener this.port.onMessage.addListener((response) => { if (response.isError === true) { this.props.notify('Uh oh. Got lost from the party.'); goTo(Create, this.props); return; } if (response.messages) { scrollThread(); } // IF CHAT IS HIDDEN AND ALLOWS NOTIFICATION if (!document.querySelector('body.visible') && this.state.allowChatNotification === true) { if (response.messages && response.messages.length !== this.state.messages.length) { const lastMessage = response.messages[response.messages.length - 1]; if (lastMessage.uid !== '') { this.props.notify( `${lastMessage.name}${ lastMessage.type === 'giphy' ? ' sent a gif.' : ': ' + lastMessage.text }` ); } } } // ADMIN MODE UPDATE COMPLETE if (response.adminOnly !== this.state.adminOnly) { this.setState({ adminModeUpdate: false }); } this.setState(response); if (response.player) this.mVideoListener.doUpdate(response.player); }); // GIPHY IPC const giphyInject = `var giphySearchInteraction=function(e){if(e.source===window){const{data}=e;if(data.watchPartyDirection==='TO_SCRIPT'){if(data.type==='WATCH_PARTY_SEARCH_GIF'){const giphyInput=document.querySelector('.giphy-search-form input');if(giphyInput){giphyInput.value=data.value;giphyInput.dispatchEvent(new Event('change',{bubbles:true}))}}}}};window.addEventListener("message",giphySearchInteraction);`; const giphyInjectScript = document.createElement('script'); giphyInjectScript.text = giphyInject; document.head.appendChild(giphyInjectScript); // DONE WITH MOUNTING TASKS this.props.updateLoading(false); } componentWillUnmount() { this.mVideoListener.end(); this.leaveParty(); } videoEventListener(video) { const { adminOnly, admin } = this.state; if (!adminOnly || (adminOnly && admin)) { this.syncPlayer(video); } } callEventListener(event) { this.setState(event); } syncPlayer(video) { const { status, showName, time, source, element, showChanged } = video; if (element) { UpdatePlayerApi({ roomId: LocalData().partyName || this.state.name, showChanged, userName: LocalData().userName, player: { status, showName, time, source, updatedBy: LocalData().userUid, url: window.location.href, }, }).catch((err) => { this.props.updateLoading(false); this.props.notify(err.message || err.statusText); console.error('Sync Party API error', err); }); } else { this.mVideoListener.start(); } } endParty() { this.props.notify('Ending party...'); this.props.updateLoading(true); const data = { partyName: LocalData().partyName, }; if (this.state.onCall) this.mCallManager.toggleCall( this.state.onCall, this.state.conference, this.state.admin, this.state.adminOnly ); DeleteRoomApi(data) .then((res) => { this.props.notify('Ended party'); removeLocalData('partyPin'); removeLocalData('partyName'); goTo(Create, this.props); this.props.updateLoading(false); }) .catch((err) => { this.props.updateLoading(false); this.props.notify(err.message || err.statusText); console.error('End Party API error', err); }); } leaveParty() { this.props.notify('Leaving party...'); this.props.updateLoading(true); const data = { partyName: LocalData().partyName, userUid: LocalData().userUid, }; if (this.state.onCall) this.mCallManager.toggleCall( this.state.onCall, this.state.conference, this.state.admin, this.state.adminOnly ); LeaveRoomApi(data) .then((res) => { this.props.notify('Left party'); removeLocalData('partyPin'); removeLocalData('partyName'); goTo(Create, this.props); this.props.updateLoading(false); }) .catch((err) => { this.props.updateLoading(false); this.props.notify(err.message || err.statusText); console.error('End Party API error', err); }); } shareParty() { const el = document.createElement('textarea'); el.value = `Join my party on Watch Party!\nParty Name: ${LocalData().partyName}\nParty Pin: ${ LocalData().partyPin }\n${window.location.href}\n---\nGet Watch Party at https:\\\\get.chromewatchparty.com`; document.body.appendChild(el); el.select(); document.execCommand('copy'); document.body.removeChild(el); this.props.notify('Copied party invitation'); } sendMessage(message) { const { sendingMessages } = this.state; const data = { id: `sending-${sendingMessages.length}`, roomId: LocalData().partyName, name: LocalData().userName, uid: LocalData().userUid, text: message, time: new Date(), type: '', }; if (this.state.gif === true) data.type = 'giphy'; if (this.state.replyTo !== null) data.replyTo = this.state.replyTo.id; sendingMessages.push(data); this.setState({ sendingMessages, }); SendMessageApi(data) .then(() => { const { sendingMessages: sm } = this.state; const rmi = sm.indexOf(data); sm.splice(rmi, 1); this.setState({ sendingMessages: sm }); scrollThread(); }) .catch((err) => { const { sendingMessages: sm } = this.state; const rmi = sm.indexOf(data); sm.splice(rmi, 1); this.setState({ sendingMessages: sm }); scrollThread(); this.props.notify(err.message || err.statusText); console.error('Send Message API error', err); }); this.setState({ replyTo: null }); scrollThread(); } sendMessageInvoker(overrideMessage) { if (overrideMessage) this.sendMessage(overrideMessage); else { const trim = this.state.newMessage.replace(/^\s+|\s+$/g, ''); if (trim !== '') this.sendMessage(trim); } this.setState({ newMessage: '' }); } gifClick(iGif) { this.sendMessageInvoker(iGif.images.original.url); } handlePhoneCall() { const { onCall, conference, admin, adminOnly } = this.state; if (window.navigator && onCall === false) { try { const overlay = document.createElement('div'); overlay.innerHTML = `

Grant Mic Permission

`; overlay.id = 'mic-permission-overlay'; document.body.appendChild(overlay); window.navigator .getUserMedia({ audio: true, }) .then(() => { document.getElementById('mic-permission-overlay').remove(); this.mCallManager.toggleCall(onCall, conference, admin, adminOnly); }) .catch(() => { document.getElementById('mic-permission-overlay').remove(); this.props.notify( 'Check your mic permission for this website. You can modify site permission by clicking on the lock icon in the top left.' ); }); } catch (err) { console.error('Handle Phone Call Error', err); } } else this.mCallManager.toggleCall(onCall, conference, admin, adminOnly); } handleMute() { this.mCallManager.toggleMute(this.state.muted); } getPrettyTime(t) { return t > 3600 ? new Date(t * 1000).toISOString().substr(11, 8) : new Date(t * 1000).toISOString().substr(14, 5); } updateAllowChatNotification() { updateLocalData({ allowChatNotification: !this.state.allowChatNotification }); this.setState({ allowChatNotification: !this.state.allowChatNotification }); } updateAllowAutoRejoin() { updateLocalData({ allowAutoRejoin: !this.state.allowAutoRejoin }); this.setState({ allowAutoRejoin: !this.state.allowAutoRejoin }); } toggleFullscreen() { if (document.fullscreen) { document.exitFullscreen(); } else { document.documentElement.requestFullscreen(); } } updateAdminOnly() { this.setState({ adminModeUpdate: true }); UpdateAdminOnly({ partyName: this.state.name, adminOnly: !this.state.adminOnly, }).catch((err) => { this.props.notify(err.message || err.statusText); console.error('Update Admin Only API error', err); }); } setReplyTo(message) { this.setState({ replyTo: message, }); scrollThread(); } getReplyToContent() { const { replyTo } = this.state; if (replyTo) { if (replyTo.type === 'giphy') { return gif; } return ( {replyTo.text} ); } return null; } setVideoVolume(v, id) { const audioOuts = document.querySelectorAll('audio.vxt-audio'); if (audioOuts.length > 0) { audioOuts.forEach((el) => { if (el.id === `audio-${id}`) { el.volume = v * 0.1; const { volumes } = this.state; volumes[id] = v; this.setState({ volumes }); } }); } } render() { const { admin, name, messages, newMessage, gif, onCall, muted, player, allowChatNotification, users, callActionPending, allowAutoRejoin, usersOnCall, isFullscreen, adminModeUpdate, adminOnly, replyTo, sendingMessages, volumes, } = this.state; const alerts = [ { condition: () => getPlayerSource() === PLAYER_SOURCE.HBOMAX || getPlayerSource() === PLAYER_SOURCE.HBONOW, message: 'Functionality may be buggy, HBO has a horrible design.', }, { condition: () => getPlayerSource() === PLAYER_SOURCE.GOOGLEPLAY, message: 'Functionality may be buggy, support on this site is in testing.', }, { condition: () => getPlayerSource() === PLAYER_SOURCE.CRUNCHYROLL, message: 'Functionality may be buggy, support on this site is in testing.', }, { condition: () => adminOnly, message: 'Party is currently in admin only mode.', }, ]; return (
} >
WATCH PARTY {name}
), }} /> {admin && ( )} } label={ Allow chat notification when side panel is closed
{allowChatNotification ? 'Notifications Enabled' : 'Notifications Disabled'}
} labelPlacement="end" /> } label={ Allow Watch Party to auto rejoin (within 1 minute)
{allowAutoRejoin ? 'Auto Rejoin Enabled' : 'Auto Rejoin Disabled'}
} labelPlacement="end" /> {admin ? ( } label={ Admin Only Mode
{adminOnly ? 'Only admin can update player and start calls right now' : 'All party members can update player and start calls right now'}
} labelPlacement="end" /> ) : null} PARTY MEMBERS
{users.map((user) => ( ))}
{Object.keys(usersOnCall).length > 0 ? ( MEMBERS IN CALL ) : null} {Object.keys(usersOnCall).length > 0 ? (
{Object.keys(usersOnCall).map((id) => { return usersOnCall[id].status === 'Connected' || usersOnCall[id].status === 'Connecting' ? ( this.setVideoVolume(v, id)} /> } > ) : null; })}
) : null}
{player ? ( {player.showName && player.status !== PLAYER_STATUS.NOT_PLAYING ? ( {player.status === PLAYER_STATUS.PAUSED ? `PAUSED AT ${this.getPrettyTime(player.time)}` : player.status === PLAYER_STATUS.PLAYING ? `STARTED PLAYING FROM ${this.getPrettyTime(player.time)}` : `SHOW IS AT ${this.getPrettyTime(player.time)}`}
{player.status === PLAYER_STATUS.PAUSED ? ( ) : ( )}
{player.showName}
) : ( Nothing playing right now )} {player && player.showName && player.status ? ( ) : null}
) : null}
{alerts.map((a) => { if (a.condition() === true) { return ( {a.message} ); } else return null; })}
REPLYING TO {this.getReplyToContent()}
this.setReplyTo(null)}>
{gif || newMessage ? null : ( {isFullscreen ? : } )} {gif || newMessage ? null : callActionPending ? ( ) : ( {onCall ? ( ) : ( )} )} {!gif && !newMessage && onCall && !muted ? ( ) : !gif && !newMessage && onCall && muted ? ( ) : null} { this.setState({ newMessage: e.target.value }); scrollThread(); if (gif) { window.postMessage({ watchPartyDirection: 'TO_SCRIPT', type: 'WATCH_PARTY_SEARCH_GIF', value: e.target.value, }); } }} InputProps={{ id: 'chat-message-box-input', endAdornment: ( this.sendMessageInvoker(newMessage)} edge="end" > ), startAdornment: ( { this.setState({ gif: !gif }); if (!gif) { window.postMessage({ watchPartyDirection: 'TO_SCRIPT', type: 'WATCH_PARTY_SEARCH_GIF', value: newMessage, }); scrollThread(); } }} edge="start" > {gif ? : } ), }} />
); } }