瀏覽代碼

Emit an event when a configuration setting gets changed

JC Brand 3 年之前
父節點
當前提交
9e48fdc91c

+ 1 - 0
CHANGES.md

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

+ 2 - 1
karma.conf.js

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

+ 36 - 0
src/headless/shared/settings/api.js

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

+ 91 - 0
src/headless/shared/settings/tests/settings.js

@@ -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']);
+        }));
+    });
+});

+ 32 - 4
src/headless/shared/settings/utils.js

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

+ 2 - 54
src/headless/tests/converse.js

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