BareRTC/web/templates/chat.html
Noah Petherbridge 029f25029d Cached Blocklist from your website
* New API endpoint: /api/blocklist where your site can pre-deliver muted
  username lists for users before they enter the chat.
* Image sharing in DMs is allowed if either party is an operator.
2023-07-30 10:32:08 -07:00

1124 lines
58 KiB
HTML

{{define "index"}}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="/static/css/bulma.min.css">
<link rel="stylesheet" type="text/css" href="/static/css/bulma-prefers-dark.css?{{.CacheHash}}">
<link rel="stylesheet" href="/static/fontawesome-free-6.1.2-web/css/all.css">
<link rel="stylesheet" type="text/css" href="/static/css/chat.css?{{.CacheHash}}">
<title>{{.Config.Title}}</title>
</head>
<body>
<div id="BareRTC-App">
<!-- Sign In modal -->
<div class="modal" :class="{'is-active': loginModal.visible}">
<div class="modal-background"></div>
<div class="modal-content">
<div class="card">
<header class="card-header has-background-info">
<p class="card-header-title has-text-light">Sign In</p>
</header>
<div class="card-content">
<form @submit.prevent="signIn()">
<div v-if="autoLogin" class="content">
<p>
Welcome to {{AsHTML .Config.Branding}}! Please just click on the "Enter Chat"
button below to log on. Your username has been pre-filled from the website that
sent you here.
</p>
<p>
This dialog box is added as an experiment to see whether it
helps iOS devices (iPads and iPhones) to log in to the chat more reliably, by
having you interact with the page before it connects to the server. Let us
know in chat if your iPhone or iPad is able to log in this way!
</p>
</div>
<div class="field">
<label class="label">Username</label>
<input class="input"
v-model="username"
placeholder="Username"
autocomplete="off"
autofocus
:disabled="autoLogin"
required>
</div>
<div class="field">
<div class="control">
<button class="button is-link">Enter Chat</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Settings modal -->
<div class="modal" :class="{'is-active': settingsModal.visible}">
<div class="modal-background"></div>
<div class="modal-content">
<div class="card">
<header class="card-header has-background-info">
<p class="card-header-title has-text-light">Chat Settings</p>
</header>
<div class="card-content">
<!-- Tab bar for the settings -->
<div class="tabs">
<ul>
<li :class="{'is-active': settingsModal.tab==='prefs'}">
<a href="#" @click.prevent="settingsModal.tab='prefs'">
Display
</a>
</li>
<li :class="{'is-active': settingsModal.tab==='sounds'}">
<a href="#" @click.prevent="settingsModal.tab='sounds'">
Sound effects
</a>
</li>
</ul>
</div>
<!-- Display preferences -->
<div v-if="settingsModal.tab==='prefs'">
<div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label">Video size</label>
</div>
<div class="field-body">
<div class="field">
<div class="control">
<div class="select is-fullwidth">
<select v-model="webcam.videoScale">
<option v-for="s in webcam.videoScaleOptions"
v-bind:key="s[0]"
:value="s[0]">
[[ s[1] ]]
</option>
</select>
</div>
</div>
</div>
</div>
</div>
<div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label">Text size</label>
</div>
<div class="field-body">
<div class="field">
<div class="control">
<div class="select is-fullwidth">
<select v-model="fontSizeClass">
<option v-for="s in config.fontSizeClasses"
v-bind:key="s[0]"
:value="s[0]">
[[ s[1] ]]
</option>
</select>
</div>
</div>
</div>
</div>
</div>
<div class="field">
<label class="label">Scrollback buffer</label>
<div class="control">
<input type="number"
class="input"
v-model="scrollback"
min="0"
inputmode="numeric">
</div>
<p class="help">
How many chat history messages to keep at once (per channel/DM thread).
Older messages will be removed so your web browser doesn't run low on memory.
A value of zero (0) will mean "unlimited" and the chat history is never trimmed.
</p>
</div>
<h3 class="subtitle mb-2" v-if="webcam.videoDevices.length > 0 || webcam.audioDevices.length > 0">
Webcam Devices
</h3>
<div class="columns is-mobile" v-if="webcam.videoDevices.length > 0 || webcam.audioDevices.length > 0">
<div class="column">
<label class="label">Video source</label>
<div class="select is-fullwidth">
<select v-model="webcam.videoDeviceID" @change="startVideo({changeCamera: true, force: true})">
<option v-for="(d, i) in webcam.videoDevices"
:value="d.id">
[[ d.label || `Camera ${i}` ]]
</option>
</select>
</div>
</div>
<div class="column">
<label class="label">Audio source</label>
<div class="select is-fullwidth">
<select v-model="webcam.audioDeviceID" @change="startVideo({changeCamera: true, force: true})">
<option v-for="(d, i) in webcam.audioDevices"
:value="d.id">
[[ d.label || `Microphone ${i}` ]]
</option>
</select>
</div>
</div>
</div>
</div>
<!-- Sound settings -->
<div v-else-if="settingsModal.tab==='sounds'">
<div class="columns is-mobile">
<div class="column is-2 pr-1">
<label class="label">DM chat</label>
</div>
<div class="column">
<div class="select is-fullwidth">
<select v-model="config.sounds.settings.DM" @change="setSoundPref('DM')">
<option v-for="s in config.sounds.available"
v-bind:key="s.name"
:value="s.name">
[[s.name]]
</option>
</select>
</div>
</div>
<div class="column is-2 pr-1">
<label class="label">Public chat</label>
</div>
<div class="column">
<div class="select is-fullwidth">
<select v-model="config.sounds.settings.Chat" @change="setSoundPref('Chat')">
<option v-for="s in config.sounds.available"
v-bind:key="s.name"
:value="s.name">
[[s.name]]
</option>
</select>
</div>
</div>
</div>
<div class="columns is-mobile">
<div class="column is-2 pr-1">
<label class="label">Room enter</label>
</div>
<div class="column">
<div class="select is-fullwidth">
<select v-model="config.sounds.settings.Enter" @change="setSoundPref('Enter')">
<option v-for="s in config.sounds.available"
v-bind:key="s.name"
:value="s.name">
[[s.name]]
</option>
</select>
</div>
</div>
<div class="column is-2 pr-1">
<label class="label">Room leave</label>
</div>
<div class="column">
<div class="select is-fullwidth">
<select v-model="config.sounds.settings.Leave" @change="setSoundPref('Leave')">
<option v-for="s in config.sounds.available"
v-bind:key="s.name"
:value="s.name">
[[s.name]]
</option>
</select>
</div>
</div>
</div>
</div>
</div>
<footer class="card-footer">
<div class="card-footer-item">
<button type="button" class="button is-primary"
@click="hideSettings()">
Close
</button>
</div>
</footer>
</div>
</div>
</div>
<!-- NSFW Modal: before user activates their webcam -->
<div class="modal" :class="{'is-active': nsfwModalCast.visible}">
<div class="modal-background"></div>
<div class="modal-content">
<div class="card">
<header class="card-header has-background-info">
<p class="card-header-title has-text-light">Broadcast my webcam</p>
</header>
<div class="card-content">
<p class="block">
You can turn on your webcam and enable others in the room to connect to yours.
The controls to stop, <i class="fa fa-microphone-slash"></i> mute audio, and
<i class="fa fa-eye"></i> see who is watching will be at the top of the page.
</p>
<p class="block mb-1">
If your camera will be featuring "<abbr title="Not Safe For Work">Explicit</abbr>" or sexual content, please mark it as such by
clicking on the <i class="fa fa-fire has-text-danger"></i> button or checking the box below to start with it enabled.
</p>
<div class="field">
<label class="checkbox">
<input type="checkbox"
v-model="webcam.nsfw">
Check this box if your webcam will <em>definitely</em> be Explicit. 😈
</label>
</div>
<p class="block mb-1">
<label class="label">Mutual webcam options:</label>
</p>
<div class="field mb-1">
<label class="checkbox">
<input type="checkbox"
v-model="webcam.mutual">
People must be sharing their own camera before they can open mine
</label>
</div>
<div class="field">
<label class="checkbox">
<input type="checkbox"
v-model="webcam.mutualOpen">
When someone opens my camera, I also open their camera automatically
</label>
</div>
<div class="field">
<div class="control has-text-centered">
<button type="button"
class="button is-link mr-4"
@click="startVideo({force: true}); nsfwModalCast.visible=false">Start webcam</button>
<button type="button"
class="button"
@click="nsfwModalCast.visible=false">Cancel</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- NSFW Modal: before user views a NSFW camera the first time -->
<div class="modal" :class="{'is-active': nsfwModalView.visible}">
<div class="modal-background"></div>
<div class="modal-content">
<div class="card">
<header class="card-header has-background-info">
<p class="card-header-title has-text-light">This camera may contain NSFW content</p>
</header>
<div class="card-content">
<p class="block">
This camera has been marked as "<abbr title="Not Safe For Work">NSFW</abbr>" and may
contain displays of sexuality. If you do not want to see this, look for cameras with
a <span class="button is-small is-info is-outlined px-1"><i class="fa fa-video"></i></span>
blue icon rather than the <span class="button is-small is-danger is-outlined px-1"><i class="fa fa-video"></i></span>
red ones.
</p>
<div class="field">
<label class="checkbox">
<input type="checkbox"
v-model="nsfwModalView.dontShowAgain">
Don't show this message again
</label>
</div>
<div class="field">
<div class="control has-text-centered">
<button type="button"
class="button is-link mr-4"
@click="openVideo(nsfwModalView.user, true); nsfwModalView.visible=false">Open webcam</button>
<button type="button"
class="button"
@click="nsfwModalView.visible=false">Cancel</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Photo Detail Modal -->
<div class="modal" id="photo-modal">
<div class="modal-background" onclick="document.querySelector('#photo-modal').classList.remove('is-active')"></div>
<div class="modal-content photo-modal">
<div class="image is-fullwidth">
<img id="modalImage">
</div>
</div>
<button class="modal-close is-large" aria-label="close" onclick="document.querySelector('#photo-modal').classList.remove('is-active')"></button>
</div>
<div class="chat-container">
<!-- Top header panel -->
<header class="chat-header">
<div class="columns is-mobile">
<div class="column is-narrow pr-1">
<strong class="is-6">{{AsHTML .Config.Branding}}</strong>
</div>
<div class="column px-1">
<!-- Stop/Start video buttons -->
<button type="button"
v-if="webcam.active"
class="button is-small is-danger px-1"
@click="stopVideo()">
<i class="fa fa-stop mr-2"></i>
Stop
</button>
<button type="button"
v-else
class="button is-small is-success px-1"
@click="startVideo({})"
:disabled="webcam.busy">
<i class="fa fa-video mr-2"></i>
Share webcam
</button>
<!-- Mute/Unmute my mic buttons (if streaming)-->
<button type="button"
v-if="webcam.active && !webcam.muted"
class="button is-small is-success ml-1 px-1"
@click="muteMe()">
<i class="fa fa-microphone mr-2"></i>
Mute
</button>
<button type="button"
v-if="webcam.active && webcam.muted"
class="button is-small is-danger ml-1 px-1"
@click="muteMe()">
<i class="fa fa-microphone-slash mr-2"></i>
Unmute
</button>
<!-- Watchers button -->
<button type="button"
v-if="webcam.active"
class="button is-small is-info is-outlined ml-1 px-1"
@click="showViewers()">
<i class="fa fa-eye mr-2"></i>
[[Object.keys(webcam.watching).length]]
</button>
<!-- NSFW toggle button -->
<button type="button"
v-if="webcam.active && config.permitNSFW"
class="button is-small px-1 ml-1"
:class="{'is-outlined is-dark': !webcam.nsfw,
'is-danger': webcam.nsfw}"
@click.prevent="webcam.nsfw = !webcam.nsfw; sendMe()"
title="Toggle the NSFW setting for your camera broadcast">
<i class="fa fa-fire mr-1"></i> Explicit
</button>
</div>
<div class="column is-narrow pl-1">
<a href="/about" target="_blank" class="button is-small is-link px-2">
<i class="fa fa-info-circle"></i>
</a>
<button type="button"
class="button is-small is-light ml-1 px-2"
@click="showSettings()"
title="Chat Settings">
<i class="fa fa-gear"></i>
</button>
</div>
</div>
</header>
<!-- Left Column: Channels & DMs -->
<div class="left-column">
<div class="card grid-card">
<header class="card-header has-background-success-dark">
<div class="columns is-mobile card-header-title has-text-light">
<div class="column is-narrow mobile-only">
<button type="button"
class="button is-success px-2"
@click="openChatPanel">
<i class="fa fa-arrow-left"></i>
</button>
</div>
<div class="column">Channels</div>
</div>
</header>
<div class="card-content">
<aside class="menu">
<p class="menu-label">
Chat Rooms
</p>
<ul class="menu-list">
<li v-for="c in activeChannels()"
v-bind:key="c.ID">
<a :href="'#'+c.ID"
@click.prevent="setChannel(c)"
:class="{'is-active': c.ID == channel}">
[[c.Name]]
<span v-if="hasUnread(c.ID)"
class="tag is-success">
[[hasUnread(c.ID)]]
</span>
</a>
</li>
</ul>
<p class="menu-label">
Private Messages
</p>
<ul class="menu-list">
<li v-for="c in activeDMs"
v-bind:key="c.channel">
<a :href="'#'+c.channel"
@click.prevent="setChannel(c.channel)"
:class="{'is-active': c.channel == channel}">
<div class="columns is-mobile">
<!-- Avatar URL if available (copied from Who List) -->
<div class="column is-narrow pr-0" style="position: relative">
<img v-if="avatarForUsername(normalizeUsername(c.channel))" :src="avatarForUsername(normalizeUsername(c.channel))"
width="24" height="24" alt="">
<img v-else src="/static/img/shy.png"
width="24" height="24">
</div>
<div class="column">
[[c.name]]
<span v-if="hasUnread(c.channel)"
class="tag is-danger">
[[hasUnread(c.channel)]]
</span>
</div>
</div>
</a>
</li>
</ul>
</aside>
</div>
</div>
</div>
<!-- Middle Column: Chat Room/History -->
<div class="chat-column">
<div class="card grid-card">
<header class="card-header"
:class="{'has-background-private': isDM, 'has-background-link': !isDM}">
<div class="columns is-mobile card-header-title has-text-light">
<div class="column is-narrow mobile-only pr-0">
<!-- Responsive mobile button to pan to Left Column -->
<button type="button"
class="button is-success px-2"
@click="openChannelsPanel">
<i v-if="isDM" class="fa fa-arrow-left"></i>
<i v-else class="fa fa-comments"></i>
<!-- Indicator badge for unread messages -->
<span v-if="hasAnyUnread() > 0"
class="tag ml-1" :class="{'is-danger': anyUnreadDMs()}">
[[hasAnyUnread()]]
</span>
</button>
</div>
<div class="column">
[[channelName]]
</div>
<div class="column is-narrow">
<!-- If a DM thread and the user has a profile URL -->
<button type="button"
v-if="this.channel.indexOf('@') === 0 && profileURLForUsername(this.channel)"
class="button is-small is-outlined is-light mr-1"
@click="openProfile({username: this.channel})">
<i class="fa fa-user"></i>
</button>
<!-- DMs: Leave convo button -->
<button type="button"
v-if="channel.indexOf('@') === 0"
class="float-right button is-small is-warning is-outlined"
@click="leaveDM()">
<i class="fa fa-trash"></i>
</button>
</div>
<!-- Who List button, only shown on public channel view -->
<div v-if="!isDM" class="column is-narrow mobile-only">
<!-- Responsive mobile button to pan to Right Column -->
<button type="button"
class="button is-success px-2"
@click="openWhoPanel">
<i class="fa fa-user-group"></i>
</button>
</div>
</div>
</header>
<div id="video-feeds" class="video-feeds" :class="webcam.videoScale" v-show="webcam.active || Object.keys(WebRTC.streams).length > 0">
<!-- Video Feeds-->
<!-- My video -->
<div class="feed" v-show="webcam.active"
:class="{'popped-out': WebRTC.poppedOut[username],
'popped-in': !WebRTC.poppedOut[username]}">
<video class="feed"
id="localVideo"
autoplay muted>
</video>
<div class="caption">
<i class="fa fa-microphone-slash mr-1 has-text-grey"
v-if="webcam.muted"></i>
[[username]]
</div>
<div class="controls">
<!-- MY Mute button -->
<button type="button"
v-if="webcam.active && !webcam.muted"
class="button is-small is-success is-outlined ml-1 px-2"
@click="muteMe()">
<i class="fa fa-microphone"></i>
</button>
<button type="button"
v-if="webcam.active && webcam.muted"
class="button is-small is-danger ml-1 px-2"
@click="muteMe()">
<i class="fa fa-microphone-slash"></i>
</button>
<!-- Pop-out MY video -->
<button type="button"
class="button is-small is-light is-outlined p-2 ml-2"
title="Pop out"
@click="popoutVideo(username)">
<i class="fa fa-up-right-from-square"></i>
</button>
</div>
</div>
<!-- Others' videos -->
<div class="feed" v-for="(stream, username) in WebRTC.streams"
v-bind:key="username"
:class="{'popped-out': WebRTC.poppedOut[username],
'popped-in': !WebRTC.poppedOut[username]}">
<video class="feed"
:id="'videofeed-'+username"
autoplay>
</video>
<div class="caption">
<i class="fa fa-microphone-slash mr-1 has-text-grey"
v-if="isSourceMuted(username)"></i>
[[username]]
<i class="fa fa-people-arrows ml-1 has-text-grey is-size-7"
:title="username+' is watching your camera too'"
v-if="isWatchingMe(username)"></i>
</div>
<div class="close">
<a href="#"
class="has-text-danger"
title="Close video"
@click="closeVideo(username, 'offerer')">
<i class="fa fa-close"></i>
</a>
</div>
<div class="controls">
<!-- Mute button -->
<button type="button"
v-if="isMuted(username)"
class="button is-small is-danger p-2"
title="Unmute this video"
@click="muteVideo(username)">
<i class="fa fa-volume-xmark"></i>
</button>
<button type="button"
v-else
class="button is-small is-success is-outlined p-2"
title="Mute this video"
@click="muteVideo(username)">
<i class="fa fa-volume-high"></i>
</button>
<!-- Pop-out -->
<button type="button"
class="button is-small is-light is-outlined p-2 ml-2"
title="Pop out"
@click="popoutVideo(username)">
<i class="fa fa-up-right-from-square"></i>
</button>
</div>
</div>
<!-- Debugging - copy a lot of these to simulate more videos -->
<!--
<div class="feed">
hi
</div>
<div class="feed">
hi
</div>
<div class="feed">
hi
</div>
<div class="feed">
hi
</div>
-->
</div>
<div class="card-content" id="chatHistory" :class="{'has-background-dm': isDM}">
<div class="autoscroll-field tag">
<label class="checkbox is-size-6" title="Automatically scroll when new chat messages come in.">
<input type="checkbox"
v-model="autoscroll"
:value="true">
Auto-scroll
</label>
</div>
<div :class="fontSizeClass">
<!-- Disclaimer at the top of DMs -->
<!-- TODO: make this disclaimer configurable for other sites to modify -->
<div class="notification is-warning is-light" v-if="isDM">
<i class="fa fa-info-circle mr-1"></i>
<strong>Reminder:</strong> please conduct yourself honorably in Direct Messages.
Please refer to {{AsHTML .Config.Branding}}'s Privacy Policy or Terms of Service with regard to DMs.
</div>
<!-- No history? -->
<div v-if="chatHistory.length === 0">
<em v-if="isDM">
Starting a direct message chat with [[channel]]. Type a message and say hello!
</em>
<em v-else>
There are no messages in this channel yet.
</em>
</div>
<div v-for="(msg, i) in chatHistory" v-bind:key="i">
<!-- Enter chat presence messages draw as a short banner -->
<div v-if="msg.action === 'presence'" class="notification is-success is-light py-1 px-3 mb-2">
<!-- Tiny avatar next to name and action buttons -->
<div class="columns is-mobile">
<div class="column is-narrow pr-0 pt-4">
<figure class="image is-16x16">
<img v-if="avatarForUsername(msg.username)"
:src="avatarForUsername(msg.username)">
<img v-else src="/static/img/shy.png">
</figure>
</div>
<div class="column">
<strong>[[ nicknameForUsername(msg.username) ]]</strong>
<small v-if="isUsernameOnline(msg.username)" class="ml-1">(@[[msg.username]])</small>
[[msg.message]]
</div>
</div>
</div>
<!-- Normal chat message: full size card w/ avatar -->
<div v-else class="box mb-2 px-4 pt-3 pb-1 position-relative">
<div class="media mb-0">
<div class="media-left">
<a :href="profileURLForUsername(msg.username)" @click.prevent="openProfile({username: msg.username})"
:class="{'cursor-default': !profileURLForUsername(msg.username)}">
<figure class="image is-48x48">
<img v-if="msg.isChatServer"
src="/static/img/server.png">
<img v-else-if="msg.isChatClient"
src="/static/img/client.png">
<img v-else-if="avatarForUsername(msg.username)"
:src="avatarForUsername(msg.username)">
<img v-else src="/static/img/shy.png">
</figure>
</a>
</div>
<div class="media-content">
<div class="columns is-mobile pb-0">
<div class="column is-narrow pb-0">
<strong
:class="{'has-text-success is-dark': msg.isChatServer,
'has-text-warning is-dark': msg.isAdmin,
'has-text-danger': msg.isChatClient}">
<!-- User nickname/display name -->
[[nicknameForUsername(msg.username)]]
</strong>
</div>
<div class="column has-text-right pb-0">
<small class="has-text-grey is-size-7" :title="msg.at">[[prettyDate(msg.at)]]</small>
</div>
</div>
<!-- User @username below it which may link to a profile URL if JWT -->
<div class="columns is-mobile pt-0" v-if="(msg.isChatClient || msg.isChatServer)">
<div class="column is-narrow pt-0">
<small v-if="!(msg.isChatClient || msg.isChatServer)">
<a v-if="profileURLForUsername(msg.username)"
:href="profileURLForUsername(msg.username)"
target="_blank"
class="has-text-grey">
@[[msg.username]]
</a>
<span v-else class="has-text-grey">@[[msg.username]]</span>
</small>
<small v-else class="has-text-grey">internal</small>
</div>
</div>
<div v-else class="columns is-mobile pt-0">
<div class="column is-narrow pt-0">
<small v-if="!(msg.isChatClient || msg.isChatServer)">
<a v-if="profileURLForUsername(msg.username)"
:href="profileURLForUsername(msg.username)"
target="_blank"
class="has-text-grey">
@[[msg.username]]
</a>
<span v-else class="has-text-grey">@[[msg.username]]</span>
</small>
<small v-else class="has-text-grey">internal</small>
</div>
<div class="column is-narrow pl-1 pt-0">
<!-- DMs button -->
<button type="button"
v-if="!(msg.username === username || isDM)"
class="button is-grey is-outlined is-small px-2"
@click="openDMs({username: msg.username})"
title="Open a Direct Message (DM) thread">
<i class="fa fa-message"></i>
</button>
<!-- Mute button -->
<button type="button"
v-if="!(msg.username === username)"
class="button is-grey is-outlined is-small px-2 ml-1"
@click="muteUser(msg.username)"
title="Mute user">
<i class="fa fa-comment-slash"
:class="{'has-text-success': isMutedUser(msg.username),
'has-text-danger': !isMutedUser(msg.username)}"></i>
</button>
<!-- Owner or admin: take back the message -->
<button type="button"
v-if="msg.username === username || isOp"
class="button is-grey is-outlined is-small px-2 ml-1"
title="Take back this message (delete it for everybody)"
@click="takeback(msg)">
<i class="fa fa-rotate-left has-text-danger"></i>
</button>
<!-- Everyone else: can hide it locally -->
<button type="button"
v-if="msg.username !== username"
class="button is-grey is-outlined is-small px-2 ml-1"
title="Hide this message (delete it only for your view)"
@click="removeMessage(msg)">
<i class="fa fa-trash"></i>
</button>
</div>
</div>
</div>
</div>
<!-- Emoji reactions menu -->
<div v-if="msg.msgID" class="dropdown is-right emoji-button"
:class="{'is-up': i >= 2}"
onclick="this.classList.toggle('is-active')">
<div class="dropdown-trigger">
<button class="button is-small px-2" aria-haspopup="true" :aria-controls="`react-menu-${msg.msgID}`">
<span>
<i class="fa fa-heart has-text-grey"></i>
<i class="fa fa-plus has-text-grey pl-1"></i>
</span>
</button>
</div>
<div class="dropdown-menu" :id="`react-menu-${msg.msgID}`" role="menu">
<div class="dropdown-content p-0">
<!-- Iterate over reactions in rows of emojis-->
<div class="columns is-mobile ml-0 mb-2 mr-1"
v-for="row in config.reactions">
<!-- Loop over the icons -->
<div class="column p-0 is-narrow"
v-for="i in row">
<button type="button"
class="button px-2 mt-1 ml-1 mr-0 mb-1"
@click="sendReact(msg, i)">
[[i]]
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Message box -->
<div class="content pl-5 py-3 mb-0">
<em v-if="msg.action === 'presence'">[[msg.message]]</em>
<div v-else v-html="msg.message"></div>
<!-- Reactions so far? -->
<div v-if="hasReactions(msg)" class="mt-1">
<span v-for="(users, emoji) in getReactions(msg)"
class="tag is-secondary mr-1 cursor-pointer"
:class="{'is-success is-light': iReacted(msg, emoji), 'is-secondary': !iReacted(msg, emoji)}"
:title="emoji + ' by: ' + users.join(', ')"
@click="sendReact(msg, emoji)">
[[emoji]] <small class="ml-1">[[users.length]]</small>
</span>
</div>
</div>
</div>
</div>
</div>
<!-- If this is a DM with a muted user, offer to unmute. -->
<div v-if="isDM && isMutedUser(channel)" class="has-text-danger">
<i class="fa fa-comment-slash"></i>
<strong>[[channel]]</strong> is currently <strong>muted</strong> so you have not been seeing their recent
chat messages or DMs.
<a href="#" v-on:click.prevent="muteUser(channel)">Unmute them?</a>
</div>
</div>
</div>
</div>
<!-- Chat Footer Frame -->
<div class="chat-footer">
<div class="card">
<div class="card-content p-2">
<div class="columns is-mobile">
<div class="column"
:class="{'pr-1': canUploadFile}">
<form @submit.prevent="sendMessage()">
<input type="text" class="input"
v-model="message"
placeholder="Message"
@keydown="sendTypingNotification()"
:disabled="!ws.connected">
</form>
</div>
<div class="column pl-1 is-narrow" v-if="canUploadFile">
<button type="button" class="button"
@click="uploadFile()">
<i class="fa fa-image"></i>
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Right Column: Who Is Online -->
<div class="right-column">
<div class="card grid-card">
<header class="card-header has-background-success-dark">
<div class="columns is-mobile card-header-title has-text-light">
<div class="column">Who Is Online</div>
<div class="column is-narrow mobile-only">
<button type="button"
class="button is-success px-2"
@click="openChatPanel">
<i class="fa fa-arrow-left"></i>
</button>
</div>
</div>
</header>
<div class="card-content p-2">
<div class="columns is-mobile mb-0">
<div class="column is-narrow">
Status:
</div>
<div class="column">
<div class="select is-small is-fullwidth">
<select v-model="status">
<option value="online">☀️ Active</option>
<option value="away">🕒 Away</option>
<option value="idle" v-show="status==='idle'">🕒 Idle</option>
<option value="hidden" v-if="jwt.claims != undefined && jwt.claims.op">🕵️ Hidden</option>
</select>
</div>
</div>
</div>
<div class="tabs has-text-small">
<ul>
<li :class="{'is-active': whoTab==='online'}">
<a class="is-size-7"
@click.prevent="whoTab='online'">
Online ([[ whoList.length ]])
</a>
</li>
<li v-if="webcam.active" :class="{'is-active': whoTab==='watching'}">
<a class="is-size-7"
@click.prevent="whoTab='watching'">
<i class="fa fa-eye mr-2"></i>
Watching ([[ Object.keys(webcam.watching).length ]])
</a>
</li>
</ul>
</div>
<!-- Who Is Online -->
<ul class="menu-list" v-if="whoTab==='online'">
<li v-for="(u, i) in whoList" v-bind:key="i">
<div class="columns is-mobile">
<!-- Avatar URL if available -->
<div class="column is-narrow pr-0" style="position: relative">
<a :href="profileURLForUsername(u.username)" @click.prevent="openProfile({username: u.username})"
:class="{'cursor-default': !profileURLForUsername(u.username)}"
class="p-0">
<img v-if="u.avatar" :src="avatarURL(u)"
width="24" height="24" alt="">
<img v-else src="/static/img/shy.png"
width="24" height="24">
<!-- Away symbol -->
<div v-if="u.status !== 'online'" class="status-away-icon" :title="'Status: '+u.status">
<i class="fa fa-clock has-text-light"></i>
</div>
</a>
</div>
<div class="column pr-0"
:class="{'pl-1': u.avatar}">
<i class="fa fa-gavel has-text-warning-dark"
v-if="u.op"
title="Operator"></i>
[[ u.username ]]
</div>
<div class="column is-narrow pl-0">
<!-- Profile button -->
<button type="button"
v-if="u.profileURL"
class="button is-small px-2 py-1"
@click="openProfile(u)"
title="Open profile page">
<i class="fa fa-user"></i>
</button>
<!-- Unmute User button (if muted) -->
<button type="button" v-if="isMutedUser(u.username)"
class="button is-small px-2 py-1"
@click="muteUser(u.username)"
title="This user is muted. Click to unmute them.">
<i class="fa fa-comment-slash has-text-danger"></i>
</button>
<!-- DM button (if not muted) -->
<button type="button" v-else
class="button is-small px-2 py-1"
@click="openDMs(u)"
title="Start direct message thread"
:disabled="u.username === username">
<i class="fa fa-message"></i>
</button>
<!-- Video button -->
<button type="button" class="button is-small px-2 py-1"
:disabled="!(u.video & VideoFlag.Active)"
:class="{
'is-danger is-outlined': (u.video & VideoFlag.Active) && (u.video & VideoFlag.NSFW),
'is-info is-outlined': (u.video & VideoFlag.Active) && !(u.video & VideoFlag.NSFW),
'cursor-notallowed': isVideoNotAllowed(u),
}"
:title="`Open video stream` +
(u.video & VideoFlag.MutualRequired ? '; mutual video sharing required' : '') +
(u.video & VideoFlag.MutualOpen ? '; will auto-open your video' : '')"
@click="openVideo(u)">
<i class="fa"
:class="webcamIconClass(u)"></i>
</button>
</div>
</div>
</li>
</ul>
<!-- Watching My Webcam -->
<ul class="menu-list" v-if="whoTab==='watching'">
<li v-for="username in Object.keys(webcam.watching)" v-bind:key="username">
<div class="columns is-mobile">
<!-- Avatar URL if available -->
<div class="column is-narrow pr-0">
<i class="fa fa-eye"></i>
</div>
<div class="column pr-0">
[[ username ]]
</div>
<div class="column is-narrow pl-0">
<!-- Boot from cam button -->
<button type="button"
class="button is-small px-2 py-1"
@click="bootUser(username)"
title="Kick this person off your cam">
<i class="fa fa-user-xmark has-text-danger"></i>
</button>
</div>
</div>
</li>
</ul>
</div>
</div>
</div>
</div>
</div><!-- /app -->
<script type="text/javascript">
const PublicChannels = {{.Config.GetChannels}};
const WebsiteURL = "{{.Config.WebsiteURL}}";
const PermitNSFW = {{AsJS .Config.PermitNSFW}};
const TURN = {{.Config.TURN}};
const UserJWTToken = {{.JWTTokenString}};
const UserJWTValid = {{if .JWTAuthOK}}true{{else}}false{{end}};
const UserJWTClaims = {{.JWTClaims.ToJSON}};
const CachedBlocklist = {{.CachedBlocklist}};
</script>
<script src="/static/js/vue-3.2.45.js"></script>
<script src="/static/js/interact.min.js"></script>
<script src="/static/js/sounds.js?{{.CacheHash}}"></script>
<script src="/static/js/BareRTC.js?{{.CacheHash}}"></script>
</body>
</html>
{{end}}