Move most side bar and user list interactions to vue.

This commit is contained in:
Richard Lewis 2019-04-13 23:44:04 +03:00 committed by Pavel Djundik
parent e73bf1e9a7
commit 467ebab31f
14 changed files with 205 additions and 168 deletions

View file

@ -2,7 +2,7 @@
<!-- TODO: move all class toggling to vue, since vue clears existing classes when changing the notified class --> <!-- TODO: move all class toggling to vue, since vue clears existing classes when changing the notified class -->
<div <div
id="viewport" id="viewport"
:class="{notified: $store.state.isNotified}" :class="viewportClasses"
role="tablist" role="tablist"
> >
<Sidebar <Sidebar
@ -54,6 +54,15 @@ export default {
activeChannel: Object, activeChannel: Object,
networks: Array, networks: Array,
}, },
computed: {
viewportClasses() {
return {
"notified": this.$store.state.isNotified,
"menu-open": this.$store.state.sidebarOpen,
"userlist-open": this.$store.state.userlistOpen,
};
},
},
mounted() { mounted() {
// Make a single throttled resize listener available to all components // Make a single throttled resize listener available to all components
this.debouncedResize = throttle(() => { this.debouncedResize = throttle(() => {

View file

@ -1,4 +1,5 @@
<template> <template>
<!-- TODO: move closed style to it's own class -->
<div <div
v-if=" v-if="
!network.isCollapsed || !network.isCollapsed ||

View file

@ -18,7 +18,7 @@
role="tabpanel" role="tabpanel"
> >
<div class="header"> <div class="header">
<button class="lt" aria-label="Toggle channel list" /> <SidebarToggle />
<span class="title">{{ channel.name }}</span> <span class="title">{{ channel.name }}</span>
<div v-if="channel.editTopic === true" class="topic-container"> <div v-if="channel.editTopic === true" class="topic-container">
<input <input
@ -44,7 +44,11 @@
class="rt-tooltip tooltipped tooltipped-w" class="rt-tooltip tooltipped tooltipped-w"
aria-label="Toggle user list" aria-label="Toggle user list"
> >
<button class="rt" aria-label="Toggle user list" /> <button
class="rt"
aria-label="Toggle user list"
@click="$root.toggleUserlist"
/>
</span> </span>
</div> </div>
<div v-if="channel.type === 'special'" class="chat-content"> <div v-if="channel.type === 'special'" class="chat-content">
@ -94,6 +98,7 @@ import ParsedMessage from "./ParsedMessage.vue";
import MessageList from "./MessageList.vue"; import MessageList from "./MessageList.vue";
import ChatInput from "./ChatInput.vue"; import ChatInput from "./ChatInput.vue";
import ChatUserList from "./ChatUserList.vue"; import ChatUserList from "./ChatUserList.vue";
import SidebarToggle from "./SidebarToggle.vue";
import ListBans from "./Special/ListBans.vue"; import ListBans from "./Special/ListBans.vue";
import ListInvites from "./Special/ListInvites.vue"; import ListInvites from "./Special/ListInvites.vue";
import ListChannels from "./Special/ListChannels.vue"; import ListChannels from "./Special/ListChannels.vue";
@ -106,6 +111,7 @@ export default {
MessageList, MessageList,
ChatInput, ChatInput,
ChatUserList, ChatUserList,
SidebarToggle,
}, },
props: { props: {
network: Object, network: Object,

View file

@ -6,10 +6,7 @@
aria-label="Connect" aria-label="Connect"
> >
<div class="header"> <div class="header">
<button <SidebarToggle />
class="lt"
aria-label="Toggle channel list"
/>
</div> </div>
<form <form
class="container" class="container"
@ -217,11 +214,13 @@
<script> <script>
import RevealPassword from "./RevealPassword.vue"; import RevealPassword from "./RevealPassword.vue";
import SidebarToggle from "./SidebarToggle.vue";
export default { export default {
name: "NetworkForm", name: "NetworkForm",
components: { components: {
RevealPassword, RevealPassword,
SidebarToggle,
}, },
props: { props: {
handleSubmit: Function, handleSubmit: Function,

View file

@ -66,7 +66,10 @@
/></span> /></span>
</footer> </footer>
</aside> </aside>
<div id="sidebar-overlay" /> <div
id="sidebar-overlay"
@click="$root.setSidebar(false)"
/>
</div> </div>
</template> </template>

View file

@ -0,0 +1,13 @@
<template>
<button
class="lt"
aria-label="Toggle channel list"
@click="$root.toggleSidebar"
/>
</template>
<script>
export default {
name: "SidebarToggle",
};
</script>

View file

@ -5,10 +5,7 @@
aria-label="Changelog" aria-label="Changelog"
> >
<div class="header"> <div class="header">
<button <SidebarToggle />
class="lt"
aria-label="Toggle channel list"
/>
</div> </div>
<div class="container"> <div class="container">
<a <a
@ -42,8 +39,13 @@
</template> </template>
<script> <script>
import SidebarToggle from "../SidebarToggle.vue";
export default { export default {
name: "Changelog", name: "Changelog",
components: {
SidebarToggle,
},
mounted() { mounted() {
}, },

View file

@ -6,10 +6,7 @@
aria-label="Help" aria-label="Help"
> >
<div class="header"> <div class="header">
<button <SidebarToggle />
class="lt"
aria-label="Toggle channel list"
/>
</div> </div>
<div class="container"> <div class="container">
<h1 class="title">Help</h1> <h1 class="title">Help</h1>
@ -659,7 +656,12 @@
</template> </template>
<script> <script>
import SidebarToggle from "../SidebarToggle.vue";
export default { export default {
name: "Help", name: "Help",
components: {
SidebarToggle,
},
}; };
</script> </script>

View file

@ -6,10 +6,7 @@
aria-label="Settings" aria-label="Settings"
> >
<div class="header"> <div class="header">
<button <SidebarToggle />
class="lt"
aria-label="Toggle channel list"
/>
</div> </div>
<form <form
ref="settingsForm" ref="settingsForm"
@ -505,12 +502,14 @@
import socket from "../../js/socket"; import socket from "../../js/socket";
import RevealPassword from "../RevealPassword.vue"; import RevealPassword from "../RevealPassword.vue";
import Session from "../Session.vue"; import Session from "../Session.vue";
import SidebarToggle from "../SidebarToggle.vue";
export default { export default {
name: "Settings", name: "Settings",
components: { components: {
RevealPassword, RevealPassword,
Session, Session,
SidebarToggle,
}, },
data() { data() {
return { return {

View file

@ -10,9 +10,7 @@ const {vueApp, findChannel} = require("./vue");
window.vueMounted = () => { window.vueMounted = () => {
require("./socket-events"); require("./socket-events");
const slideoutMenu = require("./slideout");
const contextMenuFactory = require("./contextMenuFactory"); const contextMenuFactory = require("./contextMenuFactory");
const storage = require("./localStorage");
const utils = require("./utils"); const utils = require("./utils");
require("./webpush"); require("./webpush");
require("./keybinds"); require("./keybinds");
@ -20,40 +18,6 @@ window.vueMounted = () => {
const sidebar = $("#sidebar, #footer"); const sidebar = $("#sidebar, #footer");
const viewport = $("#viewport"); const viewport = $("#viewport");
function storeSidebarVisibility(name, state) {
storage.set(name, state);
vueApp.$emit("resize");
}
// If sidebar overlay is visible and it is clicked, close the sidebar
$("#sidebar-overlay").on("click", () => {
slideoutMenu.toggle(false);
if ($(window).outerWidth() > utils.mobileViewportPixels) {
storeSidebarVisibility("thelounge.state.sidebar", false);
}
});
$("#windows").on("click", "button.lt", () => {
const isOpen = !slideoutMenu.isOpen();
slideoutMenu.toggle(isOpen);
if ($(window).outerWidth() > utils.mobileViewportPixels) {
storeSidebarVisibility("thelounge.state.sidebar", isOpen);
}
});
viewport.on("click", ".rt", function() {
const isOpen = !viewport.hasClass("userlist-open");
viewport.toggleClass("userlist-open", isOpen);
storeSidebarVisibility("thelounge.state.userlist", isOpen);
return false;
});
viewport.on("contextmenu", ".network .chan", function(e) { viewport.on("contextmenu", ".network .chan", function(e) {
return contextMenuFactory.createContextMenu($(this), e).show(); return contextMenuFactory.createContextMenu($(this), e).show();
}); });
@ -138,8 +102,8 @@ window.vueMounted = () => {
socket.emit("open", channel ? channel.channel.id : null); socket.emit("open", channel ? channel.channel.id : null);
if ($(window).outerWidth() <= utils.mobileViewportPixels) { if (!keepSidebarOpen && $(window).outerWidth() <= utils.mobileViewportPixels) {
slideoutMenu.toggle(false); vueApp.setSidebar(false);
} }
} else { } else {
vueApp.activeChannel = null; vueApp.activeChannel = null;

View file

@ -1,112 +1,112 @@
"use strict"; "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;
let menuWidth = 0;
let menuIsOpen = false;
let menuIsMoving = false;
let menuIsAbsolute = false;
class SlideoutMenu { class SlideoutMenu {
static enable() { enable() {
document.body.addEventListener("touchstart", onTouchStart, {passive: true}); this.viewport = document.getElementById("viewport");
this.menu = document.getElementById("sidebar");
this.sidebarOverlay = document.getElementById("sidebar-overlay");
this.touchStartPos = null;
this.touchCurPos = null;
this.touchStartTime = 0;
this.menuWidth = 0;
this.menuIsOpen = false;
this.menuIsMoving = false;
this.menuIsAbsolute = false;
this.onTouchStart = (e) => {
this.touchStartPos = this.touchCurPos = e.touches.item(0);
if (e.touches.length !== 1) {
this.onTouchEnd();
return;
}
const styles = window.getComputedStyle(this.menu);
this.menuWidth = parseFloat(styles.width);
this.menuIsAbsolute = styles.position === "absolute";
if (!this.menuIsOpen || this.touchStartPos.screenX > this.menuWidth) {
this.touchStartTime = Date.now();
document.body.addEventListener("touchmove", this.onTouchMove, {passive: true});
document.body.addEventListener("touchend", this.onTouchEnd, {passive: true});
}
};
this.onTouchMove = (e) => {
const touch = this.touchCurPos = e.touches.item(0);
let distX = touch.screenX - this.touchStartPos.screenX;
const distY = touch.screenY - this.touchStartPos.screenY;
if (!this.menuIsMoving) {
// tan(45°) is 1. Gestures in 0°-45° (< 1) are considered horizontal, so
// menu must be open; gestures in 45°-90° (>1) are considered vertical, so
// chat windows must be scrolled.
if (Math.abs(distY / distX) >= 1) {
this.onTouchEnd();
return;
}
const devicePixelRatio = window.devicePixelRatio || 2;
if (Math.abs(distX) > devicePixelRatio) {
this.viewport.classList.toggle("menu-dragging", true);
this.menuIsMoving = true;
}
}
// Do not animate the menu on desktop view
if (!this.menuIsAbsolute) {
return;
}
if (this.menuIsOpen) {
distX += this.menuWidth;
}
if (distX > this.menuWidth) {
distX = this.menuWidth;
} else if (distX < 0) {
distX = 0;
}
this.menu.style.transform = "translate3d(" + distX + "px, 0, 0)";
this.sidebarOverlay.style.opacity = distX / this.menuWidth;
};
this.onTouchEnd = () => {
const diff = this.touchCurPos.screenX - this.touchStartPos.screenX;
const absDiff = Math.abs(diff);
if (absDiff > this.menuWidth / 2 || Date.now() - this.touchStartTime < 180 && absDiff > 50) {
this.toggle(diff > 0);
}
document.body.removeEventListener("touchmove", this.onTouchMove);
document.body.removeEventListener("touchend", this.onTouchEnd);
this.viewport.classList.toggle("menu-dragging", false);
this.menu.style.transform = null;
this.sidebarOverlay.style.opacity = null;
this.touchStartPos = null;
this.touchCurPos = null;
this.touchStartTime = 0;
this.menuIsMoving = false;
};
document.body.addEventListener("touchstart", this.onTouchStart, {passive: true});
} }
static toggle(state) { toggle(state) {
menuIsOpen = state; this.menuIsOpen = state;
viewport.classList.toggle("menu-open", state); this.viewport.classList.toggle("menu-open", state);
} }
static isOpen() { isOpen() {
return menuIsOpen; return this.menuIsOpen;
} }
} }
function onTouchStart(e) { module.exports = (new SlideoutMenu);
touchStartPos = touchCurPos = e.touches.item(0);
if (e.touches.length !== 1) {
onTouchEnd();
return;
}
const styles = window.getComputedStyle(menu);
menuWidth = parseFloat(styles.width);
menuIsAbsolute = styles.position === "absolute";
if (!menuIsOpen || touchStartPos.screenX > menuWidth) {
touchStartTime = Date.now();
document.body.addEventListener("touchmove", onTouchMove, {passive: true});
document.body.addEventListener("touchend", onTouchEnd, {passive: true});
}
}
function onTouchMove(e) {
const touch = (touchCurPos = e.touches.item(0));
let distX = touch.screenX - touchStartPos.screenX;
const distY = touch.screenY - touchStartPos.screenY;
if (!menuIsMoving) {
// tan(45°) is 1. Gestures in 0°-45° (< 1) are considered horizontal, so
// menu must be open; gestures in 45°-90° (>1) are considered vertical, so
// chat windows must be scrolled.
if (Math.abs(distY / distX) >= 1) {
onTouchEnd();
return;
}
const devicePixelRatio = window.devicePixelRatio || 2;
if (Math.abs(distX) > devicePixelRatio) {
viewport.classList.toggle("menu-dragging", true);
menuIsMoving = true;
}
}
// Do not animate the menu on desktop view
if (!menuIsAbsolute) {
return;
}
if (menuIsOpen) {
distX += menuWidth;
}
if (distX > menuWidth) {
distX = menuWidth;
} else if (distX < 0) {
distX = 0;
}
menu.style.transform = "translate3d(" + distX + "px, 0, 0)";
sidebarOverlay.style.opacity = distX / menuWidth;
}
function onTouchEnd() {
const diff = touchCurPos.screenX - touchStartPos.screenX;
const absDiff = Math.abs(diff);
if (absDiff > menuWidth / 2 || (Date.now() - touchStartTime < 180 && absDiff > 50)) {
SlideoutMenu.toggle(diff > 0);
}
document.body.removeEventListener("touchmove", onTouchMove);
document.body.removeEventListener("touchend", onTouchEnd);
viewport.classList.toggle("menu-dragging", false);
menu.style.transform = null;
sidebarOverlay.style.opacity = null;
touchStartPos = null;
touchCurPos = null;
touchStartTime = 0;
menuIsMoving = false;
}
module.exports = SlideoutMenu;

View file

@ -4,7 +4,6 @@ const $ = require("jquery");
const escape = require("css.escape"); const escape = require("css.escape");
const socket = require("../socket"); const socket = require("../socket");
const webpush = require("../webpush"); const webpush = require("../webpush");
const slideoutMenu = require("../slideout");
const sidebar = $("#sidebar"); const sidebar = $("#sidebar");
const storage = require("../localStorage"); const storage = require("../localStorage");
const utils = require("../utils"); const utils = require("../utils");
@ -21,7 +20,7 @@ socket.on("init", function(data) {
vueApp.currentUserVisibleError = null; vueApp.currentUserVisibleError = null;
if (!vueApp.initialized) { if (!vueApp.initialized) {
vueApp.initialized = true; vueApp.onSocketInit();
if (data.token) { if (data.token) {
storage.set("token", data.token); storage.set("token", data.token);
@ -29,14 +28,11 @@ socket.on("init", function(data) {
webpush.configurePushNotifications(data.pushSubscription, data.applicationServerKey); webpush.configurePushNotifications(data.pushSubscription, data.applicationServerKey);
slideoutMenu.enable(); const viewportWidth = window.outerWidth;
const viewport = $("#viewport");
const viewportWidth = $(window).outerWidth();
let isUserlistOpen = storage.get("thelounge.state.userlist"); let isUserlistOpen = storage.get("thelounge.state.userlist");
if (viewportWidth > utils.mobileViewportPixels) { if (viewportWidth > utils.mobileViewportPixels) {
slideoutMenu.toggle(storage.get("thelounge.state.sidebar") !== "false"); vueApp.setSidebar(storage.get("thelounge.state.sidebar") !== "false");
} }
// If The Lounge is opened on a small screen (less than 1024px), and we don't have stored // If The Lounge is opened on a small screen (less than 1024px), and we don't have stored
@ -45,7 +41,7 @@ socket.on("init", function(data) {
isUserlistOpen = "true"; isUserlistOpen = "true";
} }
viewport.toggleClass("userlist-open", isUserlistOpen === "true"); vueApp.setUserlist(isUserlistOpen === "true");
$(document.body).removeClass("signed-out"); $(document.body).removeClass("signed-out");
$("#loading").remove(); $("#loading").remove();

View file

@ -1,5 +1,6 @@
import Vue from "vue"; import Vue from "vue";
import Vuex from "vuex"; import Vuex from "vuex";
const storage = require("./localStorage");
Vue.use(Vuex); Vue.use(Vuex);
@ -9,6 +10,8 @@ export default new Vuex.Store({
isNotified: false, isNotified: false,
activeWindow: null, activeWindow: null,
sessions: [], sessions: [],
sidebarOpen: false,
userlistOpen: storage.get("thelounge.state.userlist") !== "false",
}, },
mutations: { mutations: {
isConnected(state, payload) { isConnected(state, payload) {
@ -26,6 +29,12 @@ export default new Vuex.Store({
sessions(state, payload) { sessions(state, payload) {
state.sessions = payload; state.sessions = payload;
}, },
sidebarOpen(state, payload) {
state.sidebarOpen = payload;
},
userlistOpen(state, payload) {
state.userlistOpen = payload;
},
}, },
getters: { getters: {
currentSession: (state) => state.sessions.find((item) => item.current), currentSession: (state) => state.sessions.find((item) => item.current),

View file

@ -7,6 +7,8 @@ const roundBadgeNumber = require("./libs/handlebars/roundBadgeNumber");
const localetime = require("./libs/handlebars/localetime"); const localetime = require("./libs/handlebars/localetime");
const friendlysize = require("./libs/handlebars/friendlysize"); const friendlysize = require("./libs/handlebars/friendlysize");
const colorClass = require("./libs/handlebars/colorClass"); const colorClass = require("./libs/handlebars/colorClass");
const slideoutMenu = require("../js/slideout");
const storage = require("./localStorage");
Vue.filter("localetime", localetime); Vue.filter("localetime", localetime);
Vue.filter("friendlysize", friendlysize); Vue.filter("friendlysize", friendlysize);
@ -48,6 +50,38 @@ const vueApp = new Vue({
mounted() { mounted() {
Vue.nextTick(() => window.vueMounted()); Vue.nextTick(() => window.vueMounted());
}, },
methods: {
onSocketInit() {
this.initialized = true;
this.$store.commit("isConnected", true);
// TODO: handle slideut in vue
slideoutMenu.enable();
},
setSidebar(state) {
const utils = require("./utils");
this.$store.commit("sidebarOpen", state);
slideoutMenu.toggle(false);
if (window.outerWidth > utils.mobileViewportPixels) {
storage.set("thelounge.state.sidebar", state);
}
this.$emit("resize");
},
toggleSidebar() {
this.setSidebar(!this.$store.state.sidebarOpen);
},
setUserlist(state) {
storage.set("thelounge.state.userlist", state);
this.$store.commit("userlistOpen", state);
this.$emit("resize");
},
toggleUserlist() {
this.setUserlist(!this.$store.state.userlistOpen);
},
},
render(createElement) { render(createElement) {
return createElement(App, { return createElement(App, {
ref: "app", ref: "app",