Spit and polish
* Add a scrollback buffer option to the chat Settings to trim room history so your browser can manage its memory usage * Update the wording that ChatServer sends to users when the /nsfw command has been used on them * Fix the ordering of active DMs for Chromium browsers so the most recently updated DM thread moves to the top of the list * Show an indicator on videos whether the person you watch also watches you back * Fix the "X" button on the photo modal not functioning correctly
This commit is contained in:
parent
75c7511410
commit
e111899404
|
@ -44,11 +44,15 @@ func (s *Server) ProcessCommand(sub *Subscriber, msg Message) bool {
|
|||
if err != nil {
|
||||
sub.ChatServer("/nsfw: username not found: %s", username)
|
||||
} else {
|
||||
other.ChatServer("Your camera has been marked as NSFW by %s", sub.Username)
|
||||
other.ChatServer(
|
||||
"Just a friendly reminder to mark your camera as 'Explicit' by using the button at the top "+
|
||||
"of the page if you are going to be sexual on webcam. Your camera has been marked as Explicit "+
|
||||
"for you by @%s", sub.Username,
|
||||
)
|
||||
other.VideoStatus |= VideoFlagNSFW
|
||||
other.SendMe()
|
||||
s.SendWhoList()
|
||||
sub.ChatServer("%s has their camera marked as NSFW", username)
|
||||
sub.ChatServer("%s now has their camera marked as Explicit", username)
|
||||
}
|
||||
return true
|
||||
case "/help":
|
||||
|
|
|
@ -175,6 +175,7 @@ const app = Vue.createApp({
|
|||
historyScrollbox: null,
|
||||
autoscroll: true, // scroll to bottom on new messages
|
||||
fontSizeClass: "", // font size magnification
|
||||
scrollback: 1000, // scrollback buffer (messages to keep per channel)
|
||||
DMs: {},
|
||||
messageReactions: {
|
||||
// Will look like:
|
||||
|
@ -204,6 +205,7 @@ const app = Vue.createApp({
|
|||
|
||||
settingsModal: {
|
||||
visible: false,
|
||||
tab: 'prefs', // selected setting tab
|
||||
},
|
||||
|
||||
nsfwModalCast: {
|
||||
|
@ -292,6 +294,9 @@ const app = Vue.createApp({
|
|||
// Store the setting persistently.
|
||||
localStorage.fontSizeClass = this.fontSizeClass;
|
||||
},
|
||||
scrollback() {
|
||||
localStorage.scrollback = this.scrollback;
|
||||
},
|
||||
status() {
|
||||
// Send presence updates to the server.
|
||||
this.sendMe();
|
||||
|
@ -320,6 +325,26 @@ const app = Vue.createApp({
|
|||
}
|
||||
return history;
|
||||
},
|
||||
activeDMs() {
|
||||
// List your currently open DM threads, sorted by most recent.
|
||||
let result = [];
|
||||
for (let channel of Object.keys(this.channels)) {
|
||||
// @mentions only
|
||||
if (channel.indexOf("@") !== 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
result.push({
|
||||
channel: channel,
|
||||
name: channel.substring(1),
|
||||
updated: this.channels[channel].updated,
|
||||
unread: this.channels[channel].unread,
|
||||
});
|
||||
}
|
||||
|
||||
result.sort((a, b) => b.updated - a.updated);
|
||||
return result;
|
||||
},
|
||||
channelName() {
|
||||
// Return a suitable channel title.
|
||||
if (this.channel.indexOf("@") === 0) {
|
||||
|
@ -366,6 +391,10 @@ const app = Vue.createApp({
|
|||
this.webcam.videoScale = localStorage.videoScale;
|
||||
}
|
||||
|
||||
if (localStorage.scrollback != undefined) {
|
||||
this.scrollback = parseInt(localStorage.scrollback);
|
||||
}
|
||||
|
||||
// Webcam mutality preferences from last broadcast.
|
||||
if (localStorage.videoMutual === "true") {
|
||||
this.webcam.mutual = true;
|
||||
|
@ -944,6 +973,10 @@ const app = Vue.createApp({
|
|||
username: username,
|
||||
}));
|
||||
},
|
||||
isWatchingMe(username) {
|
||||
// Return whether the user is watching your camera
|
||||
return this.webcam.watching[username] === true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Front-end web app concerns.
|
||||
|
@ -1129,28 +1162,6 @@ const app = Vue.createApp({
|
|||
}
|
||||
return result;
|
||||
},
|
||||
activeDMs() {
|
||||
// List your currently open DM threads, sorted by most recent.
|
||||
let result = [];
|
||||
for (let channel of Object.keys(this.channels)) {
|
||||
// @mentions only
|
||||
if (channel.indexOf("@") !== 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
result.push({
|
||||
channel: channel,
|
||||
name: channel.substring(1),
|
||||
updated: this.channels[channel].updated,
|
||||
unread: this.channels[channel].unread,
|
||||
});
|
||||
}
|
||||
|
||||
result.sort((a, b) => {
|
||||
return a.updated < b.updated;
|
||||
});
|
||||
return result;
|
||||
},
|
||||
|
||||
// Start broadcasting my webcam.
|
||||
// - force=true to skip the NSFW modal prompt (this param is passed by the button in that modal)
|
||||
|
@ -1632,7 +1643,7 @@ const app = Vue.createApp({
|
|||
this.initHistory(channel);
|
||||
|
||||
// Append the message.
|
||||
this.channels[channel].updated = Date.now();
|
||||
this.channels[channel].updated = new Date().getTime();
|
||||
this.channels[channel].history.push({
|
||||
action: action,
|
||||
username: username,
|
||||
|
@ -1642,6 +1653,15 @@ const app = Vue.createApp({
|
|||
isChatServer,
|
||||
isChatClient,
|
||||
});
|
||||
|
||||
// Trim the history per the scrollback buffer.
|
||||
if (this.scrollback > 0 && this.channels[channel].history.length > this.scrollback) {
|
||||
this.channels[channel].history = this.channels[channel].history.slice(
|
||||
-this.scrollback,
|
||||
this.channels[channel].history.length+1,
|
||||
);
|
||||
}
|
||||
|
||||
this.scrollHistory(channel);
|
||||
|
||||
// Mark unread notifiers if this is not our channel.
|
||||
|
|
|
@ -72,227 +72,181 @@
|
|||
</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>
|
||||
<!-- 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>
|
||||
|
||||
<div class="field is-horizontal">
|
||||
<div class="field-label is-normal">
|
||||
<label class="label">Text size</label>
|
||||
<!-- 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="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 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 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">
|
||||
|
@ -417,7 +371,7 @@
|
|||
<img id="modalImage">
|
||||
</div>
|
||||
</div>
|
||||
<button class="modal-close is-large" aria-label="close"></button>
|
||||
<button class="modal-close is-large" aria-label="close" onclick="document.querySelector('#photo-modal').classList.remove('is-active')"></button>
|
||||
</div>
|
||||
|
||||
<div class="chat-container">
|
||||
|
@ -537,7 +491,7 @@
|
|||
</p>
|
||||
|
||||
<ul class="menu-list">
|
||||
<li v-for="c in activeDMs()"
|
||||
<li v-for="c in activeDMs"
|
||||
v-bind:key="c.channel">
|
||||
<a :href="'#'+c.channel"
|
||||
@click.prevent="setChannel(c.channel)"
|
||||
|
@ -681,6 +635,9 @@
|
|||
<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="#"
|
||||
|
|
Loading…
Reference in New Issue
Block a user