Skip to content

Commit

Permalink
Add ability to block JID when declining contact request
Browse files Browse the repository at this point in the history
Also adds the ability to add checkboxes to the `confirm` modal.
  • Loading branch information
jcbrand committed Jan 6, 2025
1 parent 57b20c5 commit 29219f7
Show file tree
Hide file tree
Showing 11 changed files with 122 additions and 61 deletions.
4 changes: 4 additions & 0 deletions src/headless/types/plugins/blocklist/collection.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/modal/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
50 changes: 27 additions & 23 deletions src/plugins/modal/confirm.js
Original file line number Diff line number Diff line change
@@ -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');
Expand All @@ -51,7 +55,7 @@ export default class Confirm extends BaseModal {
this.modal.hide();
}

renderModalFooter () {
renderModalFooter() {
return '';
}
}
Expand Down
62 changes: 36 additions & 26 deletions src/plugins/modal/templates/prompt.js
Original file line number Diff line number Diff line change
@@ -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 changes: 3 additions & 0 deletions src/plugins/modal/types.ts
Original file line number Diff line number Diff line change
@@ -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;
}
29 changes: 26 additions & 3 deletions src/plugins/rosterview/contactview.js
Original file line number Diff line number Diff line change
@@ -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 {

Expand Down Expand Up @@ -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;
}
Expand Down
6 changes: 3 additions & 3 deletions src/plugins/rosterview/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,19 @@
* @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';


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({
Expand Down
18 changes: 16 additions & 2 deletions src/plugins/rosterview/tests/roster.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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);
}));

Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions src/types/plugins/modal/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/types/plugins/modal/confirm.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 3 additions & 0 deletions src/types/plugins/modal/types.d.ts
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 29219f7

Please sign in to comment.