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 @@
+