diff --git a/src/headless/types/plugins/blocklist/collection.d.ts b/src/headless/types/plugins/blocklist/collection.d.ts index d710d7f72b..c5d38c86fd 100644 --- a/src/headless/types/plugins/blocklist/collection.d.ts +++ b/src/headless/types/plugins/blocklist/collection.d.ts @@ -5,6 +5,10 @@ declare class Blocklist extends Collection { model: typeof BlockedEntity; initialize(): Promise; fetched_flag: string; + /** + * @param {BlockedEntity} item + */ + rejectContactRequest(item: BlockedEntity): Promise; fetchBlocklist(): any; /** * @param {Object} deferred diff --git a/src/plugins/modal/api.js b/src/plugins/modal/api.js index 318a6f0350..e9d98278c4 100644 --- a/src/plugins/modal/api.js +++ b/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)} messages - The text to show to the user - * @param {Array} fields - An object representing a field presented to the user. + * @param {Array} fields - An object representing a field presented to the user. * @returns {Promise} A promise which resolves with an array of * filled in fields or `false` if the confirm dialog was closed or canceled. */ diff --git a/src/plugins/modal/confirm.js b/src/plugins/modal/confirm.js index ce6fb3bebd..31a6a95c9a 100644 --- a/src/plugins/modal/confirm.js +++ b/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 ''; } } diff --git a/src/plugins/modal/templates/prompt.js b/src/plugins/modal/templates/prompt.js index 990266d6b7..139828298e 100644 --- a/src/plugins/modal/templates/prompt.js +++ b/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` -
- -
-`; +/** + * @param {import("../types").Field} f + */ +function tplField(f) { + return f.type === 'checkbox' + ? html`
+ + +
` + : html`
+ +
`; +} /** * @param {import('../confirm').default} el */ export default (el) => { - return html` -
el.onConfimation(ev)}> -
- ${ el.model.get('messages')?.map(message => html`

${message}

`) } -
- ${ el.model.get('fields')?.map(f => tplField(f)) } -
- - -
-
`; -} + return html`
el.onConfimation(ev)} + > +
${el.model.get('messages')?.map(/** @param {string} msg */ (msg) => html`

${msg}

`)}
+ ${el.model.get('fields')?.map(/** @param {import('../types').Field} f */ (f) => tplField(f))} +
+ + +
+
`; +}; diff --git a/src/plugins/modal/types.ts b/src/plugins/modal/types.ts index 43b3e16548..23a84d1472 100644 --- a/src/plugins/modal/types.ts +++ b/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; } diff --git a/src/plugins/rosterview/contactview.js b/src/plugins/rosterview/contactview.js index 5aa4c06591..0af0048c32 100644 --- a/src/plugins/rosterview/contactview.js +++ b/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; } diff --git a/src/plugins/rosterview/index.js b/src/plugins/rosterview/index.js index be32c73ae6..50cacbcb98 100644 --- a/src/plugins/rosterview/index.js +++ b/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({ diff --git a/src/plugins/rosterview/tests/roster.js b/src/plugins/rosterview/tests/roster.js index 18d1c77745..72afc0ce9e 100644 --- a/src/plugins/rosterview/tests/roster.js +++ b/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); diff --git a/src/types/plugins/modal/api.d.ts b/src/types/plugins/modal/api.d.ts index 25d674a5a1..799593ca66 100644 --- a/src/types/plugins/modal/api.d.ts +++ b/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)} messages - The text to show to the user - * @param {Array} fields - An object representing a field presented to the user. + * @param {Array} fields - An object representing a field presented to the user. * @returns {Promise} 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), fields?: Array): Promise; + function confirm(title: string, messages?: (Array | string), fields?: Array): Promise; /** * Show a prompt modal to the user. * @method _converse.api.prompt diff --git a/src/types/plugins/modal/confirm.d.ts b/src/types/plugins/modal/confirm.d.ts index ddf7004a78..a7a3623c17 100644 --- a/src/types/plugins/modal/confirm.d.ts +++ b/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 \ No newline at end of file diff --git a/src/types/plugins/modal/types.d.ts b/src/types/plugins/modal/types.d.ts index aa2d0a1314..2e2a6a63ca 100644 --- a/src/types/plugins/modal/types.d.ts +++ b/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 \ No newline at end of file