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
|
whoMap: {}, // map username to wholist entry
|
||||||
muted: {}, // muted usernames for client side state
|
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.
|
// My video feed.
|
||||||
webcam: {
|
webcam: {
|
||||||
busy: false,
|
busy: false,
|
||||||
|
@ -192,6 +199,11 @@ const app = Vue.createApp({
|
||||||
|
|
||||||
// Debounce connection attempts since now every click = try to connect.
|
// Debounce connection attempts since now every click = try to connect.
|
||||||
debounceOpens: {}, // map usernames to bools
|
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.
|
// Chat history.
|
||||||
|
@ -367,6 +379,17 @@ const app = Vue.createApp({
|
||||||
this.sendMe();
|
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: {
|
computed: {
|
||||||
chatHistory() {
|
chatHistory() {
|
||||||
|
@ -557,6 +580,17 @@ const app = Vue.createApp({
|
||||||
if (localStorage.videoAutoMute === "true") {
|
if (localStorage.videoAutoMute === "true") {
|
||||||
this.webcam.autoMute = 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() {
|
signIn() {
|
||||||
|
@ -815,14 +849,7 @@ const app = Vue.createApp({
|
||||||
this.startWebRTC(msg.username, true);
|
this.startWebRTC(msg.username, true);
|
||||||
},
|
},
|
||||||
onRing(msg) {
|
onRing(msg) {
|
||||||
// Admin moderation feature: if the user has booted an admin off their camera, do not
|
// Request from a viewer to see our broadcast.
|
||||||
// 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.`);
|
|
||||||
this.startWebRTC(msg.username, false);
|
this.startWebRTC(msg.username, false);
|
||||||
},
|
},
|
||||||
onUserExited(msg) {
|
onUserExited(msg) {
|
||||||
|
@ -835,7 +862,12 @@ const app = Vue.createApp({
|
||||||
// Play sound effects if this is not the active channel or the window is not focused.
|
// Play sound effects if this is not the active channel or the window is not focused.
|
||||||
if (msg.channel.indexOf("@") === 0) {
|
if (msg.channel.indexOf("@") === 0) {
|
||||||
if (msg.channel !== this.channel || !this.windowFocused) {
|
if (msg.channel !== this.channel || !this.windowFocused) {
|
||||||
this.playSound("DM");
|
// 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) {
|
} else if (msg.channel !== this.channel || !this.windowFocused) {
|
||||||
this.playSound("Chat");
|
this.playSound("Chat");
|
||||||
|
@ -868,18 +900,20 @@ const app = Vue.createApp({
|
||||||
// User logged in or out.
|
// User logged in or out.
|
||||||
onPresence(msg) {
|
onPresence(msg) {
|
||||||
// TODO: make a dedicated leave event
|
// TODO: make a dedicated leave event
|
||||||
let isLeave = false;
|
let isLeave = false,
|
||||||
|
isJoin = false;
|
||||||
if (msg.message.indexOf("has exited the room!") > -1) {
|
if (msg.message.indexOf("has exited the room!") > -1) {
|
||||||
// Clean up data about this user.
|
// Clean up data about this user.
|
||||||
this.onUserExited(msg);
|
this.onUserExited(msg);
|
||||||
this.playSound("Leave");
|
this.playSound("Leave");
|
||||||
isLeave = true;
|
isLeave = true;
|
||||||
} else {
|
} else if (msg.message.indexOf("has joined the room!") > -1) {
|
||||||
this.playSound("Enter");
|
this.playSound("Enter");
|
||||||
|
isJoin = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push it to the history of all public channels (not leaves).
|
// Push it to the history of all public channels (depending on user preference).
|
||||||
if (!isLeave) {
|
if ((isJoin && this.prefs.joinMessages) || (isLeave && this.prefs.exitMessages)) {
|
||||||
for (let channel of this.config.channels) {
|
for (let channel of this.config.channels) {
|
||||||
this.pushHistory({
|
this.pushHistory({
|
||||||
channel: channel.ID,
|
channel: channel.ID,
|
||||||
|
@ -1078,6 +1112,13 @@ const app = Vue.createApp({
|
||||||
pc.ontrack = event => {
|
pc.ontrack = event => {
|
||||||
const stream = event.streams[0];
|
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?
|
// Do we already have it?
|
||||||
// this.ChatClient(`Received a video stream from ${username}.`);
|
// this.ChatClient(`Received a video stream from ${username}.`);
|
||||||
if (this.WebRTC.streams[username] == undefined ||
|
if (this.WebRTC.streams[username] == undefined ||
|
||||||
|
@ -1212,10 +1253,12 @@ const app = Vue.createApp({
|
||||||
// The user has our video feed open now.
|
// The user has our video feed open now.
|
||||||
if (this.isBootedAdmin(msg.username)) return;
|
if (this.isBootedAdmin(msg.username)) return;
|
||||||
this.webcam.watching[msg.username] = true;
|
this.webcam.watching[msg.username] = true;
|
||||||
|
this.playSound("Watch");
|
||||||
},
|
},
|
||||||
onUnwatch(msg) {
|
onUnwatch(msg) {
|
||||||
// The user has closed our video feed.
|
// The user has closed our video feed.
|
||||||
delete(this.webcam.watching[msg.username]);
|
delete(this.webcam.watching[msg.username]);
|
||||||
|
this.playSound("Unwatch");
|
||||||
},
|
},
|
||||||
sendWatch(username, watching) {
|
sendWatch(username, watching) {
|
||||||
// Send the watch or unwatch message to backend.
|
// Send the watch or unwatch message to backend.
|
||||||
|
@ -1681,15 +1724,26 @@ const app = Vue.createApp({
|
||||||
return;
|
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);
|
this.sendOpen(user.username);
|
||||||
|
|
||||||
// Responsive CSS -> go to chat panel to see the camera
|
// Responsive CSS -> go to chat panel to see the camera
|
||||||
this.openChatPanel();
|
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) {
|
closeVideo(username, name) {
|
||||||
// Clean up any lingering camera freeze states.
|
// Clean up any lingering camera freeze states.
|
||||||
|
@ -1762,10 +1816,16 @@ const app = Vue.createApp({
|
||||||
// - Usually a video icon
|
// - Usually a video icon
|
||||||
// - May be a crossed-out video if isVideoNotAllowed
|
// - May be a crossed-out video if isVideoNotAllowed
|
||||||
// - Or an eyeball for cameras already opened
|
// - 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) {
|
if (user.username === this.username && this.webcam.active) {
|
||||||
return 'fa-eye'; // user sees their own self camera always
|
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?
|
// Already opened?
|
||||||
if (this.WebRTC.pc[user.username] != undefined && this.WebRTC.streams[user.username] != undefined) {
|
if (this.WebRTC.pc[user.username] != undefined && this.WebRTC.streams[user.username] != undefined) {
|
||||||
return 'fa-eye';
|
return 'fa-eye';
|
||||||
|
@ -2016,6 +2076,12 @@ const app = Vue.createApp({
|
||||||
channel = this.channel;
|
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?
|
// Initialize this channel's history?
|
||||||
this.initHistory(channel);
|
this.initHistory(channel);
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,15 @@ const SoundEffects = [
|
||||||
{
|
{
|
||||||
name: "Sonar",
|
name: "Sonar",
|
||||||
filename: "sonar-ping-95840.mp3"
|
filename: "sonar-ping-95840.mp3"
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
name: "Up Chime",
|
||||||
|
filename: "notification-6175-up.mp3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Down Chime",
|
||||||
|
filename: "notification-6175-down.mp3"
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// Defaults
|
// Defaults
|
||||||
|
@ -32,4 +40,6 @@ var DefaultSounds = {
|
||||||
DM: "Trill",
|
DM: "Trill",
|
||||||
Enter: "Quiet",
|
Enter: "Quiet",
|
||||||
Leave: "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
|
Camera
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li :class="{'is-active': settingsModal.tab==='misc'}">
|
||||||
|
<a href="#" @click.prevent="settingsModal.tab='misc'">
|
||||||
|
Misc
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -181,7 +186,7 @@
|
||||||
|
|
||||||
<div class="columns is-mobile">
|
<div class="columns is-mobile">
|
||||||
<div class="column is-2 pr-1">
|
<div class="column is-2 pr-1">
|
||||||
<label class="label">DM chat</label>
|
<label class="label is-size-7">DM chat</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<div class="select is-fullwidth">
|
<div class="select is-fullwidth">
|
||||||
|
@ -196,7 +201,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="column is-2 pr-1">
|
<div class="column is-2 pr-1">
|
||||||
<label class="label">Public chat</label>
|
<label class="label is-size-7">Public chat</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<div class="select is-fullwidth">
|
<div class="select is-fullwidth">
|
||||||
|
@ -213,7 +218,7 @@
|
||||||
|
|
||||||
<div class="columns is-mobile">
|
<div class="columns is-mobile">
|
||||||
<div class="column is-2 pr-1">
|
<div class="column is-2 pr-1">
|
||||||
<label class="label">Room enter</label>
|
<label class="label is-size-7">Room enter</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<div class="select is-fullwidth">
|
<div class="select is-fullwidth">
|
||||||
|
@ -228,7 +233,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="column is-2 pr-1">
|
<div class="column is-2 pr-1">
|
||||||
<label class="label">Room leave</label>
|
<label class="label is-size-7">Room leave</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<div class="select is-fullwidth">
|
<div class="select is-fullwidth">
|
||||||
|
@ -242,6 +247,38 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Webcam preferences -->
|
<!-- Webcam preferences -->
|
||||||
|
@ -332,6 +369,52 @@
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
<footer class="card-footer">
|
<footer class="card-footer">
|
||||||
<div class="card-footer-item">
|
<div class="card-footer-item">
|
||||||
|
@ -538,7 +621,7 @@
|
||||||
<div class="select is-fullwidth">
|
<div class="select is-fullwidth">
|
||||||
<select id="classification"
|
<select id="classification"
|
||||||
v-model="reportModal.classification"
|
v-model="reportModal.classification"
|
||||||
:disabled="busy">
|
:disabled="reportModal.busy">
|
||||||
<option v-for="i in config.reportClassifications"
|
<option v-for="i in config.reportClassifications"
|
||||||
:value="i">[[i]]</option>
|
:value="i">[[i]]</option>
|
||||||
</select>
|
</select>
|
||||||
|
@ -549,7 +632,7 @@
|
||||||
<label class="label" for="reportComment">Comment:</label>
|
<label class="label" for="reportComment">Comment:</label>
|
||||||
<textarea class="textarea"
|
<textarea class="textarea"
|
||||||
v-model="reportModal.comment"
|
v-model="reportModal.comment"
|
||||||
:disabled="busy"
|
:disabled="reportModal.busy"
|
||||||
cols="80" rows="2"
|
cols="80" rows="2"
|
||||||
placeholder="Optional: describe the issue"></textarea>
|
placeholder="Optional: describe the issue"></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
@ -558,7 +641,7 @@
|
||||||
<div class="control has-text-centered">
|
<div class="control has-text-centered">
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="button is-link mr-4"
|
class="button is-link mr-4"
|
||||||
:disabled="busy"
|
:disabled="reportModal.busy"
|
||||||
@click="doReport()">Submit report</button>
|
@click="doReport()">Submit report</button>
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="button"
|
class="button"
|
||||||
|
|
Loading…
Reference in New Issue
Block a user