Implement vue-router.

This commit is contained in:
Richard Lewis 2019-10-17 16:56:44 +00:00 committed by Pavel Djundik
parent 7355c91839
commit 737afc759b
19 changed files with 221 additions and 230 deletions

View file

@ -3,12 +3,7 @@
<Sidebar :networks="networks" :active-channel="activeChannel" :overlay="$refs.overlay" />
<div id="sidebar-overlay" ref="overlay" @click="$root.setSidebar(false)" />
<article id="windows">
<Chat
v-if="activeChannel"
:network="activeChannel.network"
:channel="activeChannel.channel"
/>
<component :is="$store.state.activeWindow" ref="window" />
<router-view></router-view>
</article>
<ImageViewer ref="imageViewer" />
</div>
@ -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,

View file

@ -23,6 +23,7 @@
:aria-selected="activeChannel && channel === activeChannel.channel"
:style="closed ? {transition: 'none', opacity: 0.4} : null"
role="tab"
@click="click"
>
<slot :network="network" :channel="channel" :activeChannel="activeChannel" />
</div>
@ -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();
},
},
};
</script>

View file

@ -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;
},

View file

@ -0,0 +1,35 @@
<template>
<Chat v-if="activeChannel" :network="activeChannel.network" :channel="activeChannel.channel" />
</template>
<script>
// Temporary component for routing channels and lobbies
import Chat from "./Chat.vue";
export default {
name: "RoutedChat",
components: {
Chat,
},
computed: {
activeChannel() {
const chan_id = parseInt(this.$route.params.pathMatch);
const channel = this.$root.findChannel(chan_id);
return channel;
},
},
watch: {
activeChannel() {
this.setActiveChannel();
},
},
mounted() {
this.setActiveChannel();
},
methods: {
setActiveChannel() {
this.$root.activeChannel = this.activeChannel;
},
},
};
</script>

View file

@ -25,6 +25,7 @@
role="tab"
aria-controls="sign-in"
:aria-selected="$store.state.activeWindow === 'SignIn'"
@click="navigate('sign-in')"
/></span>
<span
class="tooltipped tooltipped-n tooltipped-no-touch"
@ -37,6 +38,7 @@
role="tab"
aria-controls="connect"
:aria-selected="$store.state.activeWindow === 'Connect'"
@click="navigate('connect')"
/></span>
<span class="tooltipped tooltipped-n tooltipped-no-touch" aria-label="Settings"
><button
@ -52,6 +54,7 @@
role="tab"
aria-controls="settings"
:aria-selected="$store.state.activeWindow === 'Settings'"
@click="navigate('settings')"
/></span>
<span class="tooltipped tooltipped-n tooltipped-no-touch" aria-label="Help"
><button
@ -62,6 +65,7 @@
role="tab"
aria-controls="help"
:aria-selected="$store.state.activeWindow === 'Help'"
@click="navigate('help')"
/></span>
</footer>
</aside>
@ -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: {

View file

@ -4,7 +4,7 @@
<SidebarToggle />
</div>
<div class="container">
<a id="back-to-help" href="#" data-target="#help" data-component="Help">« Help</a>
<router-link id="back-to-help" to="help">« Help</router-link>
<template
v-if="
@ -65,6 +65,10 @@ export default {
},
methods: {
patchChangelog() {
if (!this.$refs.changelog) {
return;
}
const links = this.$refs.changelog.querySelectorAll("a");
for (const link of links) {

View file

@ -8,12 +8,10 @@
<h2>
<small class="pull-right">
v{{ $root.serverConfiguration.version }} (<a
v{{ $root.serverConfiguration.version }} (<router-link
id="view-changelog"
href="#"
data-target="#changelog"
data-component="Changelog"
>release notes</a
to="changelog"
>release notes</router-link
>)
</small>
About The Lounge

View file

@ -36,4 +36,6 @@ module.exports = {
condensedTypesQuery,
timeFormats,
sizeUnits,
// Same value as media query in CSS that forces sidebars to become overlays
mobileViewportPixels: 768,
};

View file

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

25
client/js/router.js Normal file
View file

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

View file

@ -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;
}

View file

@ -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) {

View file

@ -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();
}
});

View file

@ -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();
});

View file

@ -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();
});

View file

@ -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() {

View file

@ -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) {

View file

@ -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",

View file

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