diff --git a/client/css/style.css b/client/css/style.css index 517f6925..8ee13368 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -352,8 +352,8 @@ p { .context-menu-action-whois::before { content: "\f05a"; /* http://fontawesome.io/icon/info-circle/ */ } .context-menu-action-ignore::before { content: "\f506"; /* https://fontawesome.com/icons/user-slash?style=solid */ } .context-menu-action-kick::before { content: "\f05e"; /* http://fontawesome.io/icon/ban/ */ } -.context-menu-action-op::before { content: "\f1fa"; /* http://fontawesome.io/icon/at/ */ } -.context-menu-action-voice::before { content: "\f067"; /* http://fontawesome.io/icon/plus/ */ } +.context-menu-action-set-mode::before { content: "\f067"; /* http://fontawesome.io/icon/plus/ */ } +.context-menu-action-revoke-mode::before { content: "\f068"; /* http://fontawesome.io/icon/minus/ */ } .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 */ } diff --git a/client/js/helpers/contextMenu.js b/client/js/helpers/contextMenu.js index ba0f402c..f2409e55 100644 --- a/client/js/helpers/contextMenu.js +++ b/client/js/helpers/contextMenu.js @@ -185,7 +185,6 @@ export function generateChannelContextMenu($root, channel, network) { export function generateUserContextMenu($root, channel, network, user) { const currentChannelUser = channel.users.find((u) => u.nick === network.nick) || {}; - const currentChannelModes = currentChannelUser.modes || []; const whois = () => { const chan = $root.$store.getters.findChannelOnCurrentNetwork(user.nick); @@ -246,66 +245,93 @@ export function generateUserContextMenu($root, channel, network, user) { }, ]; - if (currentChannelModes.includes("@")) { - items.push({ - label: "Kick", - type: "item", - class: "action-kick", - action() { - socket.emit("input", { - target: channel.id, - text: "/kick " + user.nick, - }); - }, - }); + // Bail because we don't have a special mode. + if (currentChannelUser.modes.length < 1) { + return items; + } - if (user.modes.includes("@")) { + // Names of the modes we are able to change + const modes = { + "~": ["owner", "q"], + "&": ["admin", "a"], + "@": ["operator", "o"], + "%": ["half-op", "h"], + "+": ["voice", "v"], + }; + + // Labels for the mode changes. For example .rev(['admin', 'a']) => 'Revoke admin (-a)' + const modeTextTemplate = { + revoke: (m) => `Revoke ${m[0]} (-${m[1]})`, + give: (m) => `Give ${m[0]} (+${m[1]})`, + }; + + const networkModes = network.serverOptions.PREFIX; + + /** + * Determine whether the prefix of mode p1 has access to perform actions on p2. + * + * EXAMPLE: + * compare('@', '@') => true + * compare('&', '@') => true + * compare('+', '~') => false + * @param {string} p1 The mode performing an action + * @param {string} p2 The target mode + * + * @return {boolean} whether p1 can perform an action on p2 + */ + function compare(p1, p2) { + // The modes ~ and @ can perform actions on their own mode. The others on modes below. + return "~@".indexOf(p1) > -1 + ? networkModes.indexOf(p1) <= networkModes.indexOf(p2) + : networkModes.indexOf(p1) < networkModes.indexOf(p2); + } + + networkModes.forEach((prefix) => { + if (!compare(currentChannelUser.modes[0], prefix)) { + // Our highest mode is below the current mode. Bail. + return; + } + + if (!user.modes.includes(prefix)) { + // The target doesn't already have this mode, therefore we can set it. items.push({ - label: "Revoke operator (-o)", + label: modeTextTemplate.give(modes[prefix]), type: "item", - class: "action-op", + class: "action-set-mode", action() { socket.emit("input", { target: channel.id, - text: "/deop " + user.nick, + text: "/mode +" + modes[prefix][1] + " " + user.nick, }); }, }); } else { items.push({ - label: "Give operator (+o)", + label: modeTextTemplate.revoke(modes[prefix]), type: "item", - class: "action-op", + class: "action-revoke-mode", action() { socket.emit("input", { target: channel.id, - text: "/op " + user.nick, + text: "/mode -" + modes[prefix][1] + " " + user.nick, }); }, }); } + }); - if (user.modes.includes("+")) { + // Determine if we are half-op or op depending on the network modes so we can kick. + if (!compare(networkModes.indexOf("%") > -1 ? "%" : "@", currentChannelUser.modes[0])) { + if (user.modes.length === 0 || compare(currentChannelUser.modes[0], user.modes[0])) { + // Check if the target user has no mode or a mode lower than ours. items.push({ - label: "Revoke voice (-v)", + label: "Kick", type: "item", - class: "action-voice", + class: "action-kick", action() { socket.emit("input", { target: channel.id, - text: "/devoice " + user.nick, - }); - }, - }); - } else { - items.push({ - label: "Give voice (+v)", - type: "item", - class: "action-voice", - action() { - socket.emit("input", { - target: channel.id, - text: "/voice " + user.nick, + text: "/kick " + user.nick, }); }, });