Move methods to utils

So that they can also be used independently of the components
This commit is contained in:
JC Brand 2021-02-24 15:06:20 +01:00
parent d335a11119
commit d8cab006c8
6 changed files with 247 additions and 243 deletions

View File

@ -911,7 +911,7 @@ describe("Chatboxes", function () {
describe("Special Messages", function () {
it("'/clear' can be used to clear messages in a conversation",
fit("'/clear' can be used to clear messages in a conversation",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');
@ -925,28 +925,21 @@ describe("Chatboxes", function () {
await mock.sendMessage(view, message);
expect(view.model.messages.length === 1).toBeTruthy();
let stored_messages = await view.model.messages.browserStorage.findAll();
const stored_messages = await view.model.messages.browserStorage.findAll();
expect(stored_messages.length).toBe(1);
await u.waitUntil(() => view.querySelector('.chat-msg'));
message = '/clear';
const bottom_panel = view.querySelector('converse-chat-bottom-panel');
spyOn(bottom_panel, 'clearMessages').and.callThrough();
spyOn(window, 'confirm').and.callFake(function () {
return true;
});
spyOn(window, 'confirm').and.callFake(() => true);
view.querySelector('.chat-textarea').value = message;
bottom_panel.onKeyDown({
target: view.querySelector('textarea.chat-textarea'),
preventDefault: function preventDefault () {},
keyCode: 13
});
expect(bottom_panel.clearMessages.calls.all().length).toBe(1);
await bottom_panel.clearMessages.calls.all()[0].returnValue;
expect(window.confirm).toHaveBeenCalled();
expect(view.model.messages.length, 0); // The messages must be removed from the chatbox
stored_messages = await view.model.messages.browserStorage.findAll();
expect(stored_messages.length).toBe(0);
expect(window.confirm).toHaveBeenCalledWith('Are you sure you want to clear the messages from this conversation?');
await u.waitUntil(() => view.model.messages.length === 0);
expect(_converse.api.trigger.calls.count(), 1);
expect(_converse.api.trigger.calls.mostRecent().args, ['messageSend', message]);
done();

View File

@ -3294,13 +3294,13 @@ describe("Groupchats", function () {
const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
textarea.value = '/clear';
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
spyOn(bottom_panel, 'clearMessages');
spyOn(window, 'confirm').and.callFake(() => false);
bottom_panel.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
keyCode: 13
});
expect(bottom_panel.clearMessages).toHaveBeenCalled();
expect(window.confirm).toHaveBeenCalledWith('Are you sure you want to clear the messages from this conversation?');
done();
}));

View File

@ -4,6 +4,7 @@ import { ElementView } from '@converse/skeletor/src/element.js';
import { __ } from 'i18n';
import { _converse, api, converse } from "@converse/headless/core";
import { html, render } from 'lit-html';
import { clearMessages, parseMessageForCommands } from './utils.js';
const { u } = converse.env;
@ -109,29 +110,13 @@ export default class ChatBottomPanel extends ElementView {
ev.preventDefault();
}
async clearMessages (ev) {
clearMessages (ev) {
ev?.preventDefault?.();
const result = confirm(__('Are you sure you want to clear the messages from this conversation?'));
if (result === true) {
await this.model.clearMessages();
}
return this;
clearMessages(this.model);
}
parseMessageForCommands (text) {
const match = text.replace(/^\s*/, '').match(/^\/(.*)\s*$/);
if (match) {
if (match[1] === 'clear') {
this.clearMessages();
return true;
} else if (match[1] === 'close') {
_converse.chatboxviews.get(this.getAttribute('jid'))?.close();
return true;
} else if (match[1] === 'help') {
this.model.set({ 'show_help_messages': true });
return true;
}
}
return parseMessageForCommands(this.model, text);
}
async onFormSubmitted () {

View File

@ -1,3 +1,5 @@
import { __ } from 'i18n';
import { _converse } from "@converse/headless/core";
import { html } from 'lit-html';
@ -21,3 +23,27 @@ export async function getHeadingStandaloneButton (promise_or_data) {
></a>
`;
}
async function clearMessages (chat) {
const result = confirm(__('Are you sure you want to clear the messages from this conversation?'));
if (result === true) {
await chat.clearMessages();
}
}
export function parseMessageForCommands (chat, text) {
const match = text.replace(/^\s*/, '').match(/^\/(.*)\s*$/);
if (match) {
if (match[1] === 'clear') {
clearMessages(chat);
return true;
} else if (match[1] === 'close') {
_converse.chatboxviews.get(chat.get('jid'))?.close();
return true;
} else if (match[1] === 'help') {
chat.set({ 'show_help_messages': true });
return true;
}
}
}

View File

@ -3,29 +3,9 @@ import debounce from 'lodash/debounce';
import tpl_muc_bottom_panel from './templates/muc-bottom-panel.js';
import { __ } from 'i18n';
import { _converse, api, converse } from "@converse/headless/core";
import { getAutoCompleteListItem } from './utils.js';
import { getAutoCompleteListItem, parseMessageForMUCCommands } from './utils.js';
import { render } from 'lit-html';
const { Strophe, $pres } = converse.env;
const COMMAND_TO_AFFILIATION = {
'admin': 'admin',
'ban': 'outcast',
'member': 'member',
'owner': 'owner',
'revoke': 'none'
};
const COMMAND_TO_ROLE = {
'deop': 'participant',
'kick': 'none',
'mute': 'visitor',
'op': 'moderator',
'voice': 'participant'
};
const u = converse.env.utils;
export default class MUCBottomPanel extends BottomPanel {
@ -117,194 +97,8 @@ export default class MUCBottomPanel extends BottomPanel {
super.onKeyUp(ev);
}
setRole (command, args, required_affiliations = [], required_roles = []) {
/* Check that a command to change a groupchat user's role or
* affiliation has anough arguments.
*/
const role = COMMAND_TO_ROLE[command];
if (!role) {
throw Error(`ChatRoomView#setRole called with invalid command: ${command}`);
}
if (!this.model.verifyAffiliations(required_affiliations) || !this.model.verifyRoles(required_roles)) {
return false;
}
if (!this.model.validateRoleOrAffiliationChangeArgs(command, args)) {
return false;
}
const nick_or_jid = this.model.getNickOrJIDFromCommandArgs(args);
if (!nick_or_jid) {
return false;
}
const reason = args.split(nick_or_jid, 2)[1].trim();
// We're guaranteed to have an occupant due to getNickOrJIDFromCommandArgs
const occupant = this.model.getOccupant(nick_or_jid);
this.model.setRole(occupant, role, reason, undefined, this.model.onCommandError.bind(this));
return true;
}
setAffiliation (command, args, required_affiliations) {
const affiliation = COMMAND_TO_AFFILIATION[command];
if (!affiliation) {
throw Error(`ChatRoomView#setAffiliation called with invalid command: ${command}`);
}
if (!this.model.verifyAffiliations(required_affiliations)) {
return false;
}
if (!this.model.validateRoleOrAffiliationChangeArgs(command, args)) {
return false;
}
const nick_or_jid = this.model.getNickOrJIDFromCommandArgs(args);
if (!nick_or_jid) {
return false;
}
let jid;
const reason = args.split(nick_or_jid, 2)[1].trim();
const occupant = this.model.getOccupant(nick_or_jid);
if (occupant) {
jid = occupant.get('jid');
} else {
if (u.isValidJID(nick_or_jid)) {
jid = nick_or_jid;
} else {
const message = __(
"Couldn't find a participant with that nickname. " + 'They might have left the groupchat.'
);
this.model.createMessage({ message, 'type': 'error' });
return;
}
}
const attrs = { jid, reason };
if (occupant && api.settings.get('auto_register_muc_nickname')) {
attrs['nick'] = occupant.get('nick');
}
this.model
.setAffiliation(affiliation, [attrs])
.then(() => this.model.occupants.fetchMembers())
.catch(err => this.model.onCommandError(err));
}
parseMessageForCommands (text) {
if (
api.settings.get('muc_disable_slash_commands') &&
!Array.isArray(api.settings.get('muc_disable_slash_commands'))
) {
return super.parseMessageForCommands(text);
}
text = text.replace(/^\s*/, '');
const command = (text.match(/^\/([a-zA-Z]*) ?/) || ['']).pop().toLowerCase();
if (!command) {
return false;
}
const args = text.slice(('/' + command).length + 1).trim();
if (!this.model.getAllowedCommands().includes(command)) {
return false;
}
switch (command) {
case 'admin': {
this.setAffiliation(command, args, ['owner']);
break;
}
case 'ban': {
this.setAffiliation(command, args, ['admin', 'owner']);
break;
}
case 'modtools': {
const chatview = _converse.chatboxviews.get(this.getAttribute('jid'));
chatview.showModeratorToolsModal(args);
break;
}
case 'deop': {
// FIXME: /deop only applies to setting a moderators
// role to "participant" (which only admin/owner can
// do). Moderators can however set non-moderator's role
// to participant (e.g. visitor => participant).
// Currently we don't distinguish between these two
// cases.
this.setRole(command, args, ['admin', 'owner']);
break;
}
case 'destroy': {
if (!this.model.verifyAffiliations(['owner'])) {
break;
}
const chatview = _converse.chatboxviews.get(this.getAttribute('jid'));
chatview.destroy().catch(e => this.model.onCommandError(e));
break;
}
case 'help': {
this.model.set({ 'show_help_messages': true });
break;
}
case 'kick': {
this.setRole(command, args, [], ['moderator']);
break;
}
case 'mute': {
this.setRole(command, args, [], ['moderator']);
break;
}
case 'member': {
this.setAffiliation(command, args, ['admin', 'owner']);
break;
}
case 'nick': {
if (!this.model.verifyRoles(['visitor', 'participant', 'moderator'])) {
break;
} else if (args.length === 0) {
// e.g. Your nickname is "coolguy69"
const message = __('Your nickname is "%1$s"', this.model.get('nick'));
this.model.createMessage({ message, 'type': 'error' });
} else {
const jid = Strophe.getBareJidFromJid(this.model.get('jid'));
api.send(
$pres({
from: _converse.connection.jid,
to: `${jid}/${args}`,
id: u.getUniqueId()
}).tree()
);
}
break;
}
case 'owner':
this.setAffiliation(command, args, ['owner']);
break;
case 'op': {
this.setRole(command, args, ['admin', 'owner']);
break;
}
case 'register': {
if (args.length > 1) {
this.model.createMessage({
'message': __('Error: invalid number of arguments'),
'type': 'error'
});
} else {
this.model.registerNickname().then(err_msg => {
err_msg && this.model.createMessage({ 'message': err_msg, 'type': 'error' });
});
}
break;
}
case 'revoke': {
this.setAffiliation(command, args, ['admin', 'owner']);
break;
}
case 'topic':
case 'subject':
this.model.setSubject(args);
break;
case 'voice': {
this.setRole(command, args, [], ['moderator']);
break;
}
default:
return super.parseMessageForCommands(text);
}
return true;
return parseMessageForMUCCommands(this.model, text);
}
}

View File

@ -1,7 +1,24 @@
import { _converse, api, converse } from "@converse/headless/core";
import log from "@converse/headless/log";
import { __ } from 'i18n';
import { _converse, api, converse } from "@converse/headless/core";
import { parseMessageForCommands } from 'plugins/chatview/utils.js';
const { Strophe, $iq, sizzle, u } = converse.env;
const { Strophe, $pres, $iq, sizzle, u } = converse.env;
const COMMAND_TO_AFFILIATION = {
'admin': 'admin',
'ban': 'outcast',
'member': 'member',
'owner': 'owner',
'revoke': 'none'
};
const COMMAND_TO_ROLE = {
'deop': 'participant',
'kick': 'none',
'mute': 'visitor',
'op': 'moderator',
'voice': 'participant'
};
export function getAutoCompleteListItem (text, input) {
@ -75,3 +92,192 @@ export async function fetchCommandForm (command) {
command.fields = [];
}
}
function setRole (muc, command, args, required_affiliations = [], required_roles = []) {
const role = COMMAND_TO_ROLE[command];
if (!role) {
throw Error(`ChatRoomView#setRole called with invalid command: ${command}`);
}
if (!muc.verifyAffiliations(required_affiliations) || !muc.verifyRoles(required_roles)) {
return false;
}
if (!muc.validateRoleOrAffiliationChangeArgs(command, args)) {
return false;
}
const nick_or_jid = muc.getNickOrJIDFromCommandArgs(args);
if (!nick_or_jid) {
return false;
}
const reason = args.split(nick_or_jid, 2)[1].trim();
// We're guaranteed to have an occupant due to getNickOrJIDFromCommandArgs
const occupant = muc.getOccupant(nick_or_jid);
muc.setRole(occupant, role, reason, undefined, e => muc.onCommandError(e));
return true;
}
function setAffiliation (muc, command, args, required_affiliations) {
const affiliation = COMMAND_TO_AFFILIATION[command];
if (!affiliation) {
throw Error(`ChatRoomView#setAffiliation called with invalid command: ${command}`);
}
if (!muc.verifyAffiliations(required_affiliations)) {
return false;
}
if (!muc.validateRoleOrAffiliationChangeArgs(command, args)) {
return false;
}
const nick_or_jid = muc.getNickOrJIDFromCommandArgs(args);
if (!nick_or_jid) {
return false;
}
let jid;
const reason = args.split(nick_or_jid, 2)[1].trim();
const occupant = muc.getOccupant(nick_or_jid);
if (occupant) {
jid = occupant.get('jid');
} else {
if (u.isValidJID(nick_or_jid)) {
jid = nick_or_jid;
} else {
const message = __(
"Couldn't find a participant with that nickname. " + 'They might have left the groupchat.'
);
muc.createMessage({ message, 'type': 'error' });
return;
}
}
const attrs = { jid, reason };
if (occupant && api.settings.get('auto_register_muc_nickname')) {
attrs['nick'] = occupant.get('nick');
}
muc
.setAffiliation(affiliation, [attrs])
.then(() => muc.occupants.fetchMembers())
.catch(err => muc.onCommandError(err));
}
export function parseMessageForMUCCommands (muc, text) {
if (
api.settings.get('muc_disable_slash_commands') &&
!Array.isArray(api.settings.get('muc_disable_slash_commands'))
) {
return parseMessageForCommands(muc, text);
}
text = text.replace(/^\s*/, '');
const command = (text.match(/^\/([a-zA-Z]*) ?/) || ['']).pop().toLowerCase();
if (!command) {
return false;
}
const args = text.slice(('/' + command).length + 1).trim();
if (!muc.getAllowedCommands().includes(command)) {
return false;
}
switch (command) {
case 'admin': {
setAffiliation(muc, command, args, ['owner']);
break;
}
case 'ban': {
setAffiliation(muc, command, args, ['admin', 'owner']);
break;
}
case 'modtools': {
const chatview = _converse.chatboxviews.get(muc.get('jid'));
chatview.showModeratorToolsModal(args);
break;
}
case 'deop': {
// FIXME: /deop only applies to setting a moderators
// role to "participant" (which only admin/owner can
// do). Moderators can however set non-moderator's role
// to participant (e.g. visitor => participant).
// Currently we don't distinguish between these two
// cases.
setRole(muc, command, args, ['admin', 'owner']);
break;
}
case 'destroy': {
if (!muc.verifyAffiliations(['owner'])) {
break;
}
const chatview = _converse.chatboxviews.get(muc.get('jid'));
chatview.destroy().catch(e => muc.onCommandError(e));
break;
}
case 'help': {
muc.set({ 'show_help_messages': true });
break;
}
case 'kick': {
setRole(muc, command, args, [], ['moderator']);
break;
}
case 'mute': {
setRole(muc, command, args, [], ['moderator']);
break;
}
case 'member': {
setAffiliation(muc, command, args, ['admin', 'owner']);
break;
}
case 'nick': {
if (!muc.verifyRoles(['visitor', 'participant', 'moderator'])) {
break;
} else if (args.length === 0) {
// e.g. Your nickname is "coolguy69"
const message = __('Your nickname is "%1$s"', muc.get('nick'));
muc.createMessage({ message, 'type': 'error' });
} else {
const jid = Strophe.getBareJidFromJid(muc.get('jid'));
api.send(
$pres({
from: _converse.connection.jid,
to: `${jid}/${args}`,
id: u.getUniqueId()
}).tree()
);
}
break;
}
case 'owner':
setAffiliation(muc, command, args, ['owner']);
break;
case 'op': {
setRole(muc, command, args, ['admin', 'owner']);
break;
}
case 'register': {
if (args.length > 1) {
muc.createMessage({
'message': __('Error: invalid number of arguments'),
'type': 'error'
});
} else {
muc.registerNickname().then(err_msg => {
err_msg && muc.createMessage({ 'message': err_msg, 'type': 'error' });
});
}
break;
}
case 'revoke': {
setAffiliation(muc, command, args, ['admin', 'owner']);
break;
}
case 'topic':
case 'subject':
muc.setSubject(args);
break;
case 'voice': {
setRole(muc, command, args, [], ['moderator']);
break;
}
default:
return parseMessageForCommands(muc, text);
}
return true;
}