Emit an event when a configuration setting gets changed

This commit is contained in:
JC Brand 2021-09-15 17:12:07 +02:00
parent 05dcb4e8d7
commit 9e48fdc91c
6 changed files with 164 additions and 59 deletions

View File

@ -2,6 +2,7 @@
## 9.0.0 (Unreleased)
- Emit a `change` event when a configuration setting changes
- 3 New configuration settings:
- [render_media](https://conversejs.org/docs/html/configuration.html#render-media)
- [allowed_audio_domains](https://conversejs.org/docs/html/configuration.html#allowed-audio-domains)

View File

@ -35,6 +35,7 @@ module.exports = function(config) {
{ pattern: "src/headless/plugins/roster/tests/presence.js", type: 'module' },
{ pattern: "src/headless/plugins/smacks/tests/smacks.js", type: 'module' },
{ pattern: "src/headless/plugins/status/tests/status.js", type: 'module' },
{ pattern: "src/headless/shared/settings/tests/settings.js", type: 'module' },
{ pattern: "src/headless/tests/converse.js", type: 'module' },
{ pattern: "src/headless/tests/eventemitter.js", type: 'module' },
{ pattern: "src/modals/tests/user-details-modal.js", type: 'module' },
@ -63,7 +64,6 @@ module.exports = function(config) {
{ pattern: "src/plugins/mam-views/tests/placeholder.js", type: 'module' },
{ pattern: "src/plugins/minimize/tests/minchats.js", type: 'module' },
{ pattern: "src/plugins/muc-views/tests/autocomplete.js", type: 'module' },
{ pattern: "src/plugins/muc-views/tests/mep.js", type: 'module' },
{ pattern: "src/plugins/muc-views/tests/component.js", type: 'module' },
{ pattern: "src/plugins/muc-views/tests/corrections.js", type: 'module' },
{ pattern: "src/plugins/muc-views/tests/emojis.js", type: 'module' },
@ -72,6 +72,7 @@ module.exports = function(config) {
{ pattern: "src/plugins/muc-views/tests/markers.js", type: 'module' },
{ pattern: "src/plugins/muc-views/tests/me-messages.js", type: 'module' },
{ pattern: "src/plugins/muc-views/tests/mentions.js", type: 'module' },
{ pattern: "src/plugins/muc-views/tests/mep.js", type: 'module' },
{ pattern: "src/plugins/muc-views/tests/modtools.js", type: 'module' },
{ pattern: "src/plugins/muc-views/tests/muc-api.js", type: 'module' },
{ pattern: "src/plugins/muc-views/tests/muc-mentions.js", type: 'module' },

View File

@ -5,6 +5,8 @@ import {
extendAppSettings,
getAppSetting,
getUserSettings,
registerListener,
unregisterListener,
updateAppSettings,
updateUserSettings,
} from '@converse/headless/shared/settings/utils.js';
@ -81,6 +83,40 @@ export const settings_api = {
set (key, val) {
updateAppSettings(key, val);
},
/**
* The `listen` namespace exposes methods for creating event listeners
* (aka handlers) for events related to settings.
*
* @namespace _converse.api.settings.listen
* @memberOf _converse.api.settings
*/
listen: {
/**
* Register an event listener for the passed in event.
* @method _converse.api.settings.listen.on
* @param { ('change') } name - The name of the event to listen for.
* Currently there is only the 'change' event.
* @param { Function } handler - The event handler function
* @param { Object } [context] - The context of the `this` attribute of the
* handler function.
* @example _converse.api.settings.listen.on('change', callback);
*/
on (name, handler, context) {
registerListener(name, handler, context);
},
/**
* To stop listening to an event, you can use the `not` method.
* @method _converse.api.settings.listen.not
* @param { String } name The event's name
* @param { Function } callback The callback method that is to no longer be called when the event fires
* @example _converse.api.settings.listen.not('change', callback);
*/
not (name, handler) {
unregisterListener(name, handler);
}
}
};

View File

@ -0,0 +1,91 @@
/*global mock */
describe("The \"settings\" API", function () {
it("has methods 'get' and 'set' to set configuration settings",
mock.initConverse(null, {'play_sounds': true}, (_converse) => {
const { api } = _converse;
expect(Object.keys(api.settings)).toEqual(["extend", "update", "get", "set", "listen"]);
expect(api.settings.get("play_sounds")).toBe(true);
api.settings.set("play_sounds", false);
expect(api.settings.get("play_sounds")).toBe(false);
api.settings.set({"play_sounds": true});
expect(api.settings.get("play_sounds")).toBe(true);
// Only whitelisted settings allowed.
expect(typeof api.settings.get("non_existing")).toBe("undefined");
api.settings.set("non_existing", true);
expect(typeof api.settings.get("non_existing")).toBe("undefined");
}));
it("extended via settings.extend don't override settings passed in via converse.initialize",
mock.initConverse([], {'emoji_categories': {"travel": ":rocket:"}}, (_converse) => {
expect(_converse.api.settings.get('emoji_categories')?.travel).toBe(':rocket:');
// Test that the extend command doesn't override user-provided site
// settings (i.e. settings passed in via converse.initialize).
_converse.api.settings.extend({'emoji_categories': {"travel": ":motorcycle:", "food": ":burger:"}});
expect(_converse.api.settings.get('emoji_categories')?.travel).toBe(':rocket:');
expect(_converse.api.settings.get('emoji_categories')?.food).toBe(undefined);
}));
it("only overrides the passed in properties",
mock.initConverse([],
{
'root': document.createElement('div').attachShadow({ 'mode': 'open' }),
'emoji_categories': { 'travel': ':rocket:' },
},
(_converse) => {
expect(_converse.api.settings.get('emoji_categories')?.travel).toBe(':rocket:');
// Test that the extend command doesn't override user-provided site
// settings (i.e. settings passed in via converse.initialize).
_converse.api.settings.extend({
'emoji_categories': { 'travel': ':motorcycle:', 'food': ':burger:' },
});
expect(_converse.api.settings.get('emoji_categories').travel).toBe(':rocket:');
expect(_converse.api.settings.get('emoji_categories').food).toBe(undefined);
}
)
);
});
describe("Configuration settings", function () {
describe("when set", function () {
it("will trigger a change event for which listeners can be registered",
mock.initConverse([], {}, function (_converse) {
const { api } = _converse;
let changed;
const callback = (o) => {
changed = o;
}
api.settings.listen.on('change', callback);
api.settings.set('allowed_image_domains', ['conversejs.org']);
expect(changed).toEqual({'allowed_image_domains': ['conversejs.org']});
api.settings.set('allowed_image_domains', ['conversejs.org', 'opkode.com']);
expect(changed).toEqual({'allowed_image_domains': ['conversejs.org', 'opkode.com']});
api.settings.listen.not('change', callback);
api.settings.set('allowed_image_domains', ['conversejs.org', 'opkode.com', 'inverse.chat']);
expect(changed).toEqual({'allowed_image_domains': ['conversejs.org', 'opkode.com']});
api.settings.listen.on('change:allowed_image_domains', callback);
api.settings.set('allowed_video_domains', ['inverse.chat']);
expect(changed).toEqual({'allowed_image_domains': ['conversejs.org', 'opkode.com']});
api.settings.set('allowed_image_domains', ['inverse.chat']);
expect(changed).toEqual(['inverse.chat']);
}));
});
});

View File

@ -1,10 +1,12 @@
import _converse from '@converse/headless/shared/_converse';
import assignIn from 'lodash-es/assignIn';
import isEqual from "lodash-es/isEqual.js";
import isObject from 'lodash-es/isObject';
import log from '@converse/headless/log';
import pick from 'lodash-es/pick';
import u from '@converse/headless/utils/core';
import { DEFAULT_SETTINGS } from './constants.js';
import { Events } from '@converse/skeletor/src/events.js';
import { Model } from '@converse/skeletor/src/model.js';
import { initStorage } from '@converse/headless/utils/storage.js';
@ -12,6 +14,10 @@ let init_settings = {}; // Container for settings passed in via converse.initial
let app_settings = {};
let user_settings; // User settings, populated via api.users.settings
const app_settings_emitter = {};
Object.assign(app_settings_emitter, Events);
export function getAppSettings () {
return app_settings;
}
@ -44,14 +50,36 @@ export function extendAppSettings (settings) {
u.merge(app_settings, updated_settings);
}
export function registerListener (name, func, context) {
app_settings_emitter.on(name, func, context)
}
export function unregisterListener (name, func) {
app_settings_emitter.off(name, func);
}
export function updateAppSettings (key, val) {
const o = {};
if (key == null) return this; // eslint-disable-line no-eq-null
let attrs;
if (isObject(key)) {
assignIn(app_settings, pick(key, Object.keys(DEFAULT_SETTINGS)));
attrs = key;
} else if (typeof key === 'string') {
o[key] = val;
assignIn(app_settings, pick(o, Object.keys(DEFAULT_SETTINGS)));
attrs = {};
attrs[key] = val;
}
const allowed_keys = Object.keys(pick(attrs, Object.keys(DEFAULT_SETTINGS)));
const changed = {};
allowed_keys.forEach(k => {
const val = attrs[k];
if (!isEqual(app_settings[k], val)) {
changed[k] = val;
app_settings[k] = val;
}
});
Object.keys(changed).forEach(k => app_settings_emitter.trigger('change:' + k, changed[k]));
app_settings_emitter.trigger('change', changed);
}
/**

View File

@ -246,59 +246,7 @@ describe("Converse", function() {
}));
});
describe("The \"settings\" API", function() {
it("has methods 'get' and 'set' to set configuration settings",
mock.initConverse(null, {'play_sounds': true}, (_converse) => {
expect(Object.keys(_converse.api.settings)).toEqual(["extend", "update", "get", "set"]);
expect(_converse.api.settings.get("play_sounds")).toBe(true);
_converse.api.settings.set("play_sounds", false);
expect(_converse.api.settings.get("play_sounds")).toBe(false);
_converse.api.settings.set({"play_sounds": true});
expect(_converse.api.settings.get("play_sounds")).toBe(true);
// Only whitelisted settings allowed.
expect(typeof _converse.api.settings.get("non_existing")).toBe("undefined");
_converse.api.settings.set("non_existing", true);
expect(typeof _converse.api.settings.get("non_existing")).toBe("undefined");
}));
it("extended via settings.extend don't override settings passed in via converse.initialize",
mock.initConverse([], {'emoji_categories': {"travel": ":rocket:"}}, (_converse) => {
expect(_converse.api.settings.get('emoji_categories')?.travel).toBe(':rocket:');
// Test that the extend command doesn't override user-provided site
// settings (i.e. settings passed in via converse.initialize).
_converse.api.settings.extend({'emoji_categories': {"travel": ":motorcycle:", "food": ":burger:"}});
expect(_converse.api.settings.get('emoji_categories')?.travel).toBe(':rocket:');
expect(_converse.api.settings.get('emoji_categories')?.food).toBe(undefined);
}));
it("only overrides the passed in properties",
mock.initConverse([],
{
'root': document.createElement('div').attachShadow({ 'mode': 'open' }),
'emoji_categories': { 'travel': ':rocket:' },
},
(_converse) => {
expect(_converse.api.settings.get('emoji_categories')?.travel).toBe(':rocket:');
// Test that the extend command doesn't override user-provided site
// settings (i.e. settings passed in via converse.initialize).
_converse.api.settings.extend({
'emoji_categories': { 'travel': ':motorcycle:', 'food': ':burger:' },
});
expect(_converse.api.settings.get('emoji_categories').travel).toBe(':rocket:');
expect(_converse.api.settings.get('emoji_categories').food).toBe(undefined);
}
)
);
});
describe("The \"plugins\" API", function() {
describe("The \"plugins\" API", function () {
it("only has a method 'add' for registering plugins", mock.initConverse((_converse) => {
expect(Object.keys(converse.plugins)).toEqual(["add"]);
// Cheating a little bit. We clear the plugins to test more easily.
@ -311,7 +259,7 @@ describe("Converse", function() {
_converse.pluggable.plugins = _old_plugins;
}));
describe("The \"plugins.add\" method", function() {
describe("The \"plugins.add\" method", function () {
it("throws an error when multiple plugins attempt to register with the same name",
mock.initConverse((_converse) => { // eslint-disable-line no-unused-vars