BareRTC/web/templates/chat.html
Noah Petherbridge 75c7511410 Kick off conflicting usernames + Frontend mobile fixes
* When JWT tokens are used to join the chat and the username conflicts:
  instead of renaming the new user to add a "2" it will disconnect the
  original login (sending a message that they have signed in somewhere
  else and are logged out now)
* When disconnected the text entry box will be greyed out.
* Improvements for the mobile user experience: if you're viewing the
  chat history panel and have unread messages or DMs, a number indicator
  appears on the channels button. It is grey for public channel messages
  or red if any of them are DMs
* Fix the emoji picker drop-down on the first messages of a DM thread
2023-07-17 20:38:07 -07:00

1157 lines
59 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">
<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="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>
<h3 class="subtitle mb-2">Sounds</h3>
<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 class="field is-horizontal">
<div class="field-label is-normal">
<label class="label">DM chat</label>
</div>
<div class="field-body">
<div class="field">
<div class="control">
<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>
</div>
</div>
<div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label">Public chat</label>
</div>
<div class="field-body">
<div class="field">
<div class="control">
<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>
</div>
<div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label">Room enter</label>
</div>
<div class="field-body">
<div class="field">
<div class="control">
<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>
</div>
</div>
<div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label">Room leave</label>
</div>
<div class="field-body">
<div class="field">
<div class="control">
<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>
-->
</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"></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">
[[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]]
</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">
<!-- 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 pr-1">
<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">
<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}};
</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}}