1240 lines
41 KiB
JavaScript
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>
|
|
);
|
|
}
|
|
}
|