620 lines
21 KiB
JavaScript
620 lines
21 KiB
JavaScript
/************************************************************************
|
|
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 <admin@chromewatchparty.com>, 2020
|
|
https://www.chromewatchparty.com/copyright
|
|
=========================================================================
|
|
ALL CONTENT ON 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.
|
|
|
|
Netflix logo is a trademark and intellectual property of Netflix, Inc.
|
|
Disney+ logo is a trademark and intellectual property of The Walt Disney Company
|
|
Prime logo is a trademark and intellectual property of Amazon.com, Inc.
|
|
YouTube logo is a trademark and intellectual property of Google, Inc.
|
|
Hulu logo is a trademark and intellectual property of Hulu, LLC.
|
|
HBO Now and HBO Max logos are trademark and intellectual property of WarnerMedia Direct,LLC.
|
|
************************************************************************/
|
|
/*global chrome */
|
|
import React, { Component } from 'react';
|
|
import {
|
|
Typography,
|
|
TextField,
|
|
InputAdornment,
|
|
IconButton,
|
|
ButtonGroup,
|
|
Button,
|
|
ExpansionPanel,
|
|
ExpansionPanelSummary,
|
|
ExpansionPanelDetails,
|
|
Card,
|
|
CardContent,
|
|
Tooltip,
|
|
Switch,
|
|
FormControlLabel,
|
|
Chip,
|
|
} from '@material-ui/core';
|
|
import Thread from './Thread';
|
|
import { Carousel } from '@giphy/react-components';
|
|
import { GiphyFetch } from '@giphy/js-fetch-api';
|
|
import {
|
|
SendMessageApi,
|
|
LeaveRoomApi,
|
|
DeleteRoomApi,
|
|
UpdatePlayerApi,
|
|
getPlayerSource,
|
|
PLAYER_SOURCE,
|
|
LocalData,
|
|
PLAYER_STATUS,
|
|
emptyLocalData,
|
|
updateLocalData,
|
|
} 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,
|
|
} from '@material-ui/icons';
|
|
import Create from './Create';
|
|
import { goTo } from 'route-lite';
|
|
import CallManager from './CallManager';
|
|
import VideoListener from './VideoListener';
|
|
|
|
const giphyFetch = new GiphyFetch('ojJbIMUN6XRMn4mHiFFav3rMCpXUg8sq');
|
|
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.PEACOCK,
|
|
message: 'Functionality may be buggy, support on this site is in testing.',
|
|
},
|
|
];
|
|
export default class Chat extends Component {
|
|
constructor(props) {
|
|
super(props);
|
|
this.state = {
|
|
name: '',
|
|
messages: [],
|
|
newMessage: '',
|
|
admin: null,
|
|
users: [],
|
|
usersOnCall: {},
|
|
gif: false,
|
|
onCall: false,
|
|
muted: false,
|
|
callActionPending: false,
|
|
menuPopover: true,
|
|
allowChatNotification:
|
|
LocalData().allowChatNotification !== undefined ? LocalData().allowChatNotification : true,
|
|
isFullscreen: document.fullscreen,
|
|
};
|
|
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.sendMessage = this.sendMessage.bind(this);
|
|
this.getCarousel = this.getCarousel.bind(this);
|
|
this.handlePhoneCall = this.handlePhoneCall.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.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
|
|
updateLocalData({ lastActive: new Date().getTime() });
|
|
setInterval(() => {
|
|
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,
|
|
});
|
|
// 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 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];
|
|
var notificationMessage = '';
|
|
if (lastMessage.uid !== '<system>') notificationMessage += lastMessage.name + ': ';
|
|
notificationMessage += lastMessage.text;
|
|
this.props.notify(notificationMessage);
|
|
}
|
|
}
|
|
this.setState(response);
|
|
if (response.player) this.mVideoListener.doUpdate(response.player);
|
|
const pcc = document.querySelector('.thread-container');
|
|
if (pcc) pcc.scrollTop = pcc.scrollHeight;
|
|
});
|
|
|
|
// DONE WITH MOUNTING TASKS
|
|
this.props.updateLoading(false);
|
|
}
|
|
componentWillUnmount() {
|
|
this.mVideoListener.end();
|
|
this.leaveParty();
|
|
}
|
|
videoEventListener(video) {
|
|
this.syncPlayer(video);
|
|
}
|
|
callEventListener(event) {
|
|
this.setState(event);
|
|
}
|
|
syncPlayer(video) {
|
|
const { status, showName, time, source, element, showChanged } = video;
|
|
if (element) {
|
|
UpdatePlayerApi({
|
|
roomId: LocalData().partyName,
|
|
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,
|
|
};
|
|
DeleteRoomApi(data)
|
|
.then((res) => {
|
|
this.props.notify('Ended party');
|
|
emptyLocalData();
|
|
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,
|
|
};
|
|
LeaveRoomApi(data)
|
|
.then((res) => {
|
|
this.props.notify('Left party');
|
|
emptyLocalData();
|
|
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 data = {
|
|
roomId: LocalData().partyName,
|
|
name: LocalData().userName,
|
|
uid: LocalData().userUid,
|
|
text: message,
|
|
time: new Date(),
|
|
type: '',
|
|
};
|
|
if (this.state.gif === true) data.type = 'giphy';
|
|
SendMessageApi(data).catch((err) => {
|
|
this.props.notify(err.message || err.statusText);
|
|
console.error('Send Message API error', err);
|
|
});
|
|
}
|
|
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, ev) {
|
|
ev.preventDefault();
|
|
this.sendMessageInvoker(iGif.images.original.url);
|
|
}
|
|
getCarousel(offset) {
|
|
return this.state.newMessage === ''
|
|
? giphyFetch.trending({ offset, limit: 1 })
|
|
: giphyFetch.search(this.state.newMessage, { offset, limit: 1 });
|
|
}
|
|
handlePhoneCall() {
|
|
const { onCall, conference, admin } = this.state;
|
|
this.mCallManager.toggleCall(onCall, conference, admin);
|
|
}
|
|
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 });
|
|
}
|
|
toggleFullscreen() {
|
|
if (document.fullscreen) {
|
|
document.exitFullscreen();
|
|
} else {
|
|
document.documentElement.requestFullscreen();
|
|
}
|
|
}
|
|
render() {
|
|
const {
|
|
admin,
|
|
name,
|
|
messages,
|
|
newMessage,
|
|
gif,
|
|
onCall,
|
|
muted,
|
|
player,
|
|
allowChatNotification,
|
|
users,
|
|
callActionPending,
|
|
usersOnCall,
|
|
isFullscreen,
|
|
} = this.state;
|
|
return (
|
|
<div className="party-chat-container">
|
|
<div className="party-chat-header">
|
|
<ExpansionPanel elevation={0} className="expander">
|
|
<ExpansionPanelSummary
|
|
expandIcon={
|
|
<Tooltip
|
|
placement="bottom-end"
|
|
title="Share, leave, and other controls"
|
|
aria-label="Share, leave, and other controls"
|
|
>
|
|
<KeyboardArrowDown />
|
|
</Tooltip>
|
|
}
|
|
>
|
|
<div className="party-chat-header-text">
|
|
<Typography
|
|
color="secondary"
|
|
className="brand-title"
|
|
variant="caption"
|
|
display="block"
|
|
>
|
|
WATCH PARTY
|
|
</Typography>
|
|
<Typography color="textPrimary" className="room-title" variant="h4" gutterBottom>
|
|
{name}
|
|
</Typography>
|
|
</div>
|
|
</ExpansionPanelSummary>
|
|
<ExpansionPanelDetails>
|
|
<TextField
|
|
className="share-textbox"
|
|
label="Party Details"
|
|
value={`Party Name: ${LocalData().partyName}\nParty Pin: ${LocalData().partyPin}\n${
|
|
window.location.href
|
|
}`}
|
|
multiline
|
|
rowsMax={2}
|
|
variant="outlined"
|
|
fullWidth
|
|
color="secondary"
|
|
InputProps={{
|
|
endAdornment: (
|
|
<Tooltip title="Share party" aria-label="Share party">
|
|
<InputAdornment position="end">
|
|
<IconButton onClick={this.shareParty}>
|
|
<Share />
|
|
</IconButton>
|
|
</InputAdornment>
|
|
</Tooltip>
|
|
),
|
|
}}
|
|
/>
|
|
<ButtonGroup fullWidth className="party-controls" size="small">
|
|
<Button onClick={this.leaveParty} startIcon={<Exit />}>
|
|
<Typography variant="caption" className="party-control-button-text">
|
|
Leave
|
|
</Typography>
|
|
</Button>
|
|
{admin && (
|
|
<Button onClick={this.endParty} startIcon={<Delete />}>
|
|
<Typography variant="caption" className="party-control-button-text">
|
|
End
|
|
</Typography>
|
|
</Button>
|
|
)}
|
|
</ButtonGroup>
|
|
<FormControlLabel
|
|
className="notification-switch"
|
|
control={
|
|
<Switch
|
|
checked={allowChatNotification}
|
|
onChange={this.updateAllowChatNotification}
|
|
name="allowChatNotification"
|
|
color={allowChatNotification ? 'secondary' : 'primary'}
|
|
/>
|
|
}
|
|
label={
|
|
<span>
|
|
Allow chat notification when side panel is closed
|
|
<br />
|
|
<span style={{ color: '#8a8a8a' }}>
|
|
{allowChatNotification ? 'Currently Enabled' : 'Currently Disabled'}
|
|
</span>
|
|
</span>
|
|
}
|
|
labelPlacement="end"
|
|
/>
|
|
<Typography className="party-controls-users-title" variant="overline">
|
|
PARTY MEMBERS
|
|
</Typography>
|
|
<div className="party-controls-users-container">
|
|
{users.map((user) => (
|
|
<Chip className="party-controls-users-chip" clickable size="small" label={user} />
|
|
))}
|
|
</div>
|
|
{Object.keys(usersOnCall).length > 0 ? (
|
|
<Typography className="party-controls-users-title" variant="overline">
|
|
MEMBERS IN CALL
|
|
</Typography>
|
|
) : null}
|
|
{Object.keys(usersOnCall).length > 0 ? (
|
|
<div className="party-controls-users-container">
|
|
{Object.keys(usersOnCall).map((id) => {
|
|
return usersOnCall[id].status === 'Connected' ||
|
|
usersOnCall[id].status === 'Connecting' ? (
|
|
<Chip
|
|
color={
|
|
usersOnCall[id].info.externalId === LocalData().userUid
|
|
? 'secondary'
|
|
: 'default'
|
|
}
|
|
className="party-controls-users-chip"
|
|
clickable
|
|
size="small"
|
|
label={usersOnCall[id].info.name || ' '}
|
|
/>
|
|
) : null;
|
|
})}
|
|
</div>
|
|
) : null}
|
|
</ExpansionPanelDetails>
|
|
</ExpansionPanel>
|
|
{player ? (
|
|
<Card elevation={0} className="media-info-card">
|
|
{player.showName && player.status !== PLAYER_STATUS.NOT_PLAYING ? (
|
|
<CardContent>
|
|
<Typography className="media-info-at" variant="overline">
|
|
{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)}`}
|
|
</Typography>
|
|
<div className="media-info-container">
|
|
{player.status === PLAYER_STATUS.PAUSED ? (
|
|
<PauseCircleFilled className="media-info-icon" />
|
|
) : (
|
|
<PlayCircleFilled className="media-info-icon" />
|
|
)}
|
|
<div className="media-info-right">
|
|
<Typography className="show-name" variant="subtitle1">
|
|
{player.showName}
|
|
</Typography>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
) : (
|
|
<CardContent>
|
|
<Typography className="media-info-at" variant="caption">
|
|
Nothing playing right now
|
|
</Typography>
|
|
</CardContent>
|
|
)}
|
|
{player && player.showName && player.status ? (
|
|
<Button className="media-info-link" href={player.url} startIcon={<LinkOutlined />}>
|
|
Link to show
|
|
</Button>
|
|
) : null}
|
|
</Card>
|
|
) : null}
|
|
</div>
|
|
<div className="thread-container">
|
|
<div className="alerts-container">
|
|
{alerts.map((a) => {
|
|
if (a.condition() === true) {
|
|
return (
|
|
<Card className="alert-card" variant="outlined">
|
|
<CardContent className="alert-card-content">
|
|
<Error />
|
|
<Typography variant="body2">{a.message}</Typography>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
} else return null;
|
|
})}
|
|
</div>
|
|
<Thread messages={messages} />
|
|
</div>
|
|
<div className="text-phone-panel">
|
|
{gif || newMessage ? null : (
|
|
<Tooltip title="Fullscreen with Watch Party" aria-label="Fullscreen">
|
|
<IconButton color="secondary" onClick={this.toggleFullscreen}>
|
|
{isFullscreen ? <FullscreenExit /> : <Fullscreen />}
|
|
</IconButton>
|
|
</Tooltip>
|
|
)}
|
|
{gif || newMessage ? null : (
|
|
<IconButton
|
|
disabled={callActionPending}
|
|
onClick={this.handlePhoneCall}
|
|
color={!onCall ? 'secondary' : 'primary'}
|
|
>
|
|
{onCall ? (
|
|
<Tooltip placement="top" title="Leave Call">
|
|
<PhoneDisable />
|
|
</Tooltip>
|
|
) : (
|
|
<Tooltip placement="top" title="Join Call">
|
|
<PhoneEnable />
|
|
</Tooltip>
|
|
)}
|
|
</IconButton>
|
|
)}
|
|
{!gif && !newMessage && onCall && !muted ? (
|
|
<Tooltip title="Mute Mic" placement="top">
|
|
<IconButton disabled={callActionPending} onClick={this.handleMute} color="primary">
|
|
<MicOff />
|
|
</IconButton>
|
|
</Tooltip>
|
|
) : !gif && !newMessage && onCall && muted ? (
|
|
<Tooltip title="Unmute Mic" placement="top">
|
|
<IconButton disabled={callActionPending} onClick={this.handleMute} color="secondary">
|
|
<MicOn />
|
|
</IconButton>
|
|
</Tooltip>
|
|
) : null}
|
|
<TextField
|
|
color="secondary"
|
|
multiline
|
|
rowsMax="3"
|
|
size="small"
|
|
placeholder={this.state.gif ? 'Search something...' : 'Say something...'}
|
|
helperText={
|
|
this.state.newMessage && !this.state.gif
|
|
? 'Enter to send / Shift+Enter for new line'
|
|
: this.state.gif
|
|
? 'Click on gif to send'
|
|
: null
|
|
}
|
|
variant="outlined"
|
|
className="chat-message-box addFocus"
|
|
value={newMessage}
|
|
onChange={(e) => {
|
|
this.setState({ newMessage: e.target.value });
|
|
if (this.state.gif) {
|
|
const carousel = document.querySelector('.giphy-carousel');
|
|
setTimeout(() => carousel.scroll(carousel.scrollWidth, carousel.scrollHeight), 250);
|
|
}
|
|
}}
|
|
InputProps={{
|
|
id: 'chat-message-box-input',
|
|
endAdornment: (
|
|
<InputAdornment position="end">
|
|
<IconButton
|
|
className={`${this.state.gif ? 'gif-active' : ''} ${
|
|
this.state.newMessage ? 'message-active' : ''
|
|
} message-send`}
|
|
size="small"
|
|
onClick={() => this.sendMessageInvoker(newMessage)}
|
|
edge="end"
|
|
>
|
|
<Send />
|
|
</IconButton>
|
|
</InputAdornment>
|
|
),
|
|
startAdornment: (
|
|
<InputAdornment position="start">
|
|
<IconButton onClick={() => this.setState({ gif: !gif })} edge="start">
|
|
{gif ? <Cancel /> : <Gif />}
|
|
</IconButton>
|
|
</InputAdornment>
|
|
),
|
|
}}
|
|
/>
|
|
</div>
|
|
{gif ? (
|
|
<Carousel
|
|
fetchGifs={this.getCarousel}
|
|
gifHeight={200}
|
|
gutter={6}
|
|
onGifClick={this.gifClick}
|
|
hideAttribution
|
|
/>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|
|
}
|