Use our own `confirm` dialog consistently

This commit is contained in:
JC Brand 2022-05-11 10:01:18 +02:00
parent a3c0f90476
commit a57853156e
18 changed files with 87 additions and 72 deletions

View File

@ -12,6 +12,7 @@ module.exports = function(config) {
{ pattern: 'dist/*.css.map', included: false },
{ pattern: "dist/icons.js", served: true },
{ pattern: "dist/emojis.js", served: true },
"src/shared/tests/tests.css",
"node_modules/lodash/lodash.min.js",
"dist/converse.js",
"dist/converse.css",

View File

@ -102,9 +102,9 @@ export async function onDirectMUCInvitation (message) {
let contact = _converse.roster.get(from);
contact = contact ? contact.getDisplayName() : from;
if (!reason) {
result = confirm(__('%1$s has invited you to join a groupchat: %2$s', contact, room_jid));
result = await api.confirm(__('%1$s has invited you to join a groupchat: %2$s', contact, room_jid));
} else {
result = confirm(
result = await api.confirm(
__(
'%1$s has invited you to join a groupchat: %2$s, and left the following reason: "%3$s"',
contact,
@ -114,7 +114,7 @@ export async function onDirectMUCInvitation (message) {
);
}
}
if (result === true) {
if (result) {
const chatroom = await openChatRoom(room_jid, { 'password': x_el.getAttribute('password') });
if (chatroom.session.get('connection_status') === converse.ROOMSTATUS.DISCONNECTED) {
_converse.chatboxes.get(room_jid).rejoin();

View File

@ -19,7 +19,8 @@ describe("The User Details Modal", function () {
show_modal_button.click();
const modal = _converse.api.modal.get('user-details-modal');
await u.waitUntil(() => u.isVisible(modal.el), 1000);
spyOn(window, 'confirm').and.returnValue(true);
spyOn(_converse.api, 'confirm').and.returnValue(Promise.resolve(true));
spyOn(view.model.contact, 'removeFromRoster').and.callFake(callback => callback());
let remove_contact_button = modal.el.querySelector('button.remove-contact');
expect(u.isVisible(remove_contact_button)).toBeTruthy();
@ -45,7 +46,7 @@ describe("The User Details Modal", function () {
show_modal_button.click();
let modal = _converse.api.modal.get('user-details-modal');
await u.waitUntil(() => u.isVisible(modal.el), 2000);
spyOn(window, 'confirm').and.returnValue(true);
spyOn(_converse.api, 'confirm').and.returnValue(Promise.resolve(true));
spyOn(view.model.contact, 'removeFromRoster').and.callFake((callback, errback) => errback());
let remove_contact_button = modal.el.querySelector('button.remove-contact');

View File

@ -83,11 +83,11 @@ const UserDetailsModal = BootstrapModal.extend({
u.removeClass('fa-spin', refresh_icon);
},
removeContact (ev) {
async removeContact (ev) {
ev?.preventDefault?.();
if (!api.settings.get('allow_contact_removal')) { return; }
const result = confirm(__("Are you sure you want to remove this contact?"));
if (result === true) {
const result = await api.confirm(__("Are you sure you want to remove this contact?"));
if (result) {
// XXX: The `dismissHandler` in bootstrap.native tries to
// reference the remove button after it's been cleared from
// the DOM, so we delay removing the contact to give it time.

View File

@ -557,9 +557,9 @@ describe("Bookmarks", function () {
expect(els[3].textContent).toBe("noname@conference.shakespeare.lit");
expect(els[4].textContent).toBe("The Play's the Thing");
spyOn(window, 'confirm').and.returnValue(true);
spyOn(_converse.api, 'confirm').and.callFake(() => Promise.resolve(true));
document.querySelector('#chatrooms .bookmarks.rooms-list .room-item:nth-child(2) a:nth-child(2)').click();
expect(window.confirm).toHaveBeenCalled();
expect(_converse.api.confirm).toHaveBeenCalled();
await u.waitUntil(() => document.querySelectorAll('#chatrooms div.bookmarks.rooms-list .room-item').length === 4)
els = document.querySelectorAll('#chatrooms div.bookmarks.rooms-list .room-item a.list-item-link');
expect(els[0].textContent).toBe("1st Bookmark");

View File

@ -25,11 +25,12 @@ export function getHeadingButtons (view, buttons) {
return buttons;
}
export function removeBookmarkViaEvent (ev) {
export async function removeBookmarkViaEvent (ev) {
ev.preventDefault();
const name = ev.target.getAttribute('data-bookmark-name');
const jid = ev.target.getAttribute('data-room-jid');
if (confirm(__('Are you sure you want to remove the bookmark "%1$s"?', name))) {
const result = await api.confirm(__('Are you sure you want to remove the bookmark "%1$s"?', name));
if (result) {
invokeMap(_converse.bookmarks.where({ jid }), Model.prototype.destroy);
}
}

View File

@ -49,7 +49,7 @@ describe("Chatboxes", function () {
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await mock.openChatBoxFor(_converse, contact_jid);
const view = _converse.chatboxviews.get(contact_jid);
spyOn(window, 'confirm').and.returnValue(true);
spyOn(_converse.api, 'confirm').and.callFake(() => Promise.resolve(true));
for (const i of Array(10).keys()) {
mock.sendMessage(view, `Message ${i}`);
@ -64,7 +64,7 @@ describe("Chatboxes", function () {
preventDefault: function preventDefault () {},
keyCode: 13 // Enter
});
await u.waitUntil(() => window.confirm.calls.count() === 1);
await u.waitUntil(() => _converse.api.confirm.calls.count() === 1);
await u.waitUntil(() => sizzle('converse-chat-message', view).length === 0);
expect(true).toBe(true);
}));
@ -916,15 +916,15 @@ describe("Chatboxes", function () {
message = '/clear';
const message_form = view.querySelector('converse-message-form');
spyOn(window, 'confirm').and.callFake(() => true);
spyOn(_converse.api, 'confirm').and.callFake(() => Promise.resolve(true));
view.querySelector('.chat-textarea').value = message;
message_form.onKeyDown({
target: view.querySelector('textarea.chat-textarea'),
preventDefault: function preventDefault () {},
keyCode: 13
});
await u.waitUntil(() => window.confirm.calls.count() === 1);
expect(window.confirm).toHaveBeenCalledWith('Are you sure you want to clear the messages from this conversation?');
await u.waitUntil(() => _converse.api.confirm.calls.count() === 1);
expect(_converse.api.confirm).toHaveBeenCalledWith('Are you sure you want to clear the messages from this conversation?');
await u.waitUntil(() => view.model.messages.length === 0);
await u.waitUntil(() => !view.querySelectorAll('.chat-msg__body').length);
}));

View File

@ -268,23 +268,26 @@ describe("A Chat Message", function () {
expect(view.querySelectorAll('.chat-msg .chat-msg__action').length).toBe(2);
// Test confirmation dialog
spyOn(window, 'confirm').and.returnValue(true);
spyOn(_converse.api, 'confirm').and.callFake(() => Promise.resolve(true));
textarea.value = 'But soft, what light through yonder airlock breaks?';
action = view.querySelector('.chat-msg .chat-msg__action');
action.style.opacity = 1;
action.click();
expect(window.confirm).toHaveBeenCalledWith(
await u.waitUntil(() => _converse.api.confirm.calls.count());
expect(_converse.api.confirm).toHaveBeenCalledWith(
'You have an unsent message which will be lost if you continue. Are you sure?');
expect(view.model.messages.at(0).get('correcting')).toBe(true);
expect(textarea.value).toBe('But soft, what light through yonder window breaks?');
textarea.value = 'But soft, what light through yonder airlock breaks?'
action.click();
await u.waitUntil(() => _converse.api.confirm.calls.count() === 2);
expect(view.model.messages.at(0).get('correcting')).toBe(false);
expect(window.confirm.calls.count()).toBe(2);
expect(window.confirm.calls.argsFor(0)).toEqual(
expect(_converse.api.confirm.calls.argsFor(0)).toEqual(
['You have an unsent message which will be lost if you continue. Are you sure?']);
expect(window.confirm.calls.argsFor(1)).toEqual(
expect(_converse.api.confirm.calls.argsFor(1)).toEqual(
['You have an unsent message which will be lost if you continue. Are you sure?']);
}));

View File

@ -31,8 +31,8 @@ export async function getHeadingStandaloneButton (promise_or_data) {
}
export async function clearMessages (chat) {
const result = confirm(__('Are you sure you want to clear the messages from this conversation?'));
if (result === true) {
const result = await api.confirm(__('Are you sure you want to clear the messages from this conversation?'));
if (result) {
await chat.clearMessages();
}
}

View File

@ -1385,7 +1385,7 @@ describe("Groupchats", function () {
const from_jid = name.replace(/ /g,'.').toLowerCase() + '@montague.lit';
await u.waitUntil(() => _converse.roster.get(from_jid).vcard.get('fullname'));
spyOn(window, 'confirm').and.callFake(() => true);
spyOn(_converse.api, 'confirm').and.callFake(() => Promise.resolve(true));
await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
const view = _converse.chatboxviews.get('lounge@montague.lit');
await view.close(); // Hack, otherwise we have to mock stanzas.
@ -1402,7 +1402,7 @@ describe("Groupchats", function () {
</message>`);
await _converse.onDirectMUCInvitation(stanza);
expect(window.confirm).toHaveBeenCalledWith(
expect(_converse.api.confirm).toHaveBeenCalledWith(
name + ' has invited you to join a groupchat: '+ muc_jid +
', and left the following reason: "'+reason+'"');
expect(_converse.chatboxes.models.length).toBe(2);
@ -2461,15 +2461,15 @@ describe("Groupchats", function () {
const view = _converse.chatboxviews.get('lounge@montague.lit');
const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
textarea.value = '/clear';
spyOn(window, 'confirm').and.callFake(() => false);
spyOn(_converse.api, 'confirm').and.callFake(() => Promise.resolve(false));
const message_form = view.querySelector('converse-muc-message-form');
message_form.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
keyCode: 13
});
await u.waitUntil(() => window.confirm.calls.count() === 1);
expect(window.confirm).toHaveBeenCalledWith('Are you sure you want to clear the messages from this conversation?');
await u.waitUntil(() => _converse.api.confirm.calls.count() === 1);
expect(_converse.api.confirm).toHaveBeenCalledWith('Are you sure you want to clear the messages from this conversation?');
}));
it("takes /owner to make a user an owner", mock.initConverse([], {}, async function (_converse) {

View File

@ -313,7 +313,7 @@ describe("A groupchat shown in the groupchats list", function () {
async function (_converse) {
const u = converse.env.utils;
spyOn(window, 'confirm').and.callFake(() => true);
spyOn(_converse.api, 'confirm').and.callFake(() => Promise.resolve(true));
expect(_converse.chatboxes.length).toBe(1);
await mock.waitForRoster(_converse, 'current', 0);
await mock.openChatRoom(_converse, 'lounge', 'conference.shakespeare.lit', 'JC');
@ -328,7 +328,7 @@ describe("A groupchat shown in the groupchats list", function () {
const rooms_list = document.querySelector('converse-rooms-list');
const close_el = rooms_list.querySelector(".close-room");
close_el.click();
expect(window.confirm).toHaveBeenCalledWith(
expect(_converse.api.confirm).toHaveBeenCalledWith(
'Are you sure you want to leave the groupchat lounge@conference.shakespeare.lit?');
await u.waitUntil(() => rooms_list.querySelectorAll(".open-room").length === 0);

View File

@ -60,10 +60,13 @@ export class Profile extends CustomElement {
async generateOMEMODeviceBundle (ev) {
ev.preventDefault();
if (confirm(__(
const result = await api.confirm(__(
'Are you sure you want to generate new OMEMO keys? ' +
'This will remove your old keys and all previously encrypted messages will no longer be decryptable on this device.'
))) {
'This will remove your old keys and all previously ' +
'encrypted messages will no longer be decryptable on this device.'));
if (result) {
await api.omemo.bundle.generate();
await this.setAttributes();
this.requestUpdate();

View File

@ -74,7 +74,8 @@ export class RoomsList extends CustomElement {
async closeRoom (ev) { // eslint-disable-line class-methods-use-this
ev.preventDefault();
const name = ev.target.getAttribute('data-room-name');
if (confirm(__("Are you sure you want to leave the groupchat %1$s?", name))) {
const result = await api.confirm(__("Are you sure you want to leave the groupchat %1$s?", name));
if (result) {
const jid = ev.target.getAttribute('data-room-jid');
const room = await api.rooms.get(jid);
room.close();

View File

@ -77,10 +77,12 @@ export default class RosterContact extends CustomElement {
this.model.openChat();
}
removeContact (ev) {
async removeContact (ev) {
ev?.preventDefault?.();
if (!api.settings.get('allow_contact_removal')) { return; }
if (!confirm(__("Are you sure you want to remove this contact?"))) { return; }
const result = await api.confirm(__("Are you sure you want to remove this contact?"));
if (!result) return;
try {
this.model.removeFromRoster();
@ -108,10 +110,10 @@ export default class RosterContact extends CustomElement {
this.model.authorize().subscribe();
}
declineRequest (ev) {
async declineRequest (ev) {
if (ev && ev.preventDefault) { ev.preventDefault(); }
const result = confirm(__("Are you sure you want to decline this contact request?"));
if (result === true) {
const result = await api.confirm(__("Are you sure you want to decline this contact request?"));
if (result) {
this.model.unauthorize().destroy();
}
return this;

View File

@ -447,7 +447,7 @@ describe("The Protocol", function () {
const jid = 'abram@montague.lit';
await mock.openControlBox(_converse);
await mock.waitForRoster(_converse, 'current');
spyOn(window, 'confirm').and.returnValue(true);
spyOn(_converse.api, 'confirm').and.callFake(() => Promise.resolve(true));
// We now have a contact we want to remove
expect(_converse.roster.get(jid) instanceof _converse.RosterContact).toBeTruthy();
@ -457,7 +457,7 @@ describe("The Protocol", function () {
// remove the first user
header.parentElement.querySelector('li .remove-xmpp-contact').click();
expect(window.confirm).toHaveBeenCalled();
expect(_converse.api.confirm).toHaveBeenCalled();
/* Section 8.6 Removing a Roster Item and Cancelling All
* Subscriptions
@ -478,14 +478,14 @@ describe("The Protocol", function () {
* </query>
* </iq>
*/
const sent_iq = _converse.connection.IQ_stanzas.pop();
expect(Strophe.serialize(sent_iq)).toBe(
`<iq id="${sent_iq.getAttribute('id')}" type="set" xmlns="jabber:client">`+
const iq_stanzas = _converse.connection.IQ_stanzas;
await u.waitUntil(() => Strophe.serialize(iq_stanzas.at(-1)) ===
`<iq id="${iq_stanzas.at(-1).getAttribute('id')}" type="set" xmlns="jabber:client">`+
`<query xmlns="jabber:iq:roster">`+
`<item jid="abram@montague.lit" subscription="remove"/>`+
`</query>`+
`</iq>`);
const sent_iq = iq_stanzas.at(-1);
// Receive confirmation from the contact's server
// <iq type='result' id='remove1'/>

View File

@ -642,7 +642,7 @@ describe("The Contacts Roster", function () {
const jid = name.replace(/ /g,'.').toLowerCase() + '@montague.lit';
const contact = _converse.roster.get(jid);
var sent_IQ;
spyOn(window, 'confirm').and.returnValue(true);
spyOn(_converse.api, 'confirm').and.callFake(() => Promise.resolve(true));
spyOn(contact, 'unauthorize').and.callFake(function () { return contact; });
spyOn(contact, 'removeFromRoster').and.callThrough();
const rosterview = document.querySelector('converse-roster');
@ -653,7 +653,7 @@ describe("The Contacts Roster", function () {
});
sizzle(`.remove-xmpp-contact[title="Click to remove ${name} as a contact"]`, rosterview).pop().click();
await u.waitUntil(() => (sizzle(".pending-contact-name:contains('"+name+"')", rosterview).length === 0), 1000);
expect(window.confirm).toHaveBeenCalled();
expect(_converse.api.confirm).toHaveBeenCalled();
expect(contact.removeFromRoster).toHaveBeenCalled();
expect(Strophe.serialize(sent_IQ)).toBe(
`<iq type="set" xmlns="jabber:client">`+
@ -684,18 +684,19 @@ describe("The Contacts Roster", function () {
}, 700)
const remove_el = await u.waitUntil(() => sizzle(`.remove-xmpp-contact[title="Click to remove ${name} as a contact"]`, rosterview).pop());
spyOn(window, 'confirm').and.returnValue(true);
spyOn(_converse.api, 'confirm').and.callFake(() => Promise.resolve(true));
remove_el.click();
expect(window.confirm).toHaveBeenCalled();
expect(_converse.api.confirm).toHaveBeenCalled();
const iq = _converse.connection.IQ_stanzas.pop();
expect(Strophe.serialize(iq)).toBe(
`<iq id="${iq.getAttribute('id')}" type="set" xmlns="jabber:client">`+
const iq_stanzas = _converse.connection.IQ_stanzas;
await u.waitUntil(() => Strophe.serialize(iq_stanzas.at(-1)) ===
`<iq id="${iq_stanzas.at(-1).getAttribute('id')}" type="set" xmlns="jabber:client">`+
`<query xmlns="jabber:iq:roster">`+
`<item jid="lord.capulet@montague.lit" subscription="remove"/>`+
`</query>`+
`</iq>`);
const iq = iq_stanzas.at(-1);
const stanza = u.toStanza(`<iq id="${iq.getAttribute('id')}" to="romeo@montague.lit/orchard" type="result"/>`);
_converse.connection._dataRecv(mock.createRequest(stanza));
await u.waitUntil(() => rosterview.querySelector(`ul[data-group="Pending contacts"]`) === null);
@ -709,7 +710,7 @@ describe("The Contacts Roster", function () {
await Promise.all(_converse.roster.map(contact => u.waitUntil(() => contact.vcard.get('fullname'))));
await u.waitUntil(() => _converse.roster.at(0).vcard.get('fullname'))
const rosterview = document.querySelector('converse-roster');
spyOn(window, 'confirm').and.returnValue(true);
spyOn(_converse.api, 'confirm').and.returnValue(Promise.resolve(true));
for (let i=0; i<mock.pend_names.length; i++) {
const name = mock.pend_names[i];
sizzle(`.remove-xmpp-contact[title="Click to remove ${name} as a contact"]`, rosterview).pop().click();
@ -824,16 +825,18 @@ describe("The Contacts Roster", function () {
const name = mock.cur_names[0];
const jid = name.replace(/ /g,'.').toLowerCase() + '@montague.lit';
const contact = _converse.roster.get(jid);
spyOn(window, 'confirm').and.returnValue(true);
spyOn(_converse.api, 'confirm').and.returnValue(Promise.resolve(true));
spyOn(contact, 'removeFromRoster').and.callThrough();
let sent_IQ;
spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback) {
spyOn(_converse.connection, 'sendIQ').and.callFake((iq, callback) => {
sent_IQ = iq;
callback();
});
sizzle(`.remove-xmpp-contact[title="Click to remove ${name} as a contact"]`, rosterview).pop().click();
expect(window.confirm).toHaveBeenCalled();
expect(_converse.api.confirm).toHaveBeenCalled();
await u.waitUntil(() => sent_IQ);
expect(Strophe.serialize(sent_IQ)).toBe(
`<iq type="set" xmlns="jabber:client">`+
`<query xmlns="jabber:iq:roster"><item jid="mercutio@montague.lit" subscription="remove"/></query>`+
@ -858,15 +861,13 @@ describe("The Contacts Roster", function () {
});
const rosterview = document.querySelector('converse-roster');
await u.waitUntil(() => sizzle('.roster-group', rosterview).filter(u.isVisible).map(e => e.querySelector('li')).length, 1000);
spyOn(window, 'confirm').and.returnValue(true);
spyOn(_converse.api, 'confirm').and.returnValue(Promise.resolve(true));
spyOn(contact, 'removeFromRoster').and.callThrough();
spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback) {
if (typeof callback === "function") { return callback(); }
});
spyOn(_converse.connection, 'sendIQ').and.callFake((iq, callback) => callback?.());
expect(u.isVisible(rosterview.querySelector('.roster-group'))).toBe(true);
sizzle(`.remove-xmpp-contact[title="Click to remove ${name} as a contact"]`, rosterview).pop().click();
expect(window.confirm).toHaveBeenCalled();
expect(_converse.connection.sendIQ).toHaveBeenCalled();
expect(_converse.api.confirm).toHaveBeenCalled();
await u.waitUntil(() => _converse.connection.sendIQ.calls.count());
expect(contact.removeFromRoster).toHaveBeenCalled();
await u.waitUntil(() => rosterview.querySelectorAll('.roster-group').length === 0);
}));
@ -1126,7 +1127,7 @@ describe("The Contacts Roster", function () {
await mock.openControlBox(_converse);
await mock.waitForRoster(_converse, "current", 0);
const name = mock.req_names[0];
spyOn(window, 'confirm').and.returnValue(true);
spyOn(_converse.api, 'confirm').and.returnValue(Promise.resolve(true));
_converse.roster.create({
'jid': name.replace(/ /g,'.').toLowerCase() + '@montague.lit',
'subscription': 'none',
@ -1139,7 +1140,7 @@ 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(window.confirm).toHaveBeenCalled();
expect(_converse.api.confirm).toHaveBeenCalled();
await u.waitUntil(() => rosterview.querySelector(`ul[data-group="Contact requests"]`) === null);
}));
@ -1191,12 +1192,12 @@ describe("The Contacts Roster", function () {
const name = mock.req_names.sort()[1];
const jid = name.replace(/ /g,'.').toLowerCase() + '@montague.lit';
const contact = _converse.roster.get(jid);
spyOn(window, 'confirm').and.returnValue(true);
spyOn(_converse.api, 'confirm').and.returnValue(Promise.resolve(true));
spyOn(contact, 'unauthorize').and.callFake(function () { return contact; });
const req_contact = await u.waitUntil(() => sizzle(".req-contact-name:contains('"+name+"')", rosterview).pop());
req_contact.parentElement.parentElement.querySelector('.decline-xmpp-request').click();
expect(window.confirm).toHaveBeenCalled();
expect(contact.unauthorize).toHaveBeenCalled();
expect(_converse.api.confirm).toHaveBeenCalled();
await u.waitUntil(() => contact.unauthorize.calls.count());
// There should now be one less contact
expect(_converse.roster.length).toEqual(mock.req_names.length-1);
}));

View File

@ -65,16 +65,15 @@ class MessageActions extends CustomElement {
`;
}
onMessageEditButtonClicked (ev) {
async onMessageEditButtonClicked (ev) {
ev.preventDefault();
const currently_correcting = this.model.collection.findWhere('correcting');
// TODO: Use state intead of DOM querying
// Then this code can also be put on the model
const unsent_text = u.ancestor(this, '.chatbox')?.querySelector('.chat-textarea')?.value;
if (unsent_text && (!currently_correcting || currently_correcting.getMessageText() !== unsent_text)) {
if (!confirm(__('You have an unsent message which will be lost if you continue. Are you sure?'))) {
return;
}
const result = await api.confirm(__('You have an unsent message which will be lost if you continue. Are you sure?'));
if (!result) return;
}
if (currently_correcting !== this.model) {
currently_correcting?.save('correcting', false);

View File

@ -0,0 +1,3 @@
body {
overflow: auto !important;
}