Ver código fonte

Add ability to block JID when declining contact request

Also adds the ability to add checkboxes to the `confirm` modal.
JC Brand 6 meses atrás
pai
commit
d3504ed95a

+ 4 - 0
src/headless/types/plugins/blocklist/collection.d.ts

@@ -5,6 +5,10 @@ declare class Blocklist extends Collection {
     model: typeof BlockedEntity;
     initialize(): Promise<void>;
     fetched_flag: string;
+    /**
+     * @param {BlockedEntity} item
+     */
+    rejectContactRequest(item: BlockedEntity): Promise<void>;
     fetchBlocklist(): any;
     /**
      * @param {Object} deferred

+ 1 - 1
src/plugins/modal/api.js

@@ -87,7 +87,7 @@ const modal_api = {
      * @method _converse.api.confirm
      * @param {String} title - The header text for the confirmation dialog
      * @param {(Array<String>|String)} messages - The text to show to the user
-     * @param {Array<import('./types.ts').Field>} fields - An object representing a field presented to the user.
+     * @param {Array<import('./types').Field>} fields - An object representing a field presented to the user.
      * @returns {Promise<Array|false>} A promise which resolves with an array of
      *  filled in fields or `false` if the confirm dialog was closed or canceled.
      */

+ 27 - 23
src/plugins/modal/confirm.js

@@ -1,47 +1,51 @@
 import { getOpenPromise } from '@converse/openpromise';
-import { api } from "@converse/headless";
-import BaseModal from "plugins/modal/modal.js";
-import tplPrompt from "./templates/prompt.js";
+import { api } from '@converse/headless';
+import BaseModal from 'plugins/modal/modal.js';
+import tplPrompt from './templates/prompt.js';
 
 export default class Confirm extends BaseModal {
-
-    constructor (options) {
+    constructor(options) {
         super(options);
         this.confirmation = getOpenPromise();
     }
 
-    initialize () {
+    initialize() {
         super.initialize();
-        this.listenTo(this.model, 'change', () => this.render())
-        this.addEventListener('hide.bs.modal', () => {
-            if (!this.confirmation.isResolved) {
-                this.confirmation.reject()
-            }
-        }, false);
+        this.listenTo(this.model, 'change', () => this.render());
+        this.addEventListener(
+            'hide.bs.modal',
+            () => {
+                if (!this.confirmation.isResolved) {
+                    this.confirmation.reject();
+                }
+            },
+            false
+        );
     }
 
-    renderModal () {
+    renderModal() {
         return tplPrompt(this);
     }
 
-    getModalTitle () {
+    getModalTitle() {
         return this.model.get('title');
     }
 
-    onConfimation (ev) {
+    onConfimation(ev) {
         ev.preventDefault();
         const form_data = new FormData(ev.target);
-        const fields = (this.model.get('fields') || [])
-            .map(field => {
-                const value = /** @type {string }*/(form_data.get(field.name)).trim();
-                field.value = value;
+        const fields = (this.model.get('fields') || []).map(
+            /** @param {import('./types.js').Field} field */ (field) => {
+                const value = form_data.get(field.name);
+                field.value = /** @type {string} */(value);
                 if (field.challenge) {
-                    field.challenge_failed = (value !== field.challenge);
+                    field.challenge_failed = value !== field.challenge;
                 }
                 return field;
-            });
+            }
+        );
 
-        if (fields.filter(c => c.challenge_failed).length) {
+        if (fields.filter((c) => c.challenge_failed).length) {
             this.model.set('fields', fields);
             // Setting an array doesn't trigger a change event
             this.model.trigger('change');
@@ -51,7 +55,7 @@ export default class Confirm extends BaseModal {
         this.modal.hide();
     }
 
-    renderModalFooter () {
+    renderModalFooter() {
         return '';
     }
 }

+ 36 - 26
src/plugins/modal/templates/prompt.js

@@ -1,33 +1,43 @@
-import { html } from "lit";
+import { html } from 'lit';
 import { __ } from 'i18n';
 
-
-const tplField = (f) => html`
-    <div>
-        <label class="form-label">
-            ${f.label || ''}
-            <input type="text"
-                name="${f.name}"
-                class="${(f.challenge_failed) ? 'error' : ''} form-control form-control--labeled"
-                ?required="${f.required}"
-                placeholder="${f.placeholder}" />
-        </label>
-    </div>
-`;
+/**
+ * @param {import("../types").Field} f
+ */
+function tplField(f) {
+    return f.type === 'checkbox'
+        ? html` <div class="form-check">
+              <input name="${f.name}" class="form-check-input" type="checkbox" value="" id="${f.name}" />
+              <label class="form-check-label" for="${f.name}">${f.label}</label>
+          </div>`
+        : html`<div>
+              <label class="form-label">
+                  ${f.label || ''}
+                  <input
+                      type="text"
+                      name="${f.name}"
+                      class="${f.challenge_failed ? 'error' : ''} form-control form-control--labeled"
+                      ?required="${f.required}"
+                      placeholder="${f.placeholder}"
+                  />
+              </label>
+          </div>`;
+}
 
 /**
  * @param {import('../confirm').default} el
  */
 export default (el) => {
-    return html`
-        <form class="converse-form converse-form--modal confirm" action="#" @submit=${ev => el.onConfimation(ev)}>
-            <div>
-                ${ el.model.get('messages')?.map(message => html`<p>${message}</p>`) }
-            </div>
-            ${ el.model.get('fields')?.map(f => tplField(f)) }
-            <div>
-                <button type="submit" class="btn btn-primary">${__('OK')}</button>
-                <input type="button" class="btn btn-secondary" data-bs-dismiss="modal" value="${__('Cancel')}"/>
-            </div>
-        </form>`;
-}
+    return html` <form
+        class="converse-form converse-form--modal confirm"
+        action="#"
+        @submit=${(ev) => el.onConfimation(ev)}
+    >
+        <div>${el.model.get('messages')?.map(/** @param {string} msg */ (msg) => html`<p>${msg}</p>`)}</div>
+        ${el.model.get('fields')?.map(/** @param {import('../types').Field} f */ (f) => tplField(f))}
+        <div>
+            <button type="submit" class="btn btn-primary">${__('Confirm')}</button>
+            <input type="button" class="btn btn-secondary" data-bs-dismiss="modal" value="${__('Cancel')}" />
+        </div>
+    </form>`;
+};

+ 3 - 0
src/plugins/modal/types.ts

@@ -1,7 +1,10 @@
 export type Field = {
+    type: 'text'|'checkbox'
     label: string; // The form label for the input field.
     name: string; // The name for the input field.
     challenge?: string; // A challenge value that must be provided by the user.
+    challenge_failed?: boolean;
     placeholder?: string; // The placeholder for the input field.
     required?: boolean; // Whether the field is required or not
+    value?: string;
 }

+ 26 - 3
src/plugins/rosterview/contactview.js

@@ -1,11 +1,13 @@
 import { Model } from '@converse/skeletor';
-import { _converse, api, log } from "@converse/headless";
+import { _converse, converse, api, log } from "@converse/headless";
 import { CustomElement } from 'shared/components/element.js';
 import tplRequestingContact from "./templates/requesting_contact.js";
 import tplRosterItem from "./templates/roster_item.js";
 import tplUnsavedContact from "./templates/unsaved_contact.js";
 import { __ } from 'i18n';
 
+const { Strophe } = converse.env;
+
 
 export default class RosterContact extends CustomElement {
 
@@ -105,12 +107,33 @@ export default class RosterContact extends CustomElement {
      */
     async declineRequest (ev) {
         if (ev && ev.preventDefault) { ev.preventDefault(); }
-        const result = await api.confirm(__("Are you sure you want to decline this contact request?"));
+
+        const domain = _converse.session.get('domain');
+        const blocking_supported = await api.disco.supports(Strophe.NS.BLOCKING, domain);
+
+        const result = await api.confirm(
+            __('Decline contact request'),
+            [__('Are you sure you want to decline this contact request?')],
+            blocking_supported ? [{
+                label: __('Block this user from sending you further messages'),
+                name: 'block',
+                type: 'checkbox'
+            }] : []
+        );
+
         if (result) {
             const chat = await api.chats.get(this.model.get('jid'));
             chat?.close();
+            this.model.unauthorize();
+
+            if (blocking_supported && Array.isArray(result)) {
+                const should_block = result.find((i) => i.name === 'block')?.value === 'on';
+                if (should_block) {
+                    api.blocklist.add(this.model.get('jid'));
+                }
+            }
 
-            this.model.unauthorize().destroy();
+            this.model.destroy();
         }
         return this;
     }

+ 3 - 3
src/plugins/rosterview/index.js

@@ -3,11 +3,11 @@
  * @license Mozilla Public License (MPLv2)
  */
 import { _converse, api, converse, RosterFilter } from "@converse/headless";
+import RosterContactView from './contactview.js';
+import { highlightRosterItem } from './utils.js';
 import "../modal";
 import "./modals/add-contact.js";
 import './rosterview.js';
-import RosterContactView from './contactview.js';
-import { highlightRosterItem } from './utils.js';
 
 import 'shared/styles/status.scss';
 import './styles/roster.scss';
@@ -15,7 +15,7 @@ import './styles/roster.scss';
 
 converse.plugins.add('converse-rosterview', {
 
-    dependencies: ["converse-roster", "converse-modal", "converse-chatboxviews"],
+    dependencies: ["converse-roster", "converse-modal", "converse-chatboxviews", "converse-blocklist"],
 
     initialize () {
         api.settings.extend({

+ 16 - 2
src/plugins/rosterview/tests/roster.js

@@ -1213,6 +1213,13 @@ describe("The Contacts Roster", function () {
         it("do not have a header if there aren't any", mock.initConverse([], {}, async function (_converse) {
             await mock.openControlBox(_converse);
             await mock.waitForRoster(_converse, "current", 0);
+            await mock.waitUntilDiscoConfirmed(
+                _converse,
+                _converse.domain,
+                [{ 'category': 'server', 'type': 'IM' }],
+                ['urn:xmpp:blocking']
+            );
+
             const name = mock.req_names[0];
             spyOn(_converse.api, 'confirm').and.returnValue(Promise.resolve(true));
             _converse.roster.create({
@@ -1227,7 +1234,8 @@ describe("The Contacts Roster", function () {
             expect(u.isVisible(rosterview.querySelector(`ul[data-group="Contact requests"]`))).toEqual(true);
             expect(sizzle('.roster-group', rosterview).filter(u.isVisible).map(e => e.querySelector('li')).length).toBe(1);
             sizzle('.roster-group', rosterview).filter(u.isVisible).map(e => e.querySelector('li .decline-xmpp-request'))[0].click();
-            expect(_converse.api.confirm).toHaveBeenCalled();
+
+            await u.waitUntil(() => _converse.api.confirm.calls.count);
             await u.waitUntil(() => rosterview.querySelector(`ul[data-group="Contact requests"]`) === null);
         }));
 
@@ -1272,6 +1280,12 @@ describe("The Contacts Roster", function () {
                 [], {},
                 async function (_converse) {
 
+            await mock.waitUntilDiscoConfirmed(
+                _converse,
+                _converse.domain,
+                [{ 'category': 'server', 'type': 'IM' }],
+                ['urn:xmpp:blocking']
+            );
             await mock.waitForRoster(_converse, 'current', 0);
             await mock.createContacts(_converse, 'requesting');
             await mock.openControlBox(_converse);
@@ -1284,7 +1298,7 @@ describe("The Contacts Roster", function () {
             spyOn(contact, 'unauthorize').and.callFake(function () { return contact; });
             const req_contact = await u.waitUntil(() => sizzle(".contact-name:contains('"+name+"')", rosterview).pop());
             req_contact.parentElement.parentElement.querySelector('.decline-xmpp-request').click();
-            expect(_converse.api.confirm).toHaveBeenCalled();
+            await u.waitUntil(() => _converse.api.confirm.calls.count);
             await u.waitUntil(() => contact.unauthorize.calls.count());
             // There should now be one less contact
             expect(_converse.roster.length).toEqual(mock.req_names.length-1);

+ 2 - 2
src/types/plugins/modal/api.d.ts

@@ -37,11 +37,11 @@ declare namespace modal_api {
      * @method _converse.api.confirm
      * @param {String} title - The header text for the confirmation dialog
      * @param {(Array<String>|String)} messages - The text to show to the user
-     * @param {Array<import('./types.ts').Field>} fields - An object representing a field presented to the user.
+     * @param {Array<import('./types').Field>} fields - An object representing a field presented to the user.
      * @returns {Promise<Array|false>} A promise which resolves with an array of
      *  filled in fields or `false` if the confirm dialog was closed or canceled.
      */
-    function confirm(title: string, messages?: (Array<string> | string), fields?: Array<import("./types.ts").Field>): Promise<any[] | false>;
+    function confirm(title: string, messages?: (Array<string> | string), fields?: Array<import("./types").Field>): Promise<any[] | false>;
     /**
      * Show a prompt modal to the user.
      * @method _converse.api.prompt

+ 1 - 1
src/types/plugins/modal/confirm.d.ts

@@ -6,5 +6,5 @@ export default class Confirm extends BaseModal {
     onConfimation(ev: any): void;
     renderModalFooter(): string;
 }
-import BaseModal from "plugins/modal/modal.js";
+import BaseModal from 'plugins/modal/modal.js';
 //# sourceMappingURL=confirm.d.ts.map

+ 3 - 0
src/types/plugins/modal/types.d.ts

@@ -1,8 +1,11 @@
 export type Field = {
+    type: 'text' | 'checkbox';
     label: string;
     name: string;
     challenge?: string;
+    challenge_failed?: boolean;
     placeholder?: string;
     required?: boolean;
+    value?: string;
 };
 //# sourceMappingURL=types.d.ts.map