Files
watchparty/app/src/Chat.js
T

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>
);
}
}