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>
|
||||
<ChannelWrapper ref="wrapper" v-bind="$props">
|
||||
<span class="name">{{ channel.name }}</span>
|
||||
<span v-if="channel.unread" :class="{highlight: channel.highlight}" class="badge">{{
|
||||
unreadCount
|
||||
}}</span>
|
||||
<span
|
||||
v-if="channel.unread"
|
||||
:class="{highlight: channel.highlight && !channel.muted}"
|
||||
class="badge"
|
||||
>{{ unreadCount }}</span
|
||||
>
|
||||
<template v-if="channel.type === 'channel'">
|
||||
<span
|
||||
v-if="channel.state === 0"
|
||||
|
@ -15,6 +15,7 @@
|
||||
channel.type === 'lobby' && network.status.connected && !network.status.secure,
|
||||
},
|
||||
{'not-connected': channel.type === 'lobby' && !network.status.connected},
|
||||
{'is-muted': channel.muted},
|
||||
]"
|
||||
:aria-label="getAriaLabel()"
|
||||
:title="getAriaLabel()"
|
||||
|
@ -176,7 +176,7 @@ export default {
|
||||
message.channel = this.$store.getters.findChannel(message.chanId);
|
||||
}
|
||||
|
||||
return messages;
|
||||
return messages.filter((message) => !message.channel.channel.muted);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
|
@ -645,6 +645,20 @@
|
||||
</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="subject">
|
||||
<code>/nick newnick</code>
|
||||
@ -780,6 +794,18 @@
|
||||
</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="subject">
|
||||
<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-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-mute::before { content: "\f6a9"; /* https://fontawesome.com/v5.15/icons/volume-mute?style=solid */ }
|
||||
|
||||
.channel-list-item .not-secure-icon::before {
|
||||
content: "\f071"; /* https://fontawesome.com/icons/exclamation-triangle?style=solid */
|
||||
@ -817,6 +818,10 @@ background on hover (unless active) */
|
||||
color: #f1978e;
|
||||
}
|
||||
|
||||
.channel-list-item.is-muted {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.channel-list-item::before {
|
||||
width: 14px;
|
||||
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
|
||||
items.push({
|
||||
label: closeMap[channel.type],
|
||||
|
@ -26,3 +26,4 @@ import "./setting";
|
||||
import "./history_clear";
|
||||
import "./mentions";
|
||||
import "./search";
|
||||
import "./mute_changed";
|
||||
|
@ -95,6 +95,10 @@ socket.on("msg", function (data) {
|
||||
});
|
||||
|
||||
function notifyMessage(targetId, channel, activeChannel, msg) {
|
||||
if (channel.muted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.highlight || (store.state.settings.notifyAllMessages && msg.type === "message")) {
|
||||
if (!document.hasFocus() || !activeChannel || activeChannel.channel !== channel) {
|
||||
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 channel of network.channels) {
|
||||
if (channel.muted) {
|
||||
continue;
|
||||
}
|
||||
|
||||
highlightCount += channel.highlight;
|
||||
}
|
||||
}
|
||||
|
@ -202,6 +202,7 @@ Client.prototype.connect = function (args, isStartup = false) {
|
||||
name: chan.name,
|
||||
key: chan.key || "",
|
||||
type: chan.type,
|
||||
muted: chan.muted,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
@ -41,6 +41,7 @@ function Chan(attr) {
|
||||
unread: 0,
|
||||
highlight: 0,
|
||||
users: new Map(),
|
||||
muted: false,
|
||||
});
|
||||
}
|
||||
|
||||
@ -276,6 +277,10 @@ Chan.prototype.isLoggable = function () {
|
||||
return this.type === Chan.Type.CHANNEL || this.type === Chan.Type.QUERY;
|
||||
};
|
||||
|
||||
Chan.prototype.setMuteStatus = function (muted) {
|
||||
this.muted = !!muted;
|
||||
};
|
||||
|
||||
function requestZncPlayback(channel, network, from) {
|
||||
network.irc.raw("ZNC", "*playback", "PLAY", channel.name, from.toString());
|
||||
}
|
||||
|
@ -76,6 +76,11 @@ function Network(attr) {
|
||||
new Chan({
|
||||
name: this.name,
|
||||
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;
|
||||
})
|
||||
.map(function (chan) {
|
||||
const keys = ["name"];
|
||||
const keys = ["name", "muted"];
|
||||
|
||||
if (chan.type === Chan.Type.CHANNEL) {
|
||||
keys.push("key");
|
||||
|
@ -35,6 +35,7 @@ const userInputs = [
|
||||
"rejoin",
|
||||
"topic",
|
||||
"whois",
|
||||
"mute",
|
||||
].reduce(function (plugins, name) {
|
||||
const plugin = require(`./${name}`);
|
||||
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);
|
||||
|
||||
// Query messages (unless self) always highlight
|
||||
// Query messages (unless self or muted) always highlight
|
||||
if (chan.type === Chan.Type.QUERY) {
|
||||
highlight = !self;
|
||||
} else if (chan.type === Chan.Type.CHANNEL) {
|
||||
@ -158,8 +158,8 @@ module.exports = function (irc, network) {
|
||||
|
||||
chan.pushMessage(client, msg, !msg.self);
|
||||
|
||||
// Do not send notifications for messages older than 15 minutes (znc buffer for example)
|
||||
if (msg.highlight && (!data.time || data.time > Date.now() - 900000)) {
|
||||
// Do not send notifications if the channel is muted or for messages older than 15 minutes (znc buffer for example)
|
||||
if (!chan.muted && msg.highlight && (!data.time || data.time > Date.now() - 900000)) {
|
||||
let title = chan.name;
|
||||
let body = cleanMessage;
|
||||
|
||||
|
@ -23,6 +23,7 @@ const themes = require("./plugins/packages/themes");
|
||||
themes.loadLocalThemes();
|
||||
|
||||
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
|
||||
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.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) => {
|
||||
|
@ -213,6 +213,7 @@ describe("Chan", function () {
|
||||
"id",
|
||||
"key",
|
||||
"messages",
|
||||
"muted",
|
||||
"totalMessages",
|
||||
"name",
|
||||
"state",
|
||||
|
@ -18,12 +18,12 @@ describe("Network", function () {
|
||||
saslAccount: "testaccount",
|
||||
saslPassword: "testpassword",
|
||||
channels: [
|
||||
new Chan({name: "#thelounge", key: ""}),
|
||||
new Chan({name: "&foobar", key: ""}),
|
||||
new Chan({name: "#secret", key: "foo"}),
|
||||
new Chan({name: "&secure", key: "bar"}),
|
||||
new Chan({name: "#thelounge", key: "", muted: false}),
|
||||
new Chan({name: "&foobar", key: "", muted: false}),
|
||||
new Chan({name: "#secret", key: "foo", muted: false}),
|
||||
new Chan({name: "&secure", key: "bar", muted: true}),
|
||||
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`");
|
||||
@ -52,11 +52,11 @@ describe("Network", function () {
|
||||
proxyPassword: "",
|
||||
proxyUsername: "",
|
||||
channels: [
|
||||
{name: "#thelounge", key: ""},
|
||||
{name: "&foobar", key: ""},
|
||||
{name: "#secret", key: "foo"},
|
||||
{name: "&secure", key: "bar"},
|
||||
{name: "PrivateChat", type: "query"},
|
||||
{name: "#thelounge", key: "", muted: false},
|
||||
{name: "&foobar", key: "", muted: false},
|
||||
{name: "#secret", key: "foo", muted: false},
|
||||
{name: "&secure", key: "bar", muted: true},
|
||||
{name: "PrivateChat", type: "query", muted: true},
|
||||
],
|
||||
ignoreList: [],
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user