diff --git a/client/css/style.css b/client/css/style.css index 215d1b24..03dfe793 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -222,6 +222,7 @@ kbd { #viewport .rt::before { content: "\f0c0"; /* http://fontawesome.io/icon/users/ */ } #chat button.menu::before { content: "\f142"; /* http://fontawesome.io/icon/ellipsis-v/ */ } +.context-menu-join::before { content: "\f067"; /* http://fontawesome.io/icon/plus/ */ } .context-menu-user::before { content: "\f007"; /* http://fontawesome.io/icon/user/ */ } .context-menu-close::before { content: "\f00d"; /* http://fontawesome.io/icon/times/ */ } .context-menu-list::before { content: "\f03a"; /* http://fontawesome.io/icon/list/ */ } @@ -457,7 +458,6 @@ kbd { will-change: transform; } -#sidebar button, #sidebar .chan, #sidebar .sign-out, #sidebar .empty { @@ -551,6 +551,7 @@ kbd { } #sidebar .badge, +#sidebar .add-channel, #sidebar .close { float: right; margin-left: 5px; @@ -577,7 +578,6 @@ kbd { } #sidebar .close { - border-radius: 3px; width: 18px; height: 18px; display: none; @@ -594,13 +594,38 @@ kbd { color: #fff; } +#sidebar .lobby .add-channel { + border-radius: 3px; + width: 18px; + height: 18px; + opacity: 0.4; + transition: opacity 0.2s, background-color 0.2s, transform 0.2s; +} + +#sidebar .lobby .add-channel::before { + font-size: 20px; + font-weight: normal; + display: inline-block; + line-height: 16px; + text-align: center; + content: "+"; + color: #fff; +} + +#sidebar .lobby .add-channel:hover { + opacity: 1; +} + +#sidebar .lobby .add-channel.opened { + transform: rotate(45deg); +} + #sidebar .chan.active .close { opacity: 0.4; display: unset; } #sidebar .chan.active .close:hover { - background-color: rgba(0, 0, 0, 0.1); opacity: 1; } @@ -685,7 +710,7 @@ kbd { font-size: 14px; } -#windows .input { +.input { background-color: white; border: 1px solid #cdd3da; border-radius: 2px; @@ -894,6 +919,35 @@ kbd { touch-action: pan-y; } + +/** + * Toggled via JavaScript + */ +#sidebar .join-form { + display: none; +} + +#sidebar .join-form .input { + display: block; + margin-left: auto; + margin-right: auto; + margin-top: 5px; + margin-bottom: 5px; + width: 80%; +} + +#sidebar .join-form .btn { + display: block; + width: 80%; + padding: 1px; + margin: auto; + height: 29px; +} + +#sidebar .add-channel-tooltip { + float: right; +} + #chat .show-more { display: none; padding: 10px; diff --git a/client/index.html b/client/index.html index 7a9789a3..3c3b253e 100644 --- a/client/index.html +++ b/client/index.html @@ -70,7 +70,7 @@
diff --git a/client/js/join-channel.js b/client/js/join-channel.js new file mode 100644 index 00000000..ebeea808 --- /dev/null +++ b/client/js/join-channel.js @@ -0,0 +1,88 @@ +"use strict"; + +const $ = require("jquery"); +const Mousetrap = require("mousetrap"); + +const socket = require("./socket"); +const utils = require("./utils"); + +const sidebar = $("#sidebar"); + +module.exports = { + handleKeybinds, + openForm, +}; + +function toggleButton(network) { + // Transform the + button to a × + network.find("button.add-channel").toggleClass("opened"); + + // Toggle content of tooltip + const tooltip = network.find(".add-channel-tooltip"); + const altLabel = tooltip.data("alt-label"); + tooltip.data("alt-label", tooltip.attr("aria-label")); + tooltip.attr("aria-label", altLabel); +} + +function closeForm(network) { + const form = network.find(".join-form"); + + if (form.is(":visible")) { + form.find("input[name='channel']").val(""); + form.find("input[name='key']").val(""); + form.hide(); + toggleButton(network); + } +} + +function openForm(network) { + const form = network.find(".join-form"); + + if (form.is(":hidden")) { + form.show(); + toggleButton(network); + } + + // Focus the "Channel" field even if the form was already open + form.find(".input[name='channel']").focus(); +} + +sidebar.on("click", ".add-channel", function(e) { + const id = $(e.target).data("id"); + const joinForm = $(`#join-channel-${id}`); + const network = joinForm.closest(".network"); + + if (joinForm.is(":visible")) { + closeForm(network); + } else { + openForm(network); + } + + return false; +}); + +sidebar.on("submit", ".join-form", function() { + const form = $(this); + const channel = form.find("input[name='channel']"); + const channelString = channel.val(); + const key = form.find("input[name='key']"); + const keyString = key.val(); + const chan = utils.findCurrentNetworkChan(channelString); + if (chan.length) { + chan.click(); + } else { + socket.emit("input", { + text: `/join ${channelString} ${keyString}`, + target: form.prev().data("id"), + }); + } + closeForm(form.closest(".network")); + return false; +}); + +function handleKeybinds() { + sidebar.find(".join-form input, .join-form button").each(function() { + const network = $(this).closest(".network"); + Mousetrap(this).bind("esc", () => closeForm(network)); + }); +} diff --git a/client/js/lounge.js b/client/js/lounge.js index c260bd76..826e6036 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -20,6 +20,7 @@ const utils = require("./utils"); require("./webpush"); require("./keybinds"); require("./clipboard"); +const JoinChannel = require("./join-channel"); $(function() { var sidebar = $("#sidebar, #footer"); @@ -132,6 +133,12 @@ $(function() { text: "List all channels", data: target.data("id"), }); + output += templates.contextmenu_item({ + class: "join", + action: "join", + text: "Join a channel…", + data: target.data("id"), + }); } if (target.hasClass("channel")) { output += templates.contextmenu_item({ @@ -478,6 +485,10 @@ $(function() { }); const contextMenuActions = { + join: function(itemData) { + const network = $(`#join-channel-${itemData}`).closest(".network"); + JoinChannel.openForm(network); + }, close: function(itemData) { closeChan($(`.networks .chan[data-target="${itemData}"]`)); }, diff --git a/client/js/render.js b/client/js/render.js index 3d8461cb..17772d82 100644 --- a/client/js/render.js +++ b/client/js/render.js @@ -8,6 +8,7 @@ const utils = require("./utils"); const sorting = require("./sorting"); const constants = require("./constants"); const condensed = require("./condensed"); +const JoinChannel = require("./join-channel"); const helpers_parse = require("./libs/handlebars/parse"); const chat = $("#chat"); @@ -183,6 +184,9 @@ function renderNetworks(data, singleNetwork) { }) ); + // Add keyboard handlers to the "Join a channel…" form inputs/button + JoinChannel.handleKeybinds(); + let newChannels; const channels = $.map(data.networks, function(n) { return n.channels; diff --git a/client/views/chan.tpl b/client/views/chan.tpl index 857c599f..c84608a6 100644 --- a/client/views/chan.tpl +++ b/client/views/chan.tpl @@ -1,7 +1,15 @@ {{#each channels}}
+ {{#equal type "lobby"}} + + + + {{/equal}} {{#if unread}}{{roundBadgeNumber unread}}{{/if}} {{#notEqual type "lobby"}}{{/notEqual}} {{name}}
+{{#equal type "lobby"}} + {{> join_channel}} +{{/equal}} {{/each}} diff --git a/client/views/index.js b/client/views/index.js index 9f5bf6c0..3ed35fba 100644 --- a/client/views/index.js +++ b/client/views/index.js @@ -42,6 +42,7 @@ module.exports = { msg_unhandled: require("./msg_unhandled.tpl"), network: require("./network.tpl"), image_viewer: require("./image_viewer.tpl"), + join_channel: require("./join_channel.tpl"), session: require("./session.tpl"), unread_marker: require("./unread_marker.tpl"), user: require("./user.tpl"), diff --git a/client/views/join_channel.tpl b/client/views/join_channel.tpl new file mode 100644 index 00000000..fdea0dc1 --- /dev/null +++ b/client/views/join_channel.tpl @@ -0,0 +1,5 @@ +
+ + + +