diff --git a/client/components/App.vue b/client/components/App.vue
index 8af3f566..f2ac5c4d 100644
--- a/client/components/App.vue
+++ b/client/components/App.vue
@@ -3,12 +3,7 @@
-
-
+
@@ -18,29 +13,13 @@
const throttle = require("lodash/throttle");
import Sidebar from "./Sidebar.vue";
-import NetworkList from "./NetworkList.vue";
-import Chat from "./Chat.vue";
import ImageViewer from "./ImageViewer.vue";
-import SignIn from "./Windows/SignIn.vue";
-import Settings from "./Windows/Settings.vue";
-import NetworkEdit from "./Windows/NetworkEdit.vue";
-import Connect from "./Windows/Connect.vue";
-import Help from "./Windows/Help.vue";
-import Changelog from "./Windows/Changelog.vue";
export default {
name: "App",
components: {
Sidebar,
- NetworkList,
ImageViewer,
- Chat,
- SignIn,
- Settings,
- NetworkEdit,
- Connect,
- Help,
- Changelog,
},
props: {
activeWindow: String,
diff --git a/client/components/ChannelWrapper.vue b/client/components/ChannelWrapper.vue
index 3d5ec16f..feb62695 100644
--- a/client/components/ChannelWrapper.vue
+++ b/client/components/ChannelWrapper.vue
@@ -23,6 +23,7 @@
:aria-selected="activeChannel && channel === activeChannel.channel"
:style="closed ? {transition: 'none', opacity: 0.4} : null"
role="tab"
+ @click="click"
>
@@ -80,6 +81,11 @@ export default {
return this.channel.name;
},
+ click() {
+ // TODO: Find out why this sometimes throws `uncaught exception: Object`
+ this.$router.push("chan-" + this.channel.id);
+ this.$root.closeSidebarIfNeeded();
+ },
},
};
diff --git a/client/components/Chat.vue b/client/components/Chat.vue
index ec73fd3b..8a060172 100644
--- a/client/components/Chat.vue
+++ b/client/components/Chat.vue
@@ -133,7 +133,38 @@ export default {
return undefined;
},
},
+ watch: {
+ channel(_, previousChannel) {
+ this.channelChanged(previousChannel);
+ },
+ },
+ mounted() {
+ this.channelChanged();
+ },
methods: {
+ channelChanged(previousChannel) {
+ // Triggered when active channel is set or changed
+
+ if (previousChannel) {
+ this.$root.switchOutOfChannel(previousChannel);
+ }
+
+ this.channel.highlight = 0;
+ this.channel.unread = 0;
+
+ this.$store.commit("activeWindow", null);
+ socket.emit("open", this.channel.id);
+
+ if (this.channel.usersOutdated) {
+ this.channel.usersOutdated = false;
+
+ socket.emit("names", {
+ target: this.channel.id,
+ });
+ }
+
+ this.$root.synchronizeNotifiedState();
+ },
hideUserVisibleError() {
this.$root.currentUserVisibleError = null;
},
diff --git a/client/components/RoutedChat.vue b/client/components/RoutedChat.vue
new file mode 100644
index 00000000..a9d22dd3
--- /dev/null
+++ b/client/components/RoutedChat.vue
@@ -0,0 +1,35 @@
+
+
+
+
+
diff --git a/client/components/Sidebar.vue b/client/components/Sidebar.vue
index c3e5455e..389d671c 100644
--- a/client/components/Sidebar.vue
+++ b/client/components/Sidebar.vue
@@ -25,6 +25,7 @@
role="tab"
aria-controls="sign-in"
:aria-selected="$store.state.activeWindow === 'SignIn'"
+ @click="navigate('sign-in')"
/>
@@ -178,6 +182,16 @@ export default {
this.$store.commit("sidebarOpen", state);
};
+ this.navigate = (to) => {
+ if (this.activeChannel && this.activeChannel.channel) {
+ this.$root.switchOutOfChannel(this.activeChannel.channel);
+ }
+
+ this.$root.activeChannel = null;
+ this.$root.closeSidebarIfNeeded();
+ this.$router.push(to);
+ };
+
document.body.addEventListener("touchstart", this.onTouchStart, {passive: true});
},
methods: {
diff --git a/client/components/Windows/Changelog.vue b/client/components/Windows/Changelog.vue
index 75445993..d648af53 100644
--- a/client/components/Windows/Changelog.vue
+++ b/client/components/Windows/Changelog.vue
@@ -4,7 +4,7 @@
-
« Help
+
« Help
- v{{ $root.serverConfiguration.version }} (release notesrelease notes)
About The Lounge
diff --git a/client/js/constants.js b/client/js/constants.js
index 40677f70..72985996 100644
--- a/client/js/constants.js
+++ b/client/js/constants.js
@@ -36,4 +36,6 @@ module.exports = {
condensedTypesQuery,
timeFormats,
sizeUnits,
+ // Same value as media query in CSS that forces sidebars to become overlays
+ mobileViewportPixels: 768,
};
diff --git a/client/js/lounge.js b/client/js/lounge.js
index ff268709..f2686516 100644
--- a/client/js/lounge.js
+++ b/client/js/lounge.js
@@ -6,135 +6,12 @@ const $ = require("jquery");
// our libraries
const socket = require("./socket");
-const {vueApp, findChannel} = require("./vue");
-
window.vueMounted = () => {
require("./socket-events");
require("./contextMenuFactory");
- const utils = require("./utils");
require("./webpush");
require("./keybinds");
- const sidebar = $("#sidebar, #footer");
-
- const openWindow = function openWindow(e, {pushState, replaceHistory} = {}) {
- const self = $(this);
- const target = self.attr("data-target");
-
- if (!target) {
- return;
- }
-
- // This is a rather gross hack to account for sources that are in the
- // sidebar specifically. Needs to be done better when window management gets
- // refactored.
- const inSidebar = self.parents("#sidebar, #footer").length > 0;
- const channel = inSidebar ? findChannel(Number(self.attr("data-id"))) : null;
-
- if (vueApp.activeChannel) {
- const {channel: lastChannel} = vueApp.activeChannel;
-
- // If user clicks on the currently active channel, do nothing
- if (channel && lastChannel === channel.channel) {
- return;
- }
-
- if (lastChannel.messages.length > 0) {
- lastChannel.firstUnread = lastChannel.messages[lastChannel.messages.length - 1].id;
- }
-
- if (lastChannel.messages.length > 100) {
- lastChannel.messages.splice(0, lastChannel.messages.length - 100);
- lastChannel.moreHistoryAvailable = true;
- }
- }
-
- if (channel) {
- vueApp.$store.commit("activeWindow", null);
- vueApp.activeChannel = channel;
-
- if (channel) {
- channel.channel.highlight = 0;
- channel.channel.unread = 0;
- }
-
- socket.emit("open", channel ? channel.channel.id : null);
-
- if ($(window).outerWidth() <= utils.mobileViewportPixels) {
- vueApp.setSidebar(false);
- }
- } else {
- vueApp.activeChannel = null;
- const component = self.attr("data-component");
- vueApp.$store.commit("activeWindow", component);
-
- if ($(window).outerWidth() <= utils.mobileViewportPixels) {
- vueApp.setSidebar(false);
- }
- }
-
- utils.synchronizeNotifiedState();
-
- if (self.hasClass("chan")) {
- vueApp.$nextTick(() => $("#chat-container").addClass("active"));
- }
-
- /* TODO: move to ChatInput.vue
- const chanChat = chan.find(".chat");
-
- if (chanChat.length > 0 && channel.type !== "special") {
- // On touch devices unfocus (blur) the input to correctly close the virtual keyboard
- // An explicit blur is required, as the keyboard may open back up if the focus remains
- // See https://github.com/thelounge/thelounge/issues/2257
- $("#input").trigger("ontouchstart" in window ? "blur" : "focus");
- }
- */
-
- if (channel && channel.channel.usersOutdated) {
- channel.channel.usersOutdated = false;
-
- socket.emit("names", {
- target: channel.channel.id,
- });
- }
-
- // 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
- // contains a CSS selector that targets elements which takes the user to a different view
- // when clicked. The `popstate` event listener will trigger synthetic click events using that
- // selector and thus take the user to a different view/state.
- if (pushState === false) {
- return false;
- }
-
- const state = {};
-
- if (self.prop("id")) {
- state.clickTarget = `#${self.prop("id")}`;
- } else if (self.hasClass("chan")) {
- state.clickTarget = `#sidebar .chan[data-id="${self.attr("data-id")}"]`;
- } else {
- state.clickTarget = `#footer button[data-target="${target}"]`;
- }
-
- if (history && history.pushState) {
- if (replaceHistory && history.replaceState) {
- history.replaceState(state, null, target);
- } else {
- history.pushState(state, null, target);
- }
- }
-
- return false;
- };
-
- sidebar.on("click", ".chan, button", openWindow);
- $("#windows").on("click", "#view-changelog, #back-to-help", openWindow);
-
- $(document).on("visibilitychange focus click", () => {
- utils.synchronizeNotifiedState();
- });
-
window.addEventListener("popstate", (e) => {
const {state} = e;
diff --git a/client/js/router.js b/client/js/router.js
new file mode 100644
index 00000000..0c118446
--- /dev/null
+++ b/client/js/router.js
@@ -0,0 +1,25 @@
+"use strict";
+
+const Vue = require("vue").default;
+const VueRouter = require("vue-router").default;
+Vue.use(VueRouter);
+
+const SignIn = require("../components/Windows/SignIn.vue").default;
+const Connect = require("../components/Windows/Connect.vue").default;
+const Settings = require("../components/Windows/Settings.vue").default;
+const Help = require("../components/Windows/Help.vue").default;
+const Changelog = require("../components/Windows/Changelog.vue").default;
+const RoutedChat = require("../components/RoutedChat.vue").default;
+
+const router = new VueRouter({
+ routes: [
+ {path: "/sign-in", component: SignIn},
+ {path: "/connect", component: Connect},
+ {path: "/settings", component: Settings},
+ {path: "/help", component: Help},
+ {path: "/changelog", component: Changelog},
+ {path: "/chan-*", component: RoutedChat},
+ ],
+});
+
+export default router;
diff --git a/client/js/socket-events/auth.js b/client/js/socket-events/auth.js
index b01cf6e3..bca5089d 100644
--- a/client/js/socket-events/auth.js
+++ b/client/js/socket-events/auth.js
@@ -19,8 +19,6 @@ socket.on("auth", function(data) {
if (data.serverHash > -1) {
utils.serverHash = data.serverHash;
-
- vueApp.$store.commit("activeWindow", "SignIn");
} else {
getActiveWindowComponent().inFlight = false;
}
diff --git a/client/js/socket-events/init.js b/client/js/socket-events/init.js
index 40d1de23..e7265195 100644
--- a/client/js/socket-events/init.js
+++ b/client/js/socket-events/init.js
@@ -7,6 +7,7 @@ const webpush = require("../webpush");
const sidebar = $("#sidebar");
const storage = require("../localStorage");
const utils = require("../utils");
+const constants = require("../constants");
const {vueApp, initChannel} = require("../vue");
socket.on("init", function(data) {
@@ -31,7 +32,7 @@ socket.on("init", function(data) {
const viewportWidth = window.outerWidth;
let isUserlistOpen = storage.get("thelounge.state.userlist");
- if (viewportWidth > utils.mobileViewportPixels) {
+ if (viewportWidth > constants.mobileViewportPixels) {
vueApp.setSidebar(storage.get("thelounge.state.sidebar") !== "false");
}
@@ -55,7 +56,7 @@ socket.on("init", function(data) {
vueApp.$nextTick(() => openCorrectChannel(previousActive, data.active));
utils.confirmExit();
- utils.synchronizeNotifiedState();
+ vueApp.synchronizeNotifiedState();
});
function openCorrectChannel(clientActive, serverActive) {
diff --git a/client/js/socket-events/msg.js b/client/js/socket-events/msg.js
index df5f1010..c7a381b8 100644
--- a/client/js/socket-events/msg.js
+++ b/client/js/socket-events/msg.js
@@ -2,7 +2,6 @@
const $ = require("jquery");
const socket = require("../socket");
-const utils = require("../utils");
const options = require("../options");
const cleanIrcMessage = require("../libs/handlebars/ircmessageparser/cleanIrcMessage");
const webpush = require("../webpush");
@@ -91,7 +90,7 @@ socket.on("msg", function(data) {
}
if (data.msg.self || data.msg.highlight) {
- utils.synchronizeNotifiedState();
+ vueApp.synchronizeNotifiedState();
}
});
diff --git a/client/js/socket-events/open.js b/client/js/socket-events/open.js
index e9b83c00..9605c621 100644
--- a/client/js/socket-events/open.js
+++ b/client/js/socket-events/open.js
@@ -1,7 +1,6 @@
"use strict";
const socket = require("../socket");
-const utils = require("../utils");
const {vueApp, findChannel} = require("../vue");
// Sync unread badge and marker when other clients open a channel
@@ -28,5 +27,5 @@ socket.on("open", function(id) {
}
}
- utils.synchronizeNotifiedState();
+ vueApp.synchronizeNotifiedState();
});
diff --git a/client/js/socket-events/part.js b/client/js/socket-events/part.js
index b080b87c..7d5607e9 100644
--- a/client/js/socket-events/part.js
+++ b/client/js/socket-events/part.js
@@ -2,7 +2,6 @@
const $ = require("jquery");
const socket = require("../socket");
-const utils = require("../utils");
const {vueApp, findChannel} = require("../vue");
socket.on("part", function(data) {
@@ -23,5 +22,5 @@ socket.on("part", function(data) {
);
}
- utils.synchronizeNotifiedState();
+ vueApp.synchronizeNotifiedState();
});
diff --git a/client/js/utils.js b/client/js/utils.js
index d5ed715c..9c95df89 100644
--- a/client/js/utils.js
+++ b/client/js/utils.js
@@ -7,15 +7,12 @@ const {vueApp} = require("./vue");
var serverHash = -1; // eslint-disable-line no-var
module.exports = {
- // Same value as media query in CSS that forces sidebars to become overlays
- mobileViewportPixels: 768,
findCurrentNetworkChan,
serverHash,
confirmExit,
scrollIntoViewNicely,
hasRoleInChannel,
move,
- synchronizeNotifiedState,
requestIdleCallback,
};
@@ -45,60 +42,6 @@ function scrollIntoViewNicely(el) {
el.scrollIntoView({block: "center", inline: "nearest"});
}
-const favicon = $("#favicon");
-
-function synchronizeNotifiedState() {
- updateTitle();
-
- let hasAnyHighlights = false;
-
- for (const network of vueApp.networks) {
- for (const chan of network.channels) {
- if (chan.highlight > 0) {
- hasAnyHighlights = true;
- break;
- }
- }
- }
-
- toggleNotificationMarkers(hasAnyHighlights);
-}
-
-function toggleNotificationMarkers(newState) {
- if (vueApp.$store.state.isNotified !== newState) {
- // Toggles a dot on the menu icon when there are unread notifications
- vueApp.$store.commit("isNotified", newState);
-
- // Toggles the favicon to red when there are unread notifications
- const old = favicon.prop("href");
- favicon.prop("href", favicon.data("other"));
- favicon.data("other", old);
- }
-}
-
-function updateTitle() {
- let title = vueApp.appName;
-
- if (vueApp.activeChannel) {
- title = `${vueApp.activeChannel.channel.name} — ${title}`;
- }
-
- // add highlight count to title
- let alertEventCount = 0;
-
- for (const network of vueApp.networks) {
- for (const channel of network.channels) {
- alertEventCount += channel.highlight;
- }
- }
-
- if (alertEventCount > 0) {
- title = `(${alertEventCount}) ${title}`;
- }
-
- document.title = title;
-}
-
function confirmExit() {
if ($(document.body).hasClass("public")) {
window.onbeforeunload = function() {
diff --git a/client/js/vue.js b/client/js/vue.js
index 55a32872..99d4de89 100644
--- a/client/js/vue.js
+++ b/client/js/vue.js
@@ -1,6 +1,7 @@
"use strict";
const Vue = require("vue").default;
+
const store = require("./store").default;
const App = require("../components/App.vue").default;
const roundBadgeNumber = require("./libs/handlebars/roundBadgeNumber");
@@ -8,6 +9,8 @@ const localetime = require("./libs/handlebars/localetime");
const friendlysize = require("./libs/handlebars/friendlysize");
const colorClass = require("./libs/handlebars/colorClass");
const storage = require("./localStorage");
+const router = require("./router").default;
+const constants = require("./constants");
Vue.filter("localetime", localetime);
Vue.filter("friendlysize", friendlysize);
@@ -46,12 +49,17 @@ const vueApp = new Vue({
userStyles: "",
},
},
+ router,
mounted() {
Vue.nextTick(() => window.vueMounted());
if (navigator.platform.match(/(Mac|iPhone|iPod|iPad)/i)) {
document.body.classList.add("is-apple");
}
+
+ document.addEventListener("visibilitychange", this.synchronizeNotifiedState());
+ document.addEventListener("focus", this.synchronizeNotifiedState());
+ document.addEventListener("click", this.synchronizeNotifiedState());
},
methods: {
onSocketInit() {
@@ -59,11 +67,9 @@ const vueApp = new Vue({
this.$store.commit("isConnected", true);
},
setSidebar(state) {
- const utils = require("./utils");
-
this.$store.commit("sidebarOpen", state);
- if (window.outerWidth > utils.mobileViewportPixels) {
+ if (window.outerWidth > constants.mobileViewportPixels) {
storage.set("thelounge.state.sidebar", state);
}
@@ -72,6 +78,11 @@ const vueApp = new Vue({
toggleSidebar() {
this.setSidebar(!this.$store.state.sidebarOpen);
},
+ closeSidebarIfNeeded() {
+ if (window.innerWidth <= constants.mobileViewportPixels) {
+ this.setSidebar(false);
+ }
+ },
setUserlist(state) {
storage.set("thelounge.state.userlist", state);
this.$store.commit("userlistOpen", state);
@@ -80,6 +91,78 @@ const vueApp = new Vue({
toggleUserlist() {
this.setUserlist(!this.$store.state.userlistOpen);
},
+ findChannel(id) {
+ for (const network of this.networks) {
+ for (const channel of network.channels) {
+ if (channel.id === id) {
+ return {network, channel};
+ }
+ }
+ }
+
+ return null;
+ },
+ switchOutOfChannel(channel) {
+ // When switching out of a channel, mark everything as read
+ if (channel.messages.length > 0) {
+ channel.firstUnread = channel.messages[channel.messages.length - 1].id;
+ }
+
+ if (channel.messages.length > 100) {
+ channel.messages.splice(0, channel.messages.length - 100);
+ channel.moreHistoryAvailable = true;
+ }
+ },
+ synchronizeNotifiedState() {
+ this.updateTitle();
+
+ let hasAnyHighlights = false;
+
+ for (const network of this.networks) {
+ for (const chan of network.channels) {
+ if (chan.highlight > 0) {
+ hasAnyHighlights = true;
+ break;
+ }
+ }
+ }
+
+ this.toggleNotificationMarkers(hasAnyHighlights);
+ },
+ updateTitle() {
+ let title = this.appName;
+
+ if (this.activeChannel) {
+ title = `${this.activeChannel.channel.name} — ${title}`;
+ }
+
+ // add highlight count to title
+ let alertEventCount = 0;
+
+ for (const network of this.networks) {
+ for (const channel of network.channels) {
+ alertEventCount += channel.highlight;
+ }
+ }
+
+ if (alertEventCount > 0) {
+ title = `(${alertEventCount}) ${title}`;
+ }
+
+ document.title = title;
+ },
+ toggleNotificationMarkers(newState) {
+ if (this.$store.state.isNotified !== newState) {
+ // Toggles a dot on the menu icon when there are unread notifications
+ this.$store.commit("isNotified", newState);
+
+ // Toggles the favicon to red when there are unread notifications
+ const favicon = document.getElementById("favicon");
+ const old = favicon.getAttribute("href");
+ favicon.setAttribute("href", favicon.dataset.other);
+ favicon.dataset.other = old;
+ }
+ },
},
render(createElement) {
return createElement(App, {
@@ -96,15 +179,7 @@ Vue.config.errorHandler = function(e) {
};
function findChannel(id) {
- for (const network of vueApp.networks) {
- for (const channel of network.channels) {
- if (channel.id === id) {
- return {network, channel};
- }
- }
- }
-
- return null;
+ return vueApp.findChannel(id);
}
function initChannel(channel) {
diff --git a/package.json b/package.json
index d588977b..aa55c013 100644
--- a/package.json
+++ b/package.json
@@ -114,6 +114,7 @@
"undate": "0.3.0",
"vue": "2.6.10",
"vue-loader": "15.7.2",
+ "vue-router": "3.1.3",
"vue-server-renderer": "2.6.10",
"vue-template-compiler": "2.6.10",
"vuedraggable": "2.23.2",
diff --git a/yarn.lock b/yarn.lock
index 5f352303..59e303f6 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -9078,6 +9078,11 @@ vue-loader@15.7.2:
vue-hot-reload-api "^2.3.0"
vue-style-loader "^4.1.0"
+vue-router@3.1.3:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.1.3.tgz#e6b14fabc0c0ee9fda0e2cbbda74b350e28e412b"
+ integrity sha512-8iSa4mGNXBjyuSZFCCO4fiKfvzqk+mhL0lnKuGcQtO1eoj8nq3CmbEG8FwK5QqoqwDgsjsf1GDuisDX4cdb/aQ==
+
vue-server-renderer@2.6.10:
version "2.6.10"
resolved "https://registry.yarnpkg.com/vue-server-renderer/-/vue-server-renderer-2.6.10.tgz#cb2558842ead360ae2ec1f3719b75564a805b375"