Skip to content

Commit

Permalink
Add a modal to start chats with non-roster contacts
Browse files Browse the repository at this point in the history
  • Loading branch information
jcbrand committed Jan 9, 2025
1 parent 81d8187 commit 9c51c17
Show file tree
Hide file tree
Showing 15 changed files with 114 additions and 55 deletions.
1 change: 1 addition & 0 deletions src/plugins/rosterview/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import RosterContactView from './contactview.js';
import { highlightRosterItem } from './utils.js';
import "../modal";
import "./modals/add-contact.js";
import "./modals/new-chat.js";
import './rosterview.js';

import 'shared/styles/status.scss';
Expand Down
4 changes: 2 additions & 2 deletions src/plugins/rosterview/modals/add-contact.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,10 @@ export default class AddContactModal extends BaseModal {
*/
async afterSubmission (_form, jid, name, group) {
try {
await api.roster.add({ jid, name, groups: Array.isArray(group) ? group : [group] });
await api.contacts.add({ jid, name, groups: Array.isArray(group) ? group : [group] });
} catch (e) {
log.error(e);
this.model.set('error', __('Sorry, something went wrong while adding the contact'));
this.model.set('error', __('Sorry, something went wrong'));
return;
}
this.model.clear();
Expand Down
38 changes: 13 additions & 25 deletions src/plugins/rosterview/modals/new-chat.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Strophe } from 'strophe.js';
import { _converse, api, log } from '@converse/headless';
import BaseModal from 'plugins/modal/modal.js';
import tplNewChat from './templates/new-chat.js';
import { __ } from 'i18n';

export default class NewChatModal extends BaseModal {
initialize () {
initialize() {
super.initialize();
this.listenTo(this.model, 'change', () => this.render());
this.render();
Expand All @@ -15,30 +15,18 @@ export default class NewChatModal extends BaseModal {
);
}

renderModal () {
return `
<form class="new-chat-form">
<div class="modal-body">
<div class="form-group">
<label for="jid">${__('Enter XMPP JID')}</label>
<input type="text" name="jid" class="form-control" required>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">${__('Start Chat')}</button>
</div>
</form>
`;
renderModal() {
return tplNewChat(this);
}

getModalTitle () {
return __('Start a New Chat');
getModalTitle() {
return __('Start a new chat');
}

/**
* @param {string} jid
*/
validateSubmission (jid) {
validateSubmission(jid) {
if (!jid || jid.split('@').filter((s) => !!s).length < 2) {
this.model.set('error', __('Please enter a valid XMPP address'));
return false;
Expand All @@ -51,24 +39,24 @@ export default class NewChatModal extends BaseModal {
* @param {HTMLFormElement} _form
* @param {string} jid
*/
async afterSubmission (_form, jid) {
async afterSubmission(_form, jid) {
try {
await api.chats.open(jid);
await api.chats.open(jid, {}, true);
} catch (e) {
log.error(e);
this.model.set('error', __('Sorry, something went wrong while starting the chat'));
this.model.set('error', __('Sorry, something went wrong'));
return;
}
this.model.clear();
this.modal.hide();
}

/**
* @param {Event} ev
* @param {SubmitEvent} ev
*/
async startChatFromForm (ev) {
async startChatFromForm(ev) {
ev.preventDefault();
const form = /** @type {HTMLFormElement} */(ev.target);
const form = /** @type {HTMLFormElement} */ (ev.target);
const data = new FormData(form);
const jid = /** @type {string} */ (data.get('jid') || '').trim();

Expand Down
3 changes: 1 addition & 2 deletions src/plugins/rosterview/modals/templates/add-contact.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export default (el) => {

return html`
<div class="modal-body">
<span class="modal-alert"></span>
${error ? html`<div class="alert alert-danger" role="alert">${error}</div>` : ''}
<form class="converse-form add-xmpp-contact" @submit=${ev => el.addContactFromForm(ev)}>
<div class="add-xmpp-contact__jid mb-3">
<label class="form-label clearfix" for="jid">${i18n_xmpp_address}:</label>
Expand Down Expand Up @@ -67,7 +67,6 @@ export default (el) => {
.list=${getGroupsAutoCompleteList()}
name="group"></converse-autocomplete>
</div>
${error ? html`<div><div style="display: block" class="invalid-feedback">${error}</div></div>` : ''}
<button type="submit" class="btn btn-primary">${i18n_add}</button>
</form>
</div>`;
Expand Down
20 changes: 20 additions & 0 deletions src/plugins/rosterview/modals/templates/new-chat.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { __ } from 'i18n';
import { html } from 'lit';

/**
* @param {import('../new-chat.js').default} el
*/
export default (el) => {
const i18n_start_chat = __('Start Chat');
const i18n_xmpp_address = __('XMPP Address');
return html` <div class="modal-body">
<span class="modal-alert"></span>
<form @submit=${/** @param {SubmitEvent} ev */(ev) => el.startChatFromForm(ev)}>
<div class="mb-3">
<label class="form-label" for="jid">${i18n_xmpp_address}</label>
<input type="text" name="jid" class="form-control" required />
</div>
<button type="submit" class="btn btn-primary">${i18n_start_chat}</button>
</form>
</div>`;
};
5 changes: 5 additions & 0 deletions src/plugins/rosterview/rosterview.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ export default class RosterView extends CustomElement {
api.modal.show('converse-add-contact-modal', {'model': new Model()}, ev);
}

/** @param {MouseEvent} ev */
showNewChatModal (ev) {
api.modal.show('converse-new-chat-modal', {'model': new Model()}, ev);
}

/** @param {MouseEvent} [ev] */
async syncContacts (ev) {
ev?.preventDefault();
Expand Down
28 changes: 21 additions & 7 deletions src/plugins/rosterview/templates/roster.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
/**
* @typedef {import('../rosterview').default} RosterView
*/
import { html } from 'lit';
import { repeat } from 'lit/directives/repeat.js';
import { _converse, api, constants } from '@converse/headless';
Expand All @@ -18,12 +15,13 @@ import {
const { CLOSED } = constants;

/**
* @param {RosterView} el
* @param {import('../rosterview').default} el
*/
export default (el) => {
const i18n_heading_contacts = __('Contacts');
const i18n_toggle_contacts = __('Click to toggle contacts');
const i18n_title_add_contact = __('Add a contact');
const i18n_title_new_chat = __('Start a new chat');
const roster = _converse.state.roster || [];
const contacts_map = roster.reduce((acc, contact) => populateContactsMap(acc, contact), {});
const groupnames = Object.keys(contacts_map).filter((contact) => shouldShowGroup(contact, el.model));
Expand All @@ -40,7 +38,7 @@ export default (el) => {
<a
href="#"
class="dropdown-item add-contact" role="button"
@click=${(/** @type {MouseEvent} */ ev) => el.showAddContactModal(ev)}
@click="${(/** @type {MouseEvent} */ ev) => el.showAddContactModal(ev)}"
title="${i18n_title_add_contact}"
data-toggle="modal"
data-target="#add-contact-modal"
Expand All @@ -51,11 +49,27 @@ export default (el) => {
`);
}

if (api.settings.get('allow_non_roster_messaging')) {
btns.push(html`
<a
href="#"
class="dropdown-item" role="button"
@click="${(/** @type {MouseEvent} */ ev) => el.showNewChatModal(ev)}"
title="${i18n_title_new_chat}"
data-toggle="modal"
data-target="#new-chat-modal"
>
<converse-icon class="fa fa-user-plus" size="1em"></converse-icon>
${i18n_title_new_chat}
</a>
`);
}

if (roster.length > 5) {
btns.push(html`
<a href="#"
class="dropdown-item toggle-filter" role="button"
@click=${(/** @type {MouseEvent} */ ev) => el.toggleFilter(ev)}>
@click="${(/** @type {MouseEvent} */ ev) => el.toggleFilter(ev)}">
<converse-icon size="1em" class="fa fa-filter"></converse-icon>
${is_filter_visible ? i18n_hide_filter : i18n_show_filter}
</a>
Expand All @@ -68,7 +82,7 @@ export default (el) => {
<a
href="#"
class="dropdown-item" role="button"
@click=${(/** @type {MouseEvent} */ ev) => el.syncContacts(ev)}
@click="${(/** @type {MouseEvent} */ ev) => el.syncContacts(ev)}"
title="${i18n_title_sync_contacts}"
>
<converse-icon class="fa fa-sync sync-contacts" size="1em"></converse-icon>
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/rosterview/tests/add-contact-modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ describe("The 'Add Contact' widget", function () {
input_el.value = 'ambiguous';
modal.querySelector('button[type="submit"]').click();

const feedback_el = await u.waitUntil(() => modal.querySelector('.invalid-feedback'));
const feedback_el = await u.waitUntil(() => modal.querySelector('.alert-danger'));
expect(feedback_el.textContent).toBe('Sorry, could not find a contact with that name');

input_el.value = 'existing';
Expand Down
21 changes: 9 additions & 12 deletions src/plugins/rosterview/tests/protocol.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

// See: https://xmpp.org/rfcs/rfc3921.html

const { Strophe, stx } = converse.env;
const { u, $iq, $pres, sizzle, Strophe, stx } = converse.env;

describe("The Protocol", function () {

Expand Down Expand Up @@ -40,7 +40,6 @@ describe("The Protocol", function () {
it("Subscribe to contact, contact accepts and subscribes back",
mock.initConverse([], { roster_groups: false }, async function (_converse) {

const { u, $iq, $pres, sizzle, Strophe } = converse.env;
let stanza;
await mock.waitForRoster(_converse, 'current', 0);
await mock.waitUntilDiscoConfirmed(_converse, 'montague.lit', [], ['vcard-temp']);
Expand All @@ -51,7 +50,6 @@ describe("The Protocol", function () {
mock.openControlBox(_converse);
const cbview = _converse.chatboxviews.get('controlbox');

spyOn(_converse.roster, "addContact").and.callThrough();
spyOn(_converse.roster, "sendContactAddIQ").and.callThrough();
spyOn(_converse.api.vcard, "get").and.callThrough();

Expand All @@ -74,7 +72,6 @@ describe("The Protocol", function () {
* subscription, the user's client SHOULD perform a "roster set"
* for the new roster item.
*/
expect(_converse.roster.addContact).toHaveBeenCalled();

/* The request consists of sending an IQ
* stanza of type='set' containing a <query/> element qualified by
Expand All @@ -100,14 +97,14 @@ describe("The Protocol", function () {
const IQ_stanzas = _converse.api.connection.get().IQ_stanzas;
const roster_set_stanza = IQ_stanzas.filter(s => sizzle('query[xmlns="jabber:iq:roster"]', s)).pop();

expect(Strophe.serialize(roster_set_stanza)).toBe(
`<iq id="${roster_set_stanza.getAttribute('id')}" type="set" xmlns="jabber:client">`+
`<query xmlns="jabber:iq:roster">`+
`<item jid="contact@example.org" name="Chris Contact">`+
`<group>My Buddies</group>`+
`</item>`+
`</query>`+
`</iq>`
expect(roster_set_stanza).toEqualStanza(
stx`<iq id="${roster_set_stanza.getAttribute('id')}" type="set" xmlns="jabber:client">
<query xmlns="jabber:iq:roster">
<item jid="contact@example.org" name="Chris Contact">
<group>My Buddies</group>
</item>
</query>
</iq>`
);

const sent_stanzas = [];
Expand Down
18 changes: 15 additions & 3 deletions src/types/plugins/rosterview/modals/add-contact.d.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
export default class AddContactModal extends BaseModal {
renderModal(): import("lit").TemplateResult<1>;
getModalTitle(): any;
validateSubmission(jid: any): boolean;
afterSubmission(_form: any, jid: any, name: any, group: any): void;
addContactFromForm(ev: any): Promise<void>;
/**
* @param {string} jid
*/
validateSubmission(jid: string): boolean;
/**
* @param {HTMLFormElement} _form
* @param {string} jid
* @param {string} name
* @param {FormDataEntryValue} group
*/
afterSubmission(_form: HTMLFormElement, jid: string, name: string, group: FormDataEntryValue): Promise<void>;
/**
* @param {Event} ev
*/
addContactFromForm(ev: Event): Promise<void>;
}
import BaseModal from 'plugins/modal/modal.js';
//# sourceMappingURL=add-contact.d.ts.map
19 changes: 19 additions & 0 deletions src/types/plugins/rosterview/modals/new-chat.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export default class NewChatModal extends BaseModal {
renderModal(): import("lit").TemplateResult<1>;
getModalTitle(): any;
/**
* @param {string} jid
*/
validateSubmission(jid: string): boolean;
/**
* @param {HTMLFormElement} _form
* @param {string} jid
*/
afterSubmission(_form: HTMLFormElement, jid: string): Promise<void>;
/**
* @param {SubmitEvent} ev
*/
startChatFromForm(ev: SubmitEvent): Promise<void>;
}
import BaseModal from 'plugins/modal/modal.js';
//# sourceMappingURL=new-chat.d.ts.map
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
declare function _default(el: any): import("lit").TemplateResult<1>;
declare function _default(el: import("../add-contact.js").default): import("lit").TemplateResult<1>;
export default _default;
//# sourceMappingURL=add-contact.d.ts.map
3 changes: 3 additions & 0 deletions src/types/plugins/rosterview/modals/templates/new-chat.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
declare function _default(el: import("../new-chat.js").default): import("lit").TemplateResult<1>;
export default _default;
//# sourceMappingURL=new-chat.d.ts.map
2 changes: 2 additions & 0 deletions src/types/plugins/rosterview/rosterview.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export default class RosterView extends CustomElement {
render(): import("lit").TemplateResult<1>;
/** @param {MouseEvent} ev */
showAddContactModal(ev: MouseEvent): void;
/** @param {MouseEvent} ev */
showNewChatModal(ev: MouseEvent): void;
/** @param {MouseEvent} [ev] */
syncContacts(ev?: MouseEvent): Promise<void>;
syncing_contacts: boolean;
Expand Down
3 changes: 1 addition & 2 deletions src/types/plugins/rosterview/templates/roster.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
declare function _default(el: RosterView): import("lit").TemplateResult<1>;
declare function _default(el: import("../rosterview").default): import("lit").TemplateResult<1>;
export default _default;
export type RosterView = import("../rosterview").default;
//# sourceMappingURL=roster.d.ts.map

0 comments on commit 9c51c17

Please sign in to comment.