Files
watchparty/app/src/Chat.js
T
2020-08-05 21:45:32 -04:00

906 lines
31 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 WATCH PARTY'S WEBSITE OR 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.
************************************************************************/
/*global chrome */
import React, { Component } from 'react';
import {
Typography,
TextField,
InputAdornment,
IconButton,
ButtonGroup,
Button,
ExpansionPanel,
ExpansionPanelSummary,
ExpansionPanelDetails,
Card,
CardContent,
Tooltip,
Switch,
FormControlLabel,
Chip,
CircularProgress,
Paper,
Grid,
Slider,
} from '@material-ui/core';
import Thread from './Thread';
import {
SendMessageApi,
LeaveRoomApi,
DeleteRoomApi,
UpdatePlayerApi,
getPlayerSource,
PLAYER_SOURCE,
LocalData,
PLAYER_STATUS,
updateLocalData,
UpdateAdminOnly,
scrollThread,
removeLocalData,
} 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,
VolumeDown,
VolumeUp,
CancelRounded,
} 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';
// const giphyFetch = new GiphyFetch('ojJbIMUN6XRMn4mHiFFav3rMCpXUg8sq');
export default class Chat extends Component {
constructor(props) {
super(props);
this.state = {
name: '',
messages: [],
newMessage: '',
admin: null,
adminOnly: false,
adminModeUpdate: false,
users: [],
usersOnCall: {},
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,
replyTo: null,
sendingMessages: [],
volumes: {},
};
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);
this.updateAllowAutoRejoin = this.updateAllowAutoRejoin.bind(this);
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
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.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;
}
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);
}
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);
}
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
);
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
);
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() {
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 Permission</h4>
`;
overlay.id = 'mic-permission-overlay';
document.body.appendChild(overlay);
window.navigator
.getUserMedia({
audio: true,
})
.then(() => {
document.getElementById('mic-permission-overlay').remove();
this.mCallManager.toggleCall(onCall, conference, admin, adminOnly);
})
.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.'
);
});
} catch (err) {
console.error('Handle Phone Call Error', err);
}
} else this.mCallManager.toggleCall(onCall, conference, admin, adminOnly);
}
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 });
}
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;
}
setVideoVolume(v, id) {
const audioOuts = document.querySelectorAll('audio.vxt-audio');
if (audioOuts.length > 0) {
audioOuts.forEach((el) => {
if (el.id === `audio-${id}`) {
el.volume = v * 0.1;
const { volumes } = this.state;
volumes[id] = v;
this.setState({ volumes });
}
});
}
}
render() {
const {
admin,
name,
messages,
newMessage,
gif,
onCall,
muted,
player,
allowChatNotification,
users,
callActionPending,
allowAutoRejoin,
usersOnCall,
isFullscreen,
adminModeUpdate,
adminOnly,
replyTo,
sendingMessages,
volumes,
} = this.state;
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.GOOGLEPLAY,
message: 'Functionality may be buggy, support on this site is in testing.',
},
{
condition: () => getPlayerSource() === PLAYER_SOURCE.CRUNCHYROLL,
message: 'Functionality may be buggy, support on this site is in testing.',
},
{
condition: () => adminOnly,
message: 'Party is currently in admin only mode.',
},
];
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"
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 right now'
: 'All party members can update player and start calls right now'}
</span>
</span>
}
labelPlacement="end"
/>
) : null}
<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' ? (
<Tooltip
interactive
title={
<Grid className="party-controls-users-volume" container spacing={2}>
<Grid item>
<VolumeDown />
</Grid>
<Grid item xs>
<Slider
max={10}
min={0}
step={1}
color="secondary"
value={volumes[id] || 10}
onChangeCommitted={(e, v) => this.setVideoVolume(v, id)}
/>
</Grid>
<Grid item>
<VolumeUp />
</Grid>
</Grid>
}
>
<Chip
color={
usersOnCall[id].info.externalId === LocalData().userUid
? 'secondary'
: 'default'
}
className="party-controls-users-chip"
clickable
size="small"
label={usersOnCall[id].info.name || ' '}
/>
</Tooltip>
) : 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 currently playing
</Button>
) : null}
</Card>
) : null}
</div>
<div className="thread-wrapper">
<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
setReplyTo={this.setReplyTo}
sendingMessages={sendingMessages}
sentMessages={messages}
/>
</div>
<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 ? null : callActionPending ? (
<CircularProgress className="icon-button" color="secondary" />
) : (
<IconButton
disabled={callActionPending}
onClick={this.handlePhoneCall}
color={!onCall ? 'secondary' : 'primary'}
className="icon-button"
>
{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
className="icon-button"
disabled={callActionPending}
onClick={this.handleMute}
color="primary"
>
<MicOff />
</IconButton>
</Tooltip>
) : !gif && !newMessage && onCall && muted ? (
<Tooltip title="Unmute Mic" placement="top">
<IconButton
className="icon-button"
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 / Scroll to load more'
: null
}
variant="outlined"
className="chat-message-box addFocus"
value={newMessage}
onChange={(e) => {
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>
);
}
}