diff --git a/src/headless/plugins/blocking/api.js b/src/headless/plugins/blocking/api.js index 7141629949..6943113579 100644 --- a/src/headless/plugins/blocking/api.js +++ b/src/headless/plugins/blocking/api.js @@ -1,157 +1,66 @@ import log from '@converse/headless/log.js'; -import { _converse, api, converse } from '@converse/headless/core.js'; +import { _converse, api, converse } from "@converse/headless/core.js"; +import { sendBlockingStanza } from './utils.js'; -const { Strophe, $iq, sizzle, u } = converse.env; +const { Strophe } = converse.env; -export default { - /** - * Retrieves the blocklist held by the logged in user at a JID by sending an IQ stanza. - * Saves the model variable _converse.blocked.set - * @private - * @method api.refreshBlocklist - */ - async refreshBlocklist () { - const features = await api.disco.getFeatures(_converse.domain); - if (!features?.findWhere({ 'var': Strophe.NS.BLOCKING })) { - return false; - } - if (!_converse.connection) { - return false; - } - - const iq = $iq({ - 'type': 'get', - 'id': u.getUniqueId('blocklist'), - }).c('blocklist', { 'xmlns': Strophe.NS.BLOCKING }); - - const result = await api.sendIQ(iq).catch((e) => { - log.fatal(e); - return null; - }); - if (result === null) { - const err_msg = `An error occured while fetching the blocklist`; - const { __ } = converse.env; - api.alert('error', __('Error'), err_msg); - log(err_msg, Strophe.LogLevel.WARN); - return false; - } else if (u.isErrorStanza(result)) { - log.error(`Error while fetching blocklist`); - log.error(result); - return false; - } - - const blocklist = sizzle('item', result).map((item) => item.getAttribute('jid')); - _converse.blocked.set({ 'set': new Set(blocklist) }); - return true; - }, - - /** - * Handle incoming iq stanzas in the BLOCKING namespace. Adjusts the global blocked_set. - * @private - * @method api.handleBlockingStanza - * @param { Object } [stanza] - The incoming stanza to handle - */ - handleBlockingStanza (stanza) { - if (stanza.firstElementChild.tagName === 'block') { - const users_to_block = sizzle('item', stanza).map((item) => item.getAttribute('jid')); - users_to_block.forEach(_converse.blocked.get('set').add, _converse.blocked.get('set')); - } else if (stanza.firstElementChild.tagName === 'unblock') { - const users_to_unblock = sizzle('item', stanza).map((item) => item.getAttribute('jid')); - users_to_unblock.forEach(_converse.blocked.get('set').delete, _converse.blocked.get('set')); - } else { - log.error('Received blocklist push update but could not interpret it.'); - } - // TODO: Fix this to not use the length as an update key, and - // use a more accurate update method, like a length-extendable hash - _converse.blocked.set({ 'len': _converse.blocked.get('set').size }); - }, - - /** - * Blocks JIDs by sending an IQ stanza - * @method api.blockUser - * - * @param { Array } [jid_list] - The list of JIDs to block - */ - async blockUser (jid_list) { - if (!_converse.disco_entities.get(_converse.domain)?.features?.findWhere({ 'var': Strophe.NS.BLOCKING })) { - return false; - } - if (!_converse.connection) { - return false; - } - - const block_items = jid_list.map((jid) => Strophe.xmlElement('item', { 'jid': jid })); - const block_element = Strophe.xmlElement('block', { 'xmlns': Strophe.NS.BLOCKING }); - - block_items.forEach(block_element.appendChild, block_element); - const iq = $iq({ - 'type': 'set', - 'id': u.getUniqueId('block'), - }).cnode(block_element); - - const result = await api.sendIQ(iq).catch((e) => { - log.fatal(e); - return false; - }); - const err_msg = `An error occured while trying to block user(s) ${jid_list}`; - if (result === null) { - api.alert('error', __('Error'), err_msg); - log(err_msg, Strophe.LogLevel.WARN); - return false; - } else if (u.isErrorStanza(result)) { - log.error(err_msg); - log.error(result); - return false; - } - return true; - }, - - /** - * Unblocks JIDs by sending an IQ stanza to the server JID specified - * @method api.unblockUser - * @param { Array } [jid_list] - The list of JIDs to unblock - */ - async unblockUser (jid_list) { - if (!_converse.disco_entities.get(_converse.domain)?.features?.findWhere({ 'var': Strophe.NS.BLOCKING })) { - return false; - } - if (!_converse.connection) { - return false; - } - - const unblock_items = jid_list.map((jid) => Strophe.xmlElement('item', { 'jid': jid })); - const unblock_element = Strophe.xmlElement('unblock', { 'xmlns': Strophe.NS.BLOCKING }); - - unblock_items.forEach(unblock_element.append, unblock_element); - - const iq = $iq({ - 'type': 'set', - 'id': u.getUniqueId('block'), - }).cnode(unblock_element); - - const result = await api.sendIQ(iq).catch((e) => { - log.fatal(e); - return false; - }); - const err_msg = `An error occured while trying to unblock user(s) ${jid_list}`; - if (result === null) { - api.alert('error', __('Error'), err_msg); - log(err_msg, Strophe.LogLevel.WARN); - return false; - } else if (u.isErrorStanza(result)) { - log.error(err_msg); - log.error(result); - return false; - } - return true; - }, +export default { - /** - * Retrieved the blocked set - * @method api.blockedUsers - */ - blockedUsers () { - return _converse.blocked.get('set'); - }, -}; + blocking: { + /** + * Checks if XEP-0191 is supported + */ + async supported () { + const has_feature = await api.disco.features.has(Strophe.NS.BLOCKING, _converse.domain); + if (!has_feature) { + log.info("XEP-0191 not supported, no blocklist available"); + return false; + } + log.debug("XEP-0191 available"); + return true; + }, + /** + * Retrieves the blocklist held by the logged in user at a JID by sending an IQ stanza. + * @private + * @method api.blocking.refresh + */ + async refresh () { + if (!_converse.connection) { + return false; + } + log.debug("refreshing blocklist"); + const available = await this.supported(); + if (!available) { + log.debug("XEP-0191 NOT available, not refreshing..."); + return false; + } + log.debug("getting blocklist..."); + return sendBlockingStanza( 'blocklist', 'get' ); + }, + /** + * Blocks JIDs by sending an IQ stanza + * @method api.blocking.block + * + * @param { Array } [jid_list] - The list of JIDs to block + */ + block ( jid_list ) { + sendBlockingStanza( 'block', 'set', jid_list ); + }, + /** + * Unblocks JIDs by sending an IQ stanza to the server JID specified + * @method api.blocking.unblock + * @param { Array } [jid_list] - The list of JIDs to unblock + */ + unblock ( jid_list ) { + sendBlockingStanza( 'unblock', 'set', jid_list ); + }, + /** + * Retrieve the blocked set + * @method api.blocking.blocklist + */ + blocklist () { + return _converse.blocking._blocked?.get('set') ?? new Set(); + }, + } +} diff --git a/src/headless/plugins/blocking/index.js b/src/headless/plugins/blocking/index.js index e8f13fae2c..14654ac1ca 100644 --- a/src/headless/plugins/blocking/index.js +++ b/src/headless/plugins/blocking/index.js @@ -1,7 +1,8 @@ /** * @description - * Converse.js plugin which adds support for XEP-0191: Blocking - * Allows users to block other users, which hides their messages. + * Converse.js plugin which adds support for XEP-0191: Blocking. + * Allows users to block communications with other users on the server side, + * so a user cannot receive messages from a blocked contact. */ import blocking_api from './api.js'; import { _converse, api, converse } from '@converse/headless/core.js'; @@ -27,7 +28,11 @@ converse.plugins.add('converse-blocking', { }, initialize () { - _converse.blocked = new SetModel(); + _converse.blocking = { + _blocked: new SetModel() + }; + api.promises.add(["blockListFetched"]); + Object.assign(api, blocking_api); api.listen.on('discoInitialized', onConnected); diff --git a/src/headless/plugins/blocking/utils.js b/src/headless/plugins/blocking/utils.js index f8faedb98c..7c6320c4f1 100644 --- a/src/headless/plugins/blocking/utils.js +++ b/src/headless/plugins/blocking/utils.js @@ -1,8 +1,87 @@ +import log from '@converse/headless/log.js'; import { _converse, api, converse } from "@converse/headless/core.js"; +import { __ } from 'i18n'; -const { Strophe } = converse.env; +const { Strophe, $iq, sizzle, u } = converse.env; + +/** + * Handle incoming iq stanzas in the BLOCKING namespace. Adjusts the global blocking._blocked.set. + * @method handleBlockingStanza + * @param { Object } [stanza] - The incoming stanza to handle + */ +function handleBlockingStanza ( stanza ) { + const action = stanza.firstElementChild.tagName; + const items = sizzle('item', stanza).map(item => item.getAttribute('jid')); + const msg_type = stanza.getAttribute('type'); + + log.debug(`handle blocking stanza Type ${msg_type} action ${action}`); + if (msg_type == 'result' && action == 'blocklist' ) { + log.debug(`resetting blocklist: ${items}`); + _converse.blocking._blocked.set({'set': new Set()}); + items.forEach((item) => { _converse.blocking._blocked.get('set').add(item)}); + + /** + * Triggered once the _converse.blocking._blocked list has been fetched + * @event _converse#blockListFetched + * @example _converse.api.listen.on('blockListFetched', () => { ... }); + */ + api.trigger('blockListFetched', _converse.blocking._blocked.get('set')); + log.debug("triggered blockListFetched"); + + } else if (msg_type == 'set' && action == 'block') { + log.debug(`adding people to blocklist: ${items}`); + items.forEach((item) => { _converse.blocking._blocked.get('set').add(item)}); + api.trigger('blockListUpdated', _converse.blocking._blocked.get('set')); + } else if (msg_type == 'set' && action == 'unblock') { + log.debug(`removing people from blocklist: ${items}`); + items.forEach((item) => { _converse.blocking._blocked.get('set').delete(item)}); + api.trigger('blockListUpdated', _converse.blocking._blocked.get('set')); + } else { + log.error("Received a blocklist push update but could not interpret it"); + } + return true; +} export function onConnected () { - api.refreshBlocklist(); - _converse.connection.addHandler(api.handleBlockingStanza, Strophe.NS.BLOCKING, 'iq', 'set', null, null); + _converse.connection.addHandler( + handleBlockingStanza, Strophe.NS.BLOCKING, 'iq', ['set', 'result'] + ); + api.blocking.refresh(); +} + +/** + * Send block/unblock IQ stanzas to the server for the JID specified + * @method api.sendBlockingStanza + * @param { String } action - "block", "unblock" or "blocklist" + * @param { String } iq_type - "get" or "set" + * @param { Array } [jid_list] - (optional) The list of JIDs to block or unblock + */ +export async function sendBlockingStanza ( action, iq_type = 'set', jid_list = [] ) { + if (!_converse.connection) { + return false; + } + + const element = Strophe.xmlElement(action, {'xmlns': Strophe.NS.BLOCKING}); + jid_list.forEach((jid) => { + const item = Strophe.xmlElement('item', { 'jid': jid }); + element.append(item); + }); + + const iq = $iq({ + 'type': iq_type, + 'id': u.getUniqueId(action) + }).cnode(element); + + const result = await api.sendIQ(iq).catch(e => { log.fatal(e); return false }); + const err_msg = `An error occured while trying to ${action} user(s) ${jid_list}`; + if (result === null) { + api.alert('error', __('Error'), err_msg); + log(err_msg, Strophe.LogLevel.WARN); + return false; + } else if (u.isErrorStanza(result)) { + log.error(err_msg); + log.error(result); + return false; + } + return true; } diff --git a/src/headless/plugins/chat/model.js b/src/headless/plugins/chat/model.js index 0e9eac7dd3..911ee48c82 100644 --- a/src/headless/plugins/chat/model.js +++ b/src/headless/plugins/chat/model.js @@ -39,7 +39,8 @@ const ChatBox = ModelWithContact.extend({ 'time_opened': this.get('time_opened') || (new Date()).getTime(), 'time_sent': (new Date(0)).toISOString(), 'type': _converse.PRIVATE_CHAT_TYPE, - 'url': '' + 'url': '', + 'contact_blocked': undefined } }, @@ -78,9 +79,24 @@ const ChatBox = ModelWithContact.extend({ * @example _converse.api.listen.on('chatBoxInitialized', model => { ... }); */ await api.trigger('chatBoxInitialized', this, {'Synchronous': true}); + + const blocking_supported = await api.blocking?.supported(); + if (blocking_supported) { + await api.waitUntil("blockListFetched"); + this.checkIfContactBlocked(api.blocking.blocklist()); + api.listen.on('blockListUpdated', this.checkIfContactBlocked, this); + } else this.set({'contact_blocked': undefined}); + this.initialized.resolve(); }, + checkIfContactBlocked (jid_set) { + if (jid_set.has(this.get('jid'))) + return this.set({'contact_blocked': true}); + + this.set({'contact_blocked': false}); + }, + getMessagesCollection () { return new _converse.Messages(); }, @@ -1104,7 +1120,7 @@ const ChatBox = ModelWithContact.extend({ } else if ( this.isHidden() || ( pluggable.plugins['converse-blocking'] && - api.blockedUsers()?.has(message?.get('from_real_jid')) + api.blocking?.blocklist().has(message?.get('from_real_jid')) ) ) { this.incrementUnreadMsgsCounter(message); diff --git a/src/plugins/chatview/bottom-panel.js b/src/plugins/chatview/bottom-panel.js index 114cb69bd2..a934f499aa 100644 --- a/src/plugins/chatview/bottom-panel.js +++ b/src/plugins/chatview/bottom-panel.js @@ -30,8 +30,9 @@ export default class ChatBottomPanel extends ElementView { async initialize () { this.model = await api.chatboxes.get(this.getAttribute('jid')); await this.model.initialized; - this.listenTo(this.model, 'change:num_unread', this.debouncedRender) + this.listenTo(this.model, 'change:num_unread', this.debouncedRender); this.listenTo(this.model, 'emoji-picker-autocomplete', this.autocompleteInPicker); + this.listenTo(this.model, 'change:contact_blocked', () => this.render()); this.addEventListener('focusin', ev => this.emitFocused(ev)); this.addEventListener('focusout', ev => this.emitBlurred(ev)); diff --git a/src/plugins/chatview/message-form.js b/src/plugins/chatview/message-form.js index 3ce9e80d6e..656f1321b0 100644 --- a/src/plugins/chatview/message-form.js +++ b/src/plugins/chatview/message-form.js @@ -16,6 +16,7 @@ export default class MessageForm extends ElementView { await this.model.initialized; this.listenTo(this.model.messages, 'change:correcting', this.onMessageCorrecting); this.listenTo(this.model, 'change:composing_spoiler', () => this.render()); + this.listenTo(this.model, 'change:contact_blocked', () => this.render()); this.handleEmojiSelection = ({ detail }) => { if (this.model.get('jid') === detail.jid) { diff --git a/src/plugins/chatview/templates/bottom-panel.js b/src/plugins/chatview/templates/bottom-panel.js index f60df5ffff..0a09124bcd 100644 --- a/src/plugins/chatview/templates/bottom-panel.js +++ b/src/plugins/chatview/templates/bottom-panel.js @@ -14,7 +14,7 @@ export default (o) => { return html` ${ o.model.ui.get('scrolled') && o.model.get('num_unread') ? html`
o.viewUnreadMessages(ev)}>▼ ${ unread_msgs } ▼
` : '' } - ${api.settings.get('show_toolbar') ? html` + ${api.settings.get('show_toolbar') && !o.model.get('contact_blocked') ? html` { - const label_message = o.composing_spoiler ? __('Hidden message') : __('Message'); + var label_message = o.composing_spoiler ? __('Hidden message') : __('Message'); + if (o.contact_blocked === true) label_message = __('You blocked this contact.'); const label_spoiler_hint = __('Optional hint'); const show_send_button = api.settings.get('show_send_button'); return html`
+
{ ${ show_send_button ? 'chat-textarea-send-button' : '' } ${ o.composing_spoiler ? 'spoiler' : '' }" placeholder="${label_message}">${ o.message_value || '' } +
`; } diff --git a/src/plugins/notifications/utils.js b/src/plugins/notifications/utils.js index 79ff6206d7..28fc8de0f3 100644 --- a/src/plugins/notifications/utils.js +++ b/src/plugins/notifications/utils.js @@ -71,7 +71,7 @@ export async function shouldNotifyOfGroupMessage (attrs) { if (pluggable.plugins['converse-blocking']?.enabled(_converse)) { const real_jid = attrs.from_real_jid; - if (real_jid && api.blockedUsers()?.has(real_jid)) { + if (real_jid && api.blocking?.blocklist().has(real_jid)) { // Don't show notifications for blocked users return false; } diff --git a/src/plugins/profile/blocked.js b/src/plugins/profile/blocked.js index 532b2956b5..d348b9ad23 100644 --- a/src/plugins/profile/blocked.js +++ b/src/plugins/profile/blocked.js @@ -1,22 +1,31 @@ import { CustomElement } from 'shared/components/element.js'; import { api, _converse } from '@converse/headless/core'; import { html } from 'lit'; +import { __ } from 'i18n'; class BlockedUsersProfile extends CustomElement { + async unblockContact (jid) { + const result = await api.confirm(__("Are you sure you want to unblock this contact?")); + if (result) { + const unbl_result = await api.blocking.unblock([jid]); + if (unbl_result) this.requestUpdate(); + } + } + initialize () { - this.listenTo(_converse.blocked, 'change', () => this.requestUpdate() ); + this.listenTo(_converse.blocking._blocked, 'change', () => this.requestUpdate() ); } render () { // eslint-disable-line class-methods-use-this + const i18n_unblock = __('Unblock'); // TODO: Displaying the JID bare like this is probably wrong. It should probably be escaped // sanitized, or canonicalized or something before display. The same goes for all such // displays in this commit. - const { blocked } = _converse; return html`` } diff --git a/src/plugins/profile/statusview.js b/src/plugins/profile/statusview.js index ab3c3b3819..2a5e607646 100644 --- a/src/plugins/profile/statusview.js +++ b/src/plugins/profile/statusview.js @@ -3,20 +3,33 @@ import { CustomElement } from 'shared/components/element.js'; import { _converse, api } from '@converse/headless/core'; class Profile extends CustomElement { + + static properties = { + blocked_jids: {}, + } + initialize () { this.model = _converse.xmppstatus; this.listenTo(this.model, "change", () => this.requestUpdate()); this.listenTo(this.model, "vcard:add", () => this.requestUpdate()); this.listenTo(this.model, "vcard:change", () => this.requestUpdate()); + this.blocked_jids = []; + this.listenTo(_converse, 'blockListFetched', this.updateBlocked); + this.listenTo(_converse, 'blockListUpdated', this.updateBlocked); } render () { return tplProfile(this); } + updateBlocked(jid_set) { + this.blocked_jids = Array.from(jid_set); + this.requestUpdate(); + } + showProfileModal (ev) { ev?.preventDefault(); - api.modal.show('converse-profile-modal', { model: this.model }, ev); + api.modal.show('converse-profile-modal', { model: this.model, blocked_jids: this.blocked_jids }, ev); } showStatusChangeModal (ev) { diff --git a/src/plugins/profile/templates/profile_modal.js b/src/plugins/profile/templates/profile_modal.js index ca71e30112..20b5497d5f 100644 --- a/src/plugins/profile/templates/profile_modal.js +++ b/src/plugins/profile/templates/profile_modal.js @@ -57,7 +57,7 @@ export default (el) => { ` ); - if (_converse.pluggable.plugins['converse-blocking']?.enabled(_converse)) { + if (el.blocked_jids.length > 0) { navigation_tabs.push(html`