Ver código fonte

Extract affiliation-related methods out of the ChatRoom model

and put them together in a utils file
JC Brand 4 anos atrás
pai
commit
383f5c1d60

+ 3 - 3
spec/mock.js

@@ -323,13 +323,13 @@ window.addEventListener('converse-loaded', () => {
         await mock.receiveOwnMUCPresence(_converse, muc_jid, nick);
         await mock.receiveOwnMUCPresence(_converse, muc_jid, nick);
 
 
         await room_creation_promise;
         await room_creation_promise;
-        const view = _converse.chatboxviews.get(muc_jid);
-        await u.waitUntil(() => (view.model.session.get('connection_status') === converse.ROOMSTATUS.ENTERED));
+        const model = _converse.chatboxes.get(muc_jid);
+        await u.waitUntil(() => (model.session.get('connection_status') === converse.ROOMSTATUS.ENTERED));
 
 
         const affs = _converse.muc_fetch_members;
         const affs = _converse.muc_fetch_members;
         const all_affiliations = Array.isArray(affs) ? affs :  (affs ? ['member', 'admin', 'owner'] : []);
         const all_affiliations = Array.isArray(affs) ? affs :  (affs ? ['member', 'admin', 'owner'] : []);
         await mock.returnMemberLists(_converse, muc_jid, members, all_affiliations);
         await mock.returnMemberLists(_converse, muc_jid, members, all_affiliations);
-        return view.model.messages.fetched;
+        return model.messages.fetched;
     };
     };
 
 
     mock.createContact = async function (_converse, name, ask, requesting, subscription) {
     mock.createContact = async function (_converse, name, ask, requesting, subscription) {

+ 153 - 0
src/headless/plugins/muc/affiliations/utils.js

@@ -0,0 +1,153 @@
+/**
+ * @copyright The Converse.js contributors
+ * @license Mozilla Public License (MPLv2)
+ */
+import log from "@converse/headless/log";
+import { api, converse } from '@converse/headless/core.js';
+import { difference, indexOf } from 'lodash-es';
+import { parseMemberListIQ } from '../parsers.js';
+
+const { Strophe, $iq, u } = converse.env;
+
+/**
+ * Sends an IQ stanza to the server, asking it for the relevant affiliation list .
+ * Returns an array of {@link MemberListItem} objects, representing occupants
+ * that have the given affiliation.
+ * See: https://xmpp.org/extensions/xep-0045.html#modifymember
+ * @param { ("admin"|"owner"|"member") } affiliation
+ * @param { String } muc_jid - The JID of the MUC for which the affiliation list should be fetched
+ * @returns { Promise<MemberListItem[]> }
+ */
+export async function getAffiliationList (affiliation, muc_jid) {
+    const iq = $iq({ 'to': muc_jid, 'type': 'get' })
+        .c('query', { xmlns: Strophe.NS.MUC_ADMIN })
+        .c('item', { 'affiliation': affiliation });
+    const result = await api.sendIQ(iq, null, false);
+    if (result === null) {
+        const err_msg = `Error: timeout while fetching ${affiliation} list for MUC ${muc_jid}`;
+        const err = new Error(err_msg);
+        log.warn(err_msg);
+        log.warn(result);
+        return err;
+    }
+    if (u.isErrorStanza(result)) {
+        const err_msg = `Error: not allowed to fetch ${affiliation} list for MUC ${muc_jid}`;
+        const err = new Error(err_msg);
+        log.warn(err_msg);
+        log.warn(result);
+        return err;
+    }
+    return parseMemberListIQ(result)
+        .filter(p => p)
+        .sort((a, b) => (a.nick < b.nick ? -1 : a.nick > b.nick ? 1 : 0));
+}
+
+/**
+ * Send IQ stanzas to the server to modify affiliations for users in this groupchat.
+ * See: https://xmpp.org/extensions/xep-0045.html#modifymember
+ * @param { Object[] } users
+ * @param { string } users[].jid - The JID of the user whose affiliation will change
+ * @param { Array } users[].affiliation - The new affiliation for this user
+ * @param { string } [users[].reason] - An optional reason for the affiliation change
+ * @returns { Promise }
+ */
+export function setAffiliations (muc_jid, users) {
+    const affiliations = [...new Set(users.map(u => u.affiliation))];
+    return Promise.all(affiliations.map(a => setAffiliation(a, muc_jid, users)));
+}
+
+/**
+ * Send IQ stanzas to the server to set an affiliation for
+ * the provided JIDs.
+ * See: https://xmpp.org/extensions/xep-0045.html#modifymember
+ *
+ * Prosody doesn't accept multiple JIDs' affiliations
+ * being set in one IQ stanza, so as a workaround we send
+ * a separate stanza for each JID.
+ * Related ticket: https://issues.prosody.im/345
+ *
+ * @param { String } affiliation - The affiliation
+ * @param { String|Array<String> } jids - The JID(s) of the MUCs in which the
+ *  affiliations need to be set.
+ * @param { object } members - A map of jids, affiliations and
+ *  optionally reasons. Only those entries with the
+ *  same affiliation as being currently set will be considered.
+ * @returns { Promise } A promise which resolves and fails depending on the XMPP server response.
+ */
+export function setAffiliation (affiliation, muc_jids, members) {
+    if (!Array.isArray(muc_jids)) {
+        muc_jids = [muc_jids];
+    }
+    members = members.filter(m => [undefined, affiliation].includes(m.affiliation));
+    return Promise.all(
+        muc_jids.reduce((acc, jid) => [...acc, ...members.map(m => sendAffiliationIQ(affiliation, jid, m))], [])
+    );
+}
+
+/**
+ * Send an IQ stanza specifying an affiliation change.
+ * @private
+ * @param { String } affiliation: affiliation (could also be stored on the member object).
+ * @param { String } muc_jid: The JID of the MUC in which the affiliation should be set.
+ * @param { Object } member: Map containing the member's jid and optionally a reason and affiliation.
+ */
+function sendAffiliationIQ (affiliation, muc_jid, member) {
+    const iq = $iq({ to: muc_jid, type: 'set' })
+        .c('query', { xmlns: Strophe.NS.MUC_ADMIN })
+        .c('item', {
+            'affiliation': member.affiliation || affiliation,
+            'nick': member.nick,
+            'jid': member.jid
+        });
+    if (member.reason !== undefined) {
+        iq.c('reason', member.reason);
+    }
+    return api.sendIQ(iq);
+}
+
+/**
+ * Given two lists of objects with 'jid', 'affiliation' and
+ * 'reason' properties, return a new list containing
+ * those objects that are new, changed or removed
+ * (depending on the 'remove_absentees' boolean).
+ *
+ * The affiliations for new and changed members stay the
+ * same, for removed members, the affiliation is set to 'none'.
+ *
+ * The 'reason' property is not taken into account when
+ * comparing whether affiliations have been changed.
+ * @param { boolean } exclude_existing - Indicates whether JIDs from
+ *      the new list which are also in the old list
+ *      (regardless of affiliation) should be excluded
+ *      from the delta. One reason to do this
+ *      would be when you want to add a JID only if it
+ *      doesn't have *any* existing affiliation at all.
+ * @param { boolean } remove_absentees - Indicates whether JIDs
+ *      from the old list which are not in the new list
+ *      should be considered removed and therefore be
+ *      included in the delta with affiliation set
+ *      to 'none'.
+ * @param { array } new_list - Array containing the new affiliations
+ * @param { array } old_list - Array containing the old affiliations
+ * @returns { array }
+ */
+export function computeAffiliationsDelta (exclude_existing, remove_absentees, new_list, old_list) {
+    const new_jids = new_list.map(o => o.jid);
+    const old_jids = old_list.map(o => o.jid);
+    // Get the new affiliations
+    let delta = difference(new_jids, old_jids).map(jid => new_list[indexOf(new_jids, jid)]);
+    if (!exclude_existing) {
+        // Get the changed affiliations
+        delta = delta.concat(
+            new_list.filter(item => {
+                const idx = indexOf(old_jids, item.jid);
+                return idx >= 0 ? item.affiliation !== old_list[idx].affiliation : false;
+            })
+        );
+    }
+    if (remove_absentees) {
+        // Get the removed affiliations
+        delta = delta.concat(difference(old_jids, new_jids).map(jid => ({ 'jid': jid, 'affiliation': 'none' })));
+    }
+    return delta;
+}

+ 3 - 2
src/headless/plugins/muc/index.js

@@ -13,7 +13,7 @@ import ChatRoomOccupant from './occupant.js';
 import ChatRoomOccupants from './occupants.js';
 import ChatRoomOccupants from './occupants.js';
 import log from '../../log';
 import log from '../../log';
 import muc_api from './api.js';
 import muc_api from './api.js';
-import muc_utils from './utils.js';
+import { computeAffiliationsDelta } from './affiliations/utils.js';
 import u from '../../utils/form';
 import u from '../../utils/form';
 import { Collection } from '@converse/skeletor/src/collection';
 import { Collection } from '@converse/skeletor/src/collection';
 import { _converse, api, converse } from '../../core.js';
 import { _converse, api, converse } from '../../core.js';
@@ -265,7 +265,8 @@ converse.plugins.add('converse-muc', {
             );
             );
         }
         }
 
 
-        converse.env.muc_utils = muc_utils;
+        // This is for tests (at least until we can import modules inside tests)
+        converse.env.muc_utils = { computeAffiliationsDelta };
         Object.assign(api, muc_api);
         Object.assign(api, muc_api);
 
 
         /* https://xmpp.org/extensions/xep-0045.html
         /* https://xmpp.org/extensions/xep-0045.html

+ 5 - 101
src/headless/plugins/muc/muc.js

@@ -1,14 +1,14 @@
 import log from '../../log';
 import log from '../../log';
-import muc_utils from './utils.js';
 import p from '../../utils/parse-helpers';
 import p from '../../utils/parse-helpers';
 import sizzle from 'sizzle';
 import sizzle from 'sizzle';
 import u from '../../utils/form';
 import u from '../../utils/form';
 import { Model } from '@converse/skeletor/src/model.js';
 import { Model } from '@converse/skeletor/src/model.js';
 import { Strophe, $build, $iq, $msg, $pres } from 'strophe.js/src/strophe';
 import { Strophe, $build, $iq, $msg, $pres } from 'strophe.js/src/strophe';
 import { _converse, api, converse } from '../../core.js';
 import { _converse, api, converse } from '../../core.js';
+import { computeAffiliationsDelta, setAffiliations, getAffiliationList }  from './affiliations/utils.js';
 import { debounce, intersection, invoke, isElement, pick, zipObject } from 'lodash-es';
 import { debounce, intersection, invoke, isElement, pick, zipObject } from 'lodash-es';
 import { isArchived } from '@converse/headless/shared/parsers';
 import { isArchived } from '@converse/headless/shared/parsers';
-import { parseMemberListIQ, parseMUCMessage, parseMUCPresence } from './parsers.js';
+import { parseMUCMessage, parseMUCPresence } from './parsers.js';
 import { sendMarker } from '@converse/headless/shared/actions';
 import { sendMarker } from '@converse/headless/shared/actions';
 
 
 const OWNER_COMMANDS = ['owner'];
 const OWNER_COMMANDS = ['owner'];
@@ -1136,29 +1136,6 @@ const ChatRoomMixin = {
         this.features.save(attrs);
         this.features.save(attrs);
     },
     },
 
 
-    /**
-     * Send IQ stanzas to the server to set an affiliation for
-     * the provided JIDs.
-     * See: https://xmpp.org/extensions/xep-0045.html#modifymember
-     *
-     * Prosody doesn't accept multiple JIDs' affiliations
-     * being set in one IQ stanza, so as a workaround we send
-     * a separate stanza for each JID.
-     * Related ticket: https://issues.prosody.im/345
-     *
-     * @private
-     * @method _converse.ChatRoom#setAffiliation
-     * @param { string } affiliation - The affiliation
-     * @param { object } members - A map of jids, affiliations and
-     *      optionally reasons. Only those entries with the
-     *      same affiliation as being currently set will be considered.
-     * @returns { Promise } A promise which resolves and fails depending on the XMPP server response.
-     */
-    setAffiliation (affiliation, members) {
-        members = members.filter(m => m.affiliation === undefined || m.affiliation === affiliation);
-        return Promise.all(members.map(m => this.sendAffiliationIQ(affiliation, m)));
-    },
-
     /**
     /**
      * Given a <field> element, return a copy with a <value> child if
      * Given a <field> element, return a copy with a <value> child if
      * we can find a value for it in this rooms config.
      * we can find a value for it in this rooms config.
@@ -1388,46 +1365,6 @@ const ChatRoomMixin = {
         return this.occupants.findWhere({ 'jid': _converse.bare_jid });
         return this.occupants.findWhere({ 'jid': _converse.bare_jid });
     },
     },
 
 
-    /**
-     * Send an IQ stanza specifying an affiliation change.
-     * @private
-     * @method _converse.ChatRoom#
-     * @param { String } affiliation: affiliation
-     *     (could also be stored on the member object).
-     * @param { Object } member: Map containing the member's jid and
-     *     optionally a reason and affiliation.
-     */
-    sendAffiliationIQ (affiliation, member) {
-        const iq = $iq({ to: this.get('jid'), type: 'set' })
-            .c('query', { xmlns: Strophe.NS.MUC_ADMIN })
-            .c('item', {
-                'affiliation': member.affiliation || affiliation,
-                'nick': member.nick,
-                'jid': member.jid
-            });
-        if (member.reason !== undefined) {
-            iq.c('reason', member.reason);
-        }
-        return api.sendIQ(iq);
-    },
-
-    /**
-     * Send IQ stanzas to the server to modify affiliations for users in this groupchat.
-     *
-     * See: https://xmpp.org/extensions/xep-0045.html#modifymember
-     * @private
-     * @method _converse.ChatRoom#setAffiliations
-     * @param { Object[] } members
-     * @param { string } members[].jid - The JID of the user whose affiliation will change
-     * @param { Array } members[].affiliation - The new affiliation for this user
-     * @param { string } [members[].reason] - An optional reason for the affiliation change
-     * @returns { Promise }
-     */
-    setAffiliations (members) {
-        const affiliations = [...new Set(members.map(m => m.affiliation))];
-        return Promise.all(affiliations.map(a => this.setAffiliation(a, members)));
-    },
-
     /**
     /**
      * Send an IQ stanza to modify an occupant's role
      * Send an IQ stanza to modify an occupant's role
      * @private
      * @private
@@ -1521,40 +1458,6 @@ const ChatRoomMixin = {
         );
         );
     },
     },
 
 
-    /**
-     * Sends an IQ stanza to the server, asking it for the relevant affiliation list .
-     * Returns an array of {@link MemberListItem} objects, representing occupants
-     * that have the given affiliation.
-     * See: https://xmpp.org/extensions/xep-0045.html#modifymember
-     * @private
-     * @method _converse.ChatRoom#getAffiliationList
-     * @param { ("admin"|"owner"|"member") } affiliation
-     * @returns { Promise<MemberListItem[]> }
-     */
-    async getAffiliationList (affiliation) {
-        const iq = $iq({ to: this.get('jid'), type: 'get' })
-            .c('query', { xmlns: Strophe.NS.MUC_ADMIN })
-            .c('item', { 'affiliation': affiliation });
-        const result = await api.sendIQ(iq, null, false);
-        if (result === null) {
-            const err_msg = `Error: timeout while fetching ${affiliation} list for MUC ${this.get('jid')}`;
-            const err = new Error(err_msg);
-            log.warn(err_msg);
-            log.warn(result);
-            return err;
-        }
-        if (u.isErrorStanza(result)) {
-            const err_msg = `Error: not allowed to fetch ${affiliation} list for MUC ${this.get('jid')}`;
-            const err = new Error(err_msg);
-            log.warn(err_msg);
-            log.warn(result);
-            return err;
-        }
-        return parseMemberListIQ(result)
-            .filter(p => p)
-            .sort((a, b) => (a.nick < b.nick ? -1 : a.nick > b.nick ? 1 : 0));
-    },
-
     /**
     /**
      * Fetch the lists of users with the given affiliations.
      * Fetch the lists of users with the given affiliations.
      * Then compute the delta between those users and
      * Then compute the delta between those users and
@@ -1569,10 +1472,11 @@ const ChatRoomMixin = {
      *  to update the list.
      *  to update the list.
      */
      */
     async updateMemberLists (members) {
     async updateMemberLists (members) {
+        const muc_jid = this.get('jid');
         const all_affiliations = ['member', 'admin', 'owner'];
         const all_affiliations = ['member', 'admin', 'owner'];
-        const aff_lists = await Promise.all(all_affiliations.map(a => this.getAffiliationList(a)));
+        const aff_lists = await Promise.all(all_affiliations.map(a => getAffiliationList(a, muc_jid)));
         const old_members = aff_lists.reduce((acc, val) => (u.isErrorObject(val) ? acc : [...val, ...acc]), []);
         const old_members = aff_lists.reduce((acc, val) => (u.isErrorObject(val) ? acc : [...val, ...acc]), []);
-        await this.setAffiliations(muc_utils.computeAffiliationsDelta(true, false, members, old_members));
+        await setAffiliations(muc_jid, computeAffiliationsDelta(true, false, members, old_members));
         await this.occupants.fetchMembers();
         await this.occupants.fetchMembers();
     },
     },
 
 

+ 3 - 1
src/headless/plugins/muc/occupants.js

@@ -3,6 +3,7 @@ import u from '../../utils/form';
 import { Collection } from '@converse/skeletor/src/collection';
 import { Collection } from '@converse/skeletor/src/collection';
 import { Strophe } from 'strophe.js/src/strophe';
 import { Strophe } from 'strophe.js/src/strophe';
 import { _converse, api } from '../../core.js';
 import { _converse, api } from '../../core.js';
+import { getAffiliationList } from './affiliations/utils.js';
 
 
 const MUC_ROLE_WEIGHTS = {
 const MUC_ROLE_WEIGHTS = {
     'moderator': 1,
     'moderator': 1,
@@ -43,7 +44,8 @@ const ChatRoomOccupants = Collection.extend({
         if (affiliations.length === 0) {
         if (affiliations.length === 0) {
             return;
             return;
         }
         }
-        const aff_lists = await Promise.all(affiliations.map(a => this.chatroom.getAffiliationList(a)));
+        const muc_jid = this.chatroom.get('jid');
+        const aff_lists = await Promise.all(affiliations.map(a => getAffiliationList(a, muc_jid)));
         const new_members = aff_lists.reduce((acc, val) => (u.isErrorObject(val) ? acc : [...val, ...acc]), []);
         const new_members = aff_lists.reduce((acc, val) => (u.isErrorObject(val) ? acc : [...val, ...acc]), []);
         const known_affiliations = affiliations.filter(
         const known_affiliations = affiliations.filter(
             a => !u.isErrorObject(aff_lists[affiliations.indexOf(a)])
             a => !u.isErrorObject(aff_lists[affiliations.indexOf(a)])

+ 0 - 60
src/headless/plugins/muc/utils.js

@@ -1,60 +0,0 @@
-/**
- * @copyright The Converse.js contributors
- * @license Mozilla Public License (MPLv2)
- * @description This is the MUC utilities module.
- */
-import { difference, indexOf } from "lodash-es";
-
-/**
- * The MUC utils object. Contains utility functions related to multi-user chat.
- * @namespace muc_utils
- */
-const muc_utils = {
-    /**
-     * Given two lists of objects with 'jid', 'affiliation' and
-     * 'reason' properties, return a new list containing
-     * those objects that are new, changed or removed
-     * (depending on the 'remove_absentees' boolean).
-     *
-     * The affiliations for new and changed members stay the
-     * same, for removed members, the affiliation is set to 'none'.
-     *
-     * The 'reason' property is not taken into account when
-     * comparing whether affiliations have been changed.
-     * @private
-     * @method muc_utils#computeAffiliationsDelta
-     * @param { boolean } exclude_existing - Indicates whether JIDs from
-     *      the new list which are also in the old list
-     *      (regardless of affiliation) should be excluded
-     *      from the delta. One reason to do this
-     *      would be when you want to add a JID only if it
-     *      doesn't have *any* existing affiliation at all.
-     * @param { boolean } remove_absentees - Indicates whether JIDs
-     *      from the old list which are not in the new list
-     *      should be considered removed and therefore be
-     *      included in the delta with affiliation set
-     *      to 'none'.
-     * @param { array } new_list - Array containing the new affiliations
-     * @param { array } old_list - Array containing the old affiliations
-     * @returns { array }
-     */
-    computeAffiliationsDelta (exclude_existing, remove_absentees, new_list, old_list) {
-        const new_jids = new_list.map(o => o.jid);
-        const old_jids = old_list.map(o => o.jid);
-        // Get the new affiliations
-        let delta = difference(new_jids, old_jids).map(jid => new_list[indexOf(new_jids, jid)]);
-        if (!exclude_existing) {
-            // Get the changed affiliations
-            delta = delta.concat(new_list.filter(item => {
-                const idx = indexOf(old_jids, item.jid);
-                return idx >= 0 ? (item.affiliation !== old_list[idx].affiliation) : false;
-            }));
-        }
-        if (remove_absentees) { // Get the removed affiliations
-            delta = delta.concat(difference(old_jids, new_jids).map(jid => ({'jid': jid, 'affiliation': 'none'})));
-        }
-        return delta;
-    }
-}
-
-export default muc_utils;

+ 8 - 5
src/modals/moderator-tools.js → src/plugins/muc-views/modals/moderator-tools.js

@@ -1,9 +1,10 @@
-import BootstrapModal from "./base.js";
+import BootstrapModal from "modals/base.js";
 import log from "@converse/headless/log";
 import log from "@converse/headless/log";
-import tpl_moderator_tools_modal from "./templates/moderator-tools.js";
+import tpl_moderator_tools_modal from "../templates/moderator-tools.js";
 import { AFFILIATIONS, ROLES } from "@converse/headless/plugins/muc/index.js";
 import { AFFILIATIONS, ROLES } from "@converse/headless/plugins/muc/index.js";
-import { __ } from '../i18n';
+import { __ } from 'i18n';
 import { api, converse } from "@converse/headless/core";
 import { api, converse } from "@converse/headless/core";
+import { getAffiliationList, setAffiliation } from '@converse/headless/plugins/muc/affiliations/utils.js'
 
 
 const { Strophe, sizzle } = converse.env;
 const { Strophe, sizzle } = converse.env;
 const u = converse.env.utils;
 const u = converse.env.utils;
@@ -33,7 +34,8 @@ export default BootstrapModal.extend({
             const chatroom = this.chatroomview.model;
             const chatroom = this.chatroomview.model;
             const affiliation = this.model.get('affiliation');
             const affiliation = this.model.get('affiliation');
             if (this.shouldFetchAffiliationsList()) {
             if (this.shouldFetchAffiliationsList()) {
-                this.users_with_affiliation = await chatroom.getAffiliationList(affiliation);
+                const muc_jid = chatroom.get('jid');
+                this.users_with_affiliation = await getAffiliationList(affiliation, muc_jid);
             } else {
             } else {
                 this.users_with_affiliation = chatroom.getOccupantsWithAffiliation(affiliation);
                 this.users_with_affiliation = chatroom.getOccupantsWithAffiliation(affiliation);
             }
             }
@@ -157,8 +159,9 @@ export default BootstrapModal.extend({
             'reason': data.get('reason')
             'reason': data.get('reason')
         }
         }
         const current_affiliation = this.model.get('affiliation');
         const current_affiliation = this.model.get('affiliation');
+        const muc_jid = this.chatroomview.model.get('jid');
         try {
         try {
-            await this.chatroomview.model.setAffiliation(affiliation, [attrs]);
+            await setAffiliation(affiliation, muc_jid, [attrs]);
         } catch (e) {
         } catch (e) {
             if (e === null) {
             if (e === null) {
                 this.alert(__('Timeout error while trying to set the affiliation'), 'danger');
                 this.alert(__('Timeout error while trying to set the affiliation'), 'danger');

+ 1 - 1
src/plugins/muc-views/muc.js

@@ -1,5 +1,5 @@
 import BaseChatView from 'shared/chat/baseview.js';
 import BaseChatView from 'shared/chat/baseview.js';
-import ModeratorToolsModal from 'modals/moderator-tools.js';
+import ModeratorToolsModal from './modals/moderator-tools.js';
 import log from '@converse/headless/log';
 import log from '@converse/headless/log';
 import tpl_muc from './templates/muc.js';
 import tpl_muc from './templates/muc.js';
 import { Model } from '@converse/skeletor/src/model.js';
 import { Model } from '@converse/skeletor/src/model.js';

+ 3 - 3
src/modals/templates/moderator-tools.js → src/plugins/muc-views/templates/moderator-tools.js

@@ -1,7 +1,7 @@
+import spinner from "templates/spinner.js";
+import { __ } from 'i18n';
 import { html } from "lit-html";
 import { html } from "lit-html";
-import { __ } from '../../i18n';
-import spinner from "../../templates/spinner.js";
-import { modal_header_close_button } from "./buttons.js"
+import { modal_header_close_button } from "modals/templates/buttons.js"
 
 
 
 
 function getRoleHelpText (role) {
 function getRoleHelpText (role) {

+ 9 - 7
src/plugins/muc-views/tests/muc.js

@@ -3107,7 +3107,6 @@ describe("Groupchats", function () {
 
 
             await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
             await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
             const view = _converse.chatboxviews.get('lounge@montague.lit');
             const view = _converse.chatboxviews.get('lounge@montague.lit');
-            spyOn(view.model, 'setAffiliation').and.callThrough();
             spyOn(view.model, 'validateRoleOrAffiliationChangeArgs').and.callThrough();
             spyOn(view.model, 'validateRoleOrAffiliationChangeArgs').and.callThrough();
 
 
             let presence = $pres({
             let presence = $pres({
@@ -3136,7 +3135,10 @@ describe("Groupchats", function () {
             expect(err_msg.textContent.trim()).toBe(
             expect(err_msg.textContent.trim()).toBe(
                 "Error: the \"owner\" command takes two arguments, the user's nickname and optionally a reason.");
                 "Error: the \"owner\" command takes two arguments, the user's nickname and optionally a reason.");
 
 
-            expect(view.model.setAffiliation).not.toHaveBeenCalled();
+            const sel = 'iq[type="set"] query[xmlns="http://jabber.org/protocol/muc#admin"]';
+            const stanzas = _converse.connection.IQ_stanzas.filter(s => sizzle(sel, s).length);
+            expect(stanzas.length).toBe(0);
+
             // XXX: Calling onFormSubmitted directly, trying
             // XXX: Calling onFormSubmitted directly, trying
             // again via triggering Event doesn't work for some weird
             // again via triggering Event doesn't work for some weird
             // reason.
             // reason.
@@ -3146,7 +3148,7 @@ describe("Groupchats", function () {
             expect(Array.from(view.querySelectorAll('.chat-error')).pop().textContent.trim()).toBe(
             expect(Array.from(view.querySelectorAll('.chat-error')).pop().textContent.trim()).toBe(
                 "Error: couldn't find a groupchat participant based on your arguments");
                 "Error: couldn't find a groupchat participant based on your arguments");
 
 
-            expect(view.model.setAffiliation).not.toHaveBeenCalled();
+            expect(_converse.connection.IQ_stanzas.filter(s => sizzle(sel, s).length).length).toBe(0);
 
 
             // Call now with the correct of arguments.
             // Call now with the correct of arguments.
             // XXX: Calling onFormSubmitted directly, trying
             // XXX: Calling onFormSubmitted directly, trying
@@ -3156,7 +3158,6 @@ describe("Groupchats", function () {
             bottom_panel.onFormSubmitted(new Event('submit'));
             bottom_panel.onFormSubmitted(new Event('submit'));
 
 
             expect(view.model.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(3);
             expect(view.model.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(3);
-            expect(view.model.setAffiliation).toHaveBeenCalled();
             // Check that the member list now gets updated
             // Check that the member list now gets updated
             expect(Strophe.serialize(sent_IQ)).toBe(
             expect(Strophe.serialize(sent_IQ)).toBe(
                 `<iq id="${IQ_id}" to="lounge@montague.lit" type="set" xmlns="jabber:client">`+
                 `<iq id="${IQ_id}" to="lounge@montague.lit" type="set" xmlns="jabber:client">`+
@@ -3196,7 +3197,6 @@ describe("Groupchats", function () {
 
 
             await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
             await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
             const view = _converse.chatboxviews.get('lounge@montague.lit');
             const view = _converse.chatboxviews.get('lounge@montague.lit');
-            spyOn(view.model, 'setAffiliation').and.callThrough();
             spyOn(view.model, 'validateRoleOrAffiliationChangeArgs').and.callThrough();
             spyOn(view.model, 'validateRoleOrAffiliationChangeArgs').and.callThrough();
 
 
             let presence = $pres({
             let presence = $pres({
@@ -3224,7 +3224,10 @@ describe("Groupchats", function () {
             await u.waitUntil(() => view.querySelector('.message:last-child')?.textContent?.trim() ===
             await u.waitUntil(() => view.querySelector('.message:last-child')?.textContent?.trim() ===
                 "Error: the \"ban\" command takes two arguments, the user's nickname and optionally a reason.");
                 "Error: the \"ban\" command takes two arguments, the user's nickname and optionally a reason.");
 
 
-            expect(view.model.setAffiliation).not.toHaveBeenCalled();
+            const sel = 'iq[type="set"] query[xmlns="http://jabber.org/protocol/muc#admin"]';
+            const stanzas = _converse.connection.IQ_stanzas.filter(s => sizzle(sel, s).length);
+            expect(stanzas.length).toBe(0);
+
             // Call now with the correct amount of arguments.
             // Call now with the correct amount of arguments.
             // XXX: Calling onFormSubmitted directly, trying
             // XXX: Calling onFormSubmitted directly, trying
             // again via triggering Event doesn't work for some weird
             // again via triggering Event doesn't work for some weird
@@ -3233,7 +3236,6 @@ describe("Groupchats", function () {
             bottom_panel.onFormSubmitted(new Event('submit'));
             bottom_panel.onFormSubmitted(new Event('submit'));
 
 
             expect(view.model.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(2);
             expect(view.model.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(2);
-            expect(view.model.setAffiliation).toHaveBeenCalled();
             // Check that the member list now gets updated
             // Check that the member list now gets updated
             expect(Strophe.serialize(sent_IQ)).toBe(
             expect(Strophe.serialize(sent_IQ)).toBe(
                 `<iq id="${IQ_id}" to="lounge@montague.lit" type="set" xmlns="jabber:client">`+
                 `<iq id="${IQ_id}" to="lounge@montague.lit" type="set" xmlns="jabber:client">`+

+ 10 - 9
src/plugins/muc-views/utils.js

@@ -4,6 +4,7 @@ import { __ } from 'i18n';
 import { _converse, api, converse } from "@converse/headless/core";
 import { _converse, api, converse } from "@converse/headless/core";
 import { html } from "lit-html";
 import { html } from "lit-html";
 import { parseMessageForCommands } from 'plugins/chatview/utils.js';
 import { parseMessageForCommands } from 'plugins/chatview/utils.js';
+import { setAffiliation } from '@converse/headless/plugins/muc/affiliations/utils.js';
 
 
 const { Strophe, $pres, $iq, sizzle, u } = converse.env;
 const { Strophe, $pres, $iq, sizzle, u } = converse.env;
 
 
@@ -187,10 +188,10 @@ function setRole (muc, command, args, required_affiliations = [], required_roles
 }
 }
 
 
 
 
-function setAffiliation (muc, command, args, required_affiliations) {
+function verifyAndSetAffiliation (muc, command, args, required_affiliations) {
     const affiliation = COMMAND_TO_AFFILIATION[command];
     const affiliation = COMMAND_TO_AFFILIATION[command];
     if (!affiliation) {
     if (!affiliation) {
-        throw Error(`ChatRoomView#setAffiliation called with invalid command: ${command}`);
+        throw Error(`verifyAffiliations called with invalid command: ${command}`);
     }
     }
     if (!muc.verifyAffiliations(required_affiliations)) {
     if (!muc.verifyAffiliations(required_affiliations)) {
         return false;
         return false;
@@ -223,8 +224,8 @@ function setAffiliation (muc, command, args, required_affiliations) {
     if (occupant && api.settings.get('auto_register_muc_nickname')) {
     if (occupant && api.settings.get('auto_register_muc_nickname')) {
         attrs['nick'] = occupant.get('nick');
         attrs['nick'] = occupant.get('nick');
     }
     }
-    muc
-        .setAffiliation(affiliation, [attrs])
+
+    setAffiliation(affiliation, muc.get('jid'), [attrs])
         .then(() => muc.occupants.fetchMembers())
         .then(() => muc.occupants.fetchMembers())
         .catch(err => muc.onCommandError(err));
         .catch(err => muc.onCommandError(err));
 }
 }
@@ -249,11 +250,11 @@ export function parseMessageForMUCCommands (muc, text) {
 
 
     switch (command) {
     switch (command) {
         case 'admin': {
         case 'admin': {
-            setAffiliation(muc, command, args, ['owner']);
+            verifyAndSetAffiliation(muc, command, args, ['owner']);
             break;
             break;
         }
         }
         case 'ban': {
         case 'ban': {
-            setAffiliation(muc, command, args, ['admin', 'owner']);
+            verifyAndSetAffiliation(muc, command, args, ['admin', 'owner']);
             break;
             break;
         }
         }
         case 'modtools': {
         case 'modtools': {
@@ -293,7 +294,7 @@ export function parseMessageForMUCCommands (muc, text) {
             break;
             break;
         }
         }
         case 'member': {
         case 'member': {
-            setAffiliation(muc, command, args, ['admin', 'owner']);
+            verifyAndSetAffiliation(muc, command, args, ['admin', 'owner']);
             break;
             break;
         }
         }
         case 'nick': {
         case 'nick': {
@@ -316,7 +317,7 @@ export function parseMessageForMUCCommands (muc, text) {
             break;
             break;
         }
         }
         case 'owner':
         case 'owner':
-            setAffiliation(muc, command, args, ['owner']);
+            verifyAndSetAffiliation(muc, command, args, ['owner']);
             break;
             break;
         case 'op': {
         case 'op': {
             setRole(muc, command, args, ['admin', 'owner']);
             setRole(muc, command, args, ['admin', 'owner']);
@@ -336,7 +337,7 @@ export function parseMessageForMUCCommands (muc, text) {
             break;
             break;
         }
         }
         case 'revoke': {
         case 'revoke': {
-            setAffiliation(muc, command, args, ['admin', 'owner']);
+            verifyAndSetAffiliation(muc, command, args, ['admin', 'owner']);
             break;
             break;
         }
         }
         case 'topic':
         case 'topic':