"use strict"; const $ = require("jquery"); const socket = require("./socket"); const utils = require("./utils"); const ContextMenu = require("./contextMenu"); const contextMenuActions = []; const contextMenuItems = []; const {vueApp, findChannel} = require("./vue"); addDefaultItems(); registerEvents(); /** * Used for adding context menu items. eg: * * addContextMenuItem({ * check: (target) => target.hasClass("user"), * className: "customItemName", * data: (target) => target.attr("data-name"), * displayName: "Do something", * callback: (name) => console.log(name), // print the name of the user to console * }); * * @param opts * @param {function(Object)} [opts.check] - Function to check whether item should show on the context menu, called with the target jquery element, shows if return is truthy * @param {string|function(Object)} opts.className - class name for the menu item, should be prefixed for non-default menu items (if function, called with jquery element, and uses return value) * @param {string|function(Object)} opts.data - data that will be sent to the callback function (if function, called with jquery element, and uses return value) * @param {string|function(Object)} opts.displayName - text to display on the menu item (if function, called with jquery element, and uses return value) * @param {function(Object)} opts.callback - Function to call when the context menu item is clicked, called with the data requested in opts.data */ function addContextMenuItem(opts) { opts.check = opts.check || (() => true); opts.actionId = contextMenuActions.push(opts.callback) - 1; contextMenuItems.push(opts); } function addContextDivider(opts) { opts.check = opts.check || (() => true); opts.divider = true; contextMenuItems.push(opts); } function createContextMenu(that, event) { return new ContextMenu(contextMenuItems, contextMenuActions, that, event); } function addWhoisItem() { function whois(itemData) { const chan = utils.findCurrentNetworkChan(itemData); if (chan) { vueApp.switchToChannel(chan); } socket.emit("input", { target: Number($("#chat").attr("data-id")), text: "/whois " + itemData, }); } addContextMenuItem({ check: (target) => target.hasClass("user"), className: "user", displayName: (target) => target.attr("data-name"), data: (target) => target.attr("data-name"), callback: whois, }); addContextDivider({ check: (target) => target.hasClass("user"), }); addContextMenuItem({ check: (target) => target.hasClass("user") || target.hasClass("query"), className: "action-whois", displayName: "User information", data: (target) => target.attr("data-name") || target.attr("aria-label"), callback: whois, }); } function addQueryItem() { function query(itemData) { const chan = utils.findCurrentNetworkChan(itemData); if (chan) { vueApp.switchToChannel(chan); } socket.emit("input", { target: Number($("#chat").attr("data-id")), text: "/query " + itemData, }); } addContextMenuItem({ check: (target) => target.hasClass("user"), className: "action-query", displayName: "Direct messages", data: (target) => target.attr("data-name"), callback: query, }); } function addCloseItem() { function getCloseDisplay(target) { if (target.hasClass("lobby")) { return "Remove"; } else if (target.hasClass("channel")) { return "Leave"; } return "Close"; } addContextMenuItem({ check: (target) => target.hasClass("chan"), className: "close", displayName: getCloseDisplay, data: (target) => target.attr("data-target"), callback(itemData) { const close = document.querySelector( `.networks .chan[data-target="${itemData}"] .close` ); if (close) { // TODO: After context menus are ported to Vue, figure out a direct api close.click(); } }, }); } function addConnectItem() { function connect(itemData) { socket.emit("input", { target: Number(itemData), text: "/connect", }); } addContextMenuItem({ check: (target) => target.hasClass("lobby") && target.parent().hasClass("not-connected"), className: "connect", displayName: "Connect", data: (target) => target.attr("data-id"), callback: connect, }); } function addDisconnectItem() { function disconnect(itemData) { socket.emit("input", { target: Number(itemData), text: "/disconnect", }); } addContextMenuItem({ check: (target) => target.hasClass("lobby") && !target.parent().hasClass("not-connected"), className: "disconnect", displayName: "Disconnect", data: (target) => target.attr("data-id"), callback: disconnect, }); } function addKickItem() { function kick(itemData) { socket.emit("input", { target: Number($("#chat").attr("data-id")), text: "/kick " + itemData, }); } addContextMenuItem({ check: (target) => utils.hasRoleInChannel(target.closest(".chan"), ["op"]) && target.closest(".chan").attr("data-type") === "channel", className: "action-kick", displayName: "Kick", data: (target) => target.attr("data-name"), callback: kick, }); } function addOpItem() { function op(itemData) { socket.emit("input", { target: Number($("#chat").attr("data-id")), text: "/op " + itemData, }); } addContextMenuItem({ check: (target) => utils.hasRoleInChannel(target.closest(".chan"), ["op"]) && !utils.hasRoleInChannel(target.closest(".chan"), ["op"], target.attr("data-name")), className: "action-op", displayName: "Give operator (+o)", data: (target) => target.attr("data-name"), callback: op, }); } function addDeopItem() { function deop(itemData) { socket.emit("input", { target: Number($("#chat").attr("data-id")), text: "/deop " + itemData, }); } addContextMenuItem({ check: (target) => utils.hasRoleInChannel(target.closest(".chan"), ["op"]) && utils.hasRoleInChannel(target.closest(".chan"), ["op"], target.attr("data-name")), className: "action-op", displayName: "Revoke operator (-o)", data: (target) => target.attr("data-name"), callback: deop, }); } function addVoiceItem() { function voice(itemData) { socket.emit("input", { target: Number($("#chat").attr("data-id")), text: "/voice " + itemData, }); } addContextMenuItem({ check: (target) => utils.hasRoleInChannel(target.closest(".chan"), ["op"]) && !utils.hasRoleInChannel(target.closest(".chan"), ["voice"], target.attr("data-name")), className: "action-voice", displayName: "Give voice (+v)", data: (target) => target.attr("data-name"), callback: voice, }); } function addDevoiceItem() { function devoice(itemData) { socket.emit("input", { target: Number($("#chat").attr("data-id")), text: "/devoice " + itemData, }); } addContextMenuItem({ check: (target) => utils.hasRoleInChannel(target.closest(".chan"), ["op"]) && utils.hasRoleInChannel(target.closest(".chan"), ["voice"], target.attr("data-name")), className: "action-voice", displayName: "Revoke voice (-v)", data: (target) => target.attr("data-name"), callback: devoice, }); } function addFocusItem() { function focusChan(itemData) { $(`.networks .chan[data-target="${itemData}"]`).click(); } const getClass = (target) => { if (target.hasClass("lobby")) { return "network"; } else if (target.hasClass("query")) { return "query"; } return "chan"; }; addContextMenuItem({ check: (target) => target.hasClass("chan"), className: getClass, displayName: (target) => target.attr("data-name") || target.attr("aria-label"), data: (target) => target.attr("data-target"), callback: focusChan, }); addContextDivider({ check: (target) => target.hasClass("chan"), }); } function addEditNetworkItem() { function edit(itemData) { socket.emit("network:get", itemData); } addContextMenuItem({ check: (target) => target.hasClass("lobby"), className: "edit", displayName: "Edit this network…", data: (target) => target.closest(".network").attr("data-uuid"), callback: edit, }); } function addChannelListItem() { function list(itemData) { socket.emit("input", { target: parseInt(itemData, 10), text: "/list", }); } addContextMenuItem({ check: (target) => target.hasClass("lobby"), className: "list", displayName: "List all channels", data: (target) => target.attr("data-id"), callback: list, }); } function addEditTopicItem() { function setEditTopic(itemData) { findChannel(Number(itemData)).channel.editTopic = true; document.querySelector(`#sidebar .chan[data-id="${Number(itemData)}"]`).click(); vueApp.$nextTick(() => { document.querySelector(`#chan-${Number(itemData)} .topic-input`).focus(); }); } addContextMenuItem({ check: (target) => target.hasClass("channel"), className: "edit", displayName: "Edit topic", data: (target) => target.attr("data-id"), callback: setEditTopic, }); } function addBanListItem() { function banlist(itemData) { socket.emit("input", { target: parseInt(itemData, 10), text: "/banlist", }); } addContextMenuItem({ check: (target) => target.hasClass("channel"), className: "list", displayName: "List banned users", data: (target) => target.attr("data-id"), callback: banlist, }); } function addJoinItem() { function openJoinForm(itemData) { findChannel(Number(itemData)).network.isJoinChannelShown = true; } addContextMenuItem({ check: (target) => target.hasClass("lobby"), className: "join", displayName: "Join a channel…", data: (target) => target.attr("data-id"), callback: openJoinForm, }); } function addIgnoreListItem() { function ignorelist(itemData) { socket.emit("input", { target: parseInt(itemData, 10), text: "/ignorelist", }); } addContextMenuItem({ check: (target) => target.hasClass("lobby"), className: "list", displayName: "List ignored users", data: (target) => target.attr("data-id"), callback: ignorelist, }); } function addDefaultItems() { addFocusItem(); addWhoisItem(); addQueryItem(); addKickItem(); addOpItem(); addDeopItem(); addVoiceItem(); addDevoiceItem(); addEditNetworkItem(); addJoinItem(); addChannelListItem(); addEditTopicItem(); addBanListItem(); addIgnoreListItem(); addConnectItem(); addDisconnectItem(); addCloseItem(); } function registerEvents() { const viewport = $("#viewport"); viewport.on("contextmenu", ".network .chan", function(e) { return createContextMenu($(this), e).show(); }); viewport.on("click contextmenu", ".user", function(e) { // If user is selecting text, do not open context menu // This primarily only targets mobile devices where selection is performed with touch if (!window.getSelection().isCollapsed) { return true; } return createContextMenu($(this), e).show(); }); viewport.on("click", "#chat .menu", function(e) { e.currentTarget = $( `#sidebar .chan[data-id="${$(this) .closest(".chan") .attr("data-id")}"]` )[0]; return createContextMenu($(this), e).show(); }); }