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
ipad-testing
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 {
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":

View File

@ -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.

View File

@ -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="#"