Files
watchparty/app/src/Chat.js
T
2022-03-29 23:09:52 -04:00

1240 lines
41 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 - 2022
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 INTELLECTUAL PROPERTY OF AYUSHYA AMITABH, AND MAY NOT BE
COPIED, EDITED, OR REPRODUCED WITHOUT PERMISSION.
************************************************************************/
/*global chrome*/
/*global browser*/
import React, { Component } from "react";
import {
Typography,
TextField,
InputAdornment,
IconButton,
ButtonGroup,
Button,
ExpansionPanel,
ExpansionPanelSummary,
ExpansionPanelDetails,
Card,
CardContent,
Tooltip,
Switch,
FormControlLabel,
Chip,
CircularProgress,
Paper,
Select,
MenuItem,
FormControl,
InputLabel,
} from "@material-ui/core";
import Thread from "./Thread";
import {
SendMessageApi,
LeaveRoomApi,
DeleteRoomApi,
UpdatePlayerApi,
LocalData,
PLAYER_STATUS,
updateLocalData,
UpdateAdminOnly,
scrollThread,
removeLocalData,
PLAYER_SOURCE,
getPlayerSource,
} 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,
CancelRounded,
VideocamRounded,
VideocamOffRounded,
Remove,
Add,
} 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";
import VoxeetSDK from "@voxeet/voxeet-web-sdk";
let CHROME_OR_BROWSER = chrome || browser;
export default class Chat extends Component {
constructor(props) {
super(props);
this.state = {
name: "",
messages: [],
newMessage: "",
admin: null,
adminOnly: false,
adminModeUpdate: false,
users: [],
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,
allowAutoFollow:
LocalData().allowAutoFollow !== undefined
? LocalData().allowAutoFollow
: false,
replyTo: null,
sendingMessages: [],
usingVideo: false,
audioInputs: [],
selectedAudioInput: "",
audioOutputs: [],
selectedAudioOutput: "",
videoInputs: [],
selectedVideoInput: "",
minimized: false,
hideVideoContainer: LocalData().hideVideoContainer || false,
};
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,
props.selectors,
props.nameSelectors
);
this.updateAllowAutoRejoin = this.updateAllowAutoRejoin.bind(this);
this.updateAllowAutoFollow = this.updateAllowAutoFollow.bind(this);
this.updateAllowChatNotification =
this.updateAllowChatNotification.bind(this);
this.mCallManager = new CallManager(
this.callEventListener,
this.props.notify
);
this.updateHideVideoContainer = this.updateHideVideoContainer.bind(this);
}
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_OR_BROWSER.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;
} else {
// CONTINUE TUTORIAL
if (this.props.tutorialComplete === false) {
this.props.resetTutorial("chat");
}
}
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 !== "<system>") {
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);
this.updateHideVideoContainer();
setInterval(() => {
if (
this.state.player &&
this.state.player.time &&
this.state.player.status === PLAYER_STATUS.PLAYING
) {
const oldPlayer = this.state.player;
oldPlayer.time += 1;
this.setState({
player: oldPlayer,
});
}
}, 1000);
}
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);
if (event.onCall === true) {
VoxeetSDK.mediaDevice.enumerateAudioDevices("input").then((devices) => {
this.setState({
audioInputs: devices,
selectedAudioInput: devices.length > 0 ? devices[0].deviceId : "",
});
});
VoxeetSDK.mediaDevice.enumerateAudioDevices("output").then((devices) => {
this.setState({
audioOutputs: devices,
selectedAudioOutput: devices.length > 0 ? devices[0].deviceId : "",
});
});
VoxeetSDK.mediaDevice.enumerateVideoDevices("output").then((devices) => {
this.setState({
videoInputs: devices,
selectedVideoInput: devices.length > 0 ? devices[0].deviceId : "",
});
});
}
}
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,
false
);
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,
false
);
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(useVideo = false) {
const { onCall, conference, admin, adminOnly } = this.state;
if (window.navigator && onCall === false) {
try {
const overlay = document.createElement("div");
overlay.innerHTML = `
<div id="mic-highlight"></div>
<h4 id="mic-text">Grant Mic / Camera Permission</h4>
`;
overlay.id = "mic-permission-overlay";
document.body.appendChild(overlay);
window.navigator
.getUserMedia({
audio: true,
})
.then(() => {
if (useVideo === true) {
window.navigator
.getUserMedia({
video: true,
})
.then(() => {
document.getElementById("mic-permission-overlay").remove();
this.mCallManager.toggleCall(
onCall,
conference,
admin,
adminOnly,
useVideo
);
})
.catch(() => {
document.getElementById("mic-permission-overlay").remove();
this.props.notify(
"Check your camera permission for this website. You can modify site permission by clicking on the lock (🔒) icon in the top left, then clicking site permissions.",
15000
);
});
} else {
document.getElementById("mic-permission-overlay").remove();
this.mCallManager.toggleCall(
onCall,
conference,
admin,
adminOnly,
useVideo
);
}
})
.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, then clicking site permissions.",
15000
);
});
} catch (err) {
console.error("Handle Phone Call Error", err);
}
} else
this.mCallManager.toggleCall(
onCall,
conference,
admin,
adminOnly,
useVideo
);
}
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 });
}
updateAllowAutoFollow() {
updateLocalData({ allowAutoFollow: !this.state.allowAutoFollow });
this.setState({ allowAutoFollow: !this.state.allowAutoFollow });
}
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 <img alt="gif" src={replyTo.text} />;
}
return (
<Typography variant="body2" gutterBottom>
{replyTo.text}
</Typography>
);
}
return null;
}
handleVideo() {
const { usingVideo } = this.state;
this.mCallManager.toggleVideo(!usingVideo);
}
updateHideVideoContainer() {
const hideVideoContainer = !this.state.hideVideoContainer;
document.documentElement.style.setProperty('--video-container-pos', hideVideoContainer ? 'relative' : 'fixed');
updateLocalData({ hideVideoContainer });
this.setState({ hideVideoContainer });
}
render() {
const {
admin,
name,
messages,
newMessage,
gif,
onCall,
muted,
player,
allowChatNotification,
users,
callActionPending,
allowAutoRejoin,
allowAutoFollow,
isFullscreen,
adminModeUpdate,
adminOnly,
replyTo,
sendingMessages,
usingVideo,
audioInputs,
selectedAudioInput,
audioOutputs,
selectedAudioOutput,
videoInputs,
selectedVideoInput,
minimized,
hideVideoContainer
} = this.state;
const { status } = this.props;
const { callEnabled } = status;
const alerts = [
{
condition: () => adminOnly,
message: "Party is currently in admin only mode.",
},
{
condition: () => getPlayerSource() === PLAYER_SOURCE.FUNIMATION,
message:
"If sync stops working, try (Ctrl + Shift + R) or (⌘ + Shift + R)",
},
{
condition: () =>
window.location.href.includes(PLAYER_SOURCE.PRIMEVIDEO),
message: "Support on Prime Video is untested, and may be buggy.",
},
];
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>
<div>
<Tooltip
title={
minimized ? "Expand Show Details" : "Minimize Show Details"
}
>
<IconButton
onClickCapture={(ev) => {
this.setState({ minimized: !minimized });
ev.preventDefault();
ev.stopPropagation();
}}
>
{minimized ? <Add /> : <Remove />}
</IconButton>
</Tooltip>
</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"
disableElevation
variant="contained"
color="secondary"
>
<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
? "Notifications Enabled"
: "Notifications Disabled"}
</span>
</span>
}
labelPlacement="end"
/>
<FormControlLabel
className="notification-switch"
control={
<Switch
checked={allowAutoRejoin}
onChange={this.updateAllowAutoRejoin}
name="allowAutoRejoin"
color={allowAutoRejoin ? "secondary" : "primary"}
/>
}
label={
<span>
Allow Watch Party to auto rejoin (within 1 minute)
<br />
<span style={{ color: "#8a8a8a" }}>
{allowAutoRejoin
? "Auto Rejoin Enabled"
: "Auto Rejoin Disabled"}
</span>
</span>
}
labelPlacement="end"
/>
{admin ? (
<FormControlLabel
className="notification-switch"
control={
<Switch
checked={adminOnly}
onChange={this.updateAdminOnly}
name="adminOnly"
color={adminOnly ? "secondary" : "primary"}
disabled={adminModeUpdate}
/>
}
label={
<span>
Admin Only Mode
<br />
<span style={{ color: "#8a8a8a" }}>
{adminOnly
? "Only admin can update player and start calls"
: "All party members can update player and start calls"}
</span>
</span>
}
labelPlacement="end"
/>
) : null}
<FormControlLabel
className="notification-switch"
control={
<Switch
checked={allowAutoFollow}
onChange={this.updateAllowAutoFollow}
name="allowAutoFollow"
/>
}
label={
<span>
(BETA) Allow Watch Party to change to the currently playing
<br />
<span style={{ color: "#8a8a8a" }}>
{allowAutoFollow
? "Auto Follow Enabled"
: "Auto Follow Disabled"}
</span>
</span>
}
labelPlacement="end"
/>
<FormControlLabel
className="notification-switch"
control={
<Switch
checked={hideVideoContainer}
onChange={this.updateHideVideoContainer}
name="hideVideoContainer"
/>
}
label={
<span>
Hide Video Container when sidebar is collapsed
<br />
<span style={{ color: "#8a8a8a" }}>
{hideVideoContainer
? "Video containers will be hidden"
: "Video containers will be shown"}
</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>
{onCall && (
<Typography
className="party-controls-users-title"
variant="overline"
>
DEVICE SELECT
</Typography>
)}
{onCall && (
<div className="vxt-device-selector">
<FormControl>
<InputLabel color="secondary" id="mic-select">
Mic
</InputLabel>
<Select
labelId="mic-select"
value={selectedAudioInput}
color="secondary"
variant="outlined"
onChange={(ev) => {
VoxeetSDK.mediaDevice
.selectAudioInput(ev.target.value)
.then(() => {
this.setState({
selectedAudioInput: ev.target.value,
});
});
}}
>
{audioInputs.map((v) => (
<MenuItem value={v.deviceId}>{v.label}</MenuItem>
))}
</Select>
</FormControl>
<FormControl>
<InputLabel color="secondary" id="speaker-select">
Speaker
</InputLabel>
<Select
labelId="speaker-select"
value={selectedAudioOutput}
color="secondary"
variant="outlined"
onChange={(ev) => {
VoxeetSDK.mediaDevice
.selectAudioOutput(ev.target.value)
.then(() => {
this.setState({
selectedAudioOutput: ev.target.value,
});
});
}}
>
{audioOutputs.map((v) => (
<MenuItem value={v.deviceId}>{v.label}</MenuItem>
))}
</Select>
</FormControl>
<FormControl>
<InputLabel color="secondary" id="camera-select">
Camera
</InputLabel>
<Select
labelId="camera-select"
value={selectedVideoInput}
color="secondary"
fullWidth
variant="outlined"
onChange={(ev) => {
VoxeetSDK.mediaDevice
.selectVideoInput(ev.target.value)
.then(() => {
this.setState({
selectedVideoInput: ev.target.value,
});
});
}}
>
{videoInputs.map((v) => (
<MenuItem value={v.deviceId}>{v.label}</MenuItem>
))}
</Select>
</FormControl>
</div>
)}
</ExpansionPanelDetails>
</ExpansionPanel>
{!minimized && player ? (
<Card elevation={0} className="media-info-card">
{player.showName &&
player.status !== PLAYER_STATUS.NOT_PLAYING ? (
<CardContent>
<Typography
style={{ display: "block" }}
className="media-info-at updated-by"
variant="caption"
color="secondary"
>
Last updated by {player.updatedByName}
</Typography>
<Typography className="media-info-at" variant="overline">
{player.status === PLAYER_STATUS.PAUSED
? `PAUSED AT ${this.getPrettyTime(player.time)}`
: player.status === PLAYER_STATUS.PLAYING
? `CURRENTLY PLAYING (${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>
{allowAutoFollow ? null : (
<Typography
variant="overline"
style={{ fontSize: 9, display: "block", marginTop: 12 }}
>
Watch Party will not automatically change the show for
you.
</Typography>
)}
</CardContent>
) : (
<CardContent>
<Typography
style={{ fontSize: 12, marginBottom: 12 }}
variant="h4"
>
Nothing playing right now
</Typography>
<Typography variant="overline" style={{ fontSize: 9 }}>
Pause / Play once after starting the show to start sync.
<br />
Watch Party will not automatically change the show for you.
</Typography>
</CardContent>
)}
{player && player.showName && player.status ? (
<Button
className="media-info-link"
href={player.url}
startIcon={<LinkOutlined />}
>
Link to currently playing
</Button>
) : null}
</Card>
) : null}
</div>
<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>
<div className="vxt-video-container"></div>
<Thread setReplyTo={this.setReplyTo} sentMessages={messages} />
<Thread
isSending={true}
setReplyTo={this.setReplyTo}
sentMessages={messages}
sendingMessages={sendingMessages}
/>
<Paper
square
variant="outlined"
className={`replying-to-card ${
replyTo !== null ? "replying-to-card-up" : ""
}`}
>
<div className="replying-to-content">
<Typography variant="overline" display="block" gutterBottom>
REPLYING TO
</Typography>
{this.getReplyToContent()}
</div>
<IconButton onClick={() => this.setReplyTo(null)}>
<CancelRounded />
</IconButton>
</Paper>
<div className="text-phone-panel">
{gif || newMessage ? null : (
<Tooltip
title="Fullscreen with Watch Party"
aria-label="Fullscreen"
>
<IconButton
className="icon-button"
color="secondary"
onClick={this.toggleFullscreen}
>
{isFullscreen ? <FullscreenExit /> : <Fullscreen />}
</IconButton>
</Tooltip>
)}
{gif || newMessage || !callEnabled ? null : callActionPending ? (
<CircularProgress className="icon-button" color="secondary" />
) : (
<Tooltip
placement="top"
title={
onCall
? usingVideo
? "Video is On"
: "Video is Off"
: "Join Video Call"
}
>
<IconButton
onClick={
onCall
? () => this.handleVideo()
: () => this.handlePhoneCall(true)
}
color={
(onCall && usingVideo) || !onCall ? "secondary" : "primary"
}
>
{(onCall && usingVideo) || !onCall ? (
<VideocamRounded />
) : (
<VideocamOffRounded />
)}
</IconButton>
</Tooltip>
)}
{gif || newMessage || !callEnabled ? null : callActionPending ? (
<CircularProgress className="icon-button" color="secondary" />
) : (
<Tooltip
placement="top"
title={onCall ? "Leave Call" : "Join Call"}
>
<IconButton
disabled={callActionPending}
onClick={() => this.handlePhoneCall(false)}
color={!onCall ? "secondary" : "primary"}
className="icon-button"
>
{onCall ? <PhoneDisable /> : <PhoneEnable />}
</IconButton>
</Tooltip>
)}
{!gif && !newMessage && onCall && !muted ? (
<Tooltip title="Mic is On" placement="top">
<IconButton
className="icon-button"
disabled={callActionPending}
onClick={this.handleMute}
color="secondary"
>
<MicOn />
</IconButton>
</Tooltip>
) : !gif && !newMessage && onCall && muted ? (
<Tooltip title="Mic is Off" placement="top">
<IconButton
className="icon-button"
disabled={callActionPending}
onClick={this.handleMute}
color="primary"
>
<MicOff />
</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 / Scroll to load more"
: null
}
variant="outlined"
className="chat-message-box addFocus"
value={newMessage}
onChangeCapture={(e) => {
e.stopPropagation();
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: (
<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
className={`gif-button ${
this.state.gif ? "gif-close" : ""
}`}
onClick={() => {
this.setState({ gif: !gif });
if (!gif) {
window.postMessage({
watchPartyDirection: "TO_SCRIPT",
type: "WATCH_PARTY_SEARCH_GIF",
value: newMessage,
});
scrollThread();
}
}}
edge="start"
>
{gif ? <Cancel /> : <Gif />}
</IconButton>
</InputAdornment>
),
}}
/>
</div>
<ReactGiphySearchbox
apiKey="ojJbIMUN6XRMn4mHiFFav3rMCpXUg8sq"
onSelect={this.gifClick}
masonryConfig={[{ columns: 2, imageWidth: "50%", gutter: 5 }]}
searchFormClassName="giphy-search-form"
wrapperClassName={`giphy-wrapper ${gif ? "" : "giphy-wrapper-down"}`}
listWrapperClassName="giphy-list-wrapper"
listItemClassName="giphy-list-item"
poweredByGiphy
/>
</div>
);
}
}