From 09e12affe8d5bb5218e70eeea6faa5e004c39a1d Mon Sep 17 00:00:00 2001
From: Pavel Djundik
Date: Mon, 18 Feb 2019 11:18:32 +0200
Subject: [PATCH 001/111] Begin moving windows to Vue
---
client/components/App.vue | 14 +-
client/components/RevealPassword.vue | 33 ++
client/components/Windows/Changelog.vue | 42 ++
client/components/Windows/Help.vue | 655 ++++++++++++++++++++++
client/components/Windows/NetworkEdit.vue | 243 ++++++++
client/components/Windows/Settings.vue | 415 ++++++++++++++
client/components/Windows/SignIn.vue | 104 ++++
client/css/style.css | 11 +-
client/js/lounge.js | 3 +-
client/js/socket-events/auth.js | 42 +-
client/js/socket-events/configuration.js | 4 -
client/js/socket-events/init.js | 1 -
client/js/socket-events/network.js | 2 -
client/js/utils.js | 20 -
client/js/vue.js | 7 +
client/views/reveal-password.tpl | 3 -
client/views/windows/changelog.tpl | 20 -
client/views/windows/connect.tpl | 114 ----
client/views/windows/help.tpl | 608 --------------------
client/views/windows/settings.tpl | 239 --------
client/views/windows/sign_in.tpl | 17 -
21 files changed, 1516 insertions(+), 1081 deletions(-)
create mode 100644 client/components/RevealPassword.vue
create mode 100644 client/components/Windows/Changelog.vue
create mode 100644 client/components/Windows/Help.vue
create mode 100644 client/components/Windows/NetworkEdit.vue
create mode 100644 client/components/Windows/Settings.vue
create mode 100644 client/components/Windows/SignIn.vue
delete mode 100644 client/views/reveal-password.tpl
delete mode 100644 client/views/windows/changelog.tpl
delete mode 100644 client/views/windows/connect.tpl
delete mode 100644 client/views/windows/help.tpl
delete mode 100644 client/views/windows/settings.tpl
delete mode 100644 client/views/windows/sign_in.tpl
diff --git a/client/components/App.vue b/client/components/App.vue
index 76820e4d..211b785a 100644
--- a/client/components/App.vue
+++ b/client/components/App.vue
@@ -64,13 +64,10 @@
-
-
-
-
-
+ :channel="activeChannel.channel" />
+
@@ -80,14 +77,17 @@ const throttle = require("lodash/throttle");
import NetworkList from "./NetworkList.vue";
import Chat from "./Chat.vue";
+import SignIn from "./Windows/SignIn.vue";
export default {
name: "App",
components: {
NetworkList,
Chat,
+ SignIn,
},
props: {
+ activeWindow: String,
activeChannel: Object,
networks: Array,
},
diff --git a/client/components/RevealPassword.vue b/client/components/RevealPassword.vue
new file mode 100644
index 00000000..1c629077
--- /dev/null
+++ b/client/components/RevealPassword.vue
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/client/components/Windows/Changelog.vue b/client/components/Windows/Changelog.vue
new file mode 100644
index 00000000..448bb969
--- /dev/null
+++ b/client/components/Windows/Changelog.vue
@@ -0,0 +1,42 @@
+
+
+
+
+
« Help
+
+ {{#if version}}
+
Release notes for {{ version }}
+
+ {{#if changelog}}
+
Introduction
+
{{{changelog}}}
+ {{else}}
+
Unable to retrieve releases from GitHub.
+
View release notes for this version on GitHub
+ {{/if}}
+ {{else}}
+
Loading changelog…
+ {{/if}}
+
+
+
+
+
+
diff --git a/client/components/Windows/Help.vue b/client/components/Windows/Help.vue
new file mode 100644
index 00000000..a4dfb02f
--- /dev/null
+++ b/client/components/Windows/Help.vue
@@ -0,0 +1,655 @@
+
+
+
+
+
Help
+
+
+
+ v{{ version }}
+ (release notes )
+
+ About The Lounge
+
+
+
+
+
Keyboard Shortcuts
+
+
+
+ Alt Shift ↓
+ ⌥ ⇧ ↓
+
+
+
Switch to the next lobby in the channel list.
+
+
+
+
+
+ Alt Shift ↑
+ ⌥ ⇧ ↑
+
+
+
Switch to the previous lobby in the channel list.
+
+
+
+
+
+ Alt ↓
+ ⌥ ↓
+
+
+
Switch to the next window in the channel list.
+
+
+
+
+
+ Alt ↑
+ ⌥ ↑
+
+
+
Switch to the previous window in the channel list.
+
+
+
+
+
+ Alt A
+ ⌥ A
+
+
+
Switch to the first window with unread messages.
+
+
+
+
+
+ Ctrl K
+ ⌘ K
+
+
+
+ Mark any text typed after this shortcut to be colored. After
+ hitting this shortcut, enter an integer in the range
+ 0—15
to select the desired color, or use the
+ autocompletion menu to choose a color name (see below).
+
+
+ Background color can be specified by putting a comma and
+ another integer in the range 0—15
after the
+ foreground color number (autocompletion works too).
+
+
+ A color reference can be found
+ here .
+
+
+
+
+
+
+ Ctrl B
+ ⌘ B
+
+
+
Mark all text typed after this shortcut as bold .
+
+
+
+
+
+ Ctrl U
+ ⌘ U
+
+
+
Mark all text typed after this shortcut as underlined .
+
+
+
+
+
+ Ctrl I
+ ⌘ I
+
+
+
Mark all text typed after this shortcut as italics .
+
+
+
+
+
+ Ctrl S
+ ⌘ S
+
+
+
Mark all text typed after this shortcut as struck through .
+
+
+
+
+
+ Ctrl M
+ ⌘ M
+
+
+
Mark all text typed after this shortcut as monospaced .
+
+
+
+
+
+ Ctrl O
+ ⌘ O
+
+
+
+ Mark all text typed after this shortcut to be reset to its
+ original formatting.
+
+
+
+
+
Autocompletion
+
+
+ To auto-complete nicknames, channels, commands, and emoji, type one of the characters below to open
+ a suggestion list. Use the ↑ and ↓ keys to highlight an item, and insert it by
+ pressing Tab or Enter (or by clicking the desired item).
+
+
+ Autocompletion can be disabled in settings.
+
+
+
+
+
+
+
+
+ /
+
+
+
Commands (see list of commands below)
+
+
+
+
+
+ :
+
+
+
Emoji (note: requires two search characters, to avoid conflicting with common emoticons like :)
)
+
+
+
+
Commands
+
+
+
+ /away [message]
+
+
+
Mark yourself as away with an optional message.
+
+
+
+
+
+ /back
+
+
+
Remove your away status (set with /away
).
+
+
+
+
+
+ /ban nick
+
+
+
Ban (+b
) a user from the current channel.
+ This can be a nickname or a hostmask.
+
+
+
+
+
+ /banlist
+
+
+
Load the banlist for the current channel.
+
+
+
+
+
+ /collapse
+
+
+
+ Collapse all previews in the current channel (opposite of
+ /expand
)
+
+
+
+
+
+
+ /connect host [port]
+
+
+
+ Connect to a new IRC network. If port
starts with
+ a +
sign, the connection will be made secure
+ using TLS.
+
+
Alias: /server
+
+
+
+
+
+ /ctcp target cmd [args]
+
+
+
+
+
+
+ /deop nick [...nick]
+
+
+
+ Remove op (-o
) from one or several users in the
+ current channel.
+
+
+
+
+
+
+ /devoice nick [...nick]
+
+
+
+ Remove voice (-v
) from one or several users in
+ the current channel.
+
+
+
+
+
+
+ /disconnect [message]
+
+
+
+ Disconnect from the current network with an
+ optionally-provided message.
+
+
+
+
+
+
+ /expand
+
+
+
+ Expand all previews in the current channel (opposite of
+ /collapse
)
+
+
+
+
+
+
+ /invite nick [channel]
+
+
+
+ Invite a user to the specified channel. If
+ channel
is ommitted, user will be invited to the
+ current channel.
+
+
+
+
+
+
+ /ignore nick
+
+
+
+ Block any messages from the specified user on the current network.
+ This can be a nickname or a hostmask.
+
+
+
+
+
+ /ignorelist
+
+
+
Load the list of ignored users for the current network.
+
+
+
+
+
+
+
+ /kick nick
+
+
+
Kick a user from the current channel.
+
+
+
+
+
+ /list
+
+
+
Retrieve a list of available channels on this network.
+
+
+
+
+
+ /me message
+
+
+
+ Send an action message to the current channel. The Lounge will
+ display it inline, as if the message was posted in the third
+ person.
+
+
+
+
+
+
+ /mode flags [args]
+
+
+
+ Set the given flags to the current channel if the active
+ window is a channel, another user if the active window is a
+ private message window, or yourself if the current window is a
+ server window.
+
+
+
+
+
+
+ /msg channel message
+
+
+
Send a message to the specified channel.
+
+
+
+
+
+ /nick newnick
+
+
+
Change your nickname on the current network.
+
+
+
+
+
+ /notice channel message
+
+
+
Sends a notice message to the specified channel.
+
+
+
+
+
+ /op nick [...nick]
+
+
+
+ Give op (+o
) to one or several users in the
+ current channel.
+
+
+
+
+
+
+ /part [channel]
+
+
+
+ Close the specified channel or private message window, or the
+ current channel if channel
is ommitted.
+
+
Aliases: /close
, /leave
+
+
+
+
+
+ /rejoin
+
+
+
+ Leave and immediately rejoin the current channel. Useful to
+ quickly get op from ChanServ in an empty channel, for example.
+
+
Alias: /cycle
+
+
+
+
+
+ /query nick
+
+
+
Send a private message to the specified user.
+
+
+
+
+
+ /quit [message]
+
+
+
+ Disconnect from the current network with an optional message.
+
+
+
+
+
+
+ /raw message
+
+
+
Send a raw message to the current IRC network.
+
Aliases: /quote
, /send
+
+
+
+
+
+ /slap nick
+
+
+
Slap someone in the current channel with a trout!
+
+
+
+
+
+ /topic [newtopic]
+
+
+
+ Get the topic in the current channel.
+ If newtopic
is specified, sets the
+ topic in the current channel.
+
+
+
+
+
+
+ /unban nick
+
+
+
Unban (-b
) a user from the current channel.
+ This can be a nickname or a hostmask.
+
+
+
+
+
+ /unignore nick
+
+
+
+ Unblock messages from the specified user on the current network.
+ This can be a nickname or a hostmask.
+
+
+
+
+
+ /voice nick [...nick]
+
+
+
+ Give voice (+v
) to one or several users in the
+ current channel.
+
+
+
+
+
+
+ /whois nick
+
+
+
+ Retrieve information about the given user on the current
+ network.
+
+
+
+
+
+
+
+
+
diff --git a/client/components/Windows/NetworkEdit.vue b/client/components/Windows/NetworkEdit.vue
new file mode 100644
index 00000000..e6cf4a37
--- /dev/null
+++ b/client/components/Windows/NetworkEdit.vue
@@ -0,0 +1,243 @@
+
+
+
+
+
diff --git a/client/components/Windows/Settings.vue b/client/components/Windows/Settings.vue
new file mode 100644
index 00000000..5448cf64
--- /dev/null
+++ b/client/components/Windows/Settings.vue
@@ -0,0 +1,415 @@
+
+
+
+
+
Settings
+
+
+
+
+
+ Advanced settings
+
+
+
+
+
+
+
Native app
+ Add The Lounge to Home screen
+ Open irc:// URLs with The Lounge
+
+
+ {{#unless public}}
+
+
+ Settings synchronisation
+
+
+
+
+
+
+ Synchronize settings with other clients.
+
+
Warning Checking this box will override the settings of this client with those stored on the server.
+
Warning No settings have been synced before. Enabling this will sync all settings of this client as the base for other clients.
+
+
+ {{/unless}}
+
+
Messages
+
+
+
+
+
+ Show seconds in timestamp
+
+
+
+
+ Status messages
+
+
+
+
+
+
+
+
+ Show all status messages individually
+
+
+
+ Condense status messages together
+
+
+
+ Hide all status messages
+
+
+
+
Visual Aids
+
+
+
+
+ Enable colored nicknames
+
+
+
+ Enable autocomplete
+
+
+
+
+ Nick autocomplete postfix (e.g. ,
)
+
+
+
+
+
+
Theme
+
+
+ Theme
+
+ {{#each themes}}
+
+ {{ displayName }}
+
+ {{/each}}
+
+
+ {{#if prefetch}}
+
+
Link previews
+
+
+
+
+ Auto-expand media
+
+
+
+
+
+ Auto-expand websites
+
+
+ {{/if}}
+ {{#unless public}}
+
+
Push Notifications
+
+
+
Subscribe to push notifications
+
+ Warning :
+ Push notifications are only supported over HTTPS connections.
+
+
+ Warning :
+ Push notifications are not supported by your browser.
+
+
+ {{/unless}}
+
+
Browser Notifications
+
+
+
+
+ Enable browser notifications
+
+ Warning :
+ Notifications are not supported by your browser.
+
+
+ Warning :
+ Notifications are blocked by your browser.
+
+
+
+
+
+
+ Enable notification sound
+
+
+
+
+
+
+
+ Enable notification for all messages
+
+
+
+
+
+ Custom highlights (comma-separated keywords)
+
+
+
+
+ {{#unless public}}
+ {{#unless ldapEnabled}}
+
+ {{/unless}}
+ {{/unless}}
+
+
Custom Stylesheet
+
+
+ Custom stylesheet. You can override any style with CSS here.
+
+
+
+
+ {{#unless public}}
+
+
Sessions
+
+
Current session
+
+
+
Other sessions
+
+
+ {{/unless}}
+
+
+
+
+
+
diff --git a/client/components/Windows/SignIn.vue b/client/components/Windows/SignIn.vue
new file mode 100644
index 00000000..257cdce1
--- /dev/null
+++ b/client/components/Windows/SignIn.vue
@@ -0,0 +1,104 @@
+
+
+
+
+
diff --git a/client/css/style.css b/client/css/style.css
index 96ed5c79..2c0e2bc7 100644
--- a/client/css/style.css
+++ b/client/css/style.css
@@ -908,7 +908,8 @@ background on hover (unless active) */
.window {
background: var(--window-bg-color);
- display: none;
+ display: flex;
+ flex-direction: column;
overflow-y: auto;
height: 100%;
scrollbar-width: thin;
@@ -949,11 +950,6 @@ background on hover (unless active) */
margin: 20px 0 10px;
}
-#windows .window.active {
- display: flex;
- flex-direction: column;
-}
-
#windows .header {
line-height: 45px;
height: 45px;
@@ -1749,7 +1745,6 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */
#sign-in .error {
color: #e74c3c;
- display: none; /* This message gets displayed on error only */
margin-top: 1em;
width: 100%;
}
@@ -1878,7 +1873,7 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */
content: "\f00c"; /* https://fontawesome.com/icons/check?style=solid */
}
-.password-container .reveal-password.visible span::before {
+.password-container .reveal-password-visible span::before {
content: "\f070"; /* https://fontawesome.com/icons/eye-slash?style=solid */
color: #ff4136;
}
diff --git a/client/js/lounge.js b/client/js/lounge.js
index 2fd3fb97..7dee3a0c 100644
--- a/client/js/lounge.js
+++ b/client/js/lounge.js
@@ -127,7 +127,8 @@ window.vueMounted = () => {
}
}
- if (inSidebar) {
+ if (channel) {
+ vueApp.activeWindow = null;
vueApp.activeChannel = channel;
if (channel) {
diff --git a/client/js/socket-events/auth.js b/client/js/socket-events/auth.js
index 3fd5e66d..a415310c 100644
--- a/client/js/socket-events/auth.js
+++ b/client/js/socket-events/auth.js
@@ -4,8 +4,7 @@ const $ = require("jquery");
const socket = require("../socket");
const storage = require("../localStorage");
const utils = require("../utils");
-const templates = require("../../views");
-const {vueApp} = require("../vue");
+const {vueApp, getActiveWindowComponent} = require("../vue");
socket.on("auth", function(data) {
// If we reconnected and serverHash differs, that means the server restarted
@@ -18,40 +17,19 @@ socket.on("auth", function(data) {
return;
}
- const login = $("#sign-in");
-
if (data.serverHash > -1) {
utils.serverHash = data.serverHash;
- login.html(templates.windows.sign_in());
-
- utils.togglePasswordField("#sign-in .reveal-password");
-
- login.find("form").on("submit", function() {
- const form = $(this);
-
- form.find(".btn").prop("disabled", true);
-
- const values = {};
- $.each(form.serializeArray(), function(i, obj) {
- values[obj.name] = obj.value;
- });
-
- storage.set("user", values.user);
-
- socket.emit("auth", values);
-
- return false;
- });
+ vueApp.activeWindow = "SignIn";
} else {
- login.find(".btn").prop("disabled", false);
+ getActiveWindowComponent().inFlight = false;
}
let token;
const user = storage.get("user");
if (!data.success) {
- if (login.length === 0) {
+ if (vueApp.activeWindow !== "SignIn") {
socket.disconnect();
vueApp.isConnected = false;
vueApp.currentUserVisibleError = "Authentication failed, reloading…";
@@ -61,13 +39,7 @@ socket.on("auth", function(data) {
storage.remove("token");
- const error = login.find(".error");
- error
- .show()
- .closest("form")
- .one("submit", function() {
- error.hide();
- });
+ getActiveWindowComponent().errorShown = true;
} else if (user) {
token = storage.get("token");
@@ -95,10 +67,6 @@ socket.on("auth", function(data) {
}
}
- if (user) {
- login.find("input[name='user']").val(user);
- }
-
if (token) {
return;
}
diff --git a/client/js/socket-events/configuration.js b/client/js/socket-events/configuration.js
index 8449cd3b..34f713eb 100644
--- a/client/js/socket-events/configuration.js
+++ b/client/js/socket-events/configuration.js
@@ -52,8 +52,6 @@ socket.on("configuration", function(data) {
upload.initialize(data.fileUploadMaxFileSize);
}
- utils.togglePasswordField("#change-password .reveal-password");
-
options.initialize();
webpush.initialize();
@@ -112,8 +110,6 @@ socket.on("configuration", function(data) {
// Store the "previous" value, for next time
$(this).data("lastvalue", nick);
});
-
- utils.togglePasswordField("#connect .reveal-password");
});
if ("URLSearchParams" in window) {
diff --git a/client/js/socket-events/init.js b/client/js/socket-events/init.js
index bfd2b387..6c4b3ae2 100644
--- a/client/js/socket-events/init.js
+++ b/client/js/socket-events/init.js
@@ -49,7 +49,6 @@ socket.on("init", function(data) {
$(document.body).removeClass("signed-out");
$("#loading").remove();
- $("#sign-in").remove();
if (window.g_LoungeErrorHandler) {
window.removeEventListener("error", window.g_LoungeErrorHandler);
diff --git a/client/js/socket-events/network.js b/client/js/socket-events/network.js
index 5f5d86a6..36d66e30 100644
--- a/client/js/socket-events/network.js
+++ b/client/js/socket-events/network.js
@@ -75,6 +75,4 @@ socket.on("network:info", function(data) {
sidebar.find(`.network[data-uuid="${uuid}"] .chan.lobby .name`).click();
});
-
- utils.togglePasswordField("#connect .reveal-password");
});
diff --git a/client/js/utils.js b/client/js/utils.js
index 2ad4f2b1..80d3cb43 100644
--- a/client/js/utils.js
+++ b/client/js/utils.js
@@ -18,7 +18,6 @@ module.exports = {
move,
closeChan,
synchronizeNotifiedState,
- togglePasswordField,
requestIdleCallback,
};
@@ -104,25 +103,6 @@ function updateTitle() {
document.title = title;
}
-function togglePasswordField(elem) {
- $(elem).on("click", function() {
- const $this = $(this);
- const input = $this.closest("div").find("input");
-
- input.attr("type", input.attr("type") === "password" ? "text" : "password");
-
- swapLabel($this);
- swapLabel($this.find("span"));
- $this.toggleClass("visible");
- });
-}
-
-// Given a element, swap its aria-label with the content of `data-alt-label`
-function swapLabel(element) {
- const altText = element.data("alt-label");
- element.data("alt-label", element.attr("aria-label")).attr("aria-label", altText);
-}
-
function confirmExit() {
if ($(document.body).hasClass("public")) {
window.onbeforeunload = function() {
diff --git a/client/js/vue.js b/client/js/vue.js
index 2477e80f..3171e328 100644
--- a/client/js/vue.js
+++ b/client/js/vue.js
@@ -15,6 +15,7 @@ Vue.filter("roundBadgeNumber", roundBadgeNumber);
const vueApp = new Vue({
el: "#viewport",
data: {
+ activeWindow: null,
activeChannel: null,
appName: document.title,
currentUserVisibleError: null,
@@ -48,6 +49,7 @@ const vueApp = new Vue({
},
render(createElement) {
return createElement(App, {
+ ref: "app",
props: this,
});
},
@@ -86,8 +88,13 @@ function initChannel(channel) {
}
}
+function getActiveWindowComponent() {
+ return vueApp.$refs.app.$refs.window;
+}
+
module.exports = {
vueApp,
findChannel,
initChannel,
+ getActiveWindowComponent,
};
diff --git a/client/views/reveal-password.tpl b/client/views/reveal-password.tpl
deleted file mode 100644
index f6cc9812..00000000
--- a/client/views/reveal-password.tpl
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
diff --git a/client/views/windows/changelog.tpl b/client/views/windows/changelog.tpl
deleted file mode 100644
index 5d3326f1..00000000
--- a/client/views/windows/changelog.tpl
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-
« Help
-
- {{#if version}}
-
Release notes for {{version}}
-
- {{#if changelog}}
-
Introduction
-
{{{changelog}}}
- {{else}}
-
Unable to retrieve releases from GitHub.
-
View release notes for this version on GitHub
- {{/if}}
- {{else}}
-
Loading changelog…
- {{/if}}
-
diff --git a/client/views/windows/connect.tpl b/client/views/windows/connect.tpl
deleted file mode 100644
index 628c1764..00000000
--- a/client/views/windows/connect.tpl
+++ /dev/null
@@ -1,114 +0,0 @@
-
-
-
-
-
- {{#if defaults.uuid}}
-
-
- Edit {{defaults.name}}
- {{else}}
- {{#if public}}The Lounge - {{/if}}
- Connect
- {{#unless displayNetwork}}
- {{#if lockNetwork}}
- to {{defaults.name}}
- {{/if}}
- {{/unless}}
- {{/if}}
-
-
- {{#if displayNetwork}}
-
-
-
Network settings
-
-
- Name
-
-
-
-
-
- Server
-
-
-
-
-
-
-
-
-
- Use secure connection (TLS)
-
-
-
-
-
- Only allow trusted certificates
-
-
-
-
- {{/if}}
-
-
User preferences
-
-
- Nick
-
-
-
-
- {{#unless useHexIp}}
-
- Username
-
-
-
-
- {{/unless}}
-
- Password
-
-
-
- {{> ../reveal-password}}
-
-
- Real name
-
-
-
-
- {{#if defaults.uuid}}
-
- Commands
-
-
- {{~#each defaults.commands~}}{{~this}}
-{{/each~}}
-
-
- Save
-
- {{else}}
-
- Channels
-
-
-
-
-
- Connect
-
- {{/if}}
-
-
diff --git a/client/views/windows/help.tpl b/client/views/windows/help.tpl
deleted file mode 100644
index aa16dca9..00000000
--- a/client/views/windows/help.tpl
+++ /dev/null
@@ -1,608 +0,0 @@
-
-
-
Help
-
-
-
- v{{version}}
- (release notes )
-
- About The Lounge
-
-
-
-
-
Keyboard Shortcuts
-
-
-
- Alt Shift ↓
- ⌥ ⇧ ↓
-
-
-
Switch to the next lobby in the channel list.
-
-
-
-
-
- Alt Shift ↑
- ⌥ ⇧ ↑
-
-
-
Switch to the previous lobby in the channel list.
-
-
-
-
-
- Alt ↓
- ⌥ ↓
-
-
-
Switch to the next window in the channel list.
-
-
-
-
-
- Alt ↑
- ⌥ ↑
-
-
-
Switch to the previous window in the channel list.
-
-
-
-
-
- Alt A
- ⌥ A
-
-
-
Switch to the first window with unread messages.
-
-
-
-
-
- Ctrl K
- ⌘ K
-
-
-
- Mark any text typed after this shortcut to be colored. After
- hitting this shortcut, enter an integer in the range
- 0—15
to select the desired color, or use the
- autocompletion menu to choose a color name (see below).
-
-
- Background color can be specified by putting a comma and
- another integer in the range 0—15
after the
- foreground color number (autocompletion works too).
-
-
- A color reference can be found
- here .
-
-
-
-
-
-
- Ctrl B
- ⌘ B
-
-
-
Mark all text typed after this shortcut as bold .
-
-
-
-
-
- Ctrl U
- ⌘ U
-
-
-
Mark all text typed after this shortcut as underlined .
-
-
-
-
-
- Ctrl I
- ⌘ I
-
-
-
Mark all text typed after this shortcut as italics .
-
-
-
-
-
- Ctrl S
- ⌘ S
-
-
-
Mark all text typed after this shortcut as struck through .
-
-
-
-
-
- Ctrl M
- ⌘ M
-
-
-
Mark all text typed after this shortcut as monospaced .
-
-
-
-
-
- Ctrl O
- ⌘ O
-
-
-
- Mark all text typed after this shortcut to be reset to its
- original formatting.
-
-
-
-
-
Autocompletion
-
-
- To auto-complete nicknames, channels, commands, and emoji, type one of the characters below to open
- a suggestion list. Use the ↑ and ↓ keys to highlight an item, and insert it by
- pressing Tab or Enter (or by clicking the desired item).
-
-
- Autocompletion can be disabled in settings.
-
-
-
-
-
-
-
-
- /
-
-
-
Commands (see list of commands below)
-
-
-
-
-
- :
-
-
-
Emoji (note: requires two search characters, to avoid conflicting with common emoticons like :)
)
-
-
-
-
Commands
-
-
-
- /away [message]
-
-
-
Mark yourself as away with an optional message.
-
-
-
-
-
- /back
-
-
-
Remove your away status (set with /away
).
-
-
-
-
-
- /ban nick
-
-
-
Ban (+b
) a user from the current channel.
- This can be a nickname or a hostmask.
-
-
-
-
-
- /banlist
-
-
-
Load the banlist for the current channel.
-
-
-
-
-
- /collapse
-
-
-
- Collapse all previews in the current channel (opposite of
- /expand
)
-
-
-
-
-
-
- /connect host [port]
-
-
-
- Connect to a new IRC network. If port
starts with
- a +
sign, the connection will be made secure
- using TLS.
-
-
Alias: /server
-
-
-
-
-
- /ctcp target cmd [args]
-
-
-
-
-
-
- /deop nick [...nick]
-
-
-
- Remove op (-o
) from one or several users in the
- current channel.
-
-
-
-
-
-
- /devoice nick [...nick]
-
-
-
- Remove voice (-v
) from one or several users in
- the current channel.
-
-
-
-
-
-
- /disconnect [message]
-
-
-
- Disconnect from the current network with an
- optionally-provided message.
-
-
-
-
-
-
- /expand
-
-
-
- Expand all previews in the current channel (opposite of
- /collapse
)
-
-
-
-
-
-
- /invite nick [channel]
-
-
-
- Invite a user to the specified channel. If
- channel
is omitted, user will be invited to the
- current channel.
-
-
-
-
-
-
- /ignore nick
-
-
-
- Block any messages from the specified user on the current network.
- This can be a nickname or a hostmask.
-
-
-
-
-
- /ignorelist
-
-
-
Load the list of ignored users for the current network.
-
-
-
-
-
-
-
- /kick nick
-
-
-
Kick a user from the current channel.
-
-
-
-
-
- /list
-
-
-
Retrieve a list of available channels on this network.
-
-
-
-
-
- /me message
-
-
-
- Send an action message to the current channel. The Lounge will
- display it inline, as if the message was posted in the third
- person.
-
-
-
-
-
-
- /mode flags [args]
-
-
-
- Set the given flags to the current channel if the active
- window is a channel, another user if the active window is a
- private message window, or yourself if the current window is a
- server window.
-
-
-
-
-
-
- /msg channel message
-
-
-
Send a message to the specified channel.
-
-
-
-
-
- /nick newnick
-
-
-
Change your nickname on the current network.
-
-
-
-
-
- /notice channel message
-
-
-
Sends a notice message to the specified channel.
-
-
-
-
-
- /op nick [...nick]
-
-
-
- Give op (+o
) to one or several users in the
- current channel.
-
-
-
-
-
-
- /part [channel]
-
-
-
- Close the specified channel or private message window, or the
- current channel if channel
is omitted.
-
-
Aliases: /close
, /leave
-
-
-
-
-
- /rejoin
-
-
-
- Leave and immediately rejoin the current channel. Useful to
- quickly get op from ChanServ in an empty channel, for example.
-
-
Alias: /cycle
-
-
-
-
-
- /query nick
-
-
-
Send a private message to the specified user.
-
-
-
-
-
- /quit [message]
-
-
-
- Disconnect from the current network with an optional message.
-
-
-
-
-
-
- /raw message
-
-
-
Send a raw message to the current IRC network.
-
Aliases: /quote
, /send
-
-
-
-
-
- /slap nick
-
-
-
Slap someone in the current channel with a trout!
-
-
-
-
-
- /topic [newtopic]
-
-
-
- Get the topic in the current channel.
- If newtopic
is specified, sets the
- topic in the current channel.
-
-
-
-
-
-
- /unban nick
-
-
-
Unban (-b
) a user from the current channel.
- This can be a nickname or a hostmask.
-
-
-
-
-
- /unignore nick
-
-
-
- Unblock messages from the specified user on the current network.
- This can be a nickname or a hostmask.
-
-
-
-
-
- /voice nick [...nick]
-
-
-
- Give voice (+v
) to one or several users in the
- current channel.
-
-
-
-
-
-
- /whois nick
-
-
-
- Retrieve information about the given user on the current
- network.
-
-
-
-
diff --git a/client/views/windows/settings.tpl b/client/views/windows/settings.tpl
deleted file mode 100644
index c23bd718..00000000
--- a/client/views/windows/settings.tpl
+++ /dev/null
@@ -1,239 +0,0 @@
-
-
-
Settings
-
-
-
-
-
- Advanced settings
-
-
-
-
-
-
-
Native app
- Add The Lounge to Home screen
- Open irc:// URLs with The Lounge
-
-
- {{#unless public}}
-
-
- Settings synchronisation
-
-
-
-
-
-
- Synchronize settings with other clients.
-
-
Warning Checking this box will override the settings of this client with those stored on the server.
-
Warning No settings have been synced before. Enabling this will sync all settings of this client as the base for other clients.
-
-
- {{/unless}}
-
-
Messages
-
-
-
-
-
- Show seconds in timestamp
-
-
-
-
- Status messages
-
-
-
-
-
-
-
-
- Show all status messages individually
-
-
-
- Condense status messages together
-
-
-
- Hide all status messages
-
-
-
-
Visual Aids
-
-
-
-
- Enable colored nicknames
-
-
-
- Enable autocomplete
-
-
-
-
- Nick autocomplete postfix (e.g. ,
)
-
-
-
-
-
-
Theme
-
-
- Theme
-
- {{#each themes}}
-
- {{displayName}}
-
- {{/each}}
-
-
- {{#if prefetch}}
-
-
Link previews
-
-
-
-
- Auto-expand media
-
-
-
-
-
- Auto-expand websites
-
-
- {{/if}}
- {{#unless public}}
-
-
Push Notifications
-
-
-
Subscribe to push notifications
-
- Warning :
- Push notifications are only supported over HTTPS connections.
-
-
- Warning :
- Push notifications are not supported by your browser.
-
-
- {{/unless}}
-
-
Browser Notifications
-
-
-
-
- Enable browser notifications
-
- Warning :
- Notifications are not supported by your browser.
-
-
- Warning :
- Notifications are blocked by your browser.
-
-
-
-
-
-
- Enable notification sound
-
-
-
-
-
-
-
- Enable notification for all messages
-
-
-
-
-
- Custom highlights (comma-separated keywords)
-
-
-
-
- {{#unless public}}
- {{#unless ldapEnabled}}
-
- {{/unless}}
- {{/unless}}
-
-
Custom Stylesheet
-
-
- Custom stylesheet. You can override any style with CSS here.
-
-
-
-
- {{#unless public}}
-
-
Sessions
-
-
Current session
-
-
-
Other sessions
-
-
- {{/unless}}
-
diff --git a/client/views/windows/sign_in.tpl b/client/views/windows/sign_in.tpl
deleted file mode 100644
index 07964f9d..00000000
--- a/client/views/windows/sign_in.tpl
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
- Username
-
-
-
- Password
-
- {{> ../reveal-password}}
-
-
- Authentication failed.
-
- Sign in
-
From 71f54f6a5dd58d53388c77ee2b490dd83f921255 Mon Sep 17 00:00:00 2001
From: Pavel Djundik
Date: Wed, 20 Feb 2019 11:10:18 +0200
Subject: [PATCH 002/111] Move some settings to Vue
---
client/components/App.vue | 40 +++----
client/components/Windows/Settings.vue | 133 ++++++++++++-----------
client/js/lounge.js | 13 +--
client/js/options.js | 34 ++----
client/js/socket-events/configuration.js | 5 +-
client/js/vue.js | 3 +
client/js/webpush.js | 21 ++--
7 files changed, 115 insertions(+), 134 deletions(-)
diff --git a/client/components/App.vue b/client/components/App.vue
index 211b785a..01e3f558 100644
--- a/client/components/App.vue
+++ b/client/components/App.vue
@@ -19,44 +19,42 @@
@@ -78,6 +76,7 @@ const throttle = require("lodash/throttle");
import NetworkList from "./NetworkList.vue";
import Chat from "./Chat.vue";
import SignIn from "./Windows/SignIn.vue";
+import Settings from "./Windows/Settings.vue";
export default {
name: "App",
@@ -85,6 +84,7 @@ export default {
NetworkList,
Chat,
SignIn,
+ Settings,
},
props: {
activeWindow: String,
diff --git a/client/components/Windows/Settings.vue b/client/components/Windows/Settings.vue
index 5448cf64..4db75889 100644
--- a/client/components/Windows/Settings.vue
+++ b/client/components/Windows/Settings.vue
@@ -40,8 +40,8 @@
class="btn">Open irc:// URLs with The Lounge
- {{#unless public}}
@@ -64,13 +64,13 @@
Warning No settings have been synced before. Enabling this will sync all settings of this client as the base for other clients.
- {{/unless}}
+
Messages
@@ -169,59 +169,62 @@
id="theme-select"
name="theme"
class="input">
- {{#each themes}}
-
- {{ displayName }}
+
+ {{ theme.displayName }}
- {{/each}}
- {{#if prefetch}}
-
-
Link previews
-
-
-
-
- Auto-expand media
-
-
-
-
-
- Auto-expand websites
-
-
- {{/if}}
- {{#unless public}}
-
-
Push Notifications
-
-
-
Subscribe to push notifications
-
-
Warning :
- Push notifications are only supported over HTTPS connections.
+
+
+
+
Link previews
-
-
Warning :
-
Push notifications are not supported by your browser.
+
+
+
+ Auto-expand media
+
-
- {{/unless}}
+
+
+
+ Auto-expand websites
+
+
+
+
+
+
+
Push Notifications
+
+
+
Subscribe to push notifications
+
+ Warning :
+ Push notifications are only supported over HTTPS connections.
+
+
+ Warning :
+ Push notifications are not supported by your browser.
+
+
+
+
Browser Notifications
@@ -233,12 +236,13 @@
name="desktopNotifications">
Enable browser notifications
Warning :
Notifications are not supported by your browser.
Warning :
@@ -287,9 +291,9 @@
- {{#unless public}}
- {{#unless ldapEnabled}}
-
+
- {{> ../reveal-password}}
- {{> ../reveal-password}}
- {{> ../reveal-password}}
@@ -341,8 +342,7 @@
- {{/unless}}
- {{/unless}}
+
@@ -356,14 +356,16 @@
class="sr-only">Custom stylesheet. You can override any style with CSS here.
- {{#unless public}}
-
+
Sessions
Current session
@@ -372,7 +374,6 @@
Other sessions
- {{/unless}}
@@ -410,6 +411,10 @@ export default {
socket.emit("auth", values);
},
+ onForceSyncClick() {
+ const options = require("../../js/options");
+ options.syncAllSettings(true);
+ },
},
};
diff --git a/client/js/lounge.js b/client/js/lounge.js
index 7dee3a0c..ea7e58cf 100644
--- a/client/js/lounge.js
+++ b/client/js/lounge.js
@@ -141,22 +141,18 @@ window.vueMounted = () => {
if ($(window).outerWidth() <= utils.mobileViewportPixels) {
slideoutMenu.toggle(false);
}
+ } else {
+ vueApp.activeChannel = null;
+ vueApp.activeWindow = target;
}
- const lastActive = $("#windows > .active");
-
- lastActive.removeClass("active");
-
- const chan = $(target)
- .addClass("active")
- .trigger("show");
-
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") {
@@ -165,6 +161,7 @@ window.vueMounted = () => {
// See https://github.com/thelounge/thelounge/issues/2257
$("#input").trigger("ontouchstart" in window ? "blur" : "focus");
}
+*/
if (channel && channel.channel.usersOutdated) {
channel.channel.usersOutdated = false;
diff --git a/client/js/options.js b/client/js/options.js
index 140272b0..edd3df8b 100644
--- a/client/js/options.js
+++ b/client/js/options.js
@@ -6,7 +6,6 @@ const socket = require("./socket");
const {vueApp} = require("./vue");
require("../js/autocompletion");
-const $windows = $("#windows");
const $settings = $("#settings");
const $theme = $("#theme");
const $userStyles = $("#user-specified-css");
@@ -18,8 +17,6 @@ const noCSSparamReg = /[?&]nocss/;
let $syncWarningOverride;
let $syncWarningBase;
let $forceSyncButton;
-let $warningUnsupported;
-let $warningBlocked;
// Default settings
const settings = vueApp.settings;
@@ -85,10 +82,10 @@ module.exports = {
// When notifications are not supported, this is never called (because
// checkbox state can not be changed).
function updateDesktopNotificationStatus() {
- if (Notification.permission === "denied") {
- $warningBlocked.show();
+ if (Notification.permission === "granted") {
+ vueApp.desktopNotificationState = "granted";
} else {
- $warningBlocked.hide();
+ vueApp.desktopNotificationState = "blocked";
}
}
@@ -114,10 +111,10 @@ function applySetting(name, value) {
} else if (name === "userStyles" && !noCSSparamReg.test(window.location.search)) {
$userStyles.html(value);
} else if (name === "desktopNotifications") {
- if ("Notification" in window && value && Notification.permission !== "granted") {
+ updateDesktopNotificationStatus();
+
+ if (("Notification" in window) && value && Notification.permission !== "granted") {
Notification.requestPermission(updateDesktopNotificationStatus);
- } else if (!value) {
- $warningBlocked.hide();
}
} else if (name === "advanced") {
$("#settings [data-advanced]").toggle(settings[name]);
@@ -180,9 +177,7 @@ function syncAllSettings(force = false) {
// If `save` is set to true it will pass the setting to `updateSetting()` processSetting
function processSetting(name, value, save) {
- if (name === "userStyles") {
- $settings.find("#user-specified-css-input").val(value);
- } else if (name === "highlights") {
+ if (name === "highlights") {
$settings.find(`input[name=${name}]`).val(value);
} else if (name === "nickPostfix") {
$settings.find(`input[name=${name}]`).val(value);
@@ -205,14 +200,10 @@ function processSetting(name, value, save) {
}
function initialize() {
- $warningBlocked = $settings.find("#warnBlockedDesktopNotifications");
- $warningUnsupported = $settings.find("#warnUnsupportedDesktopNotifications");
-
$syncWarningOverride = $settings.find(".sync-warning-override");
$syncWarningBase = $settings.find(".sync-warning-base");
$forceSyncButton = $settings.find(".force-sync-button");
- $warningBlocked.hide();
module.exports.initialized = true;
// Settings have now entirely updated, apply settings to the client.
@@ -222,11 +213,10 @@ function initialize() {
// If browser does not support notifications
// display proper message in settings.
- if ("Notification" in window) {
- $warningUnsupported.hide();
- $windows.on("show", "#settings", updateDesktopNotificationStatus);
+ if (("Notification" in window)) {
+ updateDesktopNotificationStatus();
} else {
- $warningUnsupported.show();
+ vueApp.desktopNotificationState = "unsupported";
}
$settings.on("change", "input, select, textarea", function(e) {
@@ -249,10 +239,6 @@ function initialize() {
}
});
- $settings.find("#forceSync").on("click", () => {
- syncAllSettings(true);
- });
-
// Local init is done, let's sync
// We always ask for synced settings even if it is disabled.
// Settings can be mandatory to sync and it is used to determine sync base state.
diff --git a/client/js/socket-events/configuration.js b/client/js/socket-events/configuration.js
index 34f713eb..8fef5fcc 100644
--- a/client/js/socket-events/configuration.js
+++ b/client/js/socket-events/configuration.js
@@ -6,7 +6,6 @@ const templates = require("../../views");
const options = require("../options");
const webpush = require("../webpush");
const connect = $("#connect");
-const utils = require("../utils");
const upload = require("../upload");
const {vueApp} = require("../vue");
@@ -33,9 +32,7 @@ socket.on("configuration", function(data) {
return;
}
- $("#settings").html(templates.windows.settings(data));
- $("#help").html(templates.windows.help(data));
- $("#changelog").html(templates.windows.changelog());
+ vueApp.serverConfiguration = data;
$("#settings").on("show", () => {
$("#session-list").html("Loading…
");
diff --git a/client/js/vue.js b/client/js/vue.js
index 3171e328..5bede74f 100644
--- a/client/js/vue.js
+++ b/client/js/vue.js
@@ -25,6 +25,9 @@ const vueApp = new Vue({
isFileUploadEnabled: false,
isNotified: false,
networks: [],
+ pushNotificationState: "unsupported",
+ desktopNotificationState: "unsupported",
+ serverConfiguration: {},
settings: {
syncSettings: false,
advanced: false,
diff --git a/client/js/webpush.js b/client/js/webpush.js
index 24272e7f..4e757cf1 100644
--- a/client/js/webpush.js
+++ b/client/js/webpush.js
@@ -3,6 +3,7 @@
const $ = require("jquery");
const storage = require("./localStorage");
const socket = require("./socket");
+const {vueApp} = require("./vue");
let pushNotificationsButton;
let clientSubscribed = null;
@@ -43,11 +44,10 @@ module.exports.initialize = () => {
pushNotificationsButton = $("#pushNotifications");
if (!isAllowedServiceWorkersHost()) {
+ vueApp.pushNotificationState = "nohttps";
return;
}
- $("#pushNotificationsHttps").hide();
-
if ("serviceWorker" in navigator) {
navigator.serviceWorker
.register("service-worker.js")
@@ -59,9 +59,7 @@ module.exports.initialize = () => {
}
return registration.pushManager.getSubscription().then((subscription) => {
- $("#pushNotificationsUnsupported").hide();
-
- pushNotificationsButton.prop("disabled", false).on("click", onPushButton);
+ vueApp.pushNotificationState = "supported";
clientSubscribed = !!subscription;
@@ -69,10 +67,9 @@ module.exports.initialize = () => {
alternatePushButton();
}
});
- })
- .catch((err) => {
- $("#pushNotificationsUnsupported span").text(err);
- });
+ }).catch(() => {
+ vueApp.pushNotificationState = "unsupported";
+ });
}
};
@@ -126,11 +123,7 @@ function onPushButton() {
})
)
.catch((err) => {
- $("#pushNotificationsUnsupported")
- .find("span")
- .text(`An error has occurred: ${err}`)
- .end()
- .show();
+ vueApp.pushNotificationState = "unsupported";
});
return false;
From 70d9d8d2263929b24badefb65384f8bfd663cbe0 Mon Sep 17 00:00:00 2001
From: Pavel Djundik
Date: Wed, 20 Feb 2019 17:01:33 +0200
Subject: [PATCH 003/111] Move help window to Vue
---
client/components/App.vue | 2 +
client/components/Windows/Help.vue | 61 +++++++++++++++---------------
2 files changed, 32 insertions(+), 31 deletions(-)
diff --git a/client/components/App.vue b/client/components/App.vue
index 01e3f558..11d68b03 100644
--- a/client/components/App.vue
+++ b/client/components/App.vue
@@ -77,6 +77,7 @@ import NetworkList from "./NetworkList.vue";
import Chat from "./Chat.vue";
import SignIn from "./Windows/SignIn.vue";
import Settings from "./Windows/Settings.vue";
+import Help from "./Windows/Help.vue";
export default {
name: "App",
@@ -85,6 +86,7 @@ export default {
Chat,
SignIn,
Settings,
+ Help,
},
props: {
activeWindow: String,
diff --git a/client/components/Windows/Help.vue b/client/components/Windows/Help.vue
index a4dfb02f..51cbafc0 100644
--- a/client/components/Windows/Help.vue
+++ b/client/components/Windows/Help.vue
@@ -14,11 +14,11 @@
- v{{ version }}
+ v{{ $root.serverConfiguration.version }}
(release notes )
+ data-target="Changelog">release notes)
About The Lounge
@@ -26,33 +26,33 @@
@@ -528,7 +528,7 @@
Close the specified channel or private message window, or the
- current channel if channel
is ommitted.
+ current channel if channel
is omitted.
Aliases: /close
, /leave
@@ -644,7 +644,6 @@
-
From 3f7889e53443107bb9e086e99b0916a1bbe44f37 Mon Sep 17 00:00:00 2001
From: Pavel Djundik
Date: Wed, 20 Feb 2019 17:09:44 +0200
Subject: [PATCH 004/111] Move changelog window to Vue
---
client/components/App.vue | 2 ++
client/components/Windows/Changelog.vue | 37 ++++++++++++++-----------
client/js/socket-events/network.js | 1 -
3 files changed, 23 insertions(+), 17 deletions(-)
diff --git a/client/components/App.vue b/client/components/App.vue
index 11d68b03..d23fd99a 100644
--- a/client/components/App.vue
+++ b/client/components/App.vue
@@ -78,6 +78,7 @@ import Chat from "./Chat.vue";
import SignIn from "./Windows/SignIn.vue";
import Settings from "./Windows/Settings.vue";
import Help from "./Windows/Help.vue";
+import Changelog from "./Windows/Changelog.vue";
export default {
name: "App",
@@ -87,6 +88,7 @@ export default {
SignIn,
Settings,
Help,
+ Changelog,
},
props: {
activeWindow: String,
diff --git a/client/components/Windows/Changelog.vue b/client/components/Windows/Changelog.vue
index 448bb969..3c637bc0 100644
--- a/client/components/Windows/Changelog.vue
+++ b/client/components/Windows/Changelog.vue
@@ -12,24 +12,26 @@
« Help
+ data-target="Help">« Help
- {{#if version}}
- Release notes for {{ version }}
+
+ Release notes for {{ version }}
- {{#if changelog}}
- Introduction
- {{{changelog}}}
- {{else}}
- Unable to retrieve releases from GitHub.
- View release notes for this version on GitHub
- {{/if}}
- {{else}}
- Loading changelog…
- {{/if}}
+
+ Introduction
+ {{ changelog }}
+
+
+ Unable to retrieve releases from GitHub.
+
+ View release notes for this version on GitHub
+
+
+
+ Loading changelog…
@@ -38,5 +40,8 @@
diff --git a/client/js/socket-events/network.js b/client/js/socket-events/network.js
index 36d66e30..29aba1ad 100644
--- a/client/js/socket-events/network.js
+++ b/client/js/socket-events/network.js
@@ -4,7 +4,6 @@ const $ = require("jquery");
const socket = require("../socket");
const templates = require("../../views");
const sidebar = $("#sidebar");
-const utils = require("../utils");
const {vueApp, initChannel, findChannel} = require("../vue");
socket.on("network", function(data) {
From e71360ad39add8d06576bdbcf10a0c54c4dda1ae Mon Sep 17 00:00:00 2001
From: Richard Lewis
Date: Fri, 22 Feb 2019 16:21:32 +0200
Subject: [PATCH 005/111] Move sidebar to its own component.
---
client/components/App.vue | 67 ++++--------------------------
client/components/Sidebar.vue | 78 +++++++++++++++++++++++++++++++++++
2 files changed, 86 insertions(+), 59 deletions(-)
create mode 100644 client/components/Sidebar.vue
diff --git a/client/components/App.vue b/client/components/App.vue
index d23fd99a..c1d629a6 100644
--- a/client/components/App.vue
+++ b/client/components/App.vue
@@ -1,63 +1,10 @@
-
-
-
+
+
const throttle = require("lodash/throttle");
+import Sidebar from "./Sidebar.vue";
import NetworkList from "./NetworkList.vue";
import Chat from "./Chat.vue";
import SignIn from "./Windows/SignIn.vue";
@@ -83,6 +31,7 @@ import Changelog from "./Windows/Changelog.vue";
export default {
name: "App",
components: {
+ Sidebar,
NetworkList,
Chat,
SignIn,
diff --git a/client/components/Sidebar.vue b/client/components/Sidebar.vue
new file mode 100644
index 00000000..57629126
--- /dev/null
+++ b/client/components/Sidebar.vue
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
From 69cb891b1a1d58a81b183452164d9ec08a49342b Mon Sep 17 00:00:00 2001
From: Richard Lewis
Date: Tue, 26 Feb 2019 22:23:41 +0200
Subject: [PATCH 006/111] Add vuex and move isConnected to vuex state.
---
client/components/ChatInput.vue | 6 +++---
client/components/MessageList.vue | 4 ++--
client/js/socket-events/auth.js | 4 ++--
client/js/socket-events/init.js | 2 +-
client/js/socket.js | 2 +-
client/js/store.js | 15 +++++++++++++++
client/js/upload.js | 6 ++----
client/js/vue.js | 3 ++-
package.json | 1 +
yarn.lock | 5 +++++
10 files changed, 34 insertions(+), 14 deletions(-)
create mode 100644 client/js/store.js
diff --git a/client/components/ChatInput.vue b/client/components/ChatInput.vue
index d10f0305..609707f7 100644
--- a/client/components/ChatInput.vue
+++ b/client/components/ChatInput.vue
@@ -30,7 +30,7 @@
id="upload"
type="button"
aria-label="Upload file"
- :disabled="!this.$root.isConnected"
+ :disabled="!$store.state.isConnected"
/>
@@ -187,7 +187,7 @@ export default {
this.$refs.input.click();
this.$refs.input.focus();
- if (!this.$root.isConnected) {
+ if (!$store.state.isConnected) {
return false;
}
diff --git a/client/components/MessageList.vue b/client/components/MessageList.vue
index 710485bc..237de9be 100644
--- a/client/components/MessageList.vue
+++ b/client/components/MessageList.vue
@@ -3,7 +3,7 @@
@@ -228,7 +228,7 @@ export default {
});
},
onShowMoreClick() {
- if (!this.$root.isConnected) {
+ if (!this.$store.state.isConnected) {
return;
}
diff --git a/client/js/socket-events/auth.js b/client/js/socket-events/auth.js
index a415310c..526881b5 100644
--- a/client/js/socket-events/auth.js
+++ b/client/js/socket-events/auth.js
@@ -11,7 +11,7 @@ socket.on("auth", function(data) {
// And we will reload the page to grab the latest version
if (utils.serverHash > -1 && data.serverHash > -1 && data.serverHash !== utils.serverHash) {
socket.disconnect();
- vueApp.isConnected = false;
+ vueApp.$store.commit("isConnected", false);
vueApp.currentUserVisibleError = "Server restarted, reloading…";
location.reload(true);
return;
@@ -31,7 +31,7 @@ socket.on("auth", function(data) {
if (!data.success) {
if (vueApp.activeWindow !== "SignIn") {
socket.disconnect();
- vueApp.isConnected = false;
+ vueApp.$store.commit("isConnected", false);
vueApp.currentUserVisibleError = "Authentication failed, reloading…";
location.reload();
return;
diff --git a/client/js/socket-events/init.js b/client/js/socket-events/init.js
index 6c4b3ae2..ac90434f 100644
--- a/client/js/socket-events/init.js
+++ b/client/js/socket-events/init.js
@@ -17,7 +17,7 @@ socket.on("init", function(data) {
const previousActive = vueApp.activeChannel && vueApp.activeChannel.channel.id;
vueApp.networks = mergeNetworkData(data.networks);
- vueApp.isConnected = true;
+ vueApp.$store.commit("isConnected", true);
vueApp.currentUserVisibleError = null;
if (!vueApp.initialized) {
diff --git a/client/js/socket.js b/client/js/socket.js
index 7ccca5ee..5023d24d 100644
--- a/client/js/socket.js
+++ b/client/js/socket.js
@@ -47,7 +47,7 @@ socket.on("authorized", function() {
function handleDisconnect(data) {
const message = data.message || data;
- vueApp.isConnected = false;
+ vueApp.$store.commit("isConnected", false);
vueApp.currentUserVisibleError = `Waiting to reconnect… (${message})`;
$("#loading-page-message").text(vueApp.currentUserVisibleError);
diff --git a/client/js/store.js b/client/js/store.js
new file mode 100644
index 00000000..3feed9ea
--- /dev/null
+++ b/client/js/store.js
@@ -0,0 +1,15 @@
+import Vue from "vue";
+import Vuex from "vuex";
+
+Vue.use(Vuex);
+
+export default new Vuex.Store({
+ state: {
+ isConnected: false
+ },
+ mutations: {
+ isConnected(state, payload) {
+ state.isConnected = payload;
+ },
+ }
+});
diff --git a/client/js/upload.js b/client/js/upload.js
index 9633db9d..e0e2ca52 100644
--- a/client/js/upload.js
+++ b/client/js/upload.js
@@ -89,7 +89,7 @@ class Uploader {
return;
}
- if (!this.vueApp.isConnected) {
+ if (!this.vueApp.$store.state.isConnected) {
this.handleResponse({
error: `You are currently disconnected, unable to initiate upload process.`,
});
@@ -177,9 +177,7 @@ class Uploader {
this.setProgress(0);
if (response.error) {
- // require here due to circular dependency
- const {vueApp} = require("./vue");
- vueApp.currentUserVisibleError = response.error;
+ this.vueApp.currentUserVisibleError = response.error;
return;
}
diff --git a/client/js/vue.js b/client/js/vue.js
index 5bede74f..0ac27ae9 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");
const localetime = require("./libs/handlebars/localetime");
@@ -21,7 +22,6 @@ const vueApp = new Vue({
currentUserVisibleError: null,
initialized: false,
isAutoCompleting: false,
- isConnected: false,
isFileUploadEnabled: false,
isNotified: false,
networks: [],
@@ -56,6 +56,7 @@ const vueApp = new Vue({
props: this,
});
},
+ store,
});
Vue.config.errorHandler = function(e) {
diff --git a/package.json b/package.json
index a549150e..10d64b97 100644
--- a/package.json
+++ b/package.json
@@ -121,6 +121,7 @@
"vue-server-renderer": "2.6.10",
"vue-template-compiler": "2.6.10",
"vuedraggable": "2.23.2",
+ "vuex": "3.1.2",
"webpack": "4.41.2",
"webpack-cli": "3.3.10",
"webpack-dev-middleware": "3.7.2",
diff --git a/yarn.lock b/yarn.lock
index ecf777b6..39ca86cb 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -9234,6 +9234,11 @@ vuedraggable@2.23.2:
dependencies:
sortablejs "^1.10.1"
+vuex@3.1.2:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.1.2.tgz#a2863f4005aa73f2587e55c3fadf3f01f69c7d4d"
+ integrity sha512-ha3jNLJqNhhrAemDXcmMJMKf1Zu4sybMPr9KxJIuOpVcsDQlTBYLLladav2U+g1AvdYDG5Gs0xBTb0M5pXXYFQ==
+
watchpack@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00"
From 0da059118d70ed96d28060b2f119d48b42f1b39f Mon Sep 17 00:00:00 2001
From: Richard Lewis
Date: Wed, 27 Feb 2019 16:15:34 +0200
Subject: [PATCH 007/111] Move isNotified to vuex.
---
client/components/App.vue | 1 +
client/components/ChatInput.vue | 2 +-
client/js/store.js | 20 ++++++++++++--------
client/js/utils.js | 10 ++++------
client/js/vue.js | 1 -
5 files changed, 18 insertions(+), 16 deletions(-)
diff --git a/client/components/App.vue b/client/components/App.vue
index c1d629a6..12637d20 100644
--- a/client/components/App.vue
+++ b/client/components/App.vue
@@ -1,6 +1,7 @@
Date: Wed, 27 Feb 2019 20:15:58 +0200
Subject: [PATCH 008/111] Move activeWindow to vuex.
---
client/components/App.vue | 2 +-
client/js/lounge.js | 4 ++--
client/js/socket-events/auth.js | 4 ++--
client/js/store.js | 4 ++++
client/js/vue.js | 1 -
5 files changed, 9 insertions(+), 6 deletions(-)
diff --git a/client/components/App.vue b/client/components/App.vue
index 12637d20..b45b5b81 100644
--- a/client/components/App.vue
+++ b/client/components/App.vue
@@ -12,7 +12,7 @@
:network="activeChannel.network"
:channel="activeChannel.channel" />
diff --git a/client/js/lounge.js b/client/js/lounge.js
index ea7e58cf..a55b98ab 100644
--- a/client/js/lounge.js
+++ b/client/js/lounge.js
@@ -128,7 +128,7 @@ window.vueMounted = () => {
}
if (channel) {
- vueApp.activeWindow = null;
+ vueApp.$store.commit("activeWindow", null);
vueApp.activeChannel = channel;
if (channel) {
@@ -143,7 +143,7 @@ window.vueMounted = () => {
}
} else {
vueApp.activeChannel = null;
- vueApp.activeWindow = target;
+ vueApp.$store.commit("activeWindow", target);
}
utils.synchronizeNotifiedState();
diff --git a/client/js/socket-events/auth.js b/client/js/socket-events/auth.js
index 526881b5..b01cf6e3 100644
--- a/client/js/socket-events/auth.js
+++ b/client/js/socket-events/auth.js
@@ -20,7 +20,7 @@ socket.on("auth", function(data) {
if (data.serverHash > -1) {
utils.serverHash = data.serverHash;
- vueApp.activeWindow = "SignIn";
+ vueApp.$store.commit("activeWindow", "SignIn");
} else {
getActiveWindowComponent().inFlight = false;
}
@@ -29,7 +29,7 @@ socket.on("auth", function(data) {
const user = storage.get("user");
if (!data.success) {
- if (vueApp.activeWindow !== "SignIn") {
+ if (vueApp.$store.state.activeWindow !== "SignIn") {
socket.disconnect();
vueApp.$store.commit("isConnected", false);
vueApp.currentUserVisibleError = "Authentication failed, reloading…";
diff --git a/client/js/store.js b/client/js/store.js
index 407282fe..5003024a 100644
--- a/client/js/store.js
+++ b/client/js/store.js
@@ -7,6 +7,7 @@ export default new Vuex.Store({
state: {
isConnected: false,
isNotified: false,
+ activeWindow: null,
},
mutations: {
isConnected(state, payload) {
@@ -15,5 +16,8 @@ export default new Vuex.Store({
isNotified(state, payload) {
state.isNotified = payload;
},
+ activeWindow(state, payload) {
+ state.activeWindow = payload;
+ },
}
});
diff --git a/client/js/vue.js b/client/js/vue.js
index 43422e84..2bec7e5e 100644
--- a/client/js/vue.js
+++ b/client/js/vue.js
@@ -16,7 +16,6 @@ Vue.filter("roundBadgeNumber", roundBadgeNumber);
const vueApp = new Vue({
el: "#viewport",
data: {
- activeWindow: null,
activeChannel: null,
appName: document.title,
currentUserVisibleError: null,
From 5a3ad194e8a7cb16ad4724818c0e6d43e6bb6cb6 Mon Sep 17 00:00:00 2001
From: Richard Lewis
Date: Fri, 1 Mar 2019 15:29:00 +0200
Subject: [PATCH 009/111] Move connect and network edit views to vue.
---
client/components/App.vue | 4 +
client/components/NetworkForm.vue | 241 +++++++++++++++++++++
client/components/Windows/Connect.vue | 31 +++
client/components/Windows/NetworkEdit.vue | 244 ++--------------------
client/components/Windows/Settings.vue | 1 +
client/js/contextMenuFactory.js | 1 -
client/js/socket-events/network.js | 20 +-
client/js/store.js | 5 +-
client/js/utils.js | 1 -
9 files changed, 305 insertions(+), 243 deletions(-)
create mode 100644 client/components/NetworkForm.vue
create mode 100644 client/components/Windows/Connect.vue
diff --git a/client/components/App.vue b/client/components/App.vue
index b45b5b81..51176513 100644
--- a/client/components/App.vue
+++ b/client/components/App.vue
@@ -26,6 +26,8 @@ import NetworkList from "./NetworkList.vue";
import Chat from "./Chat.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";
@@ -37,6 +39,8 @@ export default {
Chat,
SignIn,
Settings,
+ NetworkEdit,
+ Connect,
Help,
Changelog,
},
diff --git a/client/components/NetworkForm.vue b/client/components/NetworkForm.vue
new file mode 100644
index 00000000..8bb41ddd
--- /dev/null
+++ b/client/components/NetworkForm.vue
@@ -0,0 +1,241 @@
+
+
+
+
+
+
+
+
+
+ Edit {{ defaults.name }}
+
+
+ The Lounge -
+ Connect
+
+
+ to {{ defaults.name }}
+
+
+
+
+
+
+
+
+
Network settings
+
+
+ Name
+
+
+
+
+
+ Server
+
+
+
+
+
+
+
+
+
+ Use secure connection (TLS)
+
+
+
+
+
+ Only allow trusted certificates
+
+
+
+
+
+
+
User preferences
+
+
+ Nick
+
+
+
+
+
+
+ Username
+
+
+
+
+
+
+ Password
+
+
+
+
+
+
+
+ Real name
+
+
+
+
+
+
+ Commands
+
+
+
+
+
+ Save
+
+
+
+
+ Channels
+
+
+
+
+
+ Connect
+
+
+
+
+
+
+
+
+
diff --git a/client/components/Windows/Connect.vue b/client/components/Windows/Connect.vue
new file mode 100644
index 00000000..d9f380ff
--- /dev/null
+++ b/client/components/Windows/Connect.vue
@@ -0,0 +1,31 @@
+
+
+
+
+
diff --git a/client/components/Windows/NetworkEdit.vue b/client/components/Windows/NetworkEdit.vue
index e6cf4a37..05a41fd5 100644
--- a/client/components/Windows/NetworkEdit.vue
+++ b/client/components/Windows/NetworkEdit.vue
@@ -1,242 +1,40 @@
-
-
-
-
-
-
- {{#if defaults.uuid}}
-
-
- Edit {{ defaults.name }}
- {{else}}
- {{#if public}}The Lounge - {{/if}}
- Connect
- {{#unless displayNetwork}}
- {{#if lockNetwork}}
- to {{ defaults.name }}
- {{/if}}
- {{/unless}}
- {{/if}}
-
-
- {{#if displayNetwork}}
-
-
-
Network settings
-
-
- Name
-
-
-
-
-
- Server
-
-
-
-
-
-
-
-
-
- Use secure connection (TLS)
-
-
-
-
-
- Only allow trusted certificates
-
-
-
-
- {{/if}}
-
-
User preferences
-
-
- Nick
-
-
-
-
- {{#unless useHexIp}}
-
- Username
-
-
-
-
- {{/unless}}
-
- Password
-
-
-
- {{> ../reveal-password}}
-
-
- Real name
-
-
-
-
- {{#if defaults.uuid}}
-
- Commands
-
-
- {{~#each defaults.commands~}}{{ ~this }}
- {{/each~}}
-
-
- Save
-
- {{else}}
-
- Channels
-
-
-
-
-
- Connect
-
- {{/if}}
-
-
-
-
+
diff --git a/client/js/options.js b/client/js/options.js
index edd3df8b..8bfe5408 100644
--- a/client/js/options.js
+++ b/client/js/options.js
@@ -116,8 +116,6 @@ function applySetting(name, value) {
if (("Notification" in window) && value && Notification.permission !== "granted") {
Notification.requestPermission(updateDesktopNotificationStatus);
}
- } else if (name === "advanced") {
- $("#settings [data-advanced]").toggle(settings[name]);
}
}
@@ -219,6 +217,7 @@ function initialize() {
vueApp.desktopNotificationState = "unsupported";
}
+ /*
$settings.on("change", "input, select, textarea", function(e) {
// We only want to trigger on human triggered changes.
if (e.originalEvent) {
@@ -238,27 +237,10 @@ function initialize() {
}
}
});
+ */
// Local init is done, let's sync
// We always ask for synced settings even if it is disabled.
// Settings can be mandatory to sync and it is used to determine sync base state.
socket.emit("setting:get");
-
- // Protocol handler
- const defaultClientButton = $("#make-default-client");
-
- if (window.navigator.registerProtocolHandler) {
- defaultClientButton.on("click", function() {
- const uri = document.location.origin + document.location.pathname + "?uri=%s";
-
- window.navigator.registerProtocolHandler("irc", uri, "The Lounge");
- window.navigator.registerProtocolHandler("ircs", uri, "The Lounge");
-
- return false;
- });
-
- $("#native-app").prop("hidden", false);
- } else {
- defaultClientButton.hide();
- }
}
diff --git a/client/js/socket-events/change_password.js b/client/js/socket-events/change_password.js
deleted file mode 100644
index 09bb144e..00000000
--- a/client/js/socket-events/change_password.js
+++ /dev/null
@@ -1,31 +0,0 @@
-"use strict";
-
-const $ = require("jquery");
-const socket = require("../socket");
-
-socket.on("change-password", function(data) {
- const passwordForm = $("#change-password");
-
- if (data.error || data.success) {
- const message = data.success ? data.success : data.error;
- const feedback = passwordForm.find(".feedback");
-
- if (data.success) {
- feedback.addClass("success").removeClass("error");
- } else {
- feedback.addClass("error").removeClass("success");
- }
-
- feedback.text(message).show();
- feedback.closest("form").one("submit", function() {
- feedback.hide();
- });
- }
-
- passwordForm
- .find("input")
- .val("")
- .end()
- .find(".btn")
- .prop("disabled", false);
-});
diff --git a/client/js/socket-events/configuration.js b/client/js/socket-events/configuration.js
index 8fef5fcc..5fba3be6 100644
--- a/client/js/socket-events/configuration.js
+++ b/client/js/socket-events/configuration.js
@@ -39,12 +39,6 @@ socket.on("configuration", function(data) {
socket.emit("sessions:get");
});
- $("#play").on("click", () => {
- const pop = new Audio();
- pop.src = "audio/pop.wav";
- pop.play();
- });
-
if (data.fileUpload) {
upload.initialize(data.fileUploadMaxFileSize);
}
@@ -62,6 +56,7 @@ socket.on("configuration", function(data) {
document.querySelector('meta[name="theme-color"]').content = currentTheme.themeColor;
}
+ /*
function handleFormSubmit() {
const form = $(this);
const event = form.data("event");
@@ -79,10 +74,9 @@ socket.on("configuration", function(data) {
return false;
}
+ */
- $("#change-password form").on("submit", handleFormSubmit);
- connect.on("submit", "form", handleFormSubmit);
-
+ // TODO: move to component (this mirrors the nick to the username field if the username is empty)
connect.on("show", function() {
connect
.html(templates.windows.connect(data))
diff --git a/src/server.js b/src/server.js
index bf7191ea..6e49bc1a 100644
--- a/src/server.js
+++ b/src/server.js
@@ -423,16 +423,9 @@ function initializeClient(socket, client, token, lastMessage, openChannel) {
const p1 = data.new_password;
const p2 = data.verify_password;
- if (typeof p1 === "undefined" || p1 === "") {
+ if (typeof p1 === "undefined" || p1 === "" || p1 !== p2) {
socket.emit("change-password", {
- error: "Please enter a new password",
- });
- return;
- }
-
- if (p1 !== p2) {
- socket.emit("change-password", {
- error: "Both new password fields must match",
+ error: "",
});
return;
}
@@ -442,8 +435,7 @@ function initializeClient(socket, client, token, lastMessage, openChannel) {
.then((matching) => {
if (!matching) {
socket.emit("change-password", {
- error:
- "The current password field does not match your account password",
+ error: "password_incorrect",
});
return;
}
@@ -454,9 +446,9 @@ function initializeClient(socket, client, token, lastMessage, openChannel) {
const obj = {};
if (success) {
- obj.success = "Successfully updated your password";
+ obj.success = true;
} else {
- obj.error = "Failed to update your password";
+ obj.error = "update_failed";
}
socket.emit("change-password", obj);
From 2ef3e3e5b42610483a19b6b229e6f5f5fd30408c Mon Sep 17 00:00:00 2001
From: Richard Lewis
Date: Sun, 3 Mar 2019 17:27:57 +0200
Subject: [PATCH 012/111] Add success: false to change-password error emits.
---
src/server.js | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/server.js b/src/server.js
index 6e49bc1a..70566c71 100644
--- a/src/server.js
+++ b/src/server.js
@@ -426,6 +426,7 @@ function initializeClient(socket, client, token, lastMessage, openChannel) {
if (typeof p1 === "undefined" || p1 === "" || p1 !== p2) {
socket.emit("change-password", {
error: "",
+ success: false,
});
return;
}
@@ -436,6 +437,7 @@ function initializeClient(socket, client, token, lastMessage, openChannel) {
if (!matching) {
socket.emit("change-password", {
error: "password_incorrect",
+ success: false,
});
return;
}
@@ -443,7 +445,7 @@ function initializeClient(socket, client, token, lastMessage, openChannel) {
const hash = Helper.password.hash(p1);
client.setPassword(hash, (success) => {
- const obj = {};
+ const obj = {success: false};
if (success) {
obj.success = true;
From 111beb5f1224c9dec1d9549490abed8dcee4feb5 Mon Sep 17 00:00:00 2001
From: Richard Lewis
Date: Sun, 3 Mar 2019 18:28:57 +0200
Subject: [PATCH 013/111] Run updateSetting from Settings component and get rid
of unused code.
---
client/components/App.vue | 1 +
client/components/Windows/Settings.vue | 10 +--
client/js/options.js | 87 ++++--------------------
client/js/socket-events/configuration.js | 20 ------
4 files changed, 18 insertions(+), 100 deletions(-)
diff --git a/client/components/App.vue b/client/components/App.vue
index 6b531255..ca5d990a 100644
--- a/client/components/App.vue
+++ b/client/components/App.vue
@@ -1,4 +1,5 @@
+
{{ theme.displayName }}
@@ -495,6 +496,7 @@ export default {
},
data() {
return {
+ options: null,
canRegisterProtocol: false,
passwordChangeStatus: null,
passwordErrors: {
@@ -506,6 +508,8 @@ export default {
};
},
mounted() {
+ this.options = require("../../js/options"); // TODO: do this in a smarter way
+
// Enable protocol handler registration if supported
if (window.navigator.registerProtocolHandler) {
this.canRegisterProtocol = true;
@@ -533,11 +537,7 @@ export default {
value = event.target.value;
}
- this.storeSetting(name, value);
- },
- storeSetting(name, value) {
- // TODO: port logic from options.js
- socket.emit("setting:set", {name, value});
+ this.options.updateSetting(name, value);
},
changePassword() {
const allFields = new FormData(this.$refs.settingsForm);
diff --git a/client/js/options.js b/client/js/options.js
index 8bfe5408..360a6fb7 100644
--- a/client/js/options.js
+++ b/client/js/options.js
@@ -6,18 +6,11 @@ const socket = require("./socket");
const {vueApp} = require("./vue");
require("../js/autocompletion");
-const $settings = $("#settings");
const $theme = $("#theme");
const $userStyles = $("#user-specified-css");
const noCSSparamReg = /[?&]nocss/;
-// Not yet available at this point but used in various functionaly.
-// Will be assigned when `initialize` is called.
-let $syncWarningOverride;
-let $syncWarningBase;
-let $forceSyncButton;
-
// Default settings
const settings = vueApp.settings;
@@ -76,6 +69,7 @@ module.exports = {
syncAllSettings,
processSetting,
initialize,
+ updateSetting,
};
// Updates the checkbox and warning in settings.
@@ -125,30 +119,19 @@ function settingSetEmit(name, value) {
// When sync is `true` the setting will also be send to the backend for syncing.
function updateSetting(name, value, sync) {
- const currentOption = settings[name];
+ settings[name] = value;
+ storage.set("settings", JSON.stringify(settings));
+ applySetting(name, value);
- // Only update and process when the setting is actually changed.
- if (currentOption !== value) {
- settings[name] = value;
- storage.set("settings", JSON.stringify(settings));
- applySetting(name, value);
+ // Sync is checked, request settings from server.
+ if (name === "syncSettings" && value) {
+ socket.emit("setting:get");
+ }
- // Sync is checked, request settings from server.
- if (name === "syncSettings" && value) {
- socket.emit("setting:get");
- $syncWarningOverride.hide();
- $syncWarningBase.hide();
- $forceSyncButton.hide();
- } else if (name === "syncSettings") {
- $syncWarningOverride.show();
- $forceSyncButton.show();
- }
-
- if (settings.syncSettings && !noSync.includes(name) && sync) {
- settingSetEmit(name, value);
- } else if (alwaysSync.includes(name) && sync) {
- settingSetEmit(name, value);
- }
+ if (settings.syncSettings && !noSync.includes(name) && sync) {
+ settingSetEmit(name, value);
+ } else if (alwaysSync.includes(name) && sync) {
+ settingSetEmit(name, value);
}
}
@@ -162,31 +145,11 @@ function syncAllSettings(force = false) {
settingSetEmit(name, settings[name]);
}
}
-
- $syncWarningOverride.hide();
- $syncWarningBase.hide();
- $forceSyncButton.hide();
- } else {
- $syncWarningOverride.hide();
- $forceSyncButton.hide();
- $syncWarningBase.show();
}
}
// If `save` is set to true it will pass the setting to `updateSetting()` processSetting
function processSetting(name, value, save) {
- if (name === "highlights") {
- $settings.find(`input[name=${name}]`).val(value);
- } else if (name === "nickPostfix") {
- $settings.find(`input[name=${name}]`).val(value);
- } else if (name === "statusMessages") {
- $settings.find(`input[name=${name}][value=${value}]`).prop("checked", true);
- } else if (name === "theme") {
- $settings.find("#theme-select").val(value);
- } else if (typeof value === "boolean") {
- $settings.find(`input[name=${name}]`).prop("checked", value);
- }
-
// No need to also call processSetting when `save` is true.
// updateSetting does take care of that.
if (save) {
@@ -198,10 +161,6 @@ function processSetting(name, value, save) {
}
function initialize() {
- $syncWarningOverride = $settings.find(".sync-warning-override");
- $syncWarningBase = $settings.find(".sync-warning-base");
- $forceSyncButton = $settings.find(".force-sync-button");
-
module.exports.initialized = true;
// Settings have now entirely updated, apply settings to the client.
@@ -217,28 +176,6 @@ function initialize() {
vueApp.desktopNotificationState = "unsupported";
}
- /*
- $settings.on("change", "input, select, textarea", function(e) {
- // We only want to trigger on human triggered changes.
- if (e.originalEvent) {
- const $self = $(this);
- const type = $self.prop("type");
- const name = $self.prop("name");
-
- if (type === "radio") {
- if ($self.prop("checked")) {
- updateSetting(name, $self.val(), true);
- }
- } else if (type === "checkbox") {
- updateSetting(name, $self.prop("checked"), true);
- settings[name] = $self.prop("checked");
- } else if (type !== "password") {
- updateSetting(name, $self.val(), true);
- }
- }
- });
- */
-
// Local init is done, let's sync
// We always ask for synced settings even if it is disabled.
// Settings can be mandatory to sync and it is used to determine sync base state.
diff --git a/client/js/socket-events/configuration.js b/client/js/socket-events/configuration.js
index 5fba3be6..09a505d9 100644
--- a/client/js/socket-events/configuration.js
+++ b/client/js/socket-events/configuration.js
@@ -56,26 +56,6 @@ socket.on("configuration", function(data) {
document.querySelector('meta[name="theme-color"]').content = currentTheme.themeColor;
}
- /*
- function handleFormSubmit() {
- const form = $(this);
- const event = form.data("event");
-
- form.find(".btn").prop("disabled", true);
-
- const values = {};
- $.each(form.serializeArray(), function(i, obj) {
- if (obj.value !== "") {
- values[obj.name] = obj.value;
- }
- });
-
- socket.emit(event, values);
-
- return false;
- }
- */
-
// TODO: move to component (this mirrors the nick to the username field if the username is empty)
connect.on("show", function() {
connect
From 5b17a2fbe4a0024e0af74e60a1cfe2112268e1f9 Mon Sep 17 00:00:00 2001
From: Richard Lewis
Date: Sun, 3 Mar 2019 19:47:49 +0200
Subject: [PATCH 014/111] Port session list to vue.
---
client/components/Session.vue | 55 ++++++++++++++++++++++++
client/components/Windows/Settings.vue | 26 +++++++++--
client/css/style.css | 1 +
client/js/socket-events/configuration.js | 5 ---
client/js/socket-events/sessions_list.js | 33 +-------------
client/js/store.js | 8 ++++
client/views/session.tpl | 25 -----------
7 files changed, 89 insertions(+), 64 deletions(-)
create mode 100644 client/components/Session.vue
delete mode 100644 client/views/session.tpl
diff --git a/client/components/Session.vue b/client/components/Session.vue
new file mode 100644
index 00000000..6af7d64f
--- /dev/null
+++ b/client/components/Session.vue
@@ -0,0 +1,55 @@
+
+
+
+
+ Sign out
+
+
+ Revoke
+
+
+
+ {{ session.agent }}
+
+ {{ session.ip }}
+
+
+
+
+ Currently active
+
+
+ Last used on {{ session.lastUse | localetime }}
+
+
+
+
+
+
diff --git a/client/components/Windows/Settings.vue b/client/components/Windows/Settings.vue
index ad4ba9b3..2c754340 100644
--- a/client/components/Windows/Settings.vue
+++ b/client/components/Windows/Settings.vue
@@ -471,13 +471,29 @@
class="session-list"
>
Sessions
-
Current session
-
+
+
+
Other sessions
-
+
+
Loading…
+
+ You are not currently logged in to any other device. '
+
+
+
+
+
@@ -488,11 +504,13 @@
// const storage = require("../../js/localStorage"); // TODO: use this
import socket from "../../js/socket";
import RevealPassword from "../RevealPassword.vue";
+import Session from "../Session.vue";
export default {
name: "Settings",
components: {
RevealPassword,
+ Session,
},
data() {
return {
@@ -510,6 +528,8 @@ export default {
mounted() {
this.options = require("../../js/options"); // TODO: do this in a smarter way
+ socket.emit("sessions:get");
+
// Enable protocol handler registration if supported
if (window.navigator.registerProtocolHandler) {
this.canRegisterProtocol = true;
diff --git a/client/css/style.css b/client/css/style.css
index 2c0e2bc7..6d8d6b2f 100644
--- a/client/css/style.css
+++ b/client/css/style.css
@@ -559,6 +559,7 @@ kbd {
display: none;
flex-direction: column;
width: 220px;
+ max-height: 100%;
will-change: transform;
}
diff --git a/client/js/socket-events/configuration.js b/client/js/socket-events/configuration.js
index 09a505d9..50f85c34 100644
--- a/client/js/socket-events/configuration.js
+++ b/client/js/socket-events/configuration.js
@@ -34,11 +34,6 @@ socket.on("configuration", function(data) {
vueApp.serverConfiguration = data;
- $("#settings").on("show", () => {
- $("#session-list").html("Loading…
");
- socket.emit("sessions:get");
- });
-
if (data.fileUpload) {
upload.initialize(data.fileUploadMaxFileSize);
}
diff --git a/client/js/socket-events/sessions_list.js b/client/js/socket-events/sessions_list.js
index 91e2a57a..dd47ee14 100644
--- a/client/js/socket-events/sessions_list.js
+++ b/client/js/socket-events/sessions_list.js
@@ -1,37 +1,8 @@
"use strict";
-
-const $ = require("jquery");
-const Auth = require("../auth");
const socket = require("../socket");
-const templates = require("../../views");
+const {vueApp} = require("../vue");
socket.on("sessions:list", function(data) {
data.sort((a, b) => b.lastUse - a.lastUse);
-
- let html = "";
- data.forEach((connection) => {
- if (connection.current) {
- $("#session-current").html(templates.session(connection));
- return;
- }
-
- html += templates.session(connection);
- });
-
- if (html.length === 0) {
- html = "You are not currently logged in to any other device.
";
- }
-
- $("#session-list").html(html);
-});
-
-$("#settings").on("click", ".remove-session", function() {
- const token = $(this).data("token");
-
- if (token) {
- socket.emit("sign-out", token);
- } else {
- socket.emit("sign-out");
- Auth.signout();
- }
+ vueApp.$store.commit("sessions", data);
});
diff --git a/client/js/store.js b/client/js/store.js
index b4015184..bab7f57d 100644
--- a/client/js/store.js
+++ b/client/js/store.js
@@ -8,6 +8,7 @@ export default new Vuex.Store({
isConnected: false,
isNotified: false,
activeWindow: null,
+ sessions: [],
},
mutations: {
isConnected(state, payload) {
@@ -22,5 +23,12 @@ export default new Vuex.Store({
currentNetworkConfig(state, payload) {
state.currentNetworkConfig = payload;
},
+ sessions(state, payload) {
+ state.sessions = payload;
+ },
+ },
+ getters: {
+ currentSession: (state) => state.sessions.find((item) => item.current),
+ otherSessions: (state) => state.sessions.filter((item) => !item.current),
},
});
diff --git a/client/views/session.tpl b/client/views/session.tpl
deleted file mode 100644
index 7951bfc4..00000000
--- a/client/views/session.tpl
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
- {{#if current}}
- Sign out
- {{else}}
- Revoke
- {{/if}}
-
-
- {{agent}}
-
- {{ip}}
-
-{{#unless current}}
-
- {{#if active}}
- Currently active
- {{else}}
- Last used on {{localetime lastUse}}
- {{/if}}
-{{/unless}}
-
From e73bf1e9a7520fed28ccca3e00e361407b44fb45 Mon Sep 17 00:00:00 2001
From: Richard Lewis
Date: Sun, 3 Mar 2019 21:43:57 +0200
Subject: [PATCH 015/111] Move closeChan functionality to vue.
---
client/components/Channel.vue | 34 ++++++++++++++++++++++++----
client/components/ChannelWrapper.vue | 26 +++++++++++++++++++++
client/js/contextMenuFactory.js | 11 ++++++++-
client/js/lounge.js | 6 +----
client/js/utils.js | 26 ---------------------
5 files changed, 66 insertions(+), 37 deletions(-)
diff --git a/client/components/Channel.vue b/client/components/Channel.vue
index e48df51a..815ab750 100644
--- a/client/components/Channel.vue
+++ b/client/components/Channel.vue
@@ -1,5 +1,10 @@
-
+
{{ channel.name }}
{{
channel.unread | roundBadgeNumber
@@ -12,13 +17,27 @@
>
-
-
+
+
-
-
+
+
@@ -37,5 +56,10 @@ export default {
network: Object,
channel: Object,
},
+ methods: {
+ close() {
+ this.$refs.wrapper.close();
+ },
+ },
};
diff --git a/client/components/ChannelWrapper.vue b/client/components/ChannelWrapper.vue
index 6e010735..3638da00 100644
--- a/client/components/ChannelWrapper.vue
+++ b/client/components/ChannelWrapper.vue
@@ -19,6 +19,7 @@
:data-name="channel.name"
:aria-controls="'#chan-' + channel.id"
:aria-selected="activeChannel && channel === activeChannel.channel"
+ :style="closed ? {transition: 'none', opacity: 0.4} : null"
role="tab"
>
@@ -26,6 +27,8 @@
diff --git a/client/components/Windows/Changelog.vue b/client/components/Windows/Changelog.vue
index 449b62e1..04d705ff 100644
--- a/client/components/Windows/Changelog.vue
+++ b/client/components/Windows/Changelog.vue
@@ -5,10 +5,7 @@
aria-label="Changelog"
>
diff --git a/client/components/Windows/Settings.vue b/client/components/Windows/Settings.vue
index 2c754340..b8a0283b 100644
--- a/client/components/Windows/Settings.vue
+++ b/client/components/Windows/Settings.vue
@@ -6,10 +6,7 @@
aria-label="Settings"
>
{
require("./socket-events");
- const slideoutMenu = require("./slideout");
const contextMenuFactory = require("./contextMenuFactory");
- const storage = require("./localStorage");
const utils = require("./utils");
require("./webpush");
require("./keybinds");
@@ -20,40 +18,6 @@ window.vueMounted = () => {
const sidebar = $("#sidebar, #footer");
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) {
return contextMenuFactory.createContextMenu($(this), e).show();
});
@@ -138,8 +102,8 @@ window.vueMounted = () => {
socket.emit("open", channel ? channel.channel.id : null);
- if ($(window).outerWidth() <= utils.mobileViewportPixels) {
- slideoutMenu.toggle(false);
+ if (!keepSidebarOpen && $(window).outerWidth() <= utils.mobileViewportPixels) {
+ vueApp.setSidebar(false);
}
} else {
vueApp.activeChannel = null;
diff --git a/client/js/slideout.js b/client/js/slideout.js
index f285ac3b..ecab355b 100644
--- a/client/js/slideout.js
+++ b/client/js/slideout.js
@@ -1,112 +1,112 @@
"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 {
- static enable() {
- document.body.addEventListener("touchstart", onTouchStart, {passive: true});
+ enable() {
+ 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) {
- menuIsOpen = state;
- viewport.classList.toggle("menu-open", state);
+ toggle(state) {
+ this.menuIsOpen = state;
+ this.viewport.classList.toggle("menu-open", state);
}
- static isOpen() {
- return menuIsOpen;
+ isOpen() {
+ return this.menuIsOpen;
}
}
-function onTouchStart(e) {
- 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;
+module.exports = (new SlideoutMenu);
diff --git a/client/js/socket-events/init.js b/client/js/socket-events/init.js
index ac90434f..40d1de23 100644
--- a/client/js/socket-events/init.js
+++ b/client/js/socket-events/init.js
@@ -4,7 +4,6 @@ const $ = require("jquery");
const escape = require("css.escape");
const socket = require("../socket");
const webpush = require("../webpush");
-const slideoutMenu = require("../slideout");
const sidebar = $("#sidebar");
const storage = require("../localStorage");
const utils = require("../utils");
@@ -21,7 +20,7 @@ socket.on("init", function(data) {
vueApp.currentUserVisibleError = null;
if (!vueApp.initialized) {
- vueApp.initialized = true;
+ vueApp.onSocketInit();
if (data.token) {
storage.set("token", data.token);
@@ -29,14 +28,11 @@ socket.on("init", function(data) {
webpush.configurePushNotifications(data.pushSubscription, data.applicationServerKey);
- slideoutMenu.enable();
-
- const viewport = $("#viewport");
- const viewportWidth = $(window).outerWidth();
+ const viewportWidth = window.outerWidth;
let isUserlistOpen = storage.get("thelounge.state.userlist");
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
@@ -45,7 +41,7 @@ socket.on("init", function(data) {
isUserlistOpen = "true";
}
- viewport.toggleClass("userlist-open", isUserlistOpen === "true");
+ vueApp.setUserlist(isUserlistOpen === "true");
$(document.body).removeClass("signed-out");
$("#loading").remove();
diff --git a/client/js/store.js b/client/js/store.js
index bab7f57d..f334fa65 100644
--- a/client/js/store.js
+++ b/client/js/store.js
@@ -1,5 +1,6 @@
import Vue from "vue";
import Vuex from "vuex";
+const storage = require("./localStorage");
Vue.use(Vuex);
@@ -9,6 +10,8 @@ export default new Vuex.Store({
isNotified: false,
activeWindow: null,
sessions: [],
+ sidebarOpen: false,
+ userlistOpen: storage.get("thelounge.state.userlist") !== "false",
},
mutations: {
isConnected(state, payload) {
@@ -26,6 +29,12 @@ export default new Vuex.Store({
sessions(state, payload) {
state.sessions = payload;
},
+ sidebarOpen(state, payload) {
+ state.sidebarOpen = payload;
+ },
+ userlistOpen(state, payload) {
+ state.userlistOpen = payload;
+ },
},
getters: {
currentSession: (state) => state.sessions.find((item) => item.current),
diff --git a/client/js/vue.js b/client/js/vue.js
index 2bec7e5e..cbd2cf33 100644
--- a/client/js/vue.js
+++ b/client/js/vue.js
@@ -7,6 +7,8 @@ const roundBadgeNumber = require("./libs/handlebars/roundBadgeNumber");
const localetime = require("./libs/handlebars/localetime");
const friendlysize = require("./libs/handlebars/friendlysize");
const colorClass = require("./libs/handlebars/colorClass");
+const slideoutMenu = require("../js/slideout");
+const storage = require("./localStorage");
Vue.filter("localetime", localetime);
Vue.filter("friendlysize", friendlysize);
@@ -48,6 +50,38 @@ const vueApp = new Vue({
mounted() {
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) {
return createElement(App, {
ref: "app",
From 7fd48d8155b20deb077812826c7a361e5295e8ac Mon Sep 17 00:00:00 2001
From: Richard Lewis
Date: Fri, 2 Aug 2019 15:53:31 +0300
Subject: [PATCH 017/111] Fix enabling and disabling push notifications.
---
client/components/Windows/Settings.vue | 11 ++++++++++-
client/js/webpush.js | 13 ++++++++-----
2 files changed, 18 insertions(+), 6 deletions(-)
diff --git a/client/components/Windows/Settings.vue b/client/components/Windows/Settings.vue
index b8a0283b..ed33cc3e 100644
--- a/client/components/Windows/Settings.vue
+++ b/client/components/Windows/Settings.vue
@@ -264,8 +264,9 @@
id="pushNotifications"
type="button"
class="btn"
- disabled
+ :disabled="$root.pushNotificationState !== 'supported'"
data-text-alternate="Unsubscribe from push notifications"
+ @click="onPushButtonClick"
>Subscribe to push notifications
diff --git a/client/js/webpush.js b/client/js/webpush.js
index 4e757cf1..5b55b8aa 100644
--- a/client/js/webpush.js
+++ b/client/js/webpush.js
@@ -67,13 +67,16 @@ module.exports.initialize = () => {
alternatePushButton();
}
});
- }).catch(() => {
- vueApp.pushNotificationState = "unsupported";
- });
+ })
+ .catch(() => {
+ vueApp.pushNotificationState = "unsupported";
+ });
}
};
-function onPushButton() {
+module.exports.onPushButton = () => {
+ // TODO: move dom logic to Settings.vue
+ pushNotificationsButton = $("#pushNotifications");
pushNotificationsButton.prop("disabled", true);
navigator.serviceWorker.ready
@@ -127,7 +130,7 @@ function onPushButton() {
});
return false;
-}
+};
function alternatePushButton() {
const text = pushNotificationsButton.text();
From addd4124bf00f11ba33e47f3bba303727d39ce36 Mon Sep 17 00:00:00 2001
From: Richard Lewis
Date: Fri, 2 Aug 2019 16:12:01 +0300
Subject: [PATCH 018/111] Close sidebar when opening settings, help etc.
---
client/js/lounge.js | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/client/js/lounge.js b/client/js/lounge.js
index dafb6011..5a1062c0 100644
--- a/client/js/lounge.js
+++ b/client/js/lounge.js
@@ -108,6 +108,10 @@ window.vueMounted = () => {
} else {
vueApp.activeChannel = null;
vueApp.$store.commit("activeWindow", target);
+
+ if (!keepSidebarOpen && $(window).outerWidth() <= utils.mobileViewportPixels) {
+ vueApp.setSidebar(false);
+ }
}
utils.synchronizeNotifiedState();
From b994ecd1f13183cffe16dad0f1243d9ec380bd66 Mon Sep 17 00:00:00 2001
From: Richard Lewis
Date: Fri, 2 Aug 2019 16:29:30 +0300
Subject: [PATCH 019/111] Fix hash navigation for sidebar footer buttons.
---
client/components/Sidebar.vue | 12 ++++++++----
client/js/lounge.js | 3 ++-
2 files changed, 10 insertions(+), 5 deletions(-)
diff --git a/client/components/Sidebar.vue b/client/components/Sidebar.vue
index 0f3a2170..e7445bcb 100644
--- a/client/components/Sidebar.vue
+++ b/client/components/Sidebar.vue
@@ -25,7 +25,8 @@
aria-label="Sign in"
> {
}
} else {
vueApp.activeChannel = null;
- vueApp.$store.commit("activeWindow", target);
+ const component = self.attr("data-component");
+ vueApp.$store.commit("activeWindow", component);
if (!keepSidebarOpen && $(window).outerWidth() <= utils.mobileViewportPixels) {
vueApp.setSidebar(false);
From e0ec340de85e0cde4c6e50d0b22c3aff9165ffab Mon Sep 17 00:00:00 2001
From: Richard Lewis
Date: Sat, 3 Aug 2019 21:48:06 +0300
Subject: [PATCH 020/111] Fix oversights during rebase.
---
client/components/ChannelWrapper.vue | 4 +++-
client/js/lounge.js | 4 ++--
client/js/socket-events/index.js | 1 -
3 files changed, 5 insertions(+), 4 deletions(-)
diff --git a/client/components/ChannelWrapper.vue b/client/components/ChannelWrapper.vue
index b4a3951b..2f5bf26d 100644
--- a/client/components/ChannelWrapper.vue
+++ b/client/components/ChannelWrapper.vue
@@ -1,6 +1,7 @@
{
socket.emit("open", channel ? channel.channel.id : null);
- if (!keepSidebarOpen && $(window).outerWidth() <= utils.mobileViewportPixels) {
+ if ($(window).outerWidth() <= utils.mobileViewportPixels) {
vueApp.setSidebar(false);
}
} else {
@@ -110,7 +110,7 @@ window.vueMounted = () => {
const component = self.attr("data-component");
vueApp.$store.commit("activeWindow", component);
- if (!keepSidebarOpen && $(window).outerWidth() <= utils.mobileViewportPixels) {
+ if ($(window).outerWidth() <= utils.mobileViewportPixels) {
vueApp.setSidebar(false);
}
}
diff --git a/client/js/socket-events/index.js b/client/js/socket-events/index.js
index 5453e74f..a46ef27a 100644
--- a/client/js/socket-events/index.js
+++ b/client/js/socket-events/index.js
@@ -1,7 +1,6 @@
"use strict";
require("./auth");
-require("./change_password");
require("./commands");
require("./init");
require("./join");
From b5f2e7f0cc62e0c07aa188e7e40d1b706c3683d9 Mon Sep 17 00:00:00 2001
From: Richard Lewis
Date: Sat, 3 Aug 2019 22:03:45 +0300
Subject: [PATCH 021/111] Fix lint and format with prettier.
---
client/components/App.vue | 18 +-
client/components/Channel.vue | 22 +-
client/components/ChannelWrapper.vue | 4 +-
client/components/NetworkForm.vue | 61 ++----
client/components/RevealPassword.vue | 6 +-
client/components/Session.vue | 15 +-
client/components/Sidebar.vue | 94 ++++----
client/components/SidebarToggle.vue | 6 +-
client/components/Windows/Changelog.vue | 24 +--
client/components/Windows/Help.vue | 188 +++++++++-------
client/components/Windows/NetworkEdit.vue | 3 +-
client/components/Windows/Settings.vue | 252 ++++++++--------------
client/components/Windows/SignIn.vue | 33 +--
client/js/options.js | 4 +-
client/js/slideout.js | 9 +-
client/js/webpush.js | 2 +-
16 files changed, 302 insertions(+), 439 deletions(-)
diff --git a/client/components/App.vue b/client/components/App.vue
index 814afa08..58629f18 100644
--- a/client/components/App.vue
+++ b/client/components/App.vue
@@ -1,24 +1,14 @@
-
-
+
@@ -57,7 +47,7 @@ export default {
computed: {
viewportClasses() {
return {
- "notified": this.$store.state.isNotified,
+ notified: this.$store.state.isNotified,
"menu-open": this.$store.state.sidebarOpen,
"userlist-open": this.$store.state.userlistOpen,
};
diff --git a/client/components/Channel.vue b/client/components/Channel.vue
index 815ab750..5c9321a2 100644
--- a/client/components/Channel.vue
+++ b/client/components/Channel.vue
@@ -17,27 +17,13 @@
>
-
-
+
+
-
-
+
+
diff --git a/client/components/ChannelWrapper.vue b/client/components/ChannelWrapper.vue
index 2f5bf26d..3d5ec16f 100644
--- a/client/components/ChannelWrapper.vue
+++ b/client/components/ChannelWrapper.vue
@@ -1,13 +1,13 @@
-
+
-
+
-
+
Edit {{ defaults.name }}
- The Lounge -
+ The Lounge -
+
Connect
@@ -51,7 +39,7 @@
name="name"
:value="defaults.name"
maxlength="100"
- >
+ />
Server
@@ -66,7 +54,7 @@
maxlength="255"
required
:disabled="config.lockNetwork ? true : false"
- >
+ />
@@ -79,7 +67,7 @@
:value="defaults.port"
aria-label="Server port"
:disabled="config.lockNetwork ? true : false"
- >
+ />
@@ -90,7 +78,7 @@
name="tls"
:checked="defaults.tls ? true : false"
:disabled="config.lockNetwork ? true : false"
- >
+ />
Use secure connection (TLS)
@@ -101,7 +89,7 @@
name="rejectUnauthorized"
:checked="defaults.rejectUnauthorized ? true : false"
:disabled="config.lockNetwork ? true : false"
- >
+ />
Only allow trusted certificates
@@ -122,7 +110,7 @@
:value="defaults.nick"
maxlength="100"
required
- >
+ />
@@ -135,7 +123,7 @@
name="username"
:value="defaults.username"
maxlength="512"
- >
+ />
@@ -150,7 +138,7 @@
:type="slotProps.isVisible ? 'text' : 'password'"
name="password"
maxlength="512"
- >
+ />
@@ -163,7 +151,7 @@
name="realname"
:value="defaults.realname"
maxlength="512"
- >
+ />
@@ -179,11 +167,9 @@
/>
- Save
+
+ Save
+
@@ -196,19 +182,16 @@
class="input"
name="join"
:value="defaults.join"
- >
+ />
- Connect
+
+ Connect
+
-
diff --git a/client/components/RevealPassword.vue b/client/components/RevealPassword.vue
index 5f2b0f56..d9d1323c 100644
--- a/client/components/RevealPassword.vue
+++ b/client/components/RevealPassword.vue
@@ -6,14 +6,12 @@
type="button"
:class="[
'reveal-password tooltipped tooltipped-n tooltipped-no-delay',
- { 'reveal-password-visible': isVisible },
+ {'reveal-password-visible': isVisible},
]"
:aria-label="isVisible ? 'Hide password' : 'Show password'"
@click="onClick"
>
-
+
diff --git a/client/components/Session.vue b/client/components/Session.vue
index 6af7d64f..09328726 100644
--- a/client/components/Session.vue
+++ b/client/components/Session.vue
@@ -1,9 +1,6 @@
-
+
Sign out
@@ -14,14 +11,12 @@
{{ session.agent }}
- {{ session.ip }}
+ {{
+ session.ip
+ }}
-
+
Currently active
diff --git a/client/components/Sidebar.vue b/client/components/Sidebar.vue
index e7445bcb..8337ca5a 100644
--- a/client/components/Sidebar.vue
+++ b/client/components/Sidebar.vue
@@ -7,73 +7,63 @@
:src="`img/logo-${isPublic() ? 'horizontal-' : ''}transparent-bg.svg`"
class="logo"
alt="The Lounge"
- >
+ />
+ />
-
+
-
+
diff --git a/client/components/SidebarToggle.vue b/client/components/SidebarToggle.vue
index 60cddb60..e3657b5e 100644
--- a/client/components/SidebarToggle.vue
+++ b/client/components/SidebarToggle.vue
@@ -1,9 +1,5 @@
-
+
diff --git a/client/components/Windows/Help.vue b/client/components/Windows/Help.vue
index f88078d5..edfff8eb 100644
--- a/client/components/Windows/Help.vue
+++ b/client/components/Windows/Help.vue
@@ -1,10 +1,5 @@
-
- Mark any text typed after this shortcut to be colored. After
- hitting this shortcut, enter an integer in the range
- 0—15
to select the desired color, or use the
- autocompletion menu to choose a color name (see below).
+ Mark any text typed after this shortcut to be colored. After hitting this
+ shortcut, enter an integer in the range
+ 0—15
to select the desired color, or use the autocompletion
+ menu to choose a color name (see below).
- Background color can be specified by putting a comma and
- another integer in the range 0—15
after the
- foreground color number (autocompletion works too).
+ Background color can be specified by putting a comma and another integer in
+ the range 0—15
after the foreground color number
+ (autocompletion works too).
A color reference can be found
@@ -158,7 +167,8 @@
href="https://modern.ircdocs.horse/formatting.html#colors"
target="_blank"
rel="noopener"
- >here.
+ >here.
@@ -169,7 +179,10 @@
⌘ B
-
Mark all text typed after this shortcut as bold .
+
+ Mark all text typed after this shortcut as
+ bold .
+
@@ -179,7 +192,10 @@
⌘ U
-
Mark all text typed after this shortcut as underlined .
+
+ Mark all text typed after this shortcut as
+ underlined .
+
@@ -189,7 +205,10 @@
⌘ I
-
Mark all text typed after this shortcut as italics .
+
+ Mark all text typed after this shortcut as
+ italics .
+
@@ -199,7 +218,10 @@
⌘ S
-
Mark all text typed after this shortcut as struck through .
+
+ Mark all text typed after this shortcut as
+ struck through .
+
@@ -209,7 +231,10 @@
⌘ M
-
Mark all text typed after this shortcut as monospaced .
+
+ Mark all text typed after this shortcut as
+ monospaced .
+
@@ -220,8 +245,8 @@
- Mark all text typed after this shortcut to be reset to its
- original formatting.
+ Mark all text typed after this shortcut to be reset to its original
+ formatting.
@@ -229,9 +254,10 @@
Autocompletion
- To auto-complete nicknames, channels, commands, and emoji, type one of the characters below to open
- a suggestion list. Use the ↑ and ↓ keys to highlight an item, and insert it by
- pressing Tab or Enter (or by clicking the desired item).
+ To auto-complete nicknames, channels, commands, and emoji, type one of the
+ characters below to open a suggestion list. Use the ↑ and
+ ↓ keys to highlight an item, and insert it by pressing Tab or
+ Enter (or by clicking the desired item).
Autocompletion can be disabled in settings.
@@ -269,7 +295,10 @@
:
-
Emoji (note: requires two search characters, to avoid conflicting with common emoticons like :)
)
+
+ Emoji (note: requires two search characters, to avoid conflicting with
+ common emoticons like :)
)
+
@@ -298,8 +327,10 @@
/ban nick
-
Ban (+b
) a user from the current channel.
- This can be a nickname or a hostmask.
+
+ Ban (+b
) a user from the current channel. This can be a
+ nickname or a hostmask.
+
@@ -330,9 +361,8 @@
- Connect to a new IRC network. If port
starts with
- a +
sign, the connection will be made secure
- using TLS.
+ Connect to a new IRC network. If port
starts with a
+ +
sign, the connection will be made secure using TLS.
Alias: /server
@@ -350,7 +380,8 @@
href="https://en.wikipedia.org/wiki/Client-to-client_protocol"
target="_blank"
rel="noopener"
- >the dedicated Wikipedia article.
+ >the dedicated Wikipedia article.
@@ -361,8 +392,8 @@
- Remove op (-o
) from one or several users in the
- current channel.
+ Remove op (-o
) from one or several users in the current
+ channel.
@@ -373,8 +404,8 @@
- Remove voice (-v
) from one or several users in
- the current channel.
+ Remove voice (-v
) from one or several users in the current
+ channel.
@@ -385,8 +416,7 @@
- Disconnect from the current network with an
- optionally-provided message.
+ Disconnect from the current network with an optionally-provided message.
@@ -410,8 +440,8 @@
Invite a user to the specified channel. If
- channel
is omitted, user will be invited to the
- current channel.
+ channel
is omitted, user will be invited to the current
+ channel.
@@ -422,8 +452,9 @@
- Block any messages from the specified user on the current network.
- This can be a nickname or a hostmask.
+ Block any messages from the specified user on the current network. This can
+ be a nickname or a hostmask.
+
@@ -469,9 +500,8 @@
- Send an action message to the current channel. The Lounge will
- display it inline, as if the message was posted in the third
- person.
+ Send an action message to the current channel. The Lounge will display it
+ inline, as if the message was posted in the third person.
@@ -482,10 +512,9 @@
- Set the given flags to the current channel if the active
- window is a channel, another user if the active window is a
- private message window, or yourself if the current window is a
- server window.
+ Set the given flags to the current channel if the active window is a
+ channel, another user if the active window is a private message window, or
+ yourself if the current window is a server window.
@@ -523,8 +552,7 @@
- Give op (+o
) to one or several users in the
- current channel.
+ Give op (+o
) to one or several users in the current channel.
@@ -535,8 +563,8 @@
- Close the specified channel or private message window, or the
- current channel if channel
is omitted.
+ Close the specified channel or private message window, or the current
+ channel if channel
is omitted.
Aliases: /close
, /leave
@@ -548,8 +576,8 @@
- Leave and immediately rejoin the current channel. Useful to
- quickly get op from ChanServ in an empty channel, for example.
+ Leave and immediately rejoin the current channel. Useful to quickly get op
+ from ChanServ in an empty channel, for example.
Alias: /cycle
@@ -600,9 +628,8 @@
- Get the topic in the current channel.
- If newtopic
is specified, sets the
- topic in the current channel.
+ Get the topic in the current channel. If newtopic
is specified,
+ sets the topic in the current channel.
@@ -612,8 +639,10 @@
/unban nick
-
Unban (-b
) a user from the current channel.
- This can be a nickname or a hostmask.
+
+ Unban (-b
) a user from the current channel. This can be a
+ nickname or a hostmask.
+
@@ -623,8 +652,9 @@
- Unblock messages from the specified user on the current network.
- This can be a nickname or a hostmask.
+ Unblock messages from the specified user on the current network. This can be
+ a nickname or a hostmask.
+
@@ -634,8 +664,7 @@
- Give voice (+v
) to one or several users in the
- current channel.
+ Give voice (+v
) to one or several users in the current channel.
@@ -646,8 +675,7 @@
- Retrieve information about the given user on the current
- network.
+ Retrieve information about the given user on the current network.
diff --git a/client/components/Windows/NetworkEdit.vue b/client/components/Windows/NetworkEdit.vue
index 05a41fd5..2a8a9fd0 100644
--- a/client/components/Windows/NetworkEdit.vue
+++ b/client/components/Windows/NetworkEdit.vue
@@ -33,8 +33,7 @@ export default {
const network = this.$root.networks.find((n) => n.uuid === data.uuid);
network.name = network.channels[0].name = data.name;
- sidebar.find(`.network[data-uuid="${data.uuid}"] .chan.lobby .name`)
- .click();
+ sidebar.find(`.network[data-uuid="${data.uuid}"] .chan.lobby .name`).click();
},
},
};
diff --git a/client/components/Windows/Settings.vue b/client/components/Windows/Settings.vue
index ed33cc3e..34bdd6ba 100644
--- a/client/components/Windows/Settings.vue
+++ b/client/components/Windows/Settings.vue
@@ -1,19 +1,9 @@
-
+
-
+
Settings
@@ -23,31 +13,26 @@
v-model="$root.serverConfiguration.advanced"
type="checkbox"
name="advanced"
- >
+ />
Advanced settings
-
+
Native app
- Add The Lounge to Home screen
+
+ Add The Lounge to Home screen
+
Open irc:// URLs with The Lounge
+ >
+ Open irc:// URLs with The Lounge
+
- Synchronize settings with other clients.
+ />
+ Synchronize settings with other clients
-
- Warning Checking this box will override the settings of this client with those stored on the server.
+
+ Warning Checking this box will override the settings of
+ this client with those stored on the server.
-
- Warning No settings have been synced before. Enabling this will sync all settings of this client as the base for other clients.
+
+ Warning No settings have been synced before. Enabling this
+ will sync all settings of this client as the base for other clients.
-
@@ -104,11 +80,7 @@
@@ -118,7 +90,7 @@
v-model="$root.settings.showSeconds"
type="checkbox"
name="showSeconds"
- >
+ />
Show seconds in timestamp
@@ -143,7 +115,7 @@
type="radio"
name="statusMessages"
value="shown"
- >
+ />
Show all status messages individually
@@ -152,7 +124,7 @@
type="radio"
name="statusMessages"
value="condensed"
- >
+ />
Condense status messages together
@@ -161,7 +133,7 @@
type="radio"
name="statusMessages"
value="hidden"
- >
+ />
Hide all status messages
@@ -174,7 +146,7 @@
v-model="$root.settings.coloredNicks"
type="checkbox"
name="coloredNicks"
- >
+ />
Enable colored nicknames
@@ -182,19 +154,15 @@
v-model="$root.settings.autocomplete"
type="checkbox"
name="autocomplete"
- >
+ />
Enable autocomplete
-
+
- Nick autocomplete postfix (e.g. ,
)
+ Nick autocomplete postfix (e.g. ,
)
+ />
@@ -210,10 +178,7 @@
Theme
-
Theme
+
Theme
-
+
Auto-expand media
-
+
Auto-expand websites
@@ -267,18 +225,14 @@
:disabled="$root.pushNotificationState !== 'supported'"
data-text-alternate="Unsubscribe from push notifications"
@click="onPushButtonClick"
- >Subscribe to push notifications
-
-
Warning :
- Push notifications are only supported over HTTPS connections.
+ Subscribe to push notifications
+
+
+ Warning : Push notifications are only supported over
+ HTTPS connections.
-
+
Warning :
Push notifications are not supported by your browser.
@@ -295,22 +249,18 @@
id="desktopNotifications"
type="checkbox"
name="desktopNotifications"
- >
- Enable browser notifications
-
-
Warning :
- Notifications are not supported by your browser.
+ />
+ Enable browser notifications
+
+ Warning : Notifications are not supported by your
+ browser.
- Warning :
- Notifications are blocked by your browser.
+ Warning : Notifications are blocked by your browser.
@@ -320,42 +270,32 @@
v-model="$root.settings.notification"
type="checkbox"
name="notification"
- >
+ />
Enable notification sound
- Play sound
+ Play sound
-
+
+ />
Enable notification for all messages
-
+
- Custom highlights (comma-separated keywords)
+ Custom highlights (comma-separated keywords)
+ />
Change password
- Enter current password
+ Enter current password
+ />
- Enter desired new password
+ Enter desired new password
+ />
- Repeat new password
+ Repeat new password
+ />
- Change password
+
+ Change password
+
-
+
Custom Stylesheet
-
-
Custom stylesheet. You can override any style with CSS here.
+
+ Custom stylesheet. You can override any style with CSS here.
-
+
Sessions
Current session
-
-
@@ -520,7 +443,8 @@ export default {
passwordErrors: {
missing_fields: "Please enter a new password",
password_mismatch: "Both new password fields must match",
- password_incorrect: "The current password field does not match your account password",
+ password_incorrect:
+ "The current password field does not match your account password",
update_failed: "Failed to update your password",
},
};
@@ -541,11 +465,7 @@ export default {
},
methods: {
onChange(event) {
- const ignore = [
- "old_password",
- "new_password",
- "verify_password",
- ];
+ const ignore = ["old_password", "new_password", "verify_password"];
const name = event.target.name;
diff --git a/client/components/Windows/SignIn.vue b/client/components/Windows/SignIn.vue
index 690f1adf..e69dd572 100644
--- a/client/components/Windows/SignIn.vue
+++ b/client/components/Windows/SignIn.vue
@@ -1,30 +1,20 @@
-
-
+
+
+ />
+ />
Username
+ />
Password
@@ -54,20 +44,13 @@
autocorrect="off"
autocomplete="current-password"
required
- >
+ />
- Authentication failed.
+ Authentication failed.
- Sign in
+ Sign in
diff --git a/client/js/options.js b/client/js/options.js
index 360a6fb7..2a37455e 100644
--- a/client/js/options.js
+++ b/client/js/options.js
@@ -107,7 +107,7 @@ function applySetting(name, value) {
} else if (name === "desktopNotifications") {
updateDesktopNotificationStatus();
- if (("Notification" in window) && value && Notification.permission !== "granted") {
+ if ("Notification" in window && value && Notification.permission !== "granted") {
Notification.requestPermission(updateDesktopNotificationStatus);
}
}
@@ -170,7 +170,7 @@ function initialize() {
// If browser does not support notifications
// display proper message in settings.
- if (("Notification" in window)) {
+ if ("Notification" in window) {
updateDesktopNotificationStatus();
} else {
vueApp.desktopNotificationState = "unsupported";
diff --git a/client/js/slideout.js b/client/js/slideout.js
index ecab355b..989061d2 100644
--- a/client/js/slideout.js
+++ b/client/js/slideout.js
@@ -36,7 +36,7 @@ class SlideoutMenu {
};
this.onTouchMove = (e) => {
- const touch = this.touchCurPos = e.touches.item(0);
+ const touch = (this.touchCurPos = e.touches.item(0));
let distX = touch.screenX - this.touchStartPos.screenX;
const distY = touch.screenY - this.touchStartPos.screenY;
@@ -80,7 +80,10 @@ class SlideoutMenu {
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) {
+ if (
+ absDiff > this.menuWidth / 2 ||
+ (Date.now() - this.touchStartTime < 180 && absDiff > 50)
+ ) {
this.toggle(diff > 0);
}
@@ -109,4 +112,4 @@ class SlideoutMenu {
}
}
-module.exports = (new SlideoutMenu);
+module.exports = new SlideoutMenu();
diff --git a/client/js/webpush.js b/client/js/webpush.js
index 5b55b8aa..4bd56ef0 100644
--- a/client/js/webpush.js
+++ b/client/js/webpush.js
@@ -125,7 +125,7 @@ module.exports.onPushButton = () => {
}
})
)
- .catch((err) => {
+ .catch(() => {
vueApp.pushNotificationState = "unsupported";
});
From ee92de0ff7fa0b1a7ea06c6f9dc2337b41339673 Mon Sep 17 00:00:00 2001
From: Richard Lewis
Date: Sat, 3 Aug 2019 23:44:45 +0300
Subject: [PATCH 022/111] Fix changing theme color and properly sync settings.
---
client/components/Windows/Settings.vue | 2 +-
client/js/options.js | 16 +++++++---------
2 files changed, 8 insertions(+), 10 deletions(-)
diff --git a/client/components/Windows/Settings.vue b/client/components/Windows/Settings.vue
index 34bdd6ba..9faa0347 100644
--- a/client/components/Windows/Settings.vue
+++ b/client/components/Windows/Settings.vue
@@ -481,7 +481,7 @@ export default {
value = event.target.value;
}
- this.options.updateSetting(name, value);
+ this.options.updateSetting(name, value, true);
},
changePassword() {
const allFields = new FormData(this.$refs.settingsForm);
diff --git a/client/js/options.js b/client/js/options.js
index 2a37455e..9e36137d 100644
--- a/client/js/options.js
+++ b/client/js/options.js
@@ -84,20 +84,18 @@ function updateDesktopNotificationStatus() {
}
function applySetting(name, value) {
- if (name === "syncSettings" && value) {
- $syncWarningOverride.hide();
- $forceSyncButton.hide();
- } else if (name === "theme") {
+ if (name === "theme") {
const themeUrl = `themes/${value}.css`;
if ($theme.attr("href") !== themeUrl) {
$theme.attr("href", themeUrl);
-
- const newTheme = $settings.find("#theme-select option[value='" + value + "']");
+ const newTheme = vueApp.serverConfiguration.themes.filter(
+ (theme) => theme.name === value
+ )[0];
let themeColor = defaultThemeColor;
- if (newTheme.length > 0 && newTheme[0].dataset.themeColor) {
- themeColor = newTheme[0].dataset.themeColor;
+ if (newTheme.themeColor) {
+ themeColor = newTheme.themeColor;
}
document.querySelector('meta[name="theme-color"]').content = themeColor;
@@ -117,7 +115,7 @@ function settingSetEmit(name, value) {
socket.emit("setting:set", {name, value});
}
-// When sync is `true` the setting will also be send to the backend for syncing.
+// When sync is `true` the setting will also be sent to the backend for syncing.
function updateSetting(name, value, sync) {
settings[name] = value;
storage.set("settings", JSON.stringify(settings));
From 2b602ca333d2ee99762645aec5a3aa0166cc4786 Mon Sep 17 00:00:00 2001
From: Richard Lewis
Date: Mon, 5 Aug 2019 15:24:44 +0300
Subject: [PATCH 023/111] Move slideout menu logic to Vue.
---
client/components/App.vue | 1 +
client/components/Sidebar.vue | 104 +++++++++++++++++++++++++++++-
client/js/slideout.js | 115 ----------------------------------
client/js/store.js | 4 ++
client/js/vue.js | 5 --
5 files changed, 107 insertions(+), 122 deletions(-)
delete mode 100644 client/js/slideout.js
diff --git a/client/components/App.vue b/client/components/App.vue
index 58629f18..b58ee181 100644
--- a/client/components/App.vue
+++ b/client/components/App.vue
@@ -49,6 +49,7 @@ export default {
return {
notified: this.$store.state.isNotified,
"menu-open": this.$store.state.sidebarOpen,
+ "menu-dragging": this.$store.state.sidebarDragging,
"userlist-open": this.$store.state.userlistOpen,
};
},
diff --git a/client/components/Sidebar.vue b/client/components/Sidebar.vue
index 8337ca5a..57dd86ba 100644
--- a/client/components/Sidebar.vue
+++ b/client/components/Sidebar.vue
@@ -1,6 +1,6 @@
-
diff --git a/client/js/loading-error-handlers.js b/client/js/loading-error-handlers.js
index d6c118c6..922175fb 100644
--- a/client/js/loading-error-handlers.js
+++ b/client/js/loading-error-handlers.js
@@ -1,4 +1,4 @@
-/* eslint strict: 0, no-var: 0 */
+/* eslint strict: 0 */
"use strict";
/*
@@ -9,71 +9,66 @@
*/
(function() {
- var msg = document.getElementById("loading-page-message");
+ const msg = document.getElementById("loading-page-message");
+ msg.textContent = "Loading the app…";
- if (msg) {
- msg.textContent = "Loading the app…";
+ document
+ .getElementById("loading-reload")
+ .addEventListener("click", () => location.reload(true));
- document.getElementById("loading-reload").addEventListener("click", function() {
- location.reload(true);
- });
- }
-
- var displayReload = function displayReload() {
- var loadingReload = document.getElementById("loading-reload");
+ const displayReload = () => {
+ const loadingReload = document.getElementById("loading-reload");
if (loadingReload) {
loadingReload.style.visibility = "visible";
}
};
- var loadingSlowTimeout = setTimeout(function() {
- var loadingSlow = document.getElementById("loading-slow");
-
- // The parent element, #loading, is being removed when the app is loaded.
- // Since the timer is not cancelled, `loadingSlow` can be not found after
- // 5s. Wrap everything in this block to make sure nothing happens if the
- // element does not exist (i.e. page has loaded).
- if (loadingSlow) {
- loadingSlow.style.visibility = "visible";
- displayReload();
- }
+ const loadingSlowTimeout = setTimeout(() => {
+ const loadingSlow = document.getElementById("loading-slow");
+ loadingSlow.style.visibility = "visible";
+ displayReload();
}, 5000);
- window.g_LoungeErrorHandler = function LoungeErrorHandler(e) {
- var message = document.getElementById("loading-page-message");
- message.textContent =
- "An error has occurred that prevented the client from loading correctly.";
+ const errorHandler = (e) => {
+ msg.textContent = "An error has occurred that prevented the client from loading correctly.";
- var summary = document.createElement("summary");
+ const summary = document.createElement("summary");
summary.textContent = "More details";
- var data = document.createElement("pre");
+ const data = document.createElement("pre");
data.textContent = e.message; // e is an ErrorEvent
- var info = document.createElement("p");
+ const info = document.createElement("p");
info.textContent = "Open the developer tools of your browser for more information.";
- var details = document.createElement("details");
+ const details = document.createElement("details");
details.appendChild(summary);
details.appendChild(data);
details.appendChild(info);
- message.parentNode.insertBefore(details, message.nextSibling);
+ msg.parentNode.insertBefore(details, msg.nextSibling);
window.clearTimeout(loadingSlowTimeout);
displayReload();
};
- window.addEventListener("error", window.g_LoungeErrorHandler);
+ window.addEventListener("error", errorHandler);
+
+ window.g_TheLoungeRemoveLoading = () => {
+ delete window.g_TheLoungeRemoveLoading;
+ window.clearTimeout(loadingSlowTimeout);
+ window.removeEventListener("error", errorHandler);
+ document.getElementById("loading").remove();
+ };
// Trigger early service worker registration
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("service-worker.js");
// Handler for messages coming from the service worker
- var messageHandler = function ServiceWorkerMessageHandler(event) {
+ const messageHandler = (event) => {
if (event.data.type === "fetch-error") {
- window.g_LoungeErrorHandler({
+ errorHandler({
message: `Service worker failed to fetch an url: ${event.data.message}`,
});
diff --git a/client/js/router.js b/client/js/router.js
index 202cab6c..64fe147d 100644
--- a/client/js/router.js
+++ b/client/js/router.js
@@ -25,7 +25,7 @@ const router = new VueRouter({
});
router.afterEach((to) => {
- if (router.app.initialized) {
+ if (store.state.appLoaded) {
router.app.closeSidebarIfNeeded();
}
diff --git a/client/js/socket-events/auth.js b/client/js/socket-events/auth.js
index a2836494..296a8f97 100644
--- a/client/js/socket-events/auth.js
+++ b/client/js/socket-events/auth.js
@@ -5,76 +5,89 @@ const socket = require("../socket");
const storage = require("../localStorage");
const {getActiveWindowComponent} = require("../vue");
const store = require("../store").default;
-let lastServerHash = -1;
+let lastServerHash = null;
-socket.on("auth", function(data) {
+socket.on("auth:success", function() {
+ store.commit("currentUserVisibleError", "Loading messages…");
+ $("#loading-page-message").text(store.state.currentUserVisibleError);
+});
+
+socket.on("auth:failed", function() {
+ storage.remove("token");
+
+ if (store.state.appLoaded) {
+ return reloadPage("Authentication failed, reloading…");
+ }
+
+ // TODO: This will most likely fail getActiveWindowComponent
+ showSignIn();
+
+ // TODO: getActiveWindowComponent is the SignIn component, find a better way to set this
+ getActiveWindowComponent().errorShown = true;
+ getActiveWindowComponent().inFlight = false;
+});
+
+socket.on("auth:start", function(serverHash) {
// If we reconnected and serverHash differs, that means the server restarted
// And we will reload the page to grab the latest version
- if (lastServerHash > -1 && data.serverHash > -1 && data.serverHash !== lastServerHash) {
- socket.disconnect();
- store.commit("isConnected", false);
- store.commit("currentUserVisibleError", "Server restarted, reloading…");
- location.reload(true);
- return;
+ if (lastServerHash && serverHash !== lastServerHash) {
+ return reloadPage("Server restarted, reloading…");
}
- if (data.serverHash > -1) {
- lastServerHash = data.serverHash;
- } else {
- getActiveWindowComponent().inFlight = false;
- }
+ lastServerHash = serverHash;
- let token;
const user = storage.get("user");
+ const token = storage.get("token");
+ const doFastAuth = user && token;
- if (!data.success) {
- if (store.state.activeWindow !== "SignIn") {
- socket.disconnect();
- store.commit("isConnected", false);
- store.commit("currentUserVisibleError", "Authentication failed, reloading…");
- location.reload();
- return;
- }
+ // If we reconnect and no longer have a stored token, reload the page
+ if (store.state.appLoaded && !doFastAuth) {
+ return reloadPage("Authentication failed, reloading…");
+ }
- storage.remove("token");
+ // If we have user and token stored, perform auth without showing sign-in first
+ if (doFastAuth) {
+ store.commit("currentUserVisibleError", "Authorizing…");
+ $("#loading-page-message").text(store.state.currentUserVisibleError);
- getActiveWindowComponent().errorShown = true;
- } else if (user) {
- token = storage.get("token");
+ let lastMessage = -1;
- if (token) {
- store.commit("currentUserVisibleError", "Authorizing…");
- $("#loading-page-message").text(store.state.currentUserVisibleError);
+ for (const network of store.state.networks) {
+ for (const chan of network.channels) {
+ if (chan.messages.length > 0) {
+ const id = chan.messages[chan.messages.length - 1].id;
- let lastMessage = -1;
-
- for (const network of store.state.networks) {
- for (const chan of network.channels) {
- if (chan.messages.length > 0) {
- const id = chan.messages[chan.messages.length - 1].id;
-
- if (lastMessage < id) {
- lastMessage = id;
- }
+ if (lastMessage < id) {
+ lastMessage = id;
}
}
}
-
- const openChannel =
- (store.state.activeChannel && store.state.activeChannel.channel.id) || null;
-
- socket.emit("auth", {user, token, lastMessage, openChannel});
}
+
+ const openChannel =
+ (store.state.activeChannel && store.state.activeChannel.channel.id) || null;
+
+ socket.emit("auth:perform", {user, token, lastMessage, openChannel});
+ } else {
+ showSignIn();
+ }
+});
+
+function showSignIn() {
+ // TODO: this flashes grey background because it takes a little time for vue to mount signin
+ if (window.g_TheLoungeRemoveLoading) {
+ window.g_TheLoungeRemoveLoading();
}
- if (token) {
- return;
- }
-
- $("#loading").remove();
$("#footer")
.find(".sign-in")
.trigger("click", {
pushState: false,
});
-});
+}
+
+function reloadPage(message) {
+ socket.disconnect();
+ store.commit("currentUserVisibleError", message);
+ location.reload(true);
+}
diff --git a/client/js/socket-events/init.js b/client/js/socket-events/init.js
index 2fc1e4f4..d979db05 100644
--- a/client/js/socket-events/init.js
+++ b/client/js/socket-events/init.js
@@ -10,17 +10,14 @@ const router = require("../router");
const store = require("../store").default;
socket.on("init", function(data) {
- store.commit("currentUserVisibleError", "Rendering…");
-
- $("#loading-page-message").text(store.state.currentUserVisibleError);
-
store.commit("networks", mergeNetworkData(data.networks));
store.commit("isConnected", true);
store.commit("currentUserVisibleError", null);
- if (!vueApp.initialized) {
+ if (!store.state.appLoaded) {
router.initialize();
- vueApp.onSocketInit();
+
+ store.commit("appLoaded");
if (data.token) {
storage.set("token", data.token);
@@ -43,12 +40,10 @@ socket.on("init", function(data) {
vueApp.setUserlist(isUserlistOpen === "true");
- $(document.body).removeClass("signed-out");
- $("#loading").remove();
+ document.body.classList.remove("signed-out");
- if (window.g_LoungeErrorHandler) {
- window.removeEventListener("error", window.g_LoungeErrorHandler);
- window.g_LoungeErrorHandler = null;
+ if (window.g_TheLoungeRemoveLoading) {
+ window.g_TheLoungeRemoveLoading();
}
if (!vueApp.$route.name || vueApp.$route.name === "SignIn") {
diff --git a/client/js/socket.js b/client/js/socket.js
index 8f47c78b..1295ef80 100644
--- a/client/js/socket.js
+++ b/client/js/socket.js
@@ -38,21 +38,18 @@ socket.on("connect", function() {
$("#loading-page-message").text(store.state.currentUserVisibleError);
});
-socket.on("authorized", function() {
- store.commit("currentUserVisibleError", "Loading messages…");
- $("#loading-page-message").text(store.state.currentUserVisibleError);
-});
-
function handleDisconnect(data) {
const message = data.message || data;
store.commit("isConnected", false);
+
store.commit("currentUserVisibleError", `Waiting to reconnect… (${message})`);
$("#loading-page-message").text(store.state.currentUserVisibleError);
// If the server shuts down, socket.io skips reconnection
// and we have to manually call connect to start the process
- if (socket.io.skipReconnect) {
+ // However, do not reconnect if TL client manually closed the connection
+ if (socket.io.skipReconnect && message !== "io client disconnect") {
requestIdleCallback(() => socket.connect(), 2000);
}
}
diff --git a/client/js/store.js b/client/js/store.js
index a9f22645..b1161b2e 100644
--- a/client/js/store.js
+++ b/client/js/store.js
@@ -11,6 +11,7 @@ const store = new Vuex.Store({
settings,
},
state: {
+ appLoaded: false,
activeChannel: null,
currentUserVisibleError: null,
desktopNotificationState: "unsupported",
@@ -31,6 +32,9 @@ const store = new Vuex.Store({
versionDataExpired: false,
},
mutations: {
+ appLoaded(state) {
+ state.appLoaded = true;
+ },
activeChannel(state, channel) {
state.activeChannel = channel;
},
diff --git a/client/js/vue.js b/client/js/vue.js
index 2f456a9e..9484e86a 100644
--- a/client/js/vue.js
+++ b/client/js/vue.js
@@ -15,9 +15,6 @@ const appName = document.title;
const vueApp = new Vue({
el: "#viewport",
- data: {
- initialized: false,
- },
router,
mounted() {
if (navigator.platform.match(/(Mac|iPhone|iPod|iPad)/i)) {
@@ -43,10 +40,6 @@ const vueApp = new Vue({
}, 1);
},
methods: {
- onSocketInit() {
- this.initialized = true;
- this.$store.commit("isConnected", true);
- },
setSidebar(state) {
this.$store.commit("sidebarOpen", state);
diff --git a/src/server.js b/src/server.js
index f698f456..a49f7741 100644
--- a/src/server.js
+++ b/src/server.js
@@ -174,11 +174,8 @@ module.exports = function(options = {}) {
if (Helper.config.public) {
performAuthentication.call(socket, {});
} else {
- socket.emit("auth", {
- serverHash: serverHash,
- success: true,
- });
- socket.on("auth", performAuthentication);
+ socket.on("auth:perform", performAuthentication);
+ socket.emit("auth:start", serverHash);
}
});
@@ -337,7 +334,8 @@ function indexRequest(req, res) {
}
function initializeClient(socket, client, token, lastMessage, openChannel) {
- socket.emit("authorized");
+ socket.off("auth:perform", performAuthentication);
+ socket.emit("auth:success");
client.clientAttach(socket.id, token);
@@ -789,7 +787,7 @@ function performAuthentication(data) {
);
}
- socket.emit("auth", {success: false});
+ socket.emit("auth:failed");
return;
}
diff --git a/test/server.js b/test/server.js
index 48798c05..095d21e4 100644
--- a/test/server.js
+++ b/test/server.js
@@ -72,7 +72,7 @@ describe("Server", function() {
});
it("should emit authorized message", (done) => {
- client.on("authorized", done);
+ client.on("auth:success", done);
});
it("should create network", (done) => {
From a2a2aff2bca2aeb69b6955327375d11f5bf4969e Mon Sep 17 00:00:00 2001
From: Tim Miller-Williams
Date: Tue, 5 Nov 2019 12:30:45 +0000
Subject: [PATCH 058/111] Remove unnecessary options.initialized switch
---
client/js/options.js | 3 ---
client/js/socket-events/configuration.js | 9 +--------
2 files changed, 1 insertion(+), 11 deletions(-)
diff --git a/client/js/options.js b/client/js/options.js
index bab2a33f..fcd6e177 100644
--- a/client/js/options.js
+++ b/client/js/options.js
@@ -64,7 +64,6 @@ userSettings = null;
module.exports = {
alwaysSync,
noSync,
- initialized: false,
settings,
syncAllSettings,
processSetting,
@@ -159,8 +158,6 @@ function processSetting(name, value, save) {
}
function initialize() {
- module.exports.initialized = true;
-
// Settings have now entirely updated, apply settings to the client.
for (const name in settings) {
processSetting(name, settings[name], false);
diff --git a/client/js/socket-events/configuration.js b/client/js/socket-events/configuration.js
index bdbcba1c..c83db2d9 100644
--- a/client/js/socket-events/configuration.js
+++ b/client/js/socket-events/configuration.js
@@ -21,15 +21,8 @@ window.addEventListener("beforeinstallprompt", (installPromptEvent) => {
$("#native-app").prop("hidden", false);
});
-socket.on("configuration", function(data) {
+socket.once("configuration", function(data) {
store.commit("isFileUploadEnabled", data.fileUpload);
-
- if (options.initialized) {
- // Likely a reconnect, request sync for possibly missed settings.
- socket.emit("setting:get");
- return;
- }
-
store.commit("serverConfiguration", data);
if (data.fileUpload) {
From 703848919c8105360322345093a278f13cefb375 Mon Sep 17 00:00:00 2001
From: Tim Miller-Williams
Date: Tue, 5 Nov 2019 19:22:10 +0000
Subject: [PATCH 059/111] Separate connection event handlers from socket.js
---
client/js/socket-events/auth.js | 12 +++++-
client/js/socket-events/connection.js | 59 +++++++++++++++++++++++++++
client/js/socket-events/index.js | 1 +
client/js/socket.js | 57 +-------------------------
4 files changed, 72 insertions(+), 57 deletions(-)
create mode 100644 client/js/socket-events/connection.js
diff --git a/client/js/socket-events/auth.js b/client/js/socket-events/auth.js
index 296a8f97..632d828b 100644
--- a/client/js/socket-events/auth.js
+++ b/client/js/socket-events/auth.js
@@ -9,7 +9,7 @@ let lastServerHash = null;
socket.on("auth:success", function() {
store.commit("currentUserVisibleError", "Loading messages…");
- $("#loading-page-message").text(store.state.currentUserVisibleError);
+ updateLoadingMessage();
});
socket.on("auth:failed", function() {
@@ -48,7 +48,7 @@ socket.on("auth:start", function(serverHash) {
// If we have user and token stored, perform auth without showing sign-in first
if (doFastAuth) {
store.commit("currentUserVisibleError", "Authorizing…");
- $("#loading-page-message").text(store.state.currentUserVisibleError);
+ updateLoadingMessage();
let lastMessage = -1;
@@ -91,3 +91,11 @@ function reloadPage(message) {
store.commit("currentUserVisibleError", message);
location.reload(true);
}
+
+function updateLoadingMessage() {
+ const loading = document.getElementById("loading-page-message");
+
+ if (loading) {
+ loading.textContent = store.state.currentUserVisibleError;
+ }
+}
diff --git a/client/js/socket-events/connection.js b/client/js/socket-events/connection.js
new file mode 100644
index 00000000..50cdcc8b
--- /dev/null
+++ b/client/js/socket-events/connection.js
@@ -0,0 +1,59 @@
+const store = require("../store").default;
+const socket = require("../socket");
+
+socket.on("disconnect", handleDisconnect);
+socket.on("connect_error", handleDisconnect);
+socket.on("error", handleDisconnect);
+
+socket.on("reconnecting", function(attempt) {
+ store.commit("currentUserVisibleError", `Reconnecting… (attempt ${attempt})`);
+ updateLoadingMessage();
+});
+
+socket.on("connecting", function() {
+ store.commit("currentUserVisibleError", `Connecting…`);
+ updateLoadingMessage();
+});
+
+socket.on("connect", function() {
+ // Clear send buffer when reconnecting, socket.io would emit these
+ // immediately upon connection and it will have no effect, so we ensure
+ // nothing is sent to the server that might have happened.
+ socket.sendBuffer = [];
+
+ store.commit("currentUserVisibleError", "Finalizing connection…");
+ updateLoadingMessage();
+});
+
+function handleDisconnect(data) {
+ const message = data.message || data;
+
+ store.commit("isConnected", false);
+ store.commit("currentUserVisibleError", `Waiting to reconnect… (${message})`);
+ updateLoadingMessage();
+
+ // If the server shuts down, socket.io skips reconnection
+ // and we have to manually call connect to start the process
+ // However, do not reconnect if TL client manually closed the connection
+ if (socket.io.skipReconnect && message !== "io client disconnect") {
+ requestIdleCallback(() => socket.connect(), 2000);
+ }
+}
+
+function requestIdleCallback(callback, timeout) {
+ if (window.requestIdleCallback) {
+ // During an idle period the user agent will run idle callbacks in FIFO order
+ // until either the idle period ends or there are no more idle callbacks eligible to be run.
+ window.requestIdleCallback(callback, {timeout});
+ } else {
+ callback();
+ }
+}
+
+function updateLoadingMessage() {
+ const loading = document.getElementById("loading-page-message");
+
+ if (loading) {
+ loading.textContent = store.state.currentUserVisibleError;
+ }
+}
diff --git a/client/js/socket-events/index.js b/client/js/socket-events/index.js
index a46ef27a..902aaf1f 100644
--- a/client/js/socket-events/index.js
+++ b/client/js/socket-events/index.js
@@ -1,5 +1,6 @@
"use strict";
+require("./connection");
require("./auth");
require("./commands");
require("./init");
diff --git a/client/js/socket.js b/client/js/socket.js
index 1295ef80..02d831ba 100644
--- a/client/js/socket.js
+++ b/client/js/socket.js
@@ -1,65 +1,12 @@
"use strict";
-const $ = require("jquery");
const io = require("socket.io-client");
const socket = io({
- transports: $(document.body).data("transports"),
+ transports: JSON.parse(document.body.dataset.transports),
path: window.location.pathname + "socket.io/",
autoConnect: false,
- reconnection: !$(document.body).hasClass("public"),
+ reconnection: !document.body.classList.contains("public"),
});
module.exports = socket;
-
-const store = require("./store").default;
-
-socket.on("disconnect", handleDisconnect);
-socket.on("connect_error", handleDisconnect);
-socket.on("error", handleDisconnect);
-
-socket.on("reconnecting", function(attempt) {
- store.commit("currentUserVisibleError", `Reconnecting… (attempt ${attempt})`);
- $("#loading-page-message").text(store.state.currentUserVisibleError);
-});
-
-socket.on("connecting", function() {
- store.commit("currentUserVisibleError", `Connecting…`);
- $("#loading-page-message").text(store.state.currentUserVisibleError);
-});
-
-socket.on("connect", function() {
- // Clear send buffer when reconnecting, socket.io would emit these
- // immediately upon connection and it will have no effect, so we ensure
- // nothing is sent to the server that might have happened.
- socket.sendBuffer = [];
-
- store.commit("currentUserVisibleError", "Finalizing connection…");
- $("#loading-page-message").text(store.state.currentUserVisibleError);
-});
-
-function handleDisconnect(data) {
- const message = data.message || data;
-
- store.commit("isConnected", false);
-
- store.commit("currentUserVisibleError", `Waiting to reconnect… (${message})`);
- $("#loading-page-message").text(store.state.currentUserVisibleError);
-
- // If the server shuts down, socket.io skips reconnection
- // and we have to manually call connect to start the process
- // However, do not reconnect if TL client manually closed the connection
- if (socket.io.skipReconnect && message !== "io client disconnect") {
- requestIdleCallback(() => socket.connect(), 2000);
- }
-}
-
-function requestIdleCallback(callback, timeout) {
- if (window.requestIdleCallback) {
- // During an idle period the user agent will run idle callbacks in FIFO order
- // until either the idle period ends or there are no more idle callbacks eligible to be run.
- window.requestIdleCallback(callback, {timeout});
- } else {
- callback();
- }
-}
From 25da9dd63eb48adce2a45b82d36ad44223057d10 Mon Sep 17 00:00:00 2001
From: Tim Miller-Williams
Date: Tue, 5 Nov 2019 20:51:55 +0000
Subject: [PATCH 060/111] Rework settings such that all behavior for each
setting is kept together
Behavior includes: default value, whether setting should be synced, and
an optional 'apply' callback which is called when setting is changed in
Vuex.
---
client/components/Windows/Settings.vue | 8 +-
client/js/options.js | 178 -----------------------
client/js/settings.js | 113 ++++++++++++++
client/js/socket-events/configuration.js | 9 +-
client/js/socket-events/setting.js | 19 +--
client/js/store-settings.js | 125 ++++++++++++----
client/js/store.js | 25 +++-
7 files changed, 236 insertions(+), 241 deletions(-)
delete mode 100644 client/js/options.js
create mode 100644 client/js/settings.js
diff --git a/client/components/Windows/Settings.vue b/client/components/Windows/Settings.vue
index d324f1ef..603c0537 100644
--- a/client/components/Windows/Settings.vue
+++ b/client/components/Windows/Settings.vue
@@ -463,7 +463,6 @@ export default {
},
data() {
return {
- options: null,
canRegisterProtocol: false,
passwordChangeStatus: null,
passwordErrors: {
@@ -476,8 +475,6 @@ export default {
};
},
mounted() {
- this.options = require("../../js/options"); // TODO: do this in a smarter way
-
socket.emit("sessions:get");
// Enable protocol handler registration if supported
@@ -507,7 +504,7 @@ export default {
value = event.target.value;
}
- this.options.updateSetting(name, value, true);
+ this.$store.dispatch("settings/update", {name, value, sync: true});
},
changePassword() {
const allFields = new FormData(this.$refs.settingsForm);
@@ -540,8 +537,7 @@ export default {
socket.emit("change-password", data);
},
onForceSyncClick() {
- const options = require("../../js/options");
- options.syncAllSettings(true);
+ this.$store.dispatch("settings/syncAll", true);
},
registerProtocol() {
const uri = document.location.origin + document.location.pathname + "?uri=%s";
diff --git a/client/js/options.js b/client/js/options.js
deleted file mode 100644
index fcd6e177..00000000
--- a/client/js/options.js
+++ /dev/null
@@ -1,178 +0,0 @@
-"use strict";
-
-const $ = require("jquery");
-const storage = require("./localStorage");
-const socket = require("./socket");
-const store = require("./store").default;
-require("../js/autocompletion");
-
-const $theme = $("#theme");
-const $userStyles = $("#user-specified-css");
-
-const noCSSparamReg = /[?&]nocss/;
-
-// Default settings
-const settings = store.state.settings;
-
-const noSync = ["syncSettings"];
-
-// alwaysSync is reserved for settings that should be synced
-// to the server regardless of the clients sync setting.
-const alwaysSync = ["highlights"];
-
-const defaultThemeColor = document.querySelector('meta[name="theme-color"]').content;
-
-// Process usersettings from localstorage.
-let userSettings = JSON.parse(storage.get("settings")) || false;
-
-if (!userSettings) {
- // Enable sync by default if there are no user defined settings.
- store.commit("settings/syncSettings", true);
-} else {
- for (const key in settings) {
- // Older The Lounge versions converted highlights to an array, turn it back into a string
- if (key === "highlights" && typeof userSettings[key] === "object") {
- userSettings[key] = userSettings[key].join(", ");
- }
-
- // Make sure the setting in local storage has the same type that the code expects
- if (
- typeof userSettings[key] !== "undefined" &&
- typeof settings[key] === typeof userSettings[key]
- ) {
- store.commit(`settings/${key}`, userSettings[key]);
- }
- }
-}
-
-// Apply custom CSS and themes on page load
-// Done here and not on init because on slower devices and connections
-// it can take up to several seconds before init is called.
-if (typeof userSettings.userStyles === "string" && !noCSSparamReg.test(window.location.search)) {
- $userStyles.html(userSettings.userStyles);
-}
-
-if (
- typeof userSettings.theme === "string" &&
- $theme.attr("href") !== `themes/${userSettings.theme}.css`
-) {
- $theme.attr("href", `themes/${userSettings.theme}.css`);
-}
-
-userSettings = null;
-
-module.exports = {
- alwaysSync,
- noSync,
- settings,
- syncAllSettings,
- processSetting,
- initialize,
- updateSetting,
-};
-
-// Updates the checkbox and warning in settings.
-// When notifications are not supported, this is never called (because
-// checkbox state can not be changed).
-function updateDesktopNotificationStatus() {
- if (Notification.permission === "granted") {
- store.commit("desktopNotificationState", "granted");
- } else {
- store.commit("desktopNotificationState", "blocked");
- }
-}
-
-function applySetting(name, value) {
- if (name === "theme") {
- const themeUrl = `themes/${value}.css`;
-
- if ($theme.attr("href") !== themeUrl) {
- $theme.attr("href", themeUrl);
- const newTheme = store.state.serverConfiguration.themes.filter(
- (theme) => theme.name === value
- )[0];
- let themeColor = defaultThemeColor;
-
- if (newTheme.themeColor) {
- themeColor = newTheme.themeColor;
- }
-
- document.querySelector('meta[name="theme-color"]').content = themeColor;
- }
- } else if (name === "userStyles" && !noCSSparamReg.test(window.location.search)) {
- $userStyles.html(value);
- } else if (name === "desktopNotifications") {
- updateDesktopNotificationStatus();
-
- if ("Notification" in window && value && Notification.permission !== "granted") {
- Notification.requestPermission(updateDesktopNotificationStatus);
- }
- }
-}
-
-function settingSetEmit(name, value) {
- socket.emit("setting:set", {name, value});
-}
-
-// When sync is `true` the setting will also be sent to the backend for syncing.
-function updateSetting(name, value, sync) {
- store.commit(`settings/${name}`, value);
- storage.set("settings", JSON.stringify(settings));
- applySetting(name, value);
-
- // Sync is checked, request settings from server.
- if (name === "syncSettings" && value) {
- socket.emit("setting:get");
- }
-
- if (settings.syncSettings && !noSync.includes(name) && sync) {
- settingSetEmit(name, value);
- } else if (alwaysSync.includes(name) && sync) {
- settingSetEmit(name, value);
- }
-}
-
-function syncAllSettings(force = false) {
- // Sync all settings if sync is enabled or force is true.
- if (settings.syncSettings || force) {
- for (const name in settings) {
- if (!noSync.includes(name)) {
- settingSetEmit(name, settings[name]);
- } else if (alwaysSync.includes(name)) {
- settingSetEmit(name, settings[name]);
- }
- }
- }
-}
-
-// If `save` is set to true it will pass the setting to `updateSetting()` processSetting
-function processSetting(name, value, save) {
- // No need to also call processSetting when `save` is true.
- // updateSetting does take care of that.
- if (save) {
- // Sync is false as applySetting is never called as the result of a user changing the setting.
- updateSetting(name, value, false);
- } else {
- applySetting(name, value);
- }
-}
-
-function initialize() {
- // Settings have now entirely updated, apply settings to the client.
- for (const name in settings) {
- processSetting(name, settings[name], false);
- }
-
- // If browser does not support notifications
- // display proper message in settings.
- if ("Notification" in window) {
- updateDesktopNotificationStatus();
- } else {
- store.commit("desktopNotificationState", "unsupported");
- }
-
- // Local init is done, let's sync
- // We always ask for synced settings even if it is disabled.
- // Settings can be mandatory to sync and it is used to determine sync base state.
- socket.emit("setting:get");
-}
diff --git a/client/js/settings.js b/client/js/settings.js
new file mode 100644
index 00000000..028c309d
--- /dev/null
+++ b/client/js/settings.js
@@ -0,0 +1,113 @@
+const socket = require("./socket");
+
+const defaultSettingConfig = {
+ apply() {},
+ default: null,
+ sync: null,
+};
+
+export const config = normalizeConfig({
+ syncSettings: {
+ default: true,
+ sync: "never",
+ apply(store, value) {
+ value && socket.emit("setting:get");
+ },
+ },
+ advanced: {
+ default: false,
+ },
+ autocomplete: {
+ default: true,
+ },
+ nickPostfix: {
+ default: "",
+ },
+ coloredNicks: {
+ default: true,
+ },
+ desktopNotifications: {
+ default: false,
+ apply(store, value) {
+ store.commit("refreshDesktopNotificationState", null, {root: true});
+
+ if ("Notification" in window && value && Notification.permission !== "granted") {
+ Notification.requestPermission(() =>
+ store.commit("refreshDesktopNotificationState", null, {root: true})
+ );
+ }
+ },
+ },
+ highlights: {
+ default: "",
+ sync: "always",
+ },
+ links: {
+ default: true,
+ },
+ motd: {
+ default: true,
+ },
+ notification: {
+ default: true,
+ },
+ notifyAllMessages: {
+ default: false,
+ },
+ showSeconds: {
+ default: false,
+ },
+ statusMessages: {
+ default: "condensed",
+ },
+ theme: {
+ default: document.getElementById("theme").dataset.serverTheme,
+ apply(store, value) {
+ const themeEl = document.getElementById("theme");
+ const themeUrl = `themes/${value}.css`;
+
+ if (themeEl.attributes.href.value === themeUrl) {
+ return;
+ }
+
+ themeEl.attributes.href.value = themeUrl;
+ const newTheme = store.state.serverConfiguration.themes.filter(
+ (theme) => theme.name === value
+ )[0];
+ const themeColor =
+ newTheme.themeColor || document.querySelector('meta[name="theme-color"]').content;
+ document.querySelector('meta[name="theme-color"]').content = themeColor;
+ },
+ },
+ media: {
+ default: true,
+ },
+ userStyles: {
+ default: "",
+ apply(store, value) {
+ if (!/[?&]nocss/.test(window.location.search)) {
+ document.getElementById("user-specified-css").innerHTML = value;
+ }
+ },
+ },
+});
+
+export function createState() {
+ const state = {};
+
+ for (const settingName in config) {
+ state[settingName] = config[settingName].default;
+ }
+
+ return state;
+}
+
+function normalizeConfig(obj) {
+ const newConfig = {};
+
+ for (const settingName in obj) {
+ newConfig[settingName] = {...defaultSettingConfig, ...obj[settingName]};
+ }
+
+ return newConfig;
+}
diff --git a/client/js/socket-events/configuration.js b/client/js/socket-events/configuration.js
index c83db2d9..65ceabd8 100644
--- a/client/js/socket-events/configuration.js
+++ b/client/js/socket-events/configuration.js
@@ -2,7 +2,6 @@
const $ = require("jquery");
const socket = require("../socket");
-const options = require("../options");
const webpush = require("../webpush");
const upload = require("../upload");
const store = require("../store").default;
@@ -25,11 +24,15 @@ socket.once("configuration", function(data) {
store.commit("isFileUploadEnabled", data.fileUpload);
store.commit("serverConfiguration", data);
+ // 'theme' setting depends on serverConfiguration.themes so
+ // settings cannot be applied before this point
+ store.dispatch("settings/applyAll");
+
if (data.fileUpload) {
upload.initialize(data.fileUploadMaxFileSize);
}
- options.initialize();
+ socket.emit("setting:get");
webpush.initialize();
// If localStorage contains a theme that does not exist on this server, switch
@@ -37,7 +40,7 @@ socket.once("configuration", function(data) {
const currentTheme = data.themes.find((t) => t.name === store.state.settings.theme);
if (currentTheme === undefined) {
- options.processSetting("theme", data.defaultTheme, true);
+ store.commit("settings/update", {name: "theme", value: data.defaultTheme, sync: true});
} else if (currentTheme.themeColor) {
document.querySelector('meta[name="theme-color"]').content = currentTheme.themeColor;
}
diff --git a/client/js/socket-events/setting.js b/client/js/socket-events/setting.js
index ce916c85..cb5b3f16 100644
--- a/client/js/socket-events/setting.js
+++ b/client/js/socket-events/setting.js
@@ -1,33 +1,20 @@
"use strict";
const socket = require("../socket");
-const options = require("../options");
const store = require("../store").default;
-function evaluateSetting(name, value) {
- if (
- store.state.settings.syncSettings &&
- store.state.settings[name] !== value &&
- !options.noSync.includes(name)
- ) {
- options.processSetting(name, value, true);
- } else if (options.alwaysSync.includes(name)) {
- options.processSetting(name, value, true);
- }
-}
-
socket.on("setting:new", function(data) {
const name = data.name;
const value = data.value;
- evaluateSetting(name, value);
+ store.dispatch("settings/update", {name, value, sync: false});
});
socket.on("setting:all", function(settings) {
if (Object.keys(settings).length === 0) {
- options.syncAllSettings();
+ store.dispatch("settings/syncAll");
} else {
for (const name in settings) {
- evaluateSetting(name, settings[name]);
+ store.dispatch("settings/update", {name, value: settings[name], sync: false});
}
}
});
diff --git a/client/js/store-settings.js b/client/js/store-settings.js
index 5dc24871..b0bb0120 100644
--- a/client/js/store-settings.js
+++ b/client/js/store-settings.js
@@ -1,37 +1,100 @@
-function createMutator(propertyName) {
- return [
- propertyName,
- (state, value) => {
- state[propertyName] = value;
+const storage = require("./localStorage");
+const socket = require("./socket");
+import {config, createState} from "./settings";
+
+export function createSettingsStore(store) {
+ return {
+ namespaced: true,
+ state: assignStoredSettings(createState(), loadFromLocalStorage()),
+ mutations: {
+ set(state, {name, value}) {
+ state[name] = value;
+ },
},
- ];
+ actions: {
+ syncAll({state}, force = false) {
+ if (state.syncSettings === false || force === false) {
+ return;
+ }
+
+ for (const name in state) {
+ if (config[name].sync !== "never" || config[name].sync === "always") {
+ socket.emit("setting:set", {name, value: state[name]});
+ }
+ }
+ },
+ applyAll({state}) {
+ for (const settingName in config) {
+ config[settingName].apply(store, state[settingName]);
+ }
+ },
+ update({state, commit}, {name, value, sync = false}) {
+ if (state[name] === value) {
+ return;
+ }
+
+ const settingConfig = config[name];
+
+ if (
+ sync === false &&
+ (state.syncSettings === false || settingConfig.sync === "never")
+ ) {
+ return;
+ }
+
+ commit("set", {name, value});
+ storage.set("settings", JSON.stringify(state));
+ settingConfig.apply(store, value);
+
+ if (!sync) {
+ return;
+ }
+
+ if (
+ (state.syncSettings && settingConfig.sync !== "never") ||
+ settingConfig.sync === "always"
+ ) {
+ socket.emit("setting:set", {name, value});
+ }
+ },
+ },
+ };
}
-function createMutators(keys) {
- return Object.fromEntries(keys.map(createMutator));
+function loadFromLocalStorage() {
+ const storedSettings = JSON.parse(storage.get("settings")) || false;
+
+ if (!storedSettings) {
+ return {};
+ }
+
+ // Older The Lounge versions converted highlights to an array, turn it back into a string
+ if (typeof storedSettings.highlights === "object") {
+ storedSettings.highlights = storedSettings.highlights.join(", ");
+ }
+
+ return storedSettings;
}
-const state = {
- syncSettings: false,
- advanced: false,
- autocomplete: true,
- nickPostfix: "",
- coloredNicks: true,
- desktopNotifications: false,
- highlights: "",
- links: true,
- motd: true,
- notification: true,
- notifyAllMessages: false,
- showSeconds: false,
- statusMessages: "condensed",
- theme: document.getElementById("theme").dataset.serverTheme,
- media: true,
- userStyles: "",
-};
+/**
+ * Essentially Object.assign but does not overwrite and only assigns
+ * if key exists in both supplied objects and types match
+ *
+ * @param {object} defaultSettings
+ * @param {object} storedSettings
+ */
+function assignStoredSettings(defaultSettings, storedSettings) {
+ const newSettings = {...defaultSettings};
-export default {
- namespaced: true,
- state,
- mutations: createMutators(Object.keys(state)),
-};
+ for (const key in defaultSettings) {
+ // Make sure the setting in local storage has the same type that the code expects
+ if (
+ typeof storedSettings[key] !== "undefined" &&
+ typeof defaultSettings[key] === typeof storedSettings[key]
+ ) {
+ newSettings[key] = storedSettings[key];
+ }
+ }
+
+ return newSettings;
+}
diff --git a/client/js/store.js b/client/js/store.js
index b1161b2e..cfecd468 100644
--- a/client/js/store.js
+++ b/client/js/store.js
@@ -1,20 +1,27 @@
import Vue from "vue";
import Vuex from "vuex";
-import settings from "./store-settings";
+import {createSettingsStore} from "./store-settings";
const storage = require("./localStorage");
Vue.use(Vuex);
+function detectDesktopNotificationState() {
+ if (!("Notification" in window)) {
+ return "unsupported";
+ } else if (Notification.permission === "granted") {
+ return "granted";
+ }
+
+ return "blocked";
+}
+
const store = new Vuex.Store({
- modules: {
- settings,
- },
state: {
appLoaded: false,
activeChannel: null,
currentUserVisibleError: null,
- desktopNotificationState: "unsupported",
+ desktopNotificationState: detectDesktopNotificationState(),
isAutoCompleting: false,
isConnected: false,
isFileUploadEnabled: false,
@@ -41,8 +48,8 @@ const store = new Vuex.Store({
currentUserVisibleError(state, error) {
state.currentUserVisibleError = error;
},
- desktopNotificationState(state, desktopNotificationState) {
- state.desktopNotificationState = desktopNotificationState;
+ refreshDesktopNotificationState(state) {
+ state.desktopNotificationState = detectDesktopNotificationState();
},
isAutoCompleting(state, isAutoCompleting) {
state.isAutoCompleting = isAutoCompleting;
@@ -129,4 +136,8 @@ const store = new Vuex.Store({
},
});
+// Settings module is registered dynamically because it benefits
+// from a direct reference to the store
+store.registerModule("settings", createSettingsStore(store));
+
export default store;
From 80c6cfbd7ca967b28122f74b1c3e63b8bd79fc38 Mon Sep 17 00:00:00 2001
From: Pavel Djundik
Date: Thu, 7 Nov 2019 11:29:04 +0200
Subject: [PATCH 061/111] Use switchToChannel instead of click
---
client/components/Windows/NetworkEdit.vue | 6 ++----
client/js/socket-events/msg.js | 6 ++----
client/js/webpush.js | 8 ++++----
client/service-worker.js | 2 +-
4 files changed, 9 insertions(+), 13 deletions(-)
diff --git a/client/components/Windows/NetworkEdit.vue b/client/components/Windows/NetworkEdit.vue
index 1c36f7a3..17d2dc5d 100644
--- a/client/components/Windows/NetworkEdit.vue
+++ b/client/components/Windows/NetworkEdit.vue
@@ -8,9 +8,7 @@
diff --git a/client/js/router.js b/client/js/router.js
index 1b0ac3d6..595501a0 100644
--- a/client/js/router.js
+++ b/client/js/router.js
@@ -35,7 +35,8 @@ const router = new VueRouter({
router.beforeEach((to, from, next) => {
// Disallow navigating to non-existing routes
- if (!to.matched.length) {
+
+ if (store.state.appLoaded && !to.matched.length) {
next(false);
return;
}
@@ -84,8 +85,9 @@ function initialize() {
router.addRoutes([
{
name: "Connect",
- path: "/connect",
+ path: "/connect*",
component: Connect,
+ props: (route) => ({queryParams: route.query}),
},
{
name: "Settings",
diff --git a/client/js/socket-events/configuration.js b/client/js/socket-events/configuration.js
index 65ceabd8..0df9bfe4 100644
--- a/client/js/socket-events/configuration.js
+++ b/client/js/socket-events/configuration.js
@@ -5,6 +5,7 @@ const socket = require("../socket");
const webpush = require("../webpush");
const upload = require("../upload");
const store = require("../store").default;
+const {vueApp} = require("../vue");
window.addEventListener("beforeinstallprompt", (installPromptEvent) => {
$("#webapp-install-button")
@@ -75,16 +76,32 @@ socket.once("configuration", function(data) {
if ("URLSearchParams" in window) {
const params = new URLSearchParams(document.location.search);
+ const cleanParams = () => {
+ // Remove query parameters from url without reloading the page
+ const cleanUri =
+ window.location.origin + window.location.pathname + window.location.hash;
+ window.history.replaceState({}, document.title, cleanUri);
+ };
+
if (params.has("uri")) {
- parseIrcUri(params.get("uri") + location.hash, data);
- } else if ($(document.body).hasClass("public")) {
- parseOverrideParams(params, data);
+ // Set default connection settings from IRC protocol links
+ const uri =
+ params.get("uri") +
+ (location.hash.includes("#/") ? location.hash.split("#/")[0] : location.hash);
+ const queryParams = parseIrcUri(uri, data);
+ cleanParams();
+ vueApp.$router.push({path: "/connect", query: queryParams});
+ } else if (document.body.classList.contains("public") && document.location.search) {
+ // Set default connection settings from url params
+ const queryParams = document.location.search;
+ cleanParams();
+ vueApp.$router.push("/connect" + queryParams);
}
}
});
-function parseIrcUri(stringUri, defaults) {
- const data = Object.assign({}, defaults.defaults);
+function parseIrcUri(stringUri) {
+ const data = {};
try {
// https://tools.ietf.org/html/draft-butcher-irc-url-04
@@ -125,61 +142,9 @@ function parseIrcUri(stringUri, defaults) {
}
data.join = channel;
-
- // TODO: Need to show connect window with uri params without overriding defaults
- defaults.defaults = data;
-
- $('button[data-target="#connect"]').trigger("click");
} catch (e) {
// do nothing on invalid uri
}
-}
-
-function parseOverrideParams(params, data) {
- for (let [key, value] of params) {
- // Support `channels` as a compatibility alias with other clients
- if (key === "channels") {
- key = "join";
- }
-
- if (!Object.prototype.hasOwnProperty.call(data.defaults, key)) {
- continue;
- }
-
- // When the network is locked, URL overrides should not affect disabled fields
- if (data.lockNetwork && ["host", "port", "tls", "rejectUnauthorized"].includes(key)) {
- continue;
- }
-
- // When the network is not displayed, its name in the UI is not customizable
- if (!data.displayNetwork && key === "name") {
- continue;
- }
-
- if (key === "join") {
- value = value
- .split(",")
- .map((chan) => {
- if (!chan.match(/^[#&!+]/)) {
- return `#${chan}`;
- }
-
- return chan;
- })
- .join(", ");
- }
-
- // Override server provided defaults with parameters passed in the URL if they match the data type
- switch (typeof data.defaults[key]) {
- case "boolean":
- data.defaults[key] = value === "1" || value === "true";
- break;
- case "number":
- data.defaults[key] = Number(value);
- break;
- case "string":
- data.defaults[key] = String(value);
- break;
- }
- }
+
+ return data;
}
From 94bdff4fa0a725603578f684968bf7f3378775f3 Mon Sep 17 00:00:00 2001
From: Richard Lewis
Date: Fri, 8 Nov 2019 19:41:27 +0000
Subject: [PATCH 067/111] Implement mirroring nick to username field in vue.
---
client/components/NetworkForm.vue | 13 ++++++++++++
client/js/socket-events/configuration.js | 27 ------------------------
2 files changed, 13 insertions(+), 27 deletions(-)
diff --git a/client/components/NetworkForm.vue b/client/components/NetworkForm.vue
index a071704f..a6735fdf 100644
--- a/client/components/NetworkForm.vue
+++ b/client/components/NetworkForm.vue
@@ -108,6 +108,7 @@
:value="defaults.nick"
maxlength="100"
required
+ @input="onNickChanged"
/>
@@ -117,6 +118,7 @@
Date: Thu, 7 Nov 2019 23:43:28 +0000
Subject: [PATCH 068/111] Refactor Apple keyboard logic to be more explicit
---
client/components/Windows/Help.vue | 53 ++++++++++++++++--------------
client/css/style.css | 9 -----
client/js/vue.js | 4 ---
3 files changed, 29 insertions(+), 37 deletions(-)
diff --git a/client/components/Windows/Help.vue b/client/components/Windows/Help.vue
index 03794828..1ffb0d82 100644
--- a/client/components/Windows/Help.vue
+++ b/client/components/Windows/Help.vue
@@ -97,8 +97,8 @@
- Alt Shift ↓
- ⌥ ⇧ ↓
+ Alt Shift ↓
+ ⌥ ⇧ ↓
Switch to the next lobby in the channel list.
@@ -107,8 +107,8 @@
- Alt Shift ↑
- ⌥ ⇧ ↑
+ Alt Shift ↑
+ ⌥ ⇧ ↑
Switch to the previous lobby in the channel list.
@@ -117,8 +117,8 @@
- Alt ↓
- ⌥ ↓
+ Alt ↓
+ ⌥ ↓
Switch to the next window in the channel list.
@@ -127,8 +127,8 @@
- Alt ↑
- ⌥ ↑
+ Alt ↑
+ ⌥ ↑
Switch to the previous window in the channel list.
@@ -137,8 +137,8 @@
- Alt A
- ⌥ A
+ Alt A
+ ⌥ A
Switch to the first window with unread messages.
@@ -147,8 +147,8 @@
- Ctrl K
- ⌘ K
+ Ctrl K
+ ⌘ K
@@ -176,8 +176,8 @@
- Ctrl B
- ⌘ B
+ Ctrl B
+ ⌘ B
@@ -189,8 +189,8 @@
- Ctrl U
- ⌘ U
+ Ctrl U
+ ⌘ U
@@ -202,8 +202,8 @@
- Ctrl I
- ⌘ I
+ Ctrl I
+ ⌘ I
@@ -215,8 +215,8 @@
- Ctrl S
- ⌘ S
+ Ctrl S
+ ⌘ S
@@ -228,8 +228,8 @@
- Ctrl M
- ⌘ M
+ Ctrl M
+ ⌘ M
@@ -241,8 +241,8 @@
- Ctrl O
- ⌘ O
+ Ctrl O
+ ⌘ O
@@ -694,5 +694,10 @@ export default {
SidebarToggle,
VersionChecker,
},
+ data() {
+ return {
+ isApple: navigator.platform.match(/(Mac|iPhone|iPod|iPad)/i) || false,
+ };
+ },
};
diff --git a/client/css/style.css b/client/css/style.css
index fce2f5ca..6c687ab2 100644
--- a/client/css/style.css
+++ b/client/css/style.css
@@ -1904,15 +1904,6 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */
margin-bottom: 0;
}
-.is-apple #help .key-all,
-#help .key-apple {
- display: none;
-}
-
-.is-apple #help .key-apple {
- display: inline-block;
-}
-
.whois {
display: grid;
grid-template-columns: max-content auto;
diff --git a/client/js/vue.js b/client/js/vue.js
index 9484e86a..adf6e6e2 100644
--- a/client/js/vue.js
+++ b/client/js/vue.js
@@ -17,10 +17,6 @@ const vueApp = new Vue({
el: "#viewport",
router,
mounted() {
- 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);
From 1adbbdda2a6f1949239d9a6f62b26ecf4092457a Mon Sep 17 00:00:00 2001
From: Tim Miller-Williams
Date: Thu, 7 Nov 2019 23:44:19 +0000
Subject: [PATCH 069/111] Fix bug with joining new channels
---
client/components/JoinChannel.vue | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/client/components/JoinChannel.vue b/client/components/JoinChannel.vue
index ae85f0c9..f18ca848 100644
--- a/client/components/JoinChannel.vue
+++ b/client/components/JoinChannel.vue
@@ -59,7 +59,9 @@ export default {
},
methods: {
onSubmit() {
- const existingChannel = this.$store.getters.findChannelOnCurrentNetwork(this.channel);
+ const existingChannel = this.$store.getters.findChannelOnCurrentNetwork(
+ this.inputChannel
+ );
if (existingChannel) {
this.$root.switchToChannel(existingChannel);
From dd9efad23c9285ad0f900a75eba939ca08e95db1 Mon Sep 17 00:00:00 2001
From: Tim Miller-Williams
Date: Thu, 7 Nov 2019 23:47:45 +0000
Subject: [PATCH 070/111] Refactor sidebar behaviour to no longer use methods
in root Vue instance
---
client/components/App.vue | 2 +-
client/components/SidebarToggle.vue | 2 +-
client/js/router.js | 5 ++++-
client/js/socket-events/init.js | 2 +-
client/js/store.js | 3 +++
client/js/vue.js | 28 +++++++++++-----------------
6 files changed, 21 insertions(+), 21 deletions(-)
diff --git a/client/components/App.vue b/client/components/App.vue
index 14edd9e9..4a516101 100644
--- a/client/components/App.vue
+++ b/client/components/App.vue
@@ -1,7 +1,7 @@
-
+
diff --git a/client/components/SidebarToggle.vue b/client/components/SidebarToggle.vue
index e3657b5e..6e0a7319 100644
--- a/client/components/SidebarToggle.vue
+++ b/client/components/SidebarToggle.vue
@@ -1,5 +1,5 @@
-
+
diff --git a/client/components/Windows/SignIn.vue b/client/components/Windows/SignIn.vue
index 7ab702d9..055b2d88 100644
--- a/client/components/Windows/SignIn.vue
+++ b/client/components/Windows/SignIn.vue
@@ -72,10 +72,10 @@ export default {
};
},
mounted() {
- this.$root.$on("auth:failed", this.onAuthFailed);
+ socket.on("auth:failed", this.onAuthFailed);
},
beforeDestroy() {
- this.$root.$off("auth:failed", this.onAuthFailed);
+ socket.off("auth:failed", this.onAuthFailed);
},
methods: {
onAuthFailed() {
diff --git a/client/js/socket-events/auth.js b/client/js/socket-events/auth.js
index c3764eed..a0a415c3 100644
--- a/client/js/socket-events/auth.js
+++ b/client/js/socket-events/auth.js
@@ -2,7 +2,6 @@
const socket = require("../socket");
const storage = require("../localStorage");
-const {vueApp} = require("../vue");
const {router, navigate} = require("../router");
const store = require("../store").default;
let lastServerHash = null;
@@ -20,8 +19,6 @@ socket.on("auth:failed", function() {
}
showSignIn();
-
- vueApp.$emit("auth:failed");
});
socket.on("auth:start", function(serverHash) {
diff --git a/client/js/socket-events/configuration.js b/client/js/socket-events/configuration.js
index 825bc0b2..4cc22a67 100644
--- a/client/js/socket-events/configuration.js
+++ b/client/js/socket-events/configuration.js
@@ -27,12 +27,12 @@ socket.once("configuration", function(data) {
// 'theme' setting depends on serverConfiguration.themes so
// settings cannot be applied before this point
store.dispatch("settings/applyAll");
+ socket.emit("setting:get");
if (data.fileUpload) {
- upload.initialize(data.fileUploadMaxFileSize);
+ upload.initialize();
}
- socket.emit("setting:get");
webpush.initialize();
router.initialize();
@@ -46,83 +46,10 @@ socket.once("configuration", function(data) {
document.querySelector('meta[name="theme-color"]').content = currentTheme.themeColor;
}
- if ("URLSearchParams" in window) {
- handleQueryParams();
+ if (document.body.classList.contains("public")) {
+ window.addEventListener(
+ "beforeunload",
+ () => "Are you sure you want to navigate away from this page?"
+ );
}
});
-
-function handleQueryParams() {
- const params = new URLSearchParams(document.location.search);
-
- const cleanParams = () => {
- // Remove query parameters from url without reloading the page
- const cleanUri = window.location.origin + window.location.pathname + window.location.hash;
- window.history.replaceState({}, document.title, cleanUri);
- };
-
- if (params.has("uri")) {
- // Set default connection settings from IRC protocol links
- const uri =
- params.get("uri") +
- (location.hash.startsWith("#/") ? `#${location.hash.substring(2)}` : location.hash);
- const queryParams = parseIrcUri(uri);
-
- cleanParams();
- router.router.push({name: "Connect", query: queryParams});
- } else if (document.body.classList.contains("public") && document.location.search) {
- // Set default connection settings from url params
- const queryParams = Object.fromEntries(params.entries());
-
- cleanParams();
- router.router.push({name: "Connect", query: queryParams});
- }
-}
-
-function parseIrcUri(stringUri) {
- const data = {};
-
- try {
- // https://tools.ietf.org/html/draft-butcher-irc-url-04
- const uri = new URL(stringUri);
-
- // Replace protocol with a "special protocol" (that's what it's called in WHATWG spec)
- // So that the uri can be properly parsed
- if (uri.protocol === "irc:") {
- uri.protocol = "http:";
-
- if (!uri.port) {
- uri.port = 6667;
- }
-
- data.tls = false;
- } else if (uri.protocol === "ircs:") {
- uri.protocol = "https:";
-
- if (!uri.port) {
- uri.port = 6697;
- }
-
- data.tls = true;
- } else {
- return;
- }
-
- data.host = data.name = uri.hostname;
- data.port = uri.port;
- data.username = window.decodeURIComponent(uri.username);
- data.password = window.decodeURIComponent(uri.password);
-
- let channel = (uri.pathname + uri.hash).substr(1);
- const index = channel.indexOf(",");
-
- if (index > -1) {
- channel = channel.substring(0, index);
- }
-
- data.join = channel;
- } catch (e) {
- // do nothing on invalid uri
- }
-
- return data;
-}
diff --git a/client/js/socket-events/init.js b/client/js/socket-events/init.js
index 0de37555..4109ea7d 100644
--- a/client/js/socket-events/init.js
+++ b/client/js/socket-events/init.js
@@ -3,7 +3,6 @@
const socket = require("../socket");
const webpush = require("../webpush");
const storage = require("../localStorage");
-const constants = require("../constants");
const {initChannel} = require("../vue");
const {router, switchToChannel, navigate} = require("../router");
const store = require("../store").default;
@@ -13,36 +12,23 @@ socket.on("init", function(data) {
store.commit("isConnected", true);
store.commit("currentUserVisibleError", null);
+ if (data.token) {
+ storage.set("token", data.token);
+ }
+
if (!store.state.appLoaded) {
store.commit("appLoaded");
- if (data.token) {
- storage.set("token", data.token);
- }
-
+ // TODO: Try to move webpush key to configuration event
webpush.configurePushNotifications(data.pushSubscription, data.applicationServerKey);
- const viewportWidth = window.outerWidth;
- let isUserlistOpen = storage.get("thelounge.state.userlist");
-
- if (viewportWidth > constants.mobileViewportPixels) {
- store.commit("sidebarOpen", storage.get("thelounge.state.sidebar") !== "false");
- }
-
- // If The Lounge is opened on a small screen (less than 1024px), and we don't have stored
- // user list state, close it by default
- if (viewportWidth >= 1024 && isUserlistOpen !== "true" && isUserlistOpen !== "false") {
- isUserlistOpen = "true";
- }
-
- store.commit("userlistOpen", isUserlistOpen === "true");
-
document.body.classList.remove("signed-out");
if (window.g_TheLoungeRemoveLoading) {
window.g_TheLoungeRemoveLoading();
}
+ // TODO: Review this code and make it better
if (!router.currentRoute.name || router.currentRoute.name === "SignIn") {
const channel = store.getters.findChannel(data.active);
@@ -56,13 +42,10 @@ socket.on("init", function(data) {
navigate("Connect");
}
}
- }
- if (document.body.classList.contains("public")) {
- window.addEventListener(
- "beforeunload",
- () => "Are you sure you want to navigate away from this page?"
- );
+ if ("URLSearchParams" in window) {
+ handleQueryParams();
+ }
}
});
@@ -169,3 +152,79 @@ function mergeChannelData(oldChannels, newChannels) {
return newChannels;
}
+
+function handleQueryParams() {
+ const params = new URLSearchParams(document.location.search);
+
+ const cleanParams = () => {
+ // Remove query parameters from url without reloading the page
+ const cleanUri = window.location.origin + window.location.pathname + window.location.hash;
+ window.history.replaceState({}, document.title, cleanUri);
+ };
+
+ if (params.has("uri")) {
+ // Set default connection settings from IRC protocol links
+ const uri =
+ params.get("uri") +
+ (location.hash.startsWith("#/") ? `#${location.hash.substring(2)}` : location.hash);
+ const queryParams = parseIrcUri(uri);
+
+ cleanParams();
+ router.router.push({name: "Connect", query: queryParams});
+ } else if (document.body.classList.contains("public") && document.location.search) {
+ // Set default connection settings from url params
+ const queryParams = Object.fromEntries(params.entries());
+
+ cleanParams();
+ router.router.push({name: "Connect", query: queryParams});
+ }
+}
+
+function parseIrcUri(stringUri) {
+ const data = {};
+
+ try {
+ // https://tools.ietf.org/html/draft-butcher-irc-url-04
+ const uri = new URL(stringUri);
+
+ // Replace protocol with a "special protocol" (that's what it's called in WHATWG spec)
+ // So that the uri can be properly parsed
+ if (uri.protocol === "irc:") {
+ uri.protocol = "http:";
+
+ if (!uri.port) {
+ uri.port = 6667;
+ }
+
+ data.tls = false;
+ } else if (uri.protocol === "ircs:") {
+ uri.protocol = "https:";
+
+ if (!uri.port) {
+ uri.port = 6697;
+ }
+
+ data.tls = true;
+ } else {
+ return;
+ }
+
+ data.host = data.name = uri.hostname;
+ data.port = uri.port;
+ data.username = window.decodeURIComponent(uri.username);
+ data.password = window.decodeURIComponent(uri.password);
+
+ let channel = (uri.pathname + uri.hash).substr(1);
+ const index = channel.indexOf(",");
+
+ if (index > -1) {
+ channel = channel.substring(0, index);
+ }
+
+ data.join = channel;
+ } catch (e) {
+ // do nothing on invalid uri
+ }
+
+ return data;
+}
diff --git a/client/js/upload.js b/client/js/upload.js
index 9b2ee09b..cdfcbc29 100644
--- a/client/js/upload.js
+++ b/client/js/upload.js
@@ -5,8 +5,7 @@ const updateCursor = require("undate").update;
const store = require("./store").default;
class Uploader {
- init(maxFileSize) {
- this.maxFileSize = maxFileSize;
+ init() {
this.xhr = null;
this.fileQueue = [];
@@ -98,9 +97,10 @@ class Uploader {
}
const wasQueueEmpty = this.fileQueue.length === 0;
+ const maxFileSize = store.state.serverConfiguration.fileUploadMaxFileSize;
for (const file of files) {
- if (this.maxFileSize > 0 && file.size > this.maxFileSize) {
+ if (maxFileSize > 0 && file.size > maxFileSize) {
this.handleResponse({
error: `File ${file.name} is over the maximum allowed size`,
});
From 17365d9967c2fa13a787210e426552a64fa7bf45 Mon Sep 17 00:00:00 2001
From: Pavel Djundik
Date: Tue, 12 Nov 2019 17:51:40 +0200
Subject: [PATCH 083/111] Remove references to vue.js
---
client/components/InlineChannel.vue | 4 +--
client/js/socket-events/init.js | 5 ++--
client/js/socket-events/join.js | 3 +--
client/js/socket-events/network.js | 3 +--
client/js/store.js | 18 +++++++++++++
client/js/vue.js | 41 +++++------------------------
webpack.config-test.js | 13 +++++++++
7 files changed, 44 insertions(+), 43 deletions(-)
diff --git a/client/components/InlineChannel.vue b/client/components/InlineChannel.vue
index 82fbae8a..daab67f7 100644
--- a/client/components/InlineChannel.vue
+++ b/client/components/InlineChannel.vue
@@ -5,6 +5,8 @@
diff --git a/client/components/ChannelWrapper.vue b/client/components/ChannelWrapper.vue
index b1edf7ba..b5c4b0a0 100644
--- a/client/components/ChannelWrapper.vue
+++ b/client/components/ChannelWrapper.vue
@@ -24,6 +24,7 @@
:style="closed ? {transition: 'none', opacity: 0.4} : null"
role="tab"
@click="click"
+ @contextmenu.prevent="openContextMenu"
>
@@ -31,6 +32,7 @@
diff --git a/client/components/Chat.vue b/client/components/Chat.vue
index 7920bf08..d210eaed 100644
--- a/client/components/Chat.vue
+++ b/client/components/Chat.vue
@@ -38,7 +38,11 @@
:network="network"
:text="channel.topic"
/>
-
+
diff --git a/client/components/ChatUserList.vue b/client/components/ChatUserList.vue
index ed6f2e5a..5aeaffe3 100644
--- a/client/components/ChatUserList.vue
+++ b/client/components/ChatUserList.vue
@@ -32,6 +32,7 @@
:on-hover="hoverUser"
:active="user.original === activeUser"
:user="user"
+ :context-menu-callback="openContextMenu"
/>
@@ -41,6 +42,7 @@
:on-hover="hoverUser"
:active="user === activeUser"
:user="user"
+ :context-menu-callback="openContextMenu"
/>
@@ -52,6 +54,7 @@
const fuzzy = require("fuzzy");
import Username from "./Username.vue";
import UsernameFiltered from "./UsernameFiltered.vue";
+import {generateUserContextMenu} from "../js/helpers/contextMenu.js";
const modes = {
"~": "owner",
@@ -194,6 +197,11 @@ export default {
el.scrollIntoView({block: "nearest", inline: "nearest"});
});
},
+ openContextMenu(event, user) {
+ const {network} = this.$store.getters.findChannel(this.channel.id);
+ const items = generateUserContextMenu(this.$root, this.channel, network, user);
+ this.$root.$refs.app.openContextMenu(event, items);
+ },
},
};
diff --git a/client/components/ContextMenu.vue b/client/components/ContextMenu.vue
new file mode 100644
index 00000000..c69565cc
--- /dev/null
+++ b/client/components/ContextMenu.vue
@@ -0,0 +1,141 @@
+
+
+
+
+
diff --git a/client/components/Message.vue b/client/components/Message.vue
index 82ef608d..8229cca9 100644
--- a/client/components/Message.vue
+++ b/client/components/Message.vue
@@ -20,10 +20,11 @@
*
-
+
<
-
+
>
@@ -50,7 +51,7 @@
-
-
+
-
@@ -73,6 +74,7 @@ import Username from "./Username.vue";
import LinkPreview from "./LinkPreview.vue";
import ParsedMessage from "./ParsedMessage.vue";
import MessageTypes from "./MessageTypes";
+import {generateUserContextMenu} from "../js/helpers/contextMenu.js";
const constants = require("../js/constants");
@@ -85,6 +87,7 @@ export default {
components: MessageTypes,
props: {
message: Object,
+ channel: Object,
network: Object,
keepScrollPosition: Function,
},
@@ -104,6 +107,10 @@ export default {
isAction() {
return typeof MessageTypes["message-" + this.message.type] !== "undefined";
},
+ openUserContextMenu($event, user) {
+ const items = generateUserContextMenu(this.$root, this.channel, this.network, user);
+ this.$root.$refs.app.openContextMenu(event, items);
+ },
},
};
diff --git a/client/components/MessageList.vue b/client/components/MessageList.vue
index 54d2b9d5..561cbdac 100644
--- a/client/components/MessageList.vue
+++ b/client/components/MessageList.vue
@@ -42,6 +42,7 @@
{{ user.mode }}{{ user.nick }}
@@ -17,6 +18,7 @@ export default {
user: Object,
active: Boolean,
onHover: Function,
+ contextMenuCallback: Function,
},
computed: {
nickColor() {
@@ -27,6 +29,11 @@ export default {
hover() {
return this.onHover(this.user);
},
+ rightClick($event) {
+ if (this.contextMenuCallback) {
+ this.contextMenuCallback($event, this.user);
+ }
+ },
},
};
diff --git a/client/components/UsernameFiltered.vue b/client/components/UsernameFiltered.vue
index 769bff16..1f50c364 100644
--- a/client/components/UsernameFiltered.vue
+++ b/client/components/UsernameFiltered.vue
@@ -4,6 +4,7 @@
:data-name="user.original.nick"
role="button"
@mouseover="hover"
+ @contextmenu.prevent="rightClick($event)"
v-html="user.original.mode + user.string"
/>
@@ -17,6 +18,7 @@ export default {
user: Object,
active: Boolean,
onHover: Function,
+ contextMenuCallback: Function,
},
computed: {
nickColor() {
@@ -27,6 +29,11 @@ export default {
hover() {
this.onHover ? this.onHover(this.user.original) : null;
},
+ rightClick($event) {
+ if (this.contextMenuCallback) {
+ this.contextMenuCallback($event, this.user);
+ }
+ },
},
};
diff --git a/client/css/style.css b/client/css/style.css
index fdd6cf12..2ec817c8 100644
--- a/client/css/style.css
+++ b/client/css/style.css
@@ -2067,7 +2067,6 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */
}
#context-menu-container {
- display: none;
position: absolute;
top: 0;
left: 0;
diff --git a/client/index.html.tpl b/client/index.html.tpl
index c0b98556..86fd1cd5 100644
--- a/client/index.html.tpl
+++ b/client/index.html.tpl
@@ -64,7 +64,6 @@
-
diff --git a/client/js/contextMenu.js b/client/js/contextMenu.js
deleted file mode 100644
index 31a68105..00000000
--- a/client/js/contextMenu.js
+++ /dev/null
@@ -1,172 +0,0 @@
-"use strict";
-
-const $ = require("jquery");
-const Mousetrap = require("mousetrap");
-
-const contextMenuContainer = $("#context-menu-container");
-
-module.exports = class ContextMenu {
- constructor(contextMenuItems, contextMenuActions, selectedElement, event) {
- this.previousActiveElement = document.activeElement;
- this.contextMenuItems = contextMenuItems;
- this.contextMenuActions = contextMenuActions;
- this.selectedElement = selectedElement;
- this.event = event;
- }
-
- show() {
- const contextMenu = showContextMenu(
- this.contextMenuItems,
- this.selectedElement,
- this.event
- );
- this.bindEvents(contextMenu);
- return false;
- }
-
- hide() {
- contextMenuContainer
- .hide()
- .empty()
- .off(".contextMenu");
-
- Mousetrap.unbind("escape");
- }
-
- bindEvents(contextMenu) {
- const contextMenuActions = this.contextMenuActions;
-
- contextMenuActions.execute = (id, ...args) =>
- contextMenuActions[id] && contextMenuActions[id](...args);
-
- const clickItem = (item) => {
- const itemData = item.attr("data-data");
- const contextAction = item.attr("data-action");
-
- this.hide();
-
- contextMenuActions.execute(contextAction, itemData);
- };
-
- contextMenu.on("click", ".context-menu-item", function() {
- clickItem($(this));
- });
-
- const trap = Mousetrap(contextMenu.get(0));
-
- trap.bind(["up", "down"], (e, key) => {
- const items = contextMenu.find(".context-menu-item");
-
- let index = items.toArray().findIndex((item) => $(item).is(":focus"));
-
- if (key === "down") {
- index = (index + 1) % items.length;
- } else {
- index = Math.max(index, 0) - 1;
- }
-
- items.eq(index).trigger("focus");
- });
-
- trap.bind("enter", () => {
- const item = contextMenu.find(".context-menu-item:focus");
-
- if (item.length) {
- clickItem(item);
- }
-
- return false;
- });
-
- // Hide context menu when clicking or right clicking outside of it
- contextMenuContainer.on("click.contextMenu contextmenu.contextMenu", (e) => {
- // Do not close the menu when clicking inside of the context menu (e.g. on a divider)
- if ($(e.target).prop("id") === "context-menu") {
- return;
- }
-
- this.hide();
- return false;
- });
-
- // Hide the context menu when pressing escape within the context menu container
- Mousetrap.bind("escape", () => {
- this.hide();
-
- // Return focus to the previously focused element
- $(this.previousActiveElement).trigger("focus");
-
- return false;
- });
- }
-};
-
-function showContextMenu(contextMenuItems, selectedElement, event) {
- const target = $(event.currentTarget);
- const contextMenu = $("
", {
- id: "context-menu",
- role: "menu",
- });
-
- for (const item of contextMenuItems) {
- if (item.check(target)) {
- if (item.divider) {
- contextMenu.append('');
- } else {
- //
- contextMenu.append(
- $("", {
- class:
- "context-menu-item context-menu-" +
- (typeof item.className === "function"
- ? item.className(target)
- : item.className),
- text:
- typeof item.displayName === "function"
- ? item.displayName(target)
- : item.displayName,
- "data-action": item.actionId,
- "data-data":
- typeof item.data === "function" ? item.data(target) : item.data,
- tabindex: 0,
- role: "menuitem",
- })
- );
- }
- }
- }
-
- contextMenuContainer.html(contextMenu).show();
-
- contextMenu
- .css(positionContextMenu(contextMenu, selectedElement, event))
- .find(".context-menu-item:first-child")
- .trigger("focus");
-
- return contextMenu;
-}
-
-function positionContextMenu(contextMenu, selectedElement, e) {
- let offset;
- const menuWidth = contextMenu.outerWidth();
- const menuHeight = contextMenu.outerHeight();
-
- if (selectedElement.hasClass("menu")) {
- offset = selectedElement.offset();
- offset.left -= menuWidth - selectedElement.outerWidth();
- offset.top += selectedElement.outerHeight();
- return offset;
- }
-
- offset = {left: e.pageX, top: e.pageY};
-
- if (window.innerWidth - offset.left < menuWidth) {
- offset.left = window.innerWidth - menuWidth;
- }
-
- if (window.innerHeight - offset.top < menuHeight) {
- offset.top = window.innerHeight - menuHeight;
- }
-
- return offset;
-}
diff --git a/client/js/contextMenuFactory.js b/client/js/contextMenuFactory.js
deleted file mode 100644
index 4694414c..00000000
--- a/client/js/contextMenuFactory.js
+++ /dev/null
@@ -1,438 +0,0 @@
-"use strict";
-
-import Vue from "vue";
-
-const $ = require("jquery");
-const socket = require("./socket");
-const utils = require("./utils");
-const ContextMenu = require("./contextMenu");
-const contextMenuActions = [];
-const contextMenuItems = [];
-const {switchToChannel, navigate} = require("./router");
-const store = require("./store").default;
-
-addDefaultItems();
-registerEvents();
-
-/**
- * Used for adding context menu items. eg:
- *
- * addContextMenuItem({
- * check: (target) => target.hasClass("user"),
- * className: "customItemName",
- * data: (target) => target.attr("data-name"),
- * displayName: "Do something",
- * callback: (name) => console.log(name), // print the name of the user to console
- * });
- *
- * @param opts
- * @param {function(Object)} [opts.check] - Function to check whether item should show on the context menu, called with the target jquery element, shows if return is truthy
- * @param {string|function(Object)} opts.className - class name for the menu item, should be prefixed for non-default menu items (if function, called with jquery element, and uses return value)
- * @param {string|function(Object)} opts.data - data that will be sent to the callback function (if function, called with jquery element, and uses return value)
- * @param {string|function(Object)} opts.displayName - text to display on the menu item (if function, called with jquery element, and uses return value)
- * @param {function(Object)} opts.callback - Function to call when the context menu item is clicked, called with the data requested in opts.data
- */
-function addContextMenuItem(opts) {
- opts.check = opts.check || (() => true);
- opts.actionId = contextMenuActions.push(opts.callback) - 1;
- contextMenuItems.push(opts);
-}
-
-function addContextDivider(opts) {
- opts.check = opts.check || (() => true);
- opts.divider = true;
- contextMenuItems.push(opts);
-}
-
-function createContextMenu(that, event) {
- return new ContextMenu(contextMenuItems, contextMenuActions, that, event);
-}
-
-function addWhoisItem() {
- function whois(itemData) {
- const chan = store.getters.findChannelOnCurrentNetwork(itemData);
-
- if (chan) {
- switchToChannel(chan);
- }
-
- socket.emit("input", {
- target: Number($("#chat").attr("data-id")),
- text: "/whois " + itemData,
- });
- }
-
- addContextMenuItem({
- check: (target) => target.hasClass("user"),
- className: "user",
- displayName: (target) => target.attr("data-name"),
- data: (target) => target.attr("data-name"),
- callback: whois,
- });
-
- addContextDivider({
- check: (target) => target.hasClass("user"),
- });
-
- addContextMenuItem({
- check: (target) => target.hasClass("user") || target.hasClass("query"),
- className: "action-whois",
- displayName: "User information",
- data: (target) => target.attr("data-name") || target.attr("aria-label"),
- callback: whois,
- });
-}
-
-function addQueryItem() {
- function query(itemData) {
- const chan = store.getters.findChannelOnCurrentNetwork(itemData);
-
- if (chan) {
- switchToChannel(chan);
- }
-
- socket.emit("input", {
- target: Number($("#chat").attr("data-id")),
- text: "/query " + itemData,
- });
- }
-
- addContextMenuItem({
- check: (target) => target.hasClass("user"),
- className: "action-query",
- displayName: "Direct messages",
- data: (target) => target.attr("data-name"),
- callback: query,
- });
-}
-
-function addCloseItem() {
- function getCloseDisplay(target) {
- if (target.hasClass("lobby")) {
- return "Remove";
- } else if (target.hasClass("channel")) {
- return "Leave";
- }
-
- return "Close";
- }
-
- addContextMenuItem({
- check: (target) => target.hasClass("chan"),
- className: "close",
- displayName: getCloseDisplay,
- data: (target) => target.attr("data-target"),
- callback(itemData) {
- const close = document.querySelector(
- `.networks .chan[data-target="${itemData}"] .close`
- );
-
- if (close) {
- // TODO: After context menus are ported to Vue, figure out a direct api
- close.click();
- }
- },
- });
-}
-
-function addConnectItem() {
- function connect(itemData) {
- socket.emit("input", {
- target: Number(itemData),
- text: "/connect",
- });
- }
-
- addContextMenuItem({
- check: (target) => target.hasClass("lobby") && target.parent().hasClass("not-connected"),
- className: "connect",
- displayName: "Connect",
- data: (target) => target.attr("data-id"),
- callback: connect,
- });
-}
-
-function addDisconnectItem() {
- function disconnect(itemData) {
- socket.emit("input", {
- target: Number(itemData),
- text: "/disconnect",
- });
- }
-
- addContextMenuItem({
- check: (target) => target.hasClass("lobby") && !target.parent().hasClass("not-connected"),
- className: "disconnect",
- displayName: "Disconnect",
- data: (target) => target.attr("data-id"),
- callback: disconnect,
- });
-}
-
-function addKickItem() {
- function kick(itemData) {
- socket.emit("input", {
- target: Number($("#chat").attr("data-id")),
- text: "/kick " + itemData,
- });
- }
-
- addContextMenuItem({
- check: (target) =>
- utils.hasRoleInChannel(target.closest(".chan"), ["op"]) &&
- target.closest(".chan").attr("data-type") === "channel",
- className: "action-kick",
- displayName: "Kick",
- data: (target) => target.attr("data-name"),
- callback: kick,
- });
-}
-
-function addOpItem() {
- function op(itemData) {
- socket.emit("input", {
- target: Number($("#chat").attr("data-id")),
- text: "/op " + itemData,
- });
- }
-
- addContextMenuItem({
- check: (target) =>
- utils.hasRoleInChannel(target.closest(".chan"), ["op"]) &&
- !utils.hasRoleInChannel(target.closest(".chan"), ["op"], target.attr("data-name")),
- className: "action-op",
- displayName: "Give operator (+o)",
- data: (target) => target.attr("data-name"),
- callback: op,
- });
-}
-
-function addDeopItem() {
- function deop(itemData) {
- socket.emit("input", {
- target: Number($("#chat").attr("data-id")),
- text: "/deop " + itemData,
- });
- }
-
- addContextMenuItem({
- check: (target) =>
- utils.hasRoleInChannel(target.closest(".chan"), ["op"]) &&
- utils.hasRoleInChannel(target.closest(".chan"), ["op"], target.attr("data-name")),
- className: "action-op",
- displayName: "Revoke operator (-o)",
- data: (target) => target.attr("data-name"),
- callback: deop,
- });
-}
-
-function addVoiceItem() {
- function voice(itemData) {
- socket.emit("input", {
- target: Number($("#chat").attr("data-id")),
- text: "/voice " + itemData,
- });
- }
-
- addContextMenuItem({
- check: (target) =>
- utils.hasRoleInChannel(target.closest(".chan"), ["op"]) &&
- !utils.hasRoleInChannel(target.closest(".chan"), ["voice"], target.attr("data-name")),
- className: "action-voice",
- displayName: "Give voice (+v)",
- data: (target) => target.attr("data-name"),
- callback: voice,
- });
-}
-
-function addDevoiceItem() {
- function devoice(itemData) {
- socket.emit("input", {
- target: Number($("#chat").attr("data-id")),
- text: "/devoice " + itemData,
- });
- }
-
- addContextMenuItem({
- check: (target) =>
- utils.hasRoleInChannel(target.closest(".chan"), ["op"]) &&
- utils.hasRoleInChannel(target.closest(".chan"), ["voice"], target.attr("data-name")),
- className: "action-voice",
- displayName: "Revoke voice (-v)",
- data: (target) => target.attr("data-name"),
- callback: devoice,
- });
-}
-
-function addFocusItem() {
- function focusChan(itemData) {
- $(`.networks .chan[data-target="${itemData}"]`).click();
- }
-
- const getClass = (target) => {
- if (target.hasClass("lobby")) {
- return "network";
- } else if (target.hasClass("query")) {
- return "query";
- }
-
- return "chan";
- };
-
- addContextMenuItem({
- check: (target) => target.hasClass("chan"),
- className: getClass,
- displayName: (target) => target.attr("data-name") || target.attr("aria-label"),
- data: (target) => target.attr("data-target"),
- callback: focusChan,
- });
-
- addContextDivider({
- check: (target) => target.hasClass("chan"),
- });
-}
-
-function addEditNetworkItem() {
- function edit(networkUuid) {
- navigate("NetworkEdit", {uuid: networkUuid});
- }
-
- addContextMenuItem({
- check: (target) => target.hasClass("lobby"),
- className: "edit",
- displayName: "Edit this network…",
- data: (target) => target.closest(".network").attr("data-uuid"),
- callback: edit,
- });
-}
-
-function addChannelListItem() {
- function list(itemData) {
- socket.emit("input", {
- target: parseInt(itemData, 10),
- text: "/list",
- });
- }
-
- addContextMenuItem({
- check: (target) => target.hasClass("lobby"),
- className: "list",
- displayName: "List all channels",
- data: (target) => target.attr("data-id"),
- callback: list,
- });
-}
-
-function addEditTopicItem() {
- function setEditTopic(itemData) {
- store.getters.findChannel(Number(itemData)).channel.editTopic = true;
- document.querySelector(`#sidebar .chan[data-id="${Number(itemData)}"]`).click();
-
- Vue.nextTick(() => {
- document.querySelector(`#chan-${Number(itemData)} .topic-input`).focus();
- });
- }
-
- addContextMenuItem({
- check: (target) => target.hasClass("channel"),
- className: "edit",
- displayName: "Edit topic",
- data: (target) => target.attr("data-id"),
- callback: setEditTopic,
- });
-}
-
-function addBanListItem() {
- function banlist(itemData) {
- socket.emit("input", {
- target: parseInt(itemData, 10),
- text: "/banlist",
- });
- }
-
- addContextMenuItem({
- check: (target) => target.hasClass("channel"),
- className: "list",
- displayName: "List banned users",
- data: (target) => target.attr("data-id"),
- callback: banlist,
- });
-}
-
-function addJoinItem() {
- function openJoinForm(itemData) {
- store.getters.findChannel(Number(itemData)).network.isJoinChannelShown = true;
- }
-
- addContextMenuItem({
- check: (target) => target.hasClass("lobby"),
- className: "join",
- displayName: "Join a channel…",
- data: (target) => target.attr("data-id"),
- callback: openJoinForm,
- });
-}
-
-function addIgnoreListItem() {
- function ignorelist(itemData) {
- socket.emit("input", {
- target: parseInt(itemData, 10),
- text: "/ignorelist",
- });
- }
-
- addContextMenuItem({
- check: (target) => target.hasClass("lobby"),
- className: "list",
- displayName: "List ignored users",
- data: (target) => target.attr("data-id"),
- callback: ignorelist,
- });
-}
-
-function addDefaultItems() {
- addFocusItem();
- addWhoisItem();
- addQueryItem();
- addKickItem();
- addOpItem();
- addDeopItem();
- addVoiceItem();
- addDevoiceItem();
- addEditNetworkItem();
- addJoinItem();
- addChannelListItem();
- addEditTopicItem();
- addBanListItem();
- addIgnoreListItem();
- addConnectItem();
- addDisconnectItem();
- addCloseItem();
-}
-
-function registerEvents() {
- const viewport = $("#viewport");
-
- viewport.on("contextmenu", ".network .chan", function(e) {
- return createContextMenu($(this), e).show();
- });
-
- viewport.on("click contextmenu", ".user", function(e) {
- // If user is selecting text, do not open context menu
- // This primarily only targets mobile devices where selection is performed with touch
- if (!window.getSelection().isCollapsed) {
- return true;
- }
-
- return createContextMenu($(this), e).show();
- });
-
- viewport.on("click", "#chat .menu", function(e) {
- e.currentTarget = $(
- `#sidebar .chan[data-id="${$(this)
- .closest(".chan")
- .attr("data-id")}"]`
- )[0];
- return createContextMenu($(this), e).show();
- });
-}
diff --git a/client/js/helpers/contextMenu.js b/client/js/helpers/contextMenu.js
new file mode 100644
index 00000000..9f85e675
--- /dev/null
+++ b/client/js/helpers/contextMenu.js
@@ -0,0 +1,278 @@
+"use strict";
+import socket from "../socket";
+
+export function generateChannelContextMenu($root, channel, network) {
+ const typeMap = {
+ lobby: "network",
+ channel: "chan",
+ query: "query",
+ special: "chan",
+ };
+
+ const closeMap = {
+ lobby: "Remove",
+ channel: "Leave",
+ query: "Close",
+ special: "Close",
+ };
+
+ let items = [
+ {
+ label: channel.name,
+ type: "item",
+ class: typeMap[channel.type],
+ link: `/chan-${channel.id}`,
+ },
+ {
+ type: "divider",
+ },
+ ];
+
+ // Add menu items for lobbies
+ if (channel.type === "lobby") {
+ items = [
+ ...items,
+ {
+ label: "Edit this network…",
+ type: "item",
+ class: "edit",
+ link: `/edit-network/${network.uuid}`,
+ },
+ {
+ label: "Join a channel…",
+ type: "item",
+ class: "join",
+ action: () => (network.isJoinChannelShown = true),
+ },
+ {
+ label: "List all channels",
+ type: "item",
+ class: "list",
+ action: () =>
+ socket.emit("input", {
+ target: channel.id,
+ text: "/list",
+ }),
+ },
+ {
+ label: "List ignored users",
+ type: "item",
+ class: "list",
+ action: () =>
+ socket.emit("input", {
+ target: channel.id,
+ text: "/ignorelist",
+ }),
+ },
+ network.status.connected
+ ? {
+ label: "Disconnect",
+ type: "item",
+ class: "disconnect",
+ action: () =>
+ socket.emit("input", {
+ target: channel.id,
+ text: "/disconnect",
+ }),
+ }
+ : {
+ label: "Connect",
+ type: "item",
+ class: "connect",
+ action: () =>
+ socket.emit("input", {
+ target: channel.id,
+ text: "/connect",
+ }),
+ },
+ ];
+ }
+
+ // Add menu items for channels
+ if (channel.type === "channel") {
+ items = [
+ ...items,
+ {
+ label: "Edit topic",
+ type: "item",
+ class: "edit",
+ action() {
+ channel.editTopic = true;
+ $root.switchToChannel(channel);
+
+ $root.$nextTick(() =>
+ document.querySelector(`#chan-${channel.id} .topic-input`).focus()
+ );
+ },
+ },
+ {
+ label: "List banned users",
+ type: "item",
+ class: "list",
+ action() {
+ socket.emit("input", {
+ target: channel.id,
+ text: "/banlist",
+ });
+ },
+ },
+ ];
+ }
+
+ // Add menu items for queries
+ if (channel.type === "query") {
+ items = [
+ ...items,
+ {
+ label: "User information",
+ type: "item",
+ class: "action-whois",
+ action() {
+ $root.switchToChannel(channel);
+ socket.emit("input", {
+ target: $root.$store.state.activeChannel.channel.id,
+ text: "/whois " + channel.name,
+ });
+ },
+ },
+ ];
+ }
+
+ // Add close menu item
+ items.push({
+ label: closeMap[channel.type],
+ type: "item",
+ class: "close",
+ action() {
+ const close = document.querySelector(
+ `.networks .chan[data-target="#chan-${channel.id}"] .close`
+ );
+
+ if (close) {
+ close.click();
+ }
+ },
+ });
+
+ return items;
+}
+
+export function generateUserContextMenu($root, channel, network, user) {
+ const currentChannelUser = channel.users.filter((u) => u.nick === network.nick)[0];
+
+ const whois = () => {
+ const chan = $root.$store.getters.findChannelOnCurrentNetwork(user.nick);
+
+ if (chan) {
+ $root.switchToChannel(chan);
+ }
+
+ socket.emit("input", {
+ target: channel.id,
+ text: "/whois " + user.nick,
+ });
+ };
+
+ const items = [
+ {
+ label: user.nick,
+ type: "item",
+ class: "user",
+ action: whois,
+ },
+ {
+ type: "divider",
+ },
+ {
+ label: "User information",
+ type: "item",
+ class: "action-whois",
+ action: whois,
+ },
+ {
+ label: "Direct messages",
+ type: "item",
+ class: "action-query",
+ action() {
+ const chan = $root.$store.getters.findChannelOnCurrentNetwork(user.nick);
+
+ if (chan) {
+ $root.switchToChannel(chan);
+ }
+
+ socket.emit("input", {
+ target: channel.id,
+ text: "/query " + user.nick,
+ });
+ },
+ },
+ ];
+
+ if (currentChannelUser.mode === "@") {
+ items.push({
+ label: "Kick",
+ type: "item",
+ class: "action-kick",
+ action() {
+ socket.emit("input", {
+ target: channel.id,
+ text: "/kick " + user.nick,
+ });
+ },
+ });
+
+ if (user.mode === "@") {
+ items.push({
+ label: "Revoke operator (-o)",
+ type: "item",
+ class: "action-op",
+ action() {
+ socket.emit("input", {
+ target: channel.id,
+ text: "/deop " + user.nick,
+ });
+ },
+ });
+ } else {
+ items.push({
+ label: "Give operator (+o)",
+ type: "item",
+ class: "action-op",
+ action() {
+ socket.emit("input", {
+ target: channel.id,
+ text: "/op " + user.nick,
+ });
+ },
+ });
+ }
+
+ if (user.mode === "+") {
+ items.push({
+ label: "Revoke voice (-v)",
+ type: "item",
+ class: "action-voice",
+ action() {
+ socket.emit("input", {
+ target: channel.id,
+ text: "/devoice " + user.nick,
+ });
+ },
+ });
+ } else {
+ items.push({
+ label: "Give voice (+v)",
+ type: "item",
+ class: "action-voice",
+ action() {
+ socket.emit("input", {
+ target: channel.id,
+ text: "/voice " + user.nick,
+ });
+ },
+ });
+ }
+ }
+
+ return items;
+}
diff --git a/client/js/helpers/parse.js b/client/js/helpers/parse.js
index a93f70dc..4c7358fb 100644
--- a/client/js/helpers/parse.js
+++ b/client/js/helpers/parse.js
@@ -12,6 +12,8 @@ const LinkPreviewToggle = require("../../components/LinkPreviewToggle.vue").defa
const LinkPreviewFileSize = require("../../components/LinkPreviewFileSize.vue").default;
const InlineChannel = require("../../components/InlineChannel.vue").default;
const emojiModifiersRegex = /[\u{1f3fb}-\u{1f3ff}]/gu;
+const store = require("../store").default;
+const {generateUserContextMenu} = require("./contextMenu");
// Create an HTML `span` with styling information for a given fragment
function createFragment(fragment, createElement) {
@@ -69,7 +71,13 @@ function createFragment(fragment, createElement) {
// Transform an IRC message potentially filled with styling control codes, URLs,
// nicknames, and channels into a string of HTML elements to display on the client.
-module.exports = function parse(createElement, text, message = undefined, network = undefined) {
+module.exports = function parse(
+ createElement,
+ text,
+ message = undefined,
+ network = undefined,
+ $root
+) {
// Extract the styling information and get the plain text version from it
const styleFragments = parseStyle(text);
const cleanText = styleFragments.map((fragment) => fragment.text).join("");
@@ -188,6 +196,23 @@ module.exports = function parse(createElement, text, message = undefined, networ
dir: "auto",
"data-name": textPart.nick,
},
+ on: {
+ contextmenu($event) {
+ $event.preventDefault();
+ const channel = store.state.activeChannel.channel;
+ let user = channel.users.find((u) => u.nick === textPart.nick);
+
+ if (!user) {
+ user = {
+ nick: textPart.nick,
+ mode: "",
+ };
+ }
+
+ const items = generateUserContextMenu($root, channel, network, user);
+ $root.$refs.app.openContextMenu($event, items);
+ },
+ },
},
fragments
);
diff --git a/client/js/utils.js b/client/js/utils.js
deleted file mode 100644
index 61f8a0fc..00000000
--- a/client/js/utils.js
+++ /dev/null
@@ -1,21 +0,0 @@
-"use strict";
-
-const $ = require("jquery");
-const escape = require("css.escape");
-
-module.exports = {
- hasRoleInChannel,
-};
-
-// Given a channel element will determine if the lounge user or a given nick is one of the supplied roles.
-function hasRoleInChannel(channel, roles, nick) {
- if (!channel || !roles) {
- return false;
- }
-
- const channelID = channel.attr("data-id");
- const network = $("#sidebar .network").has(`.chan[data-id="${channelID}"]`);
- const target = nick || network.attr("data-nick");
- const user = channel.find(`.names .user[data-name="${escape(target)}"]`).first();
- return user.parent().is("." + roles.join(", ."));
-}
diff --git a/client/js/vue.js b/client/js/vue.js
index df0bb951..6157a9c7 100644
--- a/client/js/vue.js
+++ b/client/js/vue.js
@@ -13,7 +13,6 @@ const socket = require("./socket");
Vue.filter("localetime", localetime);
require("./socket-events");
-require("./contextMenuFactory");
require("./webpush");
require("./keybinds");
From fcf7488e1e7c04c34e6e49e94646aabfb4d8c252 Mon Sep 17 00:00:00 2001
From: Pavel Djundik
Date: Fri, 15 Nov 2019 20:53:38 +0200
Subject: [PATCH 087/111] Remove jquery from autocompletion
---
client/components/ChatInput.vue | 16 +++++++--
client/js/autocompletion.js | 64 +++++++++++++--------------------
2 files changed, 38 insertions(+), 42 deletions(-)
diff --git a/client/components/ChatInput.vue b/client/components/ChatInput.vue
index 2fc4e9dd..a3cec599 100644
--- a/client/components/ChatInput.vue
+++ b/client/components/ChatInput.vue
@@ -8,7 +8,6 @@
:value="channel.pendingMessage"
:placeholder="getInputPlaceholder(channel)"
:aria-label="getInputPlaceholder(channel)"
- class="mousetrap"
@input="setPendingMessage"
@keypress.enter.exact.prevent="onSubmit"
/>
@@ -49,6 +48,7 @@
-
-
-
-
+