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:
Noah 2023-07-22 18:30:45 -07:00
parent 75c7511410
commit e111899404
3 changed files with 217 additions and 236 deletions

View File

@ -44,11 +44,15 @@ func (s *Server) ProcessCommand(sub *Subscriber, msg Message) bool {
if err != nil { if err != nil {
sub.ChatServer("/nsfw: username not found: %s", username) sub.ChatServer("/nsfw: username not found: %s", username)
} else { } 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.VideoStatus |= VideoFlagNSFW
other.SendMe() other.SendMe()
s.SendWhoList() 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 return true
case "/help": case "/help":

View File

@ -175,6 +175,7 @@ const app = Vue.createApp({
historyScrollbox: null, historyScrollbox: null,
autoscroll: true, // scroll to bottom on new messages autoscroll: true, // scroll to bottom on new messages
fontSizeClass: "", // font size magnification fontSizeClass: "", // font size magnification
scrollback: 1000, // scrollback buffer (messages to keep per channel)
DMs: {}, DMs: {},
messageReactions: { messageReactions: {
// Will look like: // Will look like:
@ -204,6 +205,7 @@ const app = Vue.createApp({
settingsModal: { settingsModal: {
visible: false, visible: false,
tab: 'prefs', // selected setting tab
}, },
nsfwModalCast: { nsfwModalCast: {
@ -292,6 +294,9 @@ const app = Vue.createApp({
// Store the setting persistently. // Store the setting persistently.
localStorage.fontSizeClass = this.fontSizeClass; localStorage.fontSizeClass = this.fontSizeClass;
}, },
scrollback() {
localStorage.scrollback = this.scrollback;
},
status() { status() {
// Send presence updates to the server. // Send presence updates to the server.
this.sendMe(); this.sendMe();
@ -320,6 +325,26 @@ const app = Vue.createApp({
} }
return history; 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() { channelName() {
// Return a suitable channel title. // Return a suitable channel title.
if (this.channel.indexOf("@") === 0) { if (this.channel.indexOf("@") === 0) {
@ -366,6 +391,10 @@ const app = Vue.createApp({
this.webcam.videoScale = localStorage.videoScale; this.webcam.videoScale = localStorage.videoScale;
} }
if (localStorage.scrollback != undefined) {
this.scrollback = parseInt(localStorage.scrollback);
}
// Webcam mutality preferences from last broadcast. // Webcam mutality preferences from last broadcast.
if (localStorage.videoMutual === "true") { if (localStorage.videoMutual === "true") {
this.webcam.mutual = true; this.webcam.mutual = true;
@ -944,6 +973,10 @@ const app = Vue.createApp({
username: username, username: username,
})); }));
}, },
isWatchingMe(username) {
// Return whether the user is watching your camera
return this.webcam.watching[username] === true;
},
/** /**
* Front-end web app concerns. * Front-end web app concerns.
@ -1129,28 +1162,6 @@ const app = Vue.createApp({
} }
return result; 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. // Start broadcasting my webcam.
// - force=true to skip the NSFW modal prompt (this param is passed by the button in that modal) // - 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); this.initHistory(channel);
// Append the message. // Append the message.
this.channels[channel].updated = Date.now(); this.channels[channel].updated = new Date().getTime();
this.channels[channel].history.push({ this.channels[channel].history.push({
action: action, action: action,
username: username, username: username,
@ -1642,6 +1653,15 @@ const app = Vue.createApp({
isChatServer, isChatServer,
isChatClient, 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); this.scrollHistory(channel);
// Mark unread notifiers if this is not our channel. // Mark unread notifiers if this is not our channel.

View File

@ -72,227 +72,181 @@
</header> </header>
<div class="card-content"> <div class="card-content">
<div class="field is-horizontal"> <!-- Tab bar for the settings -->
<div class="field-label is-normal"> <div class="tabs">
<label class="label">Video size</label> <ul>
</div> <li :class="{'is-active': settingsModal.tab==='prefs'}">
<div class="field-body"> <a href="#" @click.prevent="settingsModal.tab='prefs'">
<div class="field"> Display
<div class="control"> </a>
<div class="select is-fullwidth"> </li>
<select v-model="webcam.videoScale"> <li :class="{'is-active': settingsModal.tab==='sounds'}">
<option v-for="s in webcam.videoScaleOptions" <a href="#" @click.prevent="settingsModal.tab='sounds'">
v-bind:key="s[0]" Sound effects
:value="s[0]"> </a>
[[ s[1] ]] </li>
</option> </ul>
</select> </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> </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>
<div class="field is-horizontal"> <!-- Sound settings -->
<div class="field-label is-normal"> <div v-else-if="settingsModal.tab==='sounds'">
<label class="label">Text size</label>
<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>
<div class="field-body">
<div class="field"> <div class="columns is-mobile">
<div class="control"> <div class="column is-2 pr-1">
<div class="select is-fullwidth"> <label class="label">Room enter</label>
<select v-model="fontSizeClass"> </div>
<option v-for="s in config.fontSizeClasses" <div class="column">
v-bind:key="s[0]" <div class="select is-fullwidth">
:value="s[0]"> <select v-model="config.sounds.settings.Enter" @change="setSoundPref('Enter')">
[[ s[1] ]] <option v-for="s in config.sounds.available"
</option> v-bind:key="s.name"
</select> :value="s.name">
</div> [[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> </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> </div>
<footer class="card-footer"> <footer class="card-footer">
<div class="card-footer-item"> <div class="card-footer-item">
@ -417,7 +371,7 @@
<img id="modalImage"> <img id="modalImage">
</div> </div>
</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>
<div class="chat-container"> <div class="chat-container">
@ -537,7 +491,7 @@
</p> </p>
<ul class="menu-list"> <ul class="menu-list">
<li v-for="c in activeDMs()" <li v-for="c in activeDMs"
v-bind:key="c.channel"> v-bind:key="c.channel">
<a :href="'#'+c.channel" <a :href="'#'+c.channel"
@click.prevent="setChannel(c.channel)" @click.prevent="setChannel(c.channel)"
@ -681,6 +635,9 @@
<i class="fa fa-microphone-slash mr-1 has-text-grey" <i class="fa fa-microphone-slash mr-1 has-text-grey"
v-if="isSourceMuted(username)"></i> v-if="isSourceMuted(username)"></i>
[[username]] [[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>
<div class="close"> <div class="close">
<a href="#" <a href="#"