diff --git a/.eslintrc.yml b/.eslintrc.yml
index 90261ca0..312af4bc 100644
--- a/.eslintrc.yml
+++ b/.eslintrc.yml
@@ -85,8 +85,15 @@ rules:
space-in-parens: [error, never]
space-infix-ops: error
spaced-comment: [error, always]
- strict: error
+ strict: off
template-curly-spacing: error
yoda: error
+ vue/html-indent: [error, tab]
+ vue/require-default-prop: off
-extends: eslint:recommended
+plugins:
+ - vue
+
+extends:
+ - eslint:recommended
+ - plugin:vue/recommended
diff --git a/client/components/App.vue b/client/components/App.vue
new file mode 100644
index 00000000..d20f414b
--- /dev/null
+++ b/client/components/App.vue
@@ -0,0 +1,130 @@
+
+
+
+
+
diff --git a/client/components/Channel.vue b/client/components/Channel.vue
new file mode 100644
index 00000000..4419c418
--- /dev/null
+++ b/client/components/Channel.vue
@@ -0,0 +1,90 @@
+
+
+
+
+
+ {{ channel.name }}
+
+
+
+
+
+
+ {{ channel.unread | roundBadgeNumber }}
+
+
+
+
+
+
+ {{ channel.name }}
+ {{ channel.unread | roundBadgeNumber }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/components/JoinChannel.vue b/client/components/JoinChannel.vue
new file mode 100644
index 00000000..c6d932d6
--- /dev/null
+++ b/client/components/JoinChannel.vue
@@ -0,0 +1,42 @@
+
+
+
+
+
diff --git a/client/components/Network.vue b/client/components/Network.vue
new file mode 100644
index 00000000..93359942
--- /dev/null
+++ b/client/components/Network.vue
@@ -0,0 +1,115 @@
+
+
+ You are not connected to any networks yet.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/css/style.css b/client/css/style.css
index d1e7bdf8..1a06b605 100644
--- a/client/css/style.css
+++ b/client/css/style.css
@@ -603,10 +603,6 @@ background on hover (unless active) */
padding-top: 5px;
}
-#sidebar .networks:empty {
- display: none;
-}
-
#sidebar .network,
#sidebar .network-placeholder {
position: relative;
@@ -631,7 +627,7 @@ background on hover (unless active) */
#sidebar .chan-placeholder {
border: 1px dashed #99a2b4;
border-radius: 6px;
- margin: -1px 10px;
+ margin: -1px;
}
#sidebar .network-placeholder {
@@ -779,12 +775,6 @@ background on hover (unless active) */
transform: rotate(45deg) translateZ(0);
}
-#sidebar .network .lobby:nth-last-child(2) .collapse-network {
- /* Hide collapse button if there are no channels/queries */
- width: 0;
- overflow: hidden;
-}
-
#sidebar .network .collapse-network {
width: 40px;
opacity: 0.4;
@@ -896,6 +886,7 @@ background on hover (unless active) */
line-height: 1.5;
}
+#loading,
#windows .window {
background: var(--window-bg-color);
display: none;
@@ -1605,7 +1596,7 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */
content: "Search Results";
}
-#loading.active {
+#loading {
font-size: 14px;
z-index: 1;
display: flex;
@@ -1646,7 +1637,7 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */
flex-grow: 0;
}
-#windows .logo-inverted {
+#loading .logo-inverted {
display: none; /* In dark themes, inverted logo must be used instead */
}
@@ -2410,10 +2401,6 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */
visibility: visible;
}
- #sidebar .empty::before {
- margin-top: 0;
- }
-
#viewport .lt,
#viewport .channel .rt {
display: flex;
diff --git a/client/index.html.tpl b/client/index.html.tpl
index 5047bfdb..b212747b 100644
--- a/client/index.html.tpl
+++ b/client/index.html.tpl
@@ -104,6 +104,7 @@
+
diff --git a/client/js/autocompletion.js b/client/js/autocompletion.js
index 0489cc8e..3c287383 100644
--- a/client/js/autocompletion.js
+++ b/client/js/autocompletion.js
@@ -8,7 +8,6 @@ const emojiMap = require("./libs/simplemap.json");
const options = require("./options");
const constants = require("./constants");
-const input = $("#input");
let textcomplete;
let enabled = false;
@@ -16,6 +15,7 @@ module.exports = {
enable: enableAutocomplete,
disable() {
if (enabled) {
+ const input = $("#input");
input.off("input.tabcomplete");
Mousetrap(input.get(0)).unbind("tab", "keydown");
textcomplete.destroy();
@@ -74,7 +74,7 @@ const nicksStrategy = {
}
// If there is whitespace in the input already, append space to nick
- if (position > 0 && /\s/.test(input.val())) {
+ if (position > 0 && /\s/.test($("#input").val())) {
return original + " ";
}
@@ -179,6 +179,7 @@ function enableAutocomplete() {
let tabCount = 0;
let lastMatch = "";
let currentMatches = [];
+ const input = $("#input");
input.on("input.tabcomplete", () => {
tabCount = 0;
diff --git a/client/js/lounge.js b/client/js/lounge.js
index 1167af3a..15d86460 100644
--- a/client/js/lounge.js
+++ b/client/js/lounge.js
@@ -1,7 +1,6 @@
"use strict";
// vendor libraries
-require("jquery-ui/ui/widgets/sortable");
const $ = require("jquery");
const moment = require("moment");
@@ -19,12 +18,12 @@ require("./keybinds");
require("./clipboard");
const contextMenuFactory = require("./contextMenuFactory");
+const {vueApp, findChannel} = require("./vue");
+
$(function() {
const sidebar = $("#sidebar, #footer");
const chat = $("#chat");
- $(document.body).data("app-name", document.title);
-
const viewport = $("#viewport");
function storeSidebarVisibility(name, state) {
@@ -176,16 +175,14 @@ $(function() {
self.data("id")
);
- sidebar.find(".active")
- .removeClass("active")
- .attr("aria-selected", false);
+ const channel = findChannel(self.data("id"));
- self.addClass("active")
- .attr("aria-selected", true)
- .find(".badge")
- .attr("data-highlight", 0)
- .removeClass("highlight")
- .empty();
+ vueApp.activeChannel = channel;
+
+ if (channel) {
+ channel.channel.highlight = 0;
+ channel.channel.unread = 0;
+ }
if (sidebar.find(".highlight").length === 0) {
utils.toggleNotificationMarkers(false);
diff --git a/client/js/render.js b/client/js/render.js
index d8db36ef..547d32cc 100644
--- a/client/js/render.js
+++ b/client/js/render.js
@@ -5,7 +5,6 @@ const templates = require("../views");
const options = require("./options");
const renderPreview = require("./renderPreview");
const utils = require("./utils");
-const sorting = require("./sorting");
const constants = require("./constants");
const condensed = require("./condensed");
const JoinChannel = require("./join-channel");
@@ -215,13 +214,6 @@ function renderChannelUsers(data) {
function renderNetworks(data, singleNetwork) {
const collapsed = new Set(JSON.parse(storage.get("thelounge.networks.collapsed")));
- sidebar.find(".empty").hide();
- sidebar.find(".networks").append(
- templates.network({
- networks: data.networks,
- }).trim()
- );
-
// Add keyboard handlers to the "Join a channel…" form inputs/button
JoinChannel.handleKeybinds(data.networks);
@@ -287,7 +279,6 @@ function renderNetworks(data, singleNetwork) {
}
utils.confirmExit();
- sorting();
if (sidebar.find(".highlight").length) {
utils.toggleNotificationMarkers(true);
diff --git a/client/js/slideout.js b/client/js/slideout.js
index 46fd498f..34734be8 100644
--- a/client/js/slideout.js
+++ b/client/js/slideout.js
@@ -1,9 +1,5 @@
"use strict";
-const viewport = document.getElementById("viewport");
-const menu = document.getElementById("sidebar");
-const sidebarOverlay = document.getElementById("sidebar-overlay");
-
let touchStartPos = null;
let touchCurPos = null;
let touchStartTime = 0;
@@ -14,12 +10,16 @@ let menuIsAbsolute = false;
class SlideoutMenu {
static enable() {
+ this.viewport = document.getElementById("viewport");
+ this.menu = document.getElementById("sidebar");
+ this.sidebarOverlay = document.getElementById("sidebar-overlay");
+
document.body.addEventListener("touchstart", onTouchStart, {passive: true});
}
static toggle(state) {
menuIsOpen = state;
- viewport.classList.toggle("menu-open", state);
+ this.viewport.classList.toggle("menu-open", state);
}
static isOpen() {
@@ -35,7 +35,7 @@ function onTouchStart(e) {
return;
}
- const styles = window.getComputedStyle(menu);
+ const styles = window.getComputedStyle(this.menu);
menuWidth = parseFloat(styles.width);
menuIsAbsolute = styles.position === "absolute";
@@ -65,7 +65,7 @@ function onTouchMove(e) {
const devicePixelRatio = window.devicePixelRatio || 2;
if (Math.abs(distX) > devicePixelRatio) {
- viewport.classList.toggle("menu-dragging", true);
+ this.viewport.classList.toggle("menu-dragging", true);
menuIsMoving = true;
}
}
@@ -85,8 +85,8 @@ function onTouchMove(e) {
distX = 0;
}
- menu.style.transform = "translate3d(" + distX + "px, 0, 0)";
- sidebarOverlay.style.opacity = distX / menuWidth;
+ this.menu.style.transform = "translate3d(" + distX + "px, 0, 0)";
+ this.sidebarOverlay.style.opacity = distX / menuWidth;
}
function onTouchEnd() {
@@ -99,9 +99,9 @@ function onTouchEnd() {
document.body.removeEventListener("touchmove", onTouchMove);
document.body.removeEventListener("touchend", onTouchEnd);
- viewport.classList.toggle("menu-dragging", false);
- menu.style.transform = null;
- sidebarOverlay.style.opacity = null;
+ this.viewport.classList.toggle("menu-dragging", false);
+ this.menu.style.transform = null;
+ this.sidebarOverlay.style.opacity = null;
touchStartPos = null;
touchCurPos = null;
diff --git a/client/js/socket-events/auth.js b/client/js/socket-events/auth.js
index f89077dd..cd5fa48e 100644
--- a/client/js/socket-events/auth.js
+++ b/client/js/socket-events/auth.js
@@ -21,6 +21,7 @@ socket.on("auth", function(data) {
if (data.serverHash > -1) {
utils.serverHash = data.serverHash;
+ $("#loading").remove();
login.html(templates.windows.sign_in());
utils.togglePasswordField("#sign-in .reveal-password");
diff --git a/client/js/socket-events/init.js b/client/js/socket-events/init.js
index eafeab51..b673f857 100644
--- a/client/js/socket-events/init.js
+++ b/client/js/socket-events/init.js
@@ -9,6 +9,7 @@ const slideoutMenu = require("../slideout");
const sidebar = $("#sidebar");
const storage = require("../localStorage");
const utils = require("../utils");
+const {Vue, vueApp} = require("../vue");
socket.on("init", function(data) {
$("#loading-page-message, #connection-error").text("Rendering…");
@@ -18,13 +19,12 @@ socket.on("init", function(data) {
if (lastMessageId > -1) {
previousActive = sidebar.find(".active").data("id");
- sidebar.find(".networks").empty();
}
- if (data.networks.length === 0) {
- sidebar.find(".empty").show();
- } else {
- render.renderNetworks(data);
+ vueApp.networks = data.networks;
+
+ if (data.networks.length > 0) {
+ Vue.nextTick(() => render.renderNetworks(data));
}
$("#connection-error").removeClass("shown");
@@ -66,7 +66,7 @@ socket.on("init", function(data) {
}
}
- openCorrectChannel(previousActive, data.active);
+ Vue.nextTick(() => openCorrectChannel(previousActive, data.active));
});
function openCorrectChannel(clientActive, serverActive) {
diff --git a/client/js/socket-events/join.js b/client/js/socket-events/join.js
index 69f000db..c1243840 100644
--- a/client/js/socket-events/join.js
+++ b/client/js/socket-events/join.js
@@ -6,32 +6,31 @@ const render = require("../render");
const chat = $("#chat");
const templates = require("../../views");
const sidebar = $("#sidebar");
+const {Vue, vueApp} = require("../vue");
socket.on("join", function(data) {
- const id = data.network;
- const network = sidebar.find(`.network[data-uuid="${id}"]`);
- const channels = network.children();
- const position = $(channels[data.index || channels.length - 1]); // Put channel in correct position, or the end if we don't have one
- const sidebarEntry = templates.chan({
- channels: [data.chan],
- });
- $(sidebarEntry).insertAfter(position);
+ vueApp.networks.find((n) => n.uuid === data.network)
+ .channels.splice(data.index || -1, 0, data.chan);
+
chat.append(
templates.chat({
channels: [data.chan],
})
);
- render.renderChannel(data.chan);
+
+ Vue.nextTick(() => render.renderChannel(data.chan));
// Queries do not automatically focus, unless the user did a whois
if (data.chan.type === "query" && !data.shouldOpen) {
return;
}
- sidebar.find(".chan")
- .sort(function(a, b) {
- return $(a).data("id") - $(b).data("id");
- })
- .last()
- .trigger("click");
+ Vue.nextTick(() => {
+ sidebar.find(".chan")
+ .sort(function(a, b) {
+ return $(a).data("id") - $(b).data("id");
+ })
+ .last()
+ .trigger("click");
+ });
});
diff --git a/client/js/socket-events/msg.js b/client/js/socket-events/msg.js
index bac4351b..f1e63540 100644
--- a/client/js/socket-events/msg.js
+++ b/client/js/socket-events/msg.js
@@ -10,6 +10,7 @@ const cleanIrcMessage = require("../libs/handlebars/ircmessageparser/cleanIrcMes
const webpush = require("../webpush");
const chat = $("#chat");
const sidebar = $("#sidebar");
+const {vueApp, findChannel} = require("../vue");
let pop;
@@ -30,26 +31,31 @@ socket.on("msg", function(data) {
function processReceivedMessage(data) {
let targetId = data.chan;
let target = "#chan-" + targetId;
- let channel = chat.find(target);
- let sidebarTarget = sidebar.find("[data-target='" + target + "']");
+ let channelContainer = chat.find(target);
+ let channel = findChannel(data.chan);
+
+ // Clear unread/highlight counter if self-message
+ if (data.msg.self) {
+ channel.channel.highlight = 0;
+ channel.channel.unread = 0;
+
+ utils.updateTitle();
+ }
// Display received notices and errors in currently active channel.
// Reloading the page will put them back into the lobby window.
- if (data.msg.showInActive) {
- const activeOnNetwork = sidebarTarget.parent().find(".active");
+ // We only want to put errors/notices in active channel if they arrive on the same network
+ if (data.msg.showInActive && vueApp.activeChannel && vueApp.activeChannel.network === channel.network) {
+ channel = vueApp.activeChannel;
- // We only want to put errors/notices in active channel if they arrive on the same network
- if (activeOnNetwork.length > 0) {
- targetId = data.chan = activeOnNetwork.data("id");
+ targetId = data.chan = vueApp.activeChannel.channel.id;
- target = "#chan-" + targetId;
- channel = chat.find(target);
- sidebarTarget = sidebar.find("[data-target='" + target + "']");
- }
+ target = "#chan-" + targetId;
+ channelContainer = chat.find(target);
}
- const scrollContainer = channel.find(".chat");
- const container = channel.find(".messages");
+ const scrollContainer = channelContainer.find(".chat");
+ const container = channelContainer.find(".messages");
const activeChannelId = chat.find(".chan.active").data("id");
if (data.msg.type === "channel_list" || data.msg.type === "ban_list" || data.msg.type === "ignore_list") {
@@ -60,7 +66,7 @@ function processReceivedMessage(data) {
render.appendMessage(
container,
targetId,
- channel.data("type"),
+ channelContainer.data("type"),
data.msg
);
@@ -68,7 +74,7 @@ function processReceivedMessage(data) {
scrollContainer.trigger("keepToBottom");
}
- notifyMessage(targetId, channel, data);
+ notifyMessage(targetId, channelContainer, data);
let shouldMoveMarker = data.msg.self;
@@ -95,16 +101,6 @@ function processReceivedMessage(data) {
.appendTo(container);
}
- // Clear unread/highlight counter if self-message
- if (data.msg.self) {
- sidebarTarget.find(".badge")
- .attr("data-highlight", 0)
- .removeClass("highlight")
- .empty();
-
- utils.updateTitle();
- }
-
let messageLimit = 0;
if (activeChannelId !== targetId) {
@@ -116,11 +112,11 @@ function processReceivedMessage(data) {
}
if (messageLimit > 0) {
- render.trimMessageInChannel(channel, messageLimit);
+ render.trimMessageInChannel(channelContainer, messageLimit);
}
- if ((data.msg.type === "message" || data.msg.type === "action") && channel.hasClass("channel")) {
- const nicks = channel.find(".userlist").data("nicks");
+ if ((data.msg.type === "message" || data.msg.type === "action") && channelContainer.hasClass("channel")) {
+ const nicks = channelContainer.find(".userlist").data("nicks");
if (nicks) {
const find = nicks.indexOf(data.msg.from.nick);
diff --git a/client/js/socket-events/network.js b/client/js/socket-events/network.js
index 9cf07d67..aaf0aca5 100644
--- a/client/js/socket-events/network.js
+++ b/client/js/socket-events/network.js
@@ -6,13 +6,18 @@ const render = require("../render");
const templates = require("../../views");
const sidebar = $("#sidebar");
const utils = require("../utils");
+const {Vue, vueApp} = require("../vue");
socket.on("network", function(data) {
- render.renderNetworks(data, true);
+ vueApp.networks.push(data.networks[0]);
- sidebar.find(".chan")
- .last()
- .trigger("click");
+ Vue.nextTick(() => {
+ render.renderNetworks(data, true);
+
+ sidebar.find(".chan")
+ .last()
+ .trigger("click");
+ });
$("#connect")
.find(".btn")
@@ -20,14 +25,13 @@ socket.on("network", function(data) {
});
socket.on("network_changed", function(data) {
- sidebar.find(`.network[data-uuid="${data.network}"]`).data("options", data.serverOptions);
+ vueApp.networks.find((n) => n.uuid === data.network).serverOptions = data.serverOptions;
});
socket.on("network:status", function(data) {
- sidebar
- .find(`.network[data-uuid="${data.network}"]`)
- .toggleClass("not-connected", !data.connected)
- .toggleClass("not-secure", !data.secure);
+ const network = vueApp.networks.find((n) => n.uuid === data.network);
+ network.status.connected = data.connected;
+ network.status.secure = data.secure;
});
socket.on("network:info", function(data) {
diff --git a/client/js/socket-events/open.js b/client/js/socket-events/open.js
index 88833c95..752a53cf 100644
--- a/client/js/socket-events/open.js
+++ b/client/js/socket-events/open.js
@@ -3,6 +3,7 @@
const $ = require("jquery");
const socket = require("../socket");
const utils = require("../utils");
+const {vueApp, findChannel} = require("../vue");
// Sync unread badge and marker when other clients open a channel
socket.on("open", function(id) {
@@ -10,24 +11,25 @@ socket.on("open", function(id) {
return;
}
- const channel = $("#chat #chan-" + id);
-
// Don't do anything if the channel is active on this client
- if (channel.length === 0 || channel.hasClass("active")) {
+ if (vueApp.activeChannel && vueApp.activeChannel.channel.id === id) {
return;
}
// Clear the unread badge
- $("#sidebar").find(".chan[data-id='" + id + "'] .badge")
- .attr("data-highlight", 0)
- .removeClass("highlight")
- .empty();
+ const channel = findChannel(id);
+
+ if (channel) {
+ channel.channel.highlight = 0;
+ channel.channel.unread = 0;
+ }
utils.updateTitle();
// Move unread marker to the bottom
- channel
+ const channelContainer = $("#chat #chan-" + id);
+ channelContainer
.find(".unread-marker")
.data("unread-id", 0)
- .appendTo(channel.find(".messages"));
+ .appendTo(channelContainer.find(".messages"));
});
diff --git a/client/js/socket-events/part.js b/client/js/socket-events/part.js
index 192c157b..bbf0dfd9 100644
--- a/client/js/socket-events/part.js
+++ b/client/js/socket-events/part.js
@@ -2,19 +2,19 @@
const $ = require("jquery");
const socket = require("../socket");
-const sidebar = $("#sidebar");
+const {vueApp} = require("../vue");
socket.on("part", function(data) {
- const chanMenuItem = sidebar.find(".chan[data-id='" + data.chan + "']");
-
// When parting from the active channel/query, jump to the network's lobby
- if (chanMenuItem.hasClass("active")) {
- chanMenuItem
- .parent(".network")
+ if (vueApp.activeChannel && vueApp.activeChannel.channel.id === data.chan) {
+ $("#sidebar .chan[data-id='" + data.chan + "']")
+ .closest(".network")
.find(".lobby")
.trigger("click");
}
- chanMenuItem.remove();
$("#chan-" + data.chan).remove();
+
+ const network = vueApp.networks.find((n) => n.uuid === data.network);
+ network.channels.splice(network.channels.findIndex((c) => c.id === data.chan), 1);
});
diff --git a/client/js/socket-events/quit.js b/client/js/socket-events/quit.js
index 1b2bba8b..ab7f7703 100644
--- a/client/js/socket-events/quit.js
+++ b/client/js/socket-events/quit.js
@@ -4,8 +4,11 @@ const $ = require("jquery");
const chat = $("#chat");
const socket = require("../socket");
const sidebar = $("#sidebar");
+const {Vue, vueApp} = require("../vue");
socket.on("quit", function(data) {
+ vueApp.networks.splice(vueApp.networks.findIndex((n) => n.uuid === data.network), 1);
+
const id = data.network;
const network = sidebar.find(`.network[data-uuid="${id}"]`);
@@ -14,18 +17,16 @@ socket.on("quit", function(data) {
chat.find($(this).attr("data-target")).remove();
});
- network.remove();
+ Vue.nextTick(() => {
+ const chan = sidebar.find(".chan");
- const chan = sidebar.find(".chan");
-
- if (chan.length === 0) {
- sidebar.find(".empty").show();
-
- // Open the connect window
- $("#footer .connect").trigger("click", {
- pushState: false,
- });
- } else {
- chan.eq(0).trigger("click");
- }
+ if (chan.length === 0) {
+ // Open the connect window
+ $("#footer .connect").trigger("click", {
+ pushState: false,
+ });
+ } else {
+ chan.eq(0).trigger("click");
+ }
+ });
});
diff --git a/client/js/sorting.js b/client/js/sorting.js
deleted file mode 100644
index 9864772b..00000000
--- a/client/js/sorting.js
+++ /dev/null
@@ -1,64 +0,0 @@
-"use strict";
-
-const $ = require("jquery");
-const sidebar = $("#sidebar, #footer");
-const socket = require("./socket");
-const options = require("./options");
-
-module.exports = function() {
- sidebar.find(".networks").sortable({
- axis: "y",
- containment: "parent",
- cursor: "move",
- distance: 12,
- items: ".network",
- handle: ".lobby",
- placeholder: "network-placeholder",
- forcePlaceholderSize: true,
- tolerance: "pointer", // Use the pointer to figure out where the network is in the list
-
- update() {
- const order = [];
-
- sidebar.find(".network").each(function() {
- const id = $(this).data("uuid");
- order.push(id);
- });
-
- socket.emit("sort", {
- type: "networks",
- order: order,
- });
-
- options.settings.ignoreSortSync = true;
- },
- });
- sidebar.find(".network").sortable({
- axis: "y",
- containment: "parent",
- cursor: "move",
- distance: 12,
- items: ".chan:not(.lobby)",
- placeholder: "chan-placeholder",
- forcePlaceholderSize: true,
- tolerance: "pointer", // Use the pointer to figure out where the channel is in the list
-
- update(e, ui) {
- const order = [];
- const network = ui.item.parent();
-
- network.find(".chan").each(function() {
- const id = $(this).data("id");
- order.push(id);
- });
-
- socket.emit("sort", {
- type: "channels",
- target: network.data("uuid"),
- order: order,
- });
-
- options.settings.ignoreSortSync = true;
- },
- });
-};
diff --git a/client/js/utils.js b/client/js/utils.js
index 86d9de52..153b40e3 100644
--- a/client/js/utils.js
+++ b/client/js/utils.js
@@ -3,6 +3,7 @@
const $ = require("jquery");
const escape = require("css.escape");
const viewport = $("#viewport");
+const {vueApp} = require("./vue");
var serverHash = -1; // eslint-disable-line no-var
var lastMessageId = -1; // eslint-disable-line no-var
@@ -101,18 +102,20 @@ function toggleNotificationMarkers(newState) {
}
function updateTitle() {
- let title = $(document.body).data("app-name");
- const chanTitle = $("#sidebar").find(".chan.active").attr("aria-label");
+ let title = vueApp.appName;
- if (chanTitle && chanTitle.length > 0) {
- title = `${chanTitle} — ${title}`;
+ if (vueApp.activeChannel) {
+ title = `${vueApp.activeChannel.channel.name} — ${vueApp.activeChannel.network.name} — ${title}`;
}
// add highlight count to title
let alertEventCount = 0;
- $(".badge.highlight").each(function() {
- alertEventCount += parseInt($(this).attr("data-highlight"));
- });
+
+ for (const network of vueApp.networks) {
+ for (const channel of network.channels) {
+ alertEventCount += channel.highlight;
+ }
+ }
if (alertEventCount > 0) {
title = `(${alertEventCount}) ${title}`;
diff --git a/client/js/vue.js b/client/js/vue.js
new file mode 100644
index 00000000..a00ad7b8
--- /dev/null
+++ b/client/js/vue.js
@@ -0,0 +1,39 @@
+"use strict";
+
+const Vue = require("vue").default;
+const App = require("../components/App.vue").default;
+const roundBadgeNumber = require("./libs/handlebars/roundBadgeNumber");
+
+Vue.filter("roundBadgeNumber", roundBadgeNumber);
+
+const vueApp = new Vue({
+ el: "#viewport",
+ data: {
+ appName: document.title,
+ activeChannel: null,
+ networks: [],
+ },
+ render(createElement) {
+ return createElement(App, {
+ props: this,
+ });
+ },
+});
+
+function findChannel(id) {
+ for (const network of vueApp.networks) {
+ for (const channel of network.channels) {
+ if (channel.id === id) {
+ return {network, channel};
+ }
+ }
+ }
+
+ return null;
+}
+
+module.exports = {
+ Vue,
+ vueApp,
+ findChannel,
+};
diff --git a/client/views/chan.tpl b/client/views/chan.tpl
deleted file mode 100644
index 0b7adc20..00000000
--- a/client/views/chan.tpl
+++ /dev/null
@@ -1,45 +0,0 @@
-{{#each channels}}
-
- {{#equal type "lobby"}}
-
-
- {{name}}
-
-
-
-
-
-
- {{#if unread}}{{roundBadgeNumber unread}}{{/if}}
-
-
-
-
- {{else}}
-
{{name}}
-
{{#if unread}}{{roundBadgeNumber unread}}{{/if}}
- {{#equal type "channel"}}
-
-
-
- {{else}}
-
-
-
- {{/equal}}
- {{/equal}}
-
-{{#equal type "lobby"}}
- {{> join_channel}}
-{{/equal}}
-{{/each}}
diff --git a/client/views/join_channel.tpl b/client/views/join_channel.tpl
deleted file mode 100644
index be1ae9f4..00000000
--- a/client/views/join_channel.tpl
+++ /dev/null
@@ -1,5 +0,0 @@
-
diff --git a/client/views/network.tpl b/client/views/network.tpl
deleted file mode 100644
index 3a0dda3b..00000000
--- a/client/views/network.tpl
+++ /dev/null
@@ -1,12 +0,0 @@
-{{#each networks}}
-
-{{/each}}
diff --git a/package.json b/package.json
index d0cd670c..329d31ba 100644
--- a/package.json
+++ b/package.json
@@ -16,7 +16,7 @@
"coverage": "run-s test:{client,server} && nyc --nycrc-path=test/.nycrc-report report",
"dev": "run-p watch start",
"lint:css": "stylelint --color \"client/**/*.css\"",
- "lint:js": "eslint . --report-unused-disable-directives --color",
+ "lint:js": "eslint . --ext .js,.vue --report-unused-disable-directives --color",
"start": "node index start",
"test": "run-p --aggregate-output --continue-on-error lint:* test:{client,server}",
"test:browser": "webpack-dev-server --config=webpack.config-browser.js",
@@ -80,6 +80,7 @@
"css.escape": "1.5.1",
"emoji-regex": "7.0.3",
"eslint": "5.13.0",
+ "eslint-plugin-vue": "4.5.0",
"fuzzy": "0.1.3",
"graphql-request": "1.8.2",
"handlebars": "4.1.0",
@@ -105,6 +106,10 @@
"stylelint-config-standard": "18.2.0",
"textcomplete": "0.17.1",
"undate": "0.3.0",
+ "vue": "2.5.16",
+ "vue-loader": "15.2.4",
+ "vue-template-compiler": "2.5.16",
+ "vuedraggable": "2.16.0",
"webpack": "4.29.3",
"webpack-cli": "3.2.3",
"webpack-dev-server": "3.1.14"
diff --git a/src/plugins/inputs/part.js b/src/plugins/inputs/part.js
index 9fda89aa..2dbb7a87 100644
--- a/src/plugins/inputs/part.js
+++ b/src/plugins/inputs/part.js
@@ -35,6 +35,7 @@ exports.input = function(network, chan, cmd, args) {
network.channels = _.without(network.channels, target);
target.destroy();
this.emit("part", {
+ network: network.uuid,
chan: target.id,
});
this.save();
diff --git a/src/plugins/irc-events/part.js b/src/plugins/irc-events/part.js
index 851f3b1e..a32a6c3e 100644
--- a/src/plugins/irc-events/part.js
+++ b/src/plugins/irc-events/part.js
@@ -29,6 +29,7 @@ module.exports = function(irc, network) {
chan.destroy();
client.save();
client.emit("part", {
+ network: network.uuid,
chan: chan.id,
});
} else {
diff --git a/webpack.config.js b/webpack.config.js
index 7ca4e8ff..0fc64874 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -4,6 +4,7 @@ const webpack = require("webpack");
const path = require("path");
const CopyPlugin = require("copy-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
+const VueLoaderPlugin = require("vue-loader/lib/plugin");
const config = {
mode: process.env.NODE_ENV === "production" ? "production" : "development",
@@ -19,6 +20,12 @@ const config = {
},
module: {
rules: [
+ {
+ test: /\.vue$/,
+ use: {
+ loader: "vue-loader",
+ },
+ },
{
test: /\.css$/,
include: [
@@ -98,6 +105,7 @@ const config = {
},
plugins: [
new MiniCssExtractPlugin(),
+ new VueLoaderPlugin(),
new CopyPlugin([
{
from: "./node_modules/@fortawesome/fontawesome-free/webfonts/fa-solid-900.woff*",
diff --git a/yarn.lock b/yarn.lock
index 7bf5abb0..22a599c0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -594,6 +594,20 @@
"@types/unist" "*"
"@types/vfile-message" "*"
+"@vue/component-compiler-utils@^1.2.1":
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/@vue/component-compiler-utils/-/component-compiler-utils-1.3.1.tgz#686f0b913d59590ae327b2a1cb4b6d9b931bbe0e"
+ dependencies:
+ consolidate "^0.15.1"
+ hash-sum "^1.0.2"
+ lru-cache "^4.1.2"
+ merge-source-map "^1.1.0"
+ postcss "^6.0.20"
+ postcss-selector-parser "^3.1.1"
+ prettier "^1.13.0"
+ source-map "^0.5.6"
+ vue-template-es2015-compiler "^1.6.0"
+
"@webassemblyjs/ast@1.7.11":
version "1.7.11"
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.7.11.tgz#b988582cafbb2b095e8b556526f30c90d057cace"
@@ -746,9 +760,23 @@ acorn-dynamic-import@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz#482210140582a36b83c3e342e1cfebcaa9240948"
+acorn-jsx@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b"
+ dependencies:
+ acorn "^3.0.4"
+
acorn-jsx@^5.0.0:
- version "5.0.1"
- resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.0.1.tgz#32a064fd925429216a09b141102bfdd185fae40e"
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.0.0.tgz#958584ddb60990c02c97c1bd9d521fce433bb101"
+
+acorn@^3.0.4:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a"
+
+acorn@^5.5.0:
+ version "5.7.1"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.1.tgz#f095829297706a7c9776958c0afc8930a9b9d9d8"
acorn@^6.0.2, acorn@^6.0.5:
version "6.0.5"
@@ -1198,7 +1226,11 @@ blob@0.0.5:
version "0.0.5"
resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.5.tgz#d680eeef25f8cd91ad533f5b01eed48e64caf683"
-bluebird@^3.5.1, bluebird@^3.5.3:
+bluebird@^3.1.1, bluebird@^3.5.1:
+ version "3.5.1"
+ resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9"
+
+bluebird@^3.5.3:
version "3.5.3"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.3.tgz#7d01c6f9616c9a51ab0f8c549a79dfe6ec33efa7"
@@ -1845,6 +1877,12 @@ console-control-strings@^1.0.0, console-control-strings@~1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
+consolidate@^0.15.1:
+ version "0.15.1"
+ resolved "https://registry.yarnpkg.com/consolidate/-/consolidate-0.15.1.tgz#21ab043235c71a07d45d9aad98593b0dba56bab7"
+ dependencies:
+ bluebird "^3.1.1"
+
constants-browserify@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75"
@@ -2125,6 +2163,10 @@ date-now@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
+de-indent@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
+
debug@2.6.9, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
@@ -2544,6 +2586,19 @@ escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1
version "1.0.5"
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
+eslint-plugin-vue@4.5.0:
+ version "4.5.0"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-4.5.0.tgz#09d6597f4849e31a3846c2c395fccf17685b69c3"
+ dependencies:
+ vue-eslint-parser "^2.0.3"
+
+eslint-scope@^3.7.1:
+ version "3.7.1"
+ resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-3.7.1.tgz#3d63c3edfda02e06e01a452ad88caacc7cdcb6e8"
+ dependencies:
+ esrecurse "^4.1.0"
+ estraverse "^4.1.1"
+
eslint-scope@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.0.tgz#50bf3071e9338bcdc43331794a0cb533f0136172"
@@ -2600,6 +2655,13 @@ eslint@5.13.0:
table "^5.0.2"
text-table "^0.2.0"
+espree@^3.5.2:
+ version "3.5.4"
+ resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.4.tgz#b0f447187c8a8bed944b815a660bddf5deb5d1a7"
+ dependencies:
+ acorn "^5.5.0"
+ acorn-jsx "^3.0.0"
+
espree@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/espree/-/espree-5.0.0.tgz#fc7f984b62b36a0f543b13fb9cd7b9f4a7f5b65c"
@@ -2616,7 +2678,7 @@ esprima@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
-esquery@^1.0.1:
+esquery@^1.0.0, esquery@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.1.tgz#406c51658b1f5991a5f9b62b1dc25b00e3e5c708"
dependencies:
@@ -3385,6 +3447,10 @@ hash-base@^3.0.0:
inherits "^2.0.1"
safe-buffer "^5.0.1"
+hash-sum@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/hash-sum/-/hash-sum-1.0.2.tgz#33b40777754c6432573c120cc3808bbd10d47f04"
+
hash.js@^1.0.0, hash.js@^1.0.3:
version "1.1.7"
resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42"
@@ -3398,7 +3464,7 @@ hasha@^3.0.0:
dependencies:
is-stream "^1.0.1"
-he@1.1.1:
+he@1.1.1, he@^1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd"
@@ -4434,9 +4500,9 @@ lowercase-keys@^1.0.0, lowercase-keys@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f"
-lru-cache@^4.0.1, lru-cache@^4.1.1:
- version "4.1.5"
- resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd"
+lru-cache@^4.0.1, lru-cache@^4.1.1, lru-cache@^4.1.2:
+ version "4.1.3"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.3.tgz#a1175cf3496dfc8436c156c334b4955992bce69c"
dependencies:
pseudomap "^1.0.2"
yallist "^2.1.2"
@@ -5736,7 +5802,7 @@ postcss-selector-parser@^2.0.0, postcss-selector-parser@^2.2.2:
indexes-of "^1.0.1"
uniq "^1.0.1"
-postcss-selector-parser@^3.1.0:
+postcss-selector-parser@^3.1.0, postcss-selector-parser@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz#4f875f4afb0c96573d5cf4d74011aee250a7e865"
dependencies:
@@ -5786,7 +5852,7 @@ postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0
source-map "^0.5.6"
supports-color "^3.2.3"
-postcss@^6.0.1, postcss@^6.0.23:
+postcss@^6.0.1, postcss@^6.0.20, postcss@^6.0.23:
version "6.0.23"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324"
dependencies:
@@ -5818,6 +5884,10 @@ prepend-http@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
+prettier@^1.13.0:
+ version "1.13.7"
+ resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.13.7.tgz#850f3b8af784a49a6ea2d2eaa7ed1428a34b7281"
+
primer-support@5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/primer-support/-/primer-support-5.0.0.tgz#d19c7cea59e8783400b9391943c8a2bb2ebddc5e"
@@ -6649,6 +6719,10 @@ sort-keys@^1.0.0:
dependencies:
is-plain-obj "^1.0.0"
+sortablejs@^1.7.0:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.7.0.tgz#80a2b2370abd568e1cec8c271131ef30a904fa28"
+
source-list-map@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34"
@@ -7567,6 +7641,59 @@ vm-browserify@0.0.4:
dependencies:
indexof "0.0.1"
+vue-eslint-parser@^2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-2.0.3.tgz#c268c96c6d94cfe3d938a5f7593959b0ca3360d1"
+ dependencies:
+ debug "^3.1.0"
+ eslint-scope "^3.7.1"
+ eslint-visitor-keys "^1.0.0"
+ espree "^3.5.2"
+ esquery "^1.0.0"
+ lodash "^4.17.4"
+
+vue-hot-reload-api@^2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.0.tgz#97976142405d13d8efae154749e88c4e358cf926"
+
+vue-loader@15.2.4:
+ version "15.2.4"
+ resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-15.2.4.tgz#a7b923123d3cf87230a8ff54a1c16d31a6c5dbb4"
+ dependencies:
+ "@vue/component-compiler-utils" "^1.2.1"
+ hash-sum "^1.0.2"
+ loader-utils "^1.1.0"
+ vue-hot-reload-api "^2.3.0"
+ vue-style-loader "^4.1.0"
+
+vue-style-loader@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/vue-style-loader/-/vue-style-loader-4.1.0.tgz#7588bd778e2c9f8d87bfc3c5a4a039638da7a863"
+ dependencies:
+ hash-sum "^1.0.2"
+ loader-utils "^1.0.2"
+
+vue-template-compiler@2.5.16:
+ version "2.5.16"
+ resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.5.16.tgz#93b48570e56c720cdf3f051cc15287c26fbd04cb"
+ dependencies:
+ de-indent "^1.0.2"
+ he "^1.1.0"
+
+vue-template-es2015-compiler@^1.6.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.6.0.tgz#dc42697133302ce3017524356a6c61b7b69b4a18"
+
+vue@2.5.16:
+ version "2.5.16"
+ resolved "https://registry.yarnpkg.com/vue/-/vue-2.5.16.tgz#07edb75e8412aaeed871ebafa99f4672584a0085"
+
+vuedraggable@2.16.0:
+ version "2.16.0"
+ resolved "https://registry.yarnpkg.com/vuedraggable/-/vuedraggable-2.16.0.tgz#52127081a2adb3de5fabd214d404ff3eee63575a"
+ dependencies:
+ sortablejs "^1.7.0"
+
watchpack@^1.5.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00"