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.
This commit is contained in:
parent
703848919c
commit
25da9dd63e
@ -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";
|
||||
|
@ -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");
|
||||
}
|
113
client/js/settings.js
Normal file
113
client/js/settings.js
Normal file
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user