BareRTC/web/templates/chat.html
Noah Petherbridge c5c8d08c7a Boot and Mute
* Users can now boot viewers off their camera. From the viewer's POV the
  booter has just turned off their camera and it will remain "off" for
  the remainder of the booter's session.
* Users can now mute one another: if you mute a user, you will no longer
  see that user's messages or DMs; and the muted user will never see
  your video as being active (like a boot but revokable if you unmute
  later).
2023-03-22 20:21:04 -07:00

787 lines
38 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">
<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>
<h3 class="subtitle">Sounds</h3>
<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">
If your camera will be featuring "<abbr title="Not Safe For Work">NSFW</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 NSFW. 😈
</label>
</div>
<div class="field">
<div class="control has-text-centered">
<button type="button"
class="button is-link mr-4"
@click="startVideo(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-danger is-outlined 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 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"></i>
</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"
@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-danger">
[[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}">
[[c.name]]
<span v-if="hasUnread(c.channel)"
class="tag is-danger">
[[hasUnread(c.channel)]]
</span>
</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 has-background-link">
<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"
:class="{'is-small': isDM}"
@click="openChannelsPanel">
<i v-if="isDM" class="fa fa-arrow-left"></i>
<i v-else class="fa fa-message"></i>
</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"
@click="openWhoPanel">
<i class="fa fa-user-group"></i>
</button>
</div>
</div>
</header>
<div 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">
<video class="feed"
id="localVideo"
autoplay muted>
</video>
</div>
<!-- Others' videos -->
<div class="feed" v-for="(stream, username) in WebRTC.streams" v-bind:key="username">
<video class="feed"
:id="'videofeed-'+username"
autoplay>
</video>
<div class="caption">
[[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">
<button type="button"
v-if="isMuted(username)"
class="button is-small is-danger is-outlined p-1"
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-danger is-outlined p-1"
title="Mute this video"
@click="muteVideo(username)">
<i class="fa fa-volume-high"></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">
<!-- 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" class="mb-2">
<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-24x24">
<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">
<div class="column is-narrow">
<label class="label"
:class="{'has-text-success is-dark': msg.isChatServer,
'has-text-warning is-dark': msg.isAdmin,
'has-text-danger': msg.isChatClient}">
[[msg.username]]
</label>
</div>
<div class="column is-narrow">
<small class="has-text-grey" :title="msg.at">[[prettyDate(msg.at)]]</small>
</div>
<div class="column is-narrow"
v-if="!(msg.isChatServer || msg.isChatClient || msg.username === username || isDM)">
<!-- DMs button -->
<button type="button"
class="button is-grey is-outlined is-small px-2"
@click="openDMs({username: msg.username})">
<i class="fa fa-message mr-1"></i>
DM
</button>
</div>
<div class="column is-narrow"
v-if="!(msg.isChatServer || msg.isChatClient || msg.username === username)">
<!-- Mute button -->
<button type="button"
class="button is-grey is-outlined is-small px-2"
@click="muteUser(msg.username)"
title="Mute user">
<i class="fa fa-comment-slash mr-1"
:class="{'has-text-success': isMutedUser(msg.username),
'has-text-danger': !isMutedUser(msg.username)}"></i>
[[isMutedUser(msg.username) ? 'Unmute' : 'Mute']]
</button>
</div>
</div>
</div>
</div>
<div v-if="msg.action === 'presence'">
<em>[[msg.message]]</em>
</div>
<div v-else class="content">
<div v-html="msg.message"></div>
</div>
</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()">
</form>
</div>
<div class="column pl-1 is-narrow">
<button type="button" class="button"
@click="uploadFile()">
<i class="fa fa-upload"></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"
@click="openChatPanel">
<i class="fa fa-arrow-left"></i>
</button>
</div>
</div>
</header>
<div class="card-content p-2">
<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">
<img v-if="u.avatar" :src="avatarURL(u)"
width="24" height="24"
:alt="'Avatar image for ' + u.username">
<img v-else src="/static/img/shy.png"
width="24" height="24">
</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.videoActive"
:class="{
'is-danger is-outlined': u.videoActive && u.nsfw,
'is-info is-outlined': u.videoActive && !u.nsfw,
}"
title="Open video stream"
@click="openVideo(u)">
<i class="fa fa-video"></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 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/sounds.js?{{.CacheHash}}"></script>
<script src="/static/js/BareRTC.js?{{.CacheHash}}"></script>
</body>
</html>
{{end}}