445 lines
15 KiB
JavaScript
445 lines
15 KiB
JavaScript
/*global chrome */
|
|
import React, { Component } from 'react';
|
|
import {
|
|
Typography,
|
|
TextField,
|
|
InputAdornment,
|
|
IconButton,
|
|
ButtonGroup,
|
|
Button,
|
|
ExpansionPanel,
|
|
ExpansionPanelSummary,
|
|
ExpansionPanelDetails,
|
|
Card,
|
|
CardContent,
|
|
Tooltip,
|
|
} from '@material-ui/core';
|
|
import Thread from './Thread';
|
|
import Send from '@material-ui/icons/Send';
|
|
import Exit from '@material-ui/icons/ExitToApp';
|
|
import Share from '@material-ui/icons/Share';
|
|
import Delete from '@material-ui/icons/Delete';
|
|
import PhoneEnable from '@material-ui/icons/CallRounded';
|
|
import PhoneDisable from '@material-ui/icons/CallEndRounded';
|
|
import MicOff from '@material-ui/icons/MicOff';
|
|
import MicOn from '@material-ui/icons/Mic';
|
|
import Gif from '@material-ui/icons/Gif';
|
|
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,
|
|
} from './Api';
|
|
import { goTo } from 'route-lite';
|
|
import Create from './Create';
|
|
import VideoListener from './VideoListener';
|
|
import CallManager from './CallManager';
|
|
import { MoreVert, PauseCircleFilled, LinkOutlined, PlayCircleFilled } from '@material-ui/icons';
|
|
|
|
const giphyFetch = new GiphyFetch('ojJbIMUN6XRMn4mHiFFav3rMCpXUg8sq');
|
|
const alertMessage = {
|
|
name: '<alert>',
|
|
text: 'Functionality may be buggy, support on this site is in beta.',
|
|
time: '',
|
|
uid: '<system>',
|
|
};
|
|
export default class Chat extends Component {
|
|
constructor(props) {
|
|
super(props);
|
|
this.state = {
|
|
name: '',
|
|
messages: [],
|
|
newMessage: '',
|
|
admin: null,
|
|
gif: false,
|
|
onCall: false,
|
|
muted: false,
|
|
menuPopover: true,
|
|
};
|
|
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.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);
|
|
}
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// 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;
|
|
}
|
|
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();
|
|
}
|
|
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);
|
|
}
|
|
render() {
|
|
const { admin, name, messages, newMessage, gif, onCall, muted, player } = this.state;
|
|
if (getPlayerSource() === PLAYER_SOURCE.HBONOW && messages[0] !== alertMessage)
|
|
messages.unshift(alertMessage);
|
|
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"
|
|
>
|
|
<MoreVert />
|
|
</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
|
|
label="Party Details"
|
|
value={`Party Name: ${LocalData().partyName}\nParty Pin: ${LocalData().partyPin}\n${
|
|
window.location.href
|
|
}`}
|
|
multiline
|
|
rowsMax={2}
|
|
variant="outlined"
|
|
fullWidth
|
|
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>
|
|
</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">
|
|
<Thread messages={messages} />
|
|
</div>
|
|
<div className="text-phone-panel">
|
|
{gif || newMessage ? null : (
|
|
<IconButton 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 onClick={this.handleMute} color="primary">
|
|
<MicOff />
|
|
</IconButton>
|
|
</Tooltip>
|
|
) : !gif && !newMessage && onCall && muted ? (
|
|
<Tooltip title="Unmute Mic" placement="top">
|
|
<IconButton 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' : 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 />
|
|
</IconButton>
|
|
</InputAdornment>
|
|
),
|
|
}}
|
|
/>
|
|
</div>
|
|
{gif ? (
|
|
<Carousel
|
|
fetchGifs={this.getCarousel}
|
|
gifHeight={200}
|
|
gutter={6}
|
|
onGifClick={this.gifClick}
|
|
hideAttribution
|
|
/>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|
|
}
|