diff --git a/client/css/style.css b/client/css/style.css index 1369957e..40eb7b53 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -224,6 +224,9 @@ kbd { .context-menu-chan::before { content: "\f292"; /* http://fontawesome.io/icon/hashtag/ */ } .context-menu-close::before { content: "\f00d"; /* http://fontawesome.io/icon/times/ */ } .context-menu-list::before { content: "\f03a"; /* http://fontawesome.io/icon/list/ */ } +.context-menu-action-whois::before { content: "\f05a"; /* http://fontawesome.io/icon/info-circle/ */ } +.context-menu-action-query::before { content: "\f0e6"; /* http://fontawesome.io/icon/comments-o/ */ } +.context-menu-action-kick::before { content: "\f05e"; /* http://fontawesome.io/icon/ban/ */ } .context-menu-network::before, #sidebar .chan.lobby::before, diff --git a/client/js/lounge.js b/client/js/lounge.js index 6e3757ce..dbfbe3fe 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -79,9 +79,34 @@ $(function() { if (target.hasClass("user")) { output = templates.contextmenu_item({ class: "user", + action: "whois", text: target.text(), data: target.data("name"), }); + output += templates.contextmenu_divider(); + output += templates.contextmenu_item({ + class: "action-whois", + action: "whois", + text: "User information", + data: target.data("name"), + }); + output += templates.contextmenu_item({ + class: "action-query", + action: "query", + text: "Direct messages", + data: target.data("name"), + }); + + const channel = target.closest(".chan"); + if (utils.isOpInChannel(channel) && channel.data("type") === "channel") { + output += templates.contextmenu_divider(); + output += templates.contextmenu_item({ + class: "action-kick", + action: "kick", + text: "Kick", + data: target.data("name"), + }); + } } else if (target.hasClass("chan")) { let itemClass; @@ -95,6 +120,7 @@ $(function() { output = templates.contextmenu_item({ class: itemClass, + action: "focusChan", text: target.data("title"), data: target.data("target"), }); @@ -102,12 +128,14 @@ $(function() { if (target.hasClass("lobby")) { output += templates.contextmenu_item({ class: "list", + action: "list", text: "List all channels", - data: target.data("target"), + data: target.data("id"), }); } output += templates.contextmenu_item({ class: "close", + action: "close", text: target.hasClass("lobby") ? "Disconnect" : target.hasClass("channel") ? "Leave" : "Close", data: target.data("target"), }); @@ -121,7 +149,11 @@ $(function() { return false; } - viewport.on("contextmenu", ".user, .network .chan", function(e) { + viewport.on("contextmenu", ".network .chan", function(e) { + return showContextMenu(this, e); + }); + + viewport.on("click contextmenu", ".user", function(e) { return showContextMenu(this, e); }); @@ -285,20 +317,6 @@ $(function() { $(this).closest(".msg.condensed").toggleClass("closed"); }); - chat.on("click", ".user", function() { - var name = $(this).data("name"); - var chan = utils.findCurrentNetworkChan(name); - - if (chan.length) { - chan.click(); - } - - socket.emit("input", { - target: chat.data("id"), - text: "/whois " + name, - }); - }); - sidebar.on("click", ".chan, button", function(e, data) { // Pushes states to history web API when clicking elements with a data-target attribute. // States are very trivial and only contain a single `clickTarget` property which @@ -447,24 +465,60 @@ $(function() { return false; }); - contextMenu.on("click", ".context-menu-item", function() { - switch ($(this).data("action")) { - case "close": - $(".networks .chan[data-target='" + $(this).data("data") + "'] .close").click(); - break; - case "chan": - $(".networks .chan[data-target='" + $(this).data("data") + "']").click(); - break; - case "user": - $(".channel.active .users .user[data-name='" + $(this).data("data") + "']").click(); - break; - case "list": + const contextMenuActions = { + close: function(itemData) { + $(`.networks .chan[data-target="${itemData}"] .close`).click(); + }, + focusChan: function(itemData) { + $(`.networks .chan[data-target="${itemData}"]`).click(); + }, + list: function(itemData) { socket.emit("input", { - target: chat.data("id"), + target: itemData, text: "/list", }); - break; - } + }, + whois: function(itemData) { + const chan = utils.findCurrentNetworkChan(itemData); + + if (chan.length) { + chan.click(); + } + + socket.emit("input", { + target: $("#chat").data("id"), + text: "/whois " + itemData, + }); + + $(`.channel.active .users .user[data-name="${itemData}"]`).click(); + }, + query: function(itemData) { + const chan = utils.findCurrentNetworkChan(itemData); + + if (chan.length) { + chan.click(); + } + + socket.emit("input", { + target: $("#chat").data("id"), + text: "/query " + itemData, + }); + }, + kick: function(itemData) { + socket.emit("input", { + target: $("#chat").data("id"), + text: "/kick " + itemData, + }); + }, + }; + + contextMenuActions.execute = (name, ...args) => contextMenuActions[name] && contextMenuActions[name](...args); + + contextMenu.on("click", ".context-menu-item", function() { + const $this = $(this); + const itemData = $this.data("data"); + const contextAction = $this.data("action"); + contextMenuActions.execute(contextAction, itemData); }); chat.on("input", ".search", function() { diff --git a/client/js/utils.js b/client/js/utils.js index 04974c13..9d3e7ba8 100644 --- a/client/js/utils.js +++ b/client/js/utils.js @@ -1,6 +1,7 @@ "use strict"; const $ = require("jquery"); +const escape = require("css.escape"); const input = $("#input"); var serverHash = -1; @@ -19,6 +20,7 @@ module.exports = { toggleNickEditor, toggleNotificationMarkers, requestIdleCallback, + isOpInChannel, }; function findCurrentNetworkChan(name) { @@ -37,6 +39,15 @@ function resetHeight(element) { element.style.height = element.style.minHeight; } +// Given a channel element will determine if the lounge user is Op in that channel +function isOpInChannel(channel) { + const channelID = channel.data("id"); + const network = $("#sidebar .network").has(`.chan[data-id="${channelID}"]`); + const ownNick = network.data("nick"); + const isOP = channel.find(`.users .user-mode.op .user[data-name="${escape(ownNick)}"]`).length; + return isOP; +} + // Triggering click event opens the virtual keyboard on mobile // This can only be called from another interactive event (e.g. button click) function forceFocus() { diff --git a/client/views/contextmenu_item.tpl b/client/views/contextmenu_item.tpl index e1cd4dc5..fe281d49 100644 --- a/client/views/contextmenu_item.tpl +++ b/client/views/contextmenu_item.tpl @@ -1,3 +1,3 @@ -