BareRTC/web/templates/chat.html

720 lines
34 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"
v-if="!(msg.isChatServer || msg.isChatClient || msg.username === username || isDM)">
<button type="button"
class="button is-grey is-outlined is-small px-2"
@click="openDMs({username: msg.username})">
<i class="fa fa-message"></i>
</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">
<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">
<ul class="menu-list">
<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>
<!-- DM button -->
<button type="button"
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>
</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}}