Noah Petherbridge
b966f85ecc
* Track the window focus/blur events. Leaving the tab while in a channel now means you may still hear sound effects in that channel. * Add a CORS JSON API /v1/statistics to get details from the server about who is online. The CORSHosts whitelist in the settings.toml limits domain access to the endpoint.
569 lines
26 KiB
HTML
569 lines
26 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 class="field">
|
|
<label class="label">Username</label>
|
|
<input class="input"
|
|
v-model="username"
|
|
placeholder="Username"
|
|
autocomplete="off"
|
|
autofocus
|
|
required>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<div class="control">
|
|
<button class="button is-link">Submit</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>
|
|
|
|
<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"
|
|
@click="stopVideo()">
|
|
<i class="fa fa-camera mr-2"></i>
|
|
Stop
|
|
</button>
|
|
<button type="button"
|
|
v-else
|
|
class="button is-small is-success"
|
|
@click="startVideo()"
|
|
:disabled="webcam.busy">
|
|
<i class="fa fa-camera mr-2"></i>
|
|
Start
|
|
</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>
|
|
</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 -->
|
|
<video class="feed"
|
|
v-show="webcam.active"
|
|
id="localVideo"
|
|
autoplay muted>
|
|
</video>
|
|
|
|
<!-- 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>
|
|
</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>
|
|
|
|
<div class="chat-footer">
|
|
<div class="card">
|
|
<div class="card-content p-2">
|
|
|
|
<div class="columns">
|
|
<div class="column">
|
|
<form @submit.prevent="sendMessage()">
|
|
<input type="text" class="input"
|
|
v-model="message"
|
|
placeholder="Message"
|
|
@keydown="sendTypingNotification()">
|
|
</form>
|
|
</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"
|
|
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 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}} |