Add the option to mute channels, queries, and networks (#4282)
Co-authored-by: Reto <reto@labrat.space>
This commit is contained in:
parent
337bfa489b
commit
4be9a282fa
@ -1,9 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<ChannelWrapper ref="wrapper" v-bind="$props">
|
<ChannelWrapper ref="wrapper" v-bind="$props">
|
||||||
<span class="name">{{ channel.name }}</span>
|
<span class="name">{{ channel.name }}</span>
|
||||||
<span v-if="channel.unread" :class="{highlight: channel.highlight}" class="badge">{{
|
<span
|
||||||
unreadCount
|
v-if="channel.unread"
|
||||||
}}</span>
|
:class="{highlight: channel.highlight && !channel.muted}"
|
||||||
|
class="badge"
|
||||||
|
>{{ unreadCount }}</span
|
||||||
|
>
|
||||||
<template v-if="channel.type === 'channel'">
|
<template v-if="channel.type === 'channel'">
|
||||||
<span
|
<span
|
||||||
v-if="channel.state === 0"
|
v-if="channel.state === 0"
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
channel.type === 'lobby' && network.status.connected && !network.status.secure,
|
channel.type === 'lobby' && network.status.connected && !network.status.secure,
|
||||||
},
|
},
|
||||||
{'not-connected': channel.type === 'lobby' && !network.status.connected},
|
{'not-connected': channel.type === 'lobby' && !network.status.connected},
|
||||||
|
{'is-muted': channel.muted},
|
||||||
]"
|
]"
|
||||||
:aria-label="getAriaLabel()"
|
:aria-label="getAriaLabel()"
|
||||||
:title="getAriaLabel()"
|
:title="getAriaLabel()"
|
||||||
|
@ -176,7 +176,7 @@ export default {
|
|||||||
message.channel = this.$store.getters.findChannel(message.chanId);
|
message.channel = this.$store.getters.findChannel(message.chanId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return messages;
|
return messages.filter((message) => !message.channel.channel.muted);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
@ -645,6 +645,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="help-item">
|
||||||
|
<div class="subject">
|
||||||
|
<code>/mute [...channel]</code>
|
||||||
|
</div>
|
||||||
|
<div class="description">
|
||||||
|
<p>
|
||||||
|
Prevent messages from generating any feedback for a channel. This turns off
|
||||||
|
the highlight indicator, hides mentions and inhibits push notifications.
|
||||||
|
Muting a network lobby mutes the entire network. Not specifying any channel
|
||||||
|
target mutes the current channel. Revert with <code>/unmute</code>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="help-item">
|
<div class="help-item">
|
||||||
<div class="subject">
|
<div class="subject">
|
||||||
<code>/nick newnick</code>
|
<code>/nick newnick</code>
|
||||||
@ -780,6 +794,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="help-item">
|
||||||
|
<div class="subject">
|
||||||
|
<code>/unmute [...channel]</code>
|
||||||
|
</div>
|
||||||
|
<div class="description">
|
||||||
|
<p>
|
||||||
|
Un-mutes the given channel(s) or the current channel if no channel is
|
||||||
|
provided. See <code>/mute</code> for more information.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="help-item">
|
<div class="help-item">
|
||||||
<div class="subject">
|
<div class="subject">
|
||||||
<code>/voice nick [...nick]</code>
|
<code>/voice nick [...nick]</code>
|
||||||
|
@ -369,6 +369,7 @@ p {
|
|||||||
.context-menu-network::before { content: "\f233"; /* https://fontawesome.com/icons/server?style=solid */ }
|
.context-menu-network::before { content: "\f233"; /* https://fontawesome.com/icons/server?style=solid */ }
|
||||||
.context-menu-edit::before { content: "\f303"; /* https://fontawesome.com/icons/pencil-alt?style=solid */ }
|
.context-menu-edit::before { content: "\f303"; /* https://fontawesome.com/icons/pencil-alt?style=solid */ }
|
||||||
.context-menu-clear-history::before { content: "\f1f8"; /* https://fontawesome.com/icons/trash?style=solid */ }
|
.context-menu-clear-history::before { content: "\f1f8"; /* https://fontawesome.com/icons/trash?style=solid */ }
|
||||||
|
.context-menu-mute::before { content: "\f6a9"; /* https://fontawesome.com/v5.15/icons/volume-mute?style=solid */ }
|
||||||
|
|
||||||
.channel-list-item .not-secure-icon::before {
|
.channel-list-item .not-secure-icon::before {
|
||||||
content: "\f071"; /* https://fontawesome.com/icons/exclamation-triangle?style=solid */
|
content: "\f071"; /* https://fontawesome.com/icons/exclamation-triangle?style=solid */
|
||||||
@ -817,6 +818,10 @@ background on hover (unless active) */
|
|||||||
color: #f1978e;
|
color: #f1978e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.channel-list-item.is-muted {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
.channel-list-item::before {
|
.channel-list-item::before {
|
||||||
width: 14px;
|
width: 14px;
|
||||||
margin-right: 12px;
|
margin-right: 12px;
|
||||||
|
@ -170,6 +170,31 @@ export function generateChannelContextMenu($root, channel, network) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const humanFriendlyChanTypeMap = {
|
||||||
|
lobby: "network",
|
||||||
|
channel: "channel",
|
||||||
|
query: "conversation",
|
||||||
|
};
|
||||||
|
|
||||||
|
// We don't allow the muting of Chan.Type.SPECIAL channels
|
||||||
|
const mutableChanTypes = Object.keys(humanFriendlyChanTypeMap);
|
||||||
|
|
||||||
|
if (mutableChanTypes.includes(channel.type)) {
|
||||||
|
const chanType = humanFriendlyChanTypeMap[channel.type];
|
||||||
|
|
||||||
|
items.push({
|
||||||
|
label: channel.muted ? `Unmute ${chanType}` : `Mute ${chanType}`,
|
||||||
|
type: "item",
|
||||||
|
class: "mute",
|
||||||
|
action() {
|
||||||
|
socket.emit("mute:change", {
|
||||||
|
target: channel.id,
|
||||||
|
setMutedTo: !channel.muted,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Add close menu item
|
// Add close menu item
|
||||||
items.push({
|
items.push({
|
||||||
label: closeMap[channel.type],
|
label: closeMap[channel.type],
|
||||||
|
@ -26,3 +26,4 @@ import "./setting";
|
|||||||
import "./history_clear";
|
import "./history_clear";
|
||||||
import "./mentions";
|
import "./mentions";
|
||||||
import "./search";
|
import "./search";
|
||||||
|
import "./mute_changed";
|
||||||
|
@ -95,6 +95,10 @@ socket.on("msg", function (data) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function notifyMessage(targetId, channel, activeChannel, msg) {
|
function notifyMessage(targetId, channel, activeChannel, msg) {
|
||||||
|
if (channel.muted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (msg.highlight || (store.state.settings.notifyAllMessages && msg.type === "message")) {
|
if (msg.highlight || (store.state.settings.notifyAllMessages && msg.type === "message")) {
|
||||||
if (!document.hasFocus() || !activeChannel || activeChannel.channel !== channel) {
|
if (!document.hasFocus() || !activeChannel || activeChannel.channel !== channel) {
|
||||||
if (store.state.settings.notification) {
|
if (store.state.settings.notification) {
|
||||||
|
17
client/js/socket-events/mute_changed.js
Normal file
17
client/js/socket-events/mute_changed.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import socket from "../socket";
|
||||||
|
import store from "../store";
|
||||||
|
|
||||||
|
socket.on("mute:changed", (response) => {
|
||||||
|
const {target, status} = response;
|
||||||
|
const {channel, network} = store.getters.findChannel(target);
|
||||||
|
|
||||||
|
if (channel.type === "lobby") {
|
||||||
|
for (const chan of network.channels) {
|
||||||
|
if (chan.type !== "special") {
|
||||||
|
chan.muted = status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.muted = status;
|
||||||
|
}
|
||||||
|
});
|
@ -181,6 +181,10 @@ const store = new Vuex.Store({
|
|||||||
|
|
||||||
for (const network of state.networks) {
|
for (const network of state.networks) {
|
||||||
for (const channel of network.channels) {
|
for (const channel of network.channels) {
|
||||||
|
if (channel.muted) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
highlightCount += channel.highlight;
|
highlightCount += channel.highlight;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -202,6 +202,7 @@ Client.prototype.connect = function (args, isStartup = false) {
|
|||||||
name: chan.name,
|
name: chan.name,
|
||||||
key: chan.key || "",
|
key: chan.key || "",
|
||||||
type: chan.type,
|
type: chan.type,
|
||||||
|
muted: chan.muted,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -41,6 +41,7 @@ function Chan(attr) {
|
|||||||
unread: 0,
|
unread: 0,
|
||||||
highlight: 0,
|
highlight: 0,
|
||||||
users: new Map(),
|
users: new Map(),
|
||||||
|
muted: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -276,6 +277,10 @@ Chan.prototype.isLoggable = function () {
|
|||||||
return this.type === Chan.Type.CHANNEL || this.type === Chan.Type.QUERY;
|
return this.type === Chan.Type.CHANNEL || this.type === Chan.Type.QUERY;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Chan.prototype.setMuteStatus = function (muted) {
|
||||||
|
this.muted = !!muted;
|
||||||
|
};
|
||||||
|
|
||||||
function requestZncPlayback(channel, network, from) {
|
function requestZncPlayback(channel, network, from) {
|
||||||
network.irc.raw("ZNC", "*playback", "PLAY", channel.name, from.toString());
|
network.irc.raw("ZNC", "*playback", "PLAY", channel.name, from.toString());
|
||||||
}
|
}
|
||||||
|
@ -76,6 +76,11 @@ function Network(attr) {
|
|||||||
new Chan({
|
new Chan({
|
||||||
name: this.name,
|
name: this.name,
|
||||||
type: Chan.Type.LOBBY,
|
type: Chan.Type.LOBBY,
|
||||||
|
// The lobby only starts as muted if every channel (unless it's special) is muted.
|
||||||
|
// This is A) easier to implement and B) stops some confusion on startup.
|
||||||
|
muted:
|
||||||
|
this.channels.length >= 1 &&
|
||||||
|
this.channels.every((chan) => chan.muted || chan.type === Chan.Type.SPECIAL),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -546,7 +551,7 @@ Network.prototype.export = function () {
|
|||||||
return channel.type === Chan.Type.CHANNEL || channel.type === Chan.Type.QUERY;
|
return channel.type === Chan.Type.CHANNEL || channel.type === Chan.Type.QUERY;
|
||||||
})
|
})
|
||||||
.map(function (chan) {
|
.map(function (chan) {
|
||||||
const keys = ["name"];
|
const keys = ["name", "muted"];
|
||||||
|
|
||||||
if (chan.type === Chan.Type.CHANNEL) {
|
if (chan.type === Chan.Type.CHANNEL) {
|
||||||
keys.push("key");
|
keys.push("key");
|
||||||
|
@ -35,6 +35,7 @@ const userInputs = [
|
|||||||
"rejoin",
|
"rejoin",
|
||||||
"topic",
|
"topic",
|
||||||
"whois",
|
"whois",
|
||||||
|
"mute",
|
||||||
].reduce(function (plugins, name) {
|
].reduce(function (plugins, name) {
|
||||||
const plugin = require(`./${name}`);
|
const plugin = require(`./${name}`);
|
||||||
plugin.commands.forEach((command) => plugins.set(command, plugin));
|
plugin.commands.forEach((command) => plugins.set(command, plugin));
|
||||||
|
61
src/plugins/inputs/mute.js
Normal file
61
src/plugins/inputs/mute.js
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
"use strict";
|
||||||
|
const Msg = require("../../models/msg");
|
||||||
|
|
||||||
|
exports.commands = ["mute", "unmute"];
|
||||||
|
|
||||||
|
function args_to_channels(network, args) {
|
||||||
|
const targets = [];
|
||||||
|
|
||||||
|
for (const arg of args) {
|
||||||
|
const target = network.channels.find((c) => c.name === arg);
|
||||||
|
|
||||||
|
if (target) {
|
||||||
|
targets.push(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return targets;
|
||||||
|
}
|
||||||
|
|
||||||
|
function change_mute_state(client, target, valueToSet) {
|
||||||
|
if (target.type === "special") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
target.setMuteStatus(valueToSet);
|
||||||
|
client.emit("mute:changed", {
|
||||||
|
target: target.id,
|
||||||
|
status: valueToSet,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.input = function (network, chan, cmd, args) {
|
||||||
|
const valueToSet = cmd === "mute" ? true : false;
|
||||||
|
const client = this;
|
||||||
|
|
||||||
|
if (args.length === 0) {
|
||||||
|
change_mute_state(client, chan, valueToSet);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targets = args_to_channels(network, args);
|
||||||
|
|
||||||
|
if (targets.length !== args.length) {
|
||||||
|
const targetNames = targets.map((ch) => ch.name);
|
||||||
|
const missing = args.filter((x) => !targetNames.includes(x));
|
||||||
|
chan.pushMessage(
|
||||||
|
client,
|
||||||
|
new Msg({
|
||||||
|
type: Msg.Type.ERROR,
|
||||||
|
text: `No open ${
|
||||||
|
missing.length === 1 ? "channel or user" : "channels or users"
|
||||||
|
} found for ${missing.join(",")}`,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const target of targets) {
|
||||||
|
change_mute_state(client, target, valueToSet);
|
||||||
|
}
|
||||||
|
};
|
@ -97,7 +97,7 @@ module.exports = function (irc, network) {
|
|||||||
|
|
||||||
from = chan.getUser(data.nick);
|
from = chan.getUser(data.nick);
|
||||||
|
|
||||||
// Query messages (unless self) always highlight
|
// Query messages (unless self or muted) always highlight
|
||||||
if (chan.type === Chan.Type.QUERY) {
|
if (chan.type === Chan.Type.QUERY) {
|
||||||
highlight = !self;
|
highlight = !self;
|
||||||
} else if (chan.type === Chan.Type.CHANNEL) {
|
} else if (chan.type === Chan.Type.CHANNEL) {
|
||||||
@ -158,8 +158,8 @@ module.exports = function (irc, network) {
|
|||||||
|
|
||||||
chan.pushMessage(client, msg, !msg.self);
|
chan.pushMessage(client, msg, !msg.self);
|
||||||
|
|
||||||
// Do not send notifications for messages older than 15 minutes (znc buffer for example)
|
// Do not send notifications if the channel is muted or for messages older than 15 minutes (znc buffer for example)
|
||||||
if (msg.highlight && (!data.time || data.time > Date.now() - 900000)) {
|
if (!chan.muted && msg.highlight && (!data.time || data.time > Date.now() - 900000)) {
|
||||||
let title = chan.name;
|
let title = chan.name;
|
||||||
let body = cleanMessage;
|
let body = cleanMessage;
|
||||||
|
|
||||||
|
@ -23,6 +23,7 @@ const themes = require("./plugins/packages/themes");
|
|||||||
themes.loadLocalThemes();
|
themes.loadLocalThemes();
|
||||||
|
|
||||||
const packages = require("./plugins/packages/index");
|
const packages = require("./plugins/packages/index");
|
||||||
|
const Chan = require("./models/chan");
|
||||||
|
|
||||||
// A random number that will force clients to reload the page if it differs
|
// A random number that will force clients to reload the page if it differs
|
||||||
const serverHash = Math.floor(Date.now() * Math.random());
|
const serverHash = Math.floor(Date.now() * Math.random());
|
||||||
@ -655,6 +656,32 @@ function initializeClient(socket, client, token, lastMessage, openChannel) {
|
|||||||
socket.emit("search:results", results);
|
socket.emit("search:results", results);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on("mute:change", ({target, setMutedTo}) => {
|
||||||
|
const {chan, network} = client.find(target);
|
||||||
|
|
||||||
|
// If the user mutes the lobby, we mute the entire network.
|
||||||
|
if (chan.type === Chan.Type.LOBBY) {
|
||||||
|
for (const channel of network.channels) {
|
||||||
|
if (channel.type !== Chan.Type.SPECIAL) {
|
||||||
|
channel.setMuteStatus(setMutedTo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (chan.type !== Chan.Type.SPECIAL) {
|
||||||
|
chan.setMuteStatus(setMutedTo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const attachedClient of Object.keys(client.attachedClients)) {
|
||||||
|
manager.sockets.in(attachedClient).emit("mute:changed", {
|
||||||
|
target,
|
||||||
|
status: setMutedTo,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
client.save();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.on("sign-out", (tokenToSignOut) => {
|
socket.on("sign-out", (tokenToSignOut) => {
|
||||||
|
@ -213,6 +213,7 @@ describe("Chan", function () {
|
|||||||
"id",
|
"id",
|
||||||
"key",
|
"key",
|
||||||
"messages",
|
"messages",
|
||||||
|
"muted",
|
||||||
"totalMessages",
|
"totalMessages",
|
||||||
"name",
|
"name",
|
||||||
"state",
|
"state",
|
||||||
|
@ -18,12 +18,12 @@ describe("Network", function () {
|
|||||||
saslAccount: "testaccount",
|
saslAccount: "testaccount",
|
||||||
saslPassword: "testpassword",
|
saslPassword: "testpassword",
|
||||||
channels: [
|
channels: [
|
||||||
new Chan({name: "#thelounge", key: ""}),
|
new Chan({name: "#thelounge", key: "", muted: false}),
|
||||||
new Chan({name: "&foobar", key: ""}),
|
new Chan({name: "&foobar", key: "", muted: false}),
|
||||||
new Chan({name: "#secret", key: "foo"}),
|
new Chan({name: "#secret", key: "foo", muted: false}),
|
||||||
new Chan({name: "&secure", key: "bar"}),
|
new Chan({name: "&secure", key: "bar", muted: true}),
|
||||||
new Chan({name: "Channel List", type: Chan.Type.SPECIAL}),
|
new Chan({name: "Channel List", type: Chan.Type.SPECIAL}),
|
||||||
new Chan({name: "PrivateChat", type: Chan.Type.QUERY}),
|
new Chan({name: "PrivateChat", type: Chan.Type.QUERY, muted: true}),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
network.setNick("chillin`");
|
network.setNick("chillin`");
|
||||||
@ -52,11 +52,11 @@ describe("Network", function () {
|
|||||||
proxyPassword: "",
|
proxyPassword: "",
|
||||||
proxyUsername: "",
|
proxyUsername: "",
|
||||||
channels: [
|
channels: [
|
||||||
{name: "#thelounge", key: ""},
|
{name: "#thelounge", key: "", muted: false},
|
||||||
{name: "&foobar", key: ""},
|
{name: "&foobar", key: "", muted: false},
|
||||||
{name: "#secret", key: "foo"},
|
{name: "#secret", key: "foo", muted: false},
|
||||||
{name: "&secure", key: "bar"},
|
{name: "&secure", key: "bar", muted: true},
|
||||||
{name: "PrivateChat", type: "query"},
|
{name: "PrivateChat", type: "query", muted: true},
|
||||||
],
|
],
|
||||||
ignoreList: [],
|
ignoreList: [],
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user