Reduce Notification Spam; Unsolicited DMs Option; New SFX
* New "Misc" tab added to the Settings modal with options to reduce spam and improve privacy. * Opt in (or out) for public channel join/leave presence notifications * New option to auto-ignore unsolicited DMs * New sound effects for Watched and Unwatched (your camera) * Reduces spam so ChatServer doesn't need to tell you every time somebody opens your camera. * New spinner icon when opening someone else's camera. * If their cam takes a while to appear, the video button shows a spinner icon as feedback so we avoid ChatClient spam giving you acknowledgement of the cam trying to open.
This commit is contained in:
parent
c4f90fadd2
commit
59fc04b99e
|
@ -123,6 +123,13 @@ const app = Vue.createApp({
|
|||
whoMap: {}, // map username to wholist entry
|
||||
muted: {}, // muted usernames for client side state
|
||||
|
||||
// Misc. user preferences (TODO: move all of them here)
|
||||
prefs: {
|
||||
joinMessages: true, // show "has entered the room" in public channels
|
||||
exitMessages: false, // hide exit messages by default in public channels
|
||||
closeDMs: false, // ignore unsolicited DMs
|
||||
},
|
||||
|
||||
// My video feed.
|
||||
webcam: {
|
||||
busy: false,
|
||||
|
@ -192,6 +199,11 @@ const app = Vue.createApp({
|
|||
|
||||
// Debounce connection attempts since now every click = try to connect.
|
||||
debounceOpens: {}, // map usernames to bools
|
||||
|
||||
// Timeouts for open camera attempts. e.g.: when you click to view
|
||||
// a camera, the icon changes to a spinner for a few seconds to see
|
||||
// whether the video goes on to open.
|
||||
openTimeouts: {}, // map usernames to timeouts
|
||||
},
|
||||
|
||||
// Chat history.
|
||||
|
@ -367,6 +379,17 @@ const app = Vue.createApp({
|
|||
this.sendMe();
|
||||
}
|
||||
},
|
||||
|
||||
// Misc preference watches
|
||||
"prefs.joinMessages": function() {
|
||||
localStorage.joinMessages = this.prefs.joinMessages;
|
||||
},
|
||||
"prefs.exitMessages": function() {
|
||||
localStorage.exitMessages = this.prefs.exitMessages;
|
||||
},
|
||||
"prefs.closeDMs": function() {
|
||||
localStorage.closeDMs = this.prefs.closeDMs;
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
chatHistory() {
|
||||
|
@ -557,6 +580,17 @@ const app = Vue.createApp({
|
|||
if (localStorage.videoAutoMute === "true") {
|
||||
this.webcam.autoMute = true;
|
||||
}
|
||||
|
||||
// Misc preferences
|
||||
if (localStorage.joinMessages != undefined) {
|
||||
this.prefs.joinMessages = localStorage.joinMessages === "true";
|
||||
}
|
||||
if (localStorage.exitMessages != undefined) {
|
||||
this.prefs.exitMessages = localStorage.exitMessages === "true";
|
||||
}
|
||||
if (localStorage.closeDMs != undefined) {
|
||||
this.prefs.closeDMs = localStorage.closeDMs === "true";
|
||||
}
|
||||
},
|
||||
|
||||
signIn() {
|
||||
|
@ -815,14 +849,7 @@ const app = Vue.createApp({
|
|||
this.startWebRTC(msg.username, true);
|
||||
},
|
||||
onRing(msg) {
|
||||
// Admin moderation feature: if the user has booted an admin off their camera, do not
|
||||
// notify if the admin re-opens their camera.
|
||||
if (this.isBootedAdmin(msg.username)) {
|
||||
this.startWebRTC(msg.username, false);
|
||||
return;
|
||||
}
|
||||
|
||||
this.ChatServer(`${msg.username} has opened your camera.`);
|
||||
// Request from a viewer to see our broadcast.
|
||||
this.startWebRTC(msg.username, false);
|
||||
},
|
||||
onUserExited(msg) {
|
||||
|
@ -835,8 +862,13 @@ const app = Vue.createApp({
|
|||
// Play sound effects if this is not the active channel or the window is not focused.
|
||||
if (msg.channel.indexOf("@") === 0) {
|
||||
if (msg.channel !== this.channel || !this.windowFocused) {
|
||||
// If we are ignoring unsolicited DMs, don't play the sound effect here.
|
||||
if (this.prefs.closeDMs && this.channels[msg.channel] == undefined) {
|
||||
console.log("Unsolicited DM received");
|
||||
} else {
|
||||
this.playSound("DM");
|
||||
}
|
||||
}
|
||||
} else if (msg.channel !== this.channel || !this.windowFocused) {
|
||||
this.playSound("Chat");
|
||||
}
|
||||
|
@ -868,18 +900,20 @@ const app = Vue.createApp({
|
|||
// User logged in or out.
|
||||
onPresence(msg) {
|
||||
// TODO: make a dedicated leave event
|
||||
let isLeave = false;
|
||||
let isLeave = false,
|
||||
isJoin = false;
|
||||
if (msg.message.indexOf("has exited the room!") > -1) {
|
||||
// Clean up data about this user.
|
||||
this.onUserExited(msg);
|
||||
this.playSound("Leave");
|
||||
isLeave = true;
|
||||
} else {
|
||||
} else if (msg.message.indexOf("has joined the room!") > -1) {
|
||||
this.playSound("Enter");
|
||||
isJoin = true;
|
||||
}
|
||||
|
||||
// Push it to the history of all public channels (not leaves).
|
||||
if (!isLeave) {
|
||||
// Push it to the history of all public channels (depending on user preference).
|
||||
if ((isJoin && this.prefs.joinMessages) || (isLeave && this.prefs.exitMessages)) {
|
||||
for (let channel of this.config.channels) {
|
||||
this.pushHistory({
|
||||
channel: channel.ID,
|
||||
|
@ -1078,6 +1112,13 @@ const app = Vue.createApp({
|
|||
pc.ontrack = event => {
|
||||
const stream = event.streams[0];
|
||||
|
||||
// We've received a video! If we had an "open camera spinner timeout",
|
||||
// clear it before it expires.
|
||||
if (this.WebRTC.openTimeouts[username] != undefined) {
|
||||
clearTimeout(this.WebRTC.openTimeouts[username]);
|
||||
delete(this.WebRTC.openTimeouts[username]);
|
||||
}
|
||||
|
||||
// Do we already have it?
|
||||
// this.ChatClient(`Received a video stream from ${username}.`);
|
||||
if (this.WebRTC.streams[username] == undefined ||
|
||||
|
@ -1212,10 +1253,12 @@ const app = Vue.createApp({
|
|||
// The user has our video feed open now.
|
||||
if (this.isBootedAdmin(msg.username)) return;
|
||||
this.webcam.watching[msg.username] = true;
|
||||
this.playSound("Watch");
|
||||
},
|
||||
onUnwatch(msg) {
|
||||
// The user has closed our video feed.
|
||||
delete(this.webcam.watching[msg.username]);
|
||||
this.playSound("Unwatch");
|
||||
},
|
||||
sendWatch(username, watching) {
|
||||
// Send the watch or unwatch message to backend.
|
||||
|
@ -1681,15 +1724,26 @@ const app = Vue.createApp({
|
|||
return;
|
||||
}
|
||||
|
||||
// Set a timeout: the video icon becomes a spinner and we wait a while
|
||||
// to see if the connection went thru. This gives the user feedback and we
|
||||
// can avoid a spammy 'ChatClient' notification message.
|
||||
if (this.WebRTC.openTimeouts[user.username] != undefined) {
|
||||
clearTimeout(this.WebRTC.openTimeouts[user.username]);
|
||||
delete(this.WebRTC.openTimeouts[user.username]);
|
||||
}
|
||||
this.WebRTC.openTimeouts[user.username] = setTimeout(() => {
|
||||
// It timed out.
|
||||
this.ChatClient(
|
||||
`There was an error opening <strong>${user.username}</strong>'s camera.`,
|
||||
);
|
||||
delete(this.WebRTC.openTimeouts[user.username]);
|
||||
}, 10000);
|
||||
|
||||
// Send the ChatServer 'open' command.
|
||||
this.sendOpen(user.username);
|
||||
|
||||
// Responsive CSS -> go to chat panel to see the camera
|
||||
this.openChatPanel();
|
||||
|
||||
// Send some feedback to the chat window.
|
||||
this.ChatClient(
|
||||
`A request was sent to open <strong>${user.username}</strong>'s camera which should (hopefully) appear on your screen soon.`,
|
||||
);
|
||||
},
|
||||
closeVideo(username, name) {
|
||||
// Clean up any lingering camera freeze states.
|
||||
|
@ -1762,10 +1816,16 @@ const app = Vue.createApp({
|
|||
// - Usually a video icon
|
||||
// - May be a crossed-out video if isVideoNotAllowed
|
||||
// - Or an eyeball for cameras already opened
|
||||
// - Or a spinner if we are actively trying to open the video
|
||||
if (user.username === this.username && this.webcam.active) {
|
||||
return 'fa-eye'; // user sees their own self camera always
|
||||
}
|
||||
|
||||
// In spinner mode? (Trying to open the video)
|
||||
if (this.WebRTC.openTimeouts[user.username] != undefined) {
|
||||
return 'fa-spinner fa-spin';
|
||||
}
|
||||
|
||||
// Already opened?
|
||||
if (this.WebRTC.pc[user.username] != undefined && this.WebRTC.streams[user.username] != undefined) {
|
||||
return 'fa-eye';
|
||||
|
@ -2016,6 +2076,12 @@ const app = Vue.createApp({
|
|||
channel = this.channel;
|
||||
}
|
||||
|
||||
// Are we ignoring DMs?
|
||||
if (this.prefs.closeDMs && channel.indexOf('@') === 0) {
|
||||
// Don't allow an (incoming) DM to initialize a new chat room for us.
|
||||
if (username !== this.username && this.channels[channel] == undefined) return;
|
||||
}
|
||||
|
||||
// Initialize this channel's history?
|
||||
this.initHistory(channel);
|
||||
|
||||
|
|
|
@ -23,7 +23,15 @@ const SoundEffects = [
|
|||
{
|
||||
name: "Sonar",
|
||||
filename: "sonar-ping-95840.mp3"
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Up Chime",
|
||||
filename: "notification-6175-up.mp3"
|
||||
},
|
||||
{
|
||||
name: "Down Chime",
|
||||
filename: "notification-6175-down.mp3"
|
||||
},
|
||||
];
|
||||
|
||||
// Defaults
|
||||
|
@ -32,4 +40,6 @@ var DefaultSounds = {
|
|||
DM: "Trill",
|
||||
Enter: "Quiet",
|
||||
Leave: "Quiet",
|
||||
Watch: "Up Chime",
|
||||
Unwatch: "Quiet",
|
||||
};
|
BIN
web/static/sfx/notification-6175-down.mp3
Normal file
BIN
web/static/sfx/notification-6175-down.mp3
Normal file
Binary file not shown.
BIN
web/static/sfx/notification-6175-up.mp3
Normal file
BIN
web/static/sfx/notification-6175-up.mp3
Normal file
Binary file not shown.
|
@ -90,6 +90,11 @@
|
|||
Camera
|
||||
</a>
|
||||
</li>
|
||||
<li :class="{'is-active': settingsModal.tab==='misc'}">
|
||||
<a href="#" @click.prevent="settingsModal.tab='misc'">
|
||||
Misc
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
@ -181,7 +186,7 @@
|
|||
|
||||
<div class="columns is-mobile">
|
||||
<div class="column is-2 pr-1">
|
||||
<label class="label">DM chat</label>
|
||||
<label class="label is-size-7">DM chat</label>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="select is-fullwidth">
|
||||
|
@ -196,7 +201,7 @@
|
|||
</div>
|
||||
|
||||
<div class="column is-2 pr-1">
|
||||
<label class="label">Public chat</label>
|
||||
<label class="label is-size-7">Public chat</label>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="select is-fullwidth">
|
||||
|
@ -213,7 +218,7 @@
|
|||
|
||||
<div class="columns is-mobile">
|
||||
<div class="column is-2 pr-1">
|
||||
<label class="label">Room enter</label>
|
||||
<label class="label is-size-7">Room enter</label>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="select is-fullwidth">
|
||||
|
@ -228,7 +233,7 @@
|
|||
</div>
|
||||
|
||||
<div class="column is-2 pr-1">
|
||||
<label class="label">Room leave</label>
|
||||
<label class="label is-size-7">Room leave</label>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="select is-fullwidth">
|
||||
|
@ -242,6 +247,38 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns is-mobile">
|
||||
<div class="column is-2 pr-1">
|
||||
<label class="label is-size-7">Watched</label>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="select is-fullwidth">
|
||||
<select v-model="config.sounds.settings.Watch" @change="setSoundPref('Watch')">
|
||||
<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 is-size-7">Unwatched</label>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="select is-fullwidth">
|
||||
<select v-model="config.sounds.settings.Unwatch" @change="setSoundPref('Unwatch')">
|
||||
<option v-for="s in config.sounds.available"
|
||||
v-bind:key="s.name"
|
||||
:value="s.name">
|
||||
[[s.name]]
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Webcam preferences -->
|
||||
|
@ -332,6 +369,52 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Misc preferences -->
|
||||
<div v-if="settingsModal.tab==='misc'">
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Presence messages in public channels</label>
|
||||
<div class="columns is-mobile mb-0">
|
||||
<div class="column py-1">
|
||||
<label class="checkbox" title="Show 'has joined the room' messages in public channels">
|
||||
<input type="checkbox"
|
||||
v-model="prefs.joinMessages"
|
||||
:value="true">
|
||||
Join room
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="column py-1">
|
||||
<label class="checkbox" title="Show 'has exited the room' messages in public channels">
|
||||
<input type="checkbox"
|
||||
v-model="prefs.exitMessages"
|
||||
:value="true">
|
||||
Exit room
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help mt-0">
|
||||
Whether to show <em>'has joined the room'</em> style messages in public channels.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label mb-0">Direct Messages</label>
|
||||
<label class="checkbox">
|
||||
<input type="checkbox"
|
||||
v-model="prefs.closeDMs"
|
||||
:value="true">
|
||||
Ignore unsolicited DMs from others
|
||||
</label>
|
||||
<p class="help">
|
||||
If you check this box, other chatters may not initiate DMs with you: their messages
|
||||
will be (silently) ignored. You may still initiate DM chats with others, unless they
|
||||
also have closed their DMs with this setting.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<footer class="card-footer">
|
||||
<div class="card-footer-item">
|
||||
|
@ -538,7 +621,7 @@
|
|||
<div class="select is-fullwidth">
|
||||
<select id="classification"
|
||||
v-model="reportModal.classification"
|
||||
:disabled="busy">
|
||||
:disabled="reportModal.busy">
|
||||
<option v-for="i in config.reportClassifications"
|
||||
:value="i">[[i]]</option>
|
||||
</select>
|
||||
|
@ -549,7 +632,7 @@
|
|||
<label class="label" for="reportComment">Comment:</label>
|
||||
<textarea class="textarea"
|
||||
v-model="reportModal.comment"
|
||||
:disabled="busy"
|
||||
:disabled="reportModal.busy"
|
||||
cols="80" rows="2"
|
||||
placeholder="Optional: describe the issue"></textarea>
|
||||
</div>
|
||||
|
@ -558,7 +641,7 @@
|
|||
<div class="control has-text-centered">
|
||||
<button type="button"
|
||||
class="button is-link mr-4"
|
||||
:disabled="busy"
|
||||
:disabled="reportModal.busy"
|
||||
@click="doReport()">Submit report</button>
|
||||
<button type="button"
|
||||
class="button"
|
||||
|
|
Loading…
Reference in New Issue
Block a user