diff --git a/package-lock.json b/package-lock.json index c3fa29cd84..e34acb192d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9868,8 +9868,8 @@ }, "node_modules/strophe.js": { "version": "3.1.0", - "resolved": "git+ssh://git@github.com/strophe/strophejs.git#b4f3369ba07dee4f04750de6709ea8ebe8adf9a5", - "integrity": "sha512-M/T9Pio3eG7GUzVmQUSNg+XzjFwQ6qhzI+Z3uSwUIItxxpRIB8lQB2Afb0L7lbQiRYB7/9tS03GxksdqjfrS5g==", + "resolved": "git+ssh://git@github.com/strophe/strophejs.git#f54d83199ec62272ee8b4987b2e63f188e1b2e29", + "integrity": "sha512-ivy/25C19VudvLDMPhW4oZ4gIpicc0+AnnBzzV/YUikTbaS/ujy4Y/vO416alCFovqEmcc3AEXoQ4O8KqYEKug==", "license": "MIT", "optionalDependencies": { "@types/jsdom": "^21.1.7", @@ -11121,7 +11121,7 @@ "pluggable.js": "3.0.1", "sizzle": "^2.3.5", "sprintf-js": "^1.1.2", - "strophe.js": "strophe/strophejs#b4f3369ba07dee4f04750de6709ea8ebe8adf9a5", + "strophe.js": "strophe/strophejs#f54d83199ec62272ee8b4987b2e63f188e1b2e29", "urijs": "^1.19.10" }, "devDependencies": {} diff --git a/src/headless/package.json b/src/headless/package.json index 1a5f7de063..1957d9fe10 100644 --- a/src/headless/package.json +++ b/src/headless/package.json @@ -42,7 +42,7 @@ "pluggable.js": "3.0.1", "sizzle": "^2.3.5", "sprintf-js": "^1.1.2", - "strophe.js": "strophe/strophejs#b4f3369ba07dee4f04750de6709ea8ebe8adf9a5", + "strophe.js": "strophe/strophejs#f54d83199ec62272ee8b4987b2e63f188e1b2e29", "urijs": "^1.19.10" }, "devDependencies": {} diff --git a/src/headless/plugins/bookmarks/collection.js b/src/headless/plugins/bookmarks/collection.js index 0e6260da24..9a4119e60d 100644 --- a/src/headless/plugins/bookmarks/collection.js +++ b/src/headless/plugins/bookmarks/collection.js @@ -179,10 +179,8 @@ class Bookmarks extends Collection { * @param {import('./types').BookmarkAttrs} attrs */ onBookmarkError(iq, attrs) { - const { __ } = _converse; log.error('Error while trying to add bookmark'); log.error(iq); - api.alert('error', __('Error'), [__('Sorry, something went wrong while trying to save your bookmark.')]); this.get(attrs.jid)?.destroy(); } diff --git a/src/headless/plugins/chat/message.js b/src/headless/plugins/chat/message.js index 556f1fd14f..c3949028c7 100644 --- a/src/headless/plugins/chat/message.js +++ b/src/headless/plugins/chat/message.js @@ -35,6 +35,9 @@ class Message extends ModelWithContact(ColorAwareModel(Model)) { constructor (models, options) { super(models, options); this.file = null; + + /** @type {import('./types').MessageAttributes} */ + this.attributes; } async initialize () { @@ -209,6 +212,9 @@ class Message extends ModelWithContact(ColorAwareModel(Model)) { return api.sendIQ(iq); } + /** + * @param {Element} stanza + */ getUploadRequestMetadata (stanza) { // eslint-disable-line class-methods-use-this const headers = sizzle(`slot[xmlns="${Strophe.NS.HTTPUPLOAD}"] put header`, stanza); // https://xmpp.org/extensions/xep-0363.html#request diff --git a/src/headless/plugins/chat/model.js b/src/headless/plugins/chat/model.js index aa6633c807..479c81234d 100644 --- a/src/headless/plugins/chat/model.js +++ b/src/headless/plugins/chat/model.js @@ -157,11 +157,16 @@ class ChatBox extends ModelWithMessages(ModelWithContact(ColorAwareModel(ChatBox if (to_bare_jid !== _converse.session.get('bare_jid')) { return false; } + if (attrs.is_markable) { - if (this.contact && !attrs.is_archived && !attrs.is_carbon) { + if (this.contact && + !['none', 'to'].includes(this.contact.get('subscription')) && + !attrs.is_archived && + !attrs.is_carbon) { sendMarker(attrs.from, attrs.msgid, 'received'); } return false; + } else if (attrs.marker_id) { const message = this.messages.findWhere({'msgid': attrs.marker_id}); const field_name = `marker_${attrs.marker}`; diff --git a/src/headless/plugins/chat/types.ts b/src/headless/plugins/chat/types.ts index 9ff0613b0b..c96c636be9 100644 --- a/src/headless/plugins/chat/types.ts +++ b/src/headless/plugins/chat/types.ts @@ -1,5 +1,13 @@ import {EncryptionAttrs} from "../../shared/types"; +// Represents a XEP-0372 reference +export type Reference = { + begin: number; + end: number; + type: string; + uri: string; +} + export type MessageErrorAttributes = { is_error: boolean; // Whether an error was received for this message error: string; // The error name @@ -41,7 +49,7 @@ export type MessageAttributes = EncryptionAttrs & MessageErrorAttributes & { plaintext: string; // The decrypted text of this message, in case it was encrypted. receipt_id: string; // The `id` attribute of a XEP-0184 element received: string; // An ISO8601 string recording the time that the message was received - references: Array; // A list of objects representing XEP-0372 references + references: Array; // A list of objects representing XEP-0372 references replace_id: string; // The `id` attribute of a XEP-0308 element retracted: string; // An ISO8601 string recording the time that the message was retracted retracted_id: string; // The `id` attribute of a XEP-424 element diff --git a/src/headless/plugins/disco/api.js b/src/headless/plugins/disco/api.js index 5254e30191..0dd870cb7c 100644 --- a/src/headless/plugins/disco/api.js +++ b/src/headless/plugins/disco/api.js @@ -3,15 +3,14 @@ * @typedef {import('./entities').default} DiscoEntities * @typedef {import('@converse/skeletor').Collection} Collection */ +import { getOpenPromise } from '@converse/openpromise'; import _converse from '../../shared/_converse.js'; import api from '../../shared/api/index.js'; import converse from '../../shared/api/public.js'; -import log from "../../log.js"; -import { getOpenPromise } from '@converse/openpromise'; +import log from '../../log.js'; const { Strophe, $iq } = converse.env; - export default { /** * The XEP-0030 service discovery API @@ -34,12 +33,12 @@ export default { * @param { String } xmlns The XML namespace * @example _converse.api.disco.stream.getFeature('ver', 'urn:xmpp:features:rosterver') */ - async getFeature (name, xmlns) { + async getFeature(name, xmlns) { await api.waitUntil('streamFeaturesAdded'); - const stream_features = /** @type {Collection} */(_converse.state.stream_features); + const stream_features = /** @type {Collection} */ (_converse.state.stream_features); if (!name || !xmlns) { - throw new Error("name and xmlns need to be provided when calling disco.stream.getFeature"); + throw new Error('name and xmlns need to be provided when calling disco.stream.getFeature'); } if (stream_features === undefined && !api.connection.connected()) { // Happens during tests when disco lookups happen asynchronously after teardown. @@ -47,8 +46,8 @@ export default { log.warn(msg); return; } - return stream_features.findWhere({'name': name, 'xmlns': xmlns}); - } + return stream_features.findWhere({ 'name': name, 'xmlns': xmlns }); + }, }, /** @@ -65,32 +64,34 @@ export default { * Lets you add new identities for this client (i.e. instance of Converse) * @method api.disco.own.identities.add * - * @param { String } category - server, client, gateway, directory, etc. - * @param { String } type - phone, pc, web, etc. - * @param { String } name - "Converse" - * @param { String } lang - en, el, de, etc. + * @param {String} category - server, client, gateway, directory, etc. + * @param {String} type - phone, pc, web, etc. + * @param {String} name - "Converse" + * @param {String} lang - en, el, de, etc. * * @example _converse.api.disco.own.identities.clear(); */ - add (category, type, name, lang) { - const disco = /** @type {DiscoState} */(_converse.state.disco); - for (var i=0; i e.get('parent_jids')?.includes(jid)); + items(jid) { + const disco_entities = /** @type {DiscoEntities} */ (_converse.state.disco_entities); + return disco_entities.filter((e) => e.get('parent_jids')?.includes(jid)); }, /** @@ -238,19 +241,19 @@ export default { * `ignore_cache: true` in the options parameter. * * @method api.disco.entities.create - * @param { object } data - * @param { string } data.jid - The Jabber ID of the entity - * @param { string } data.parent_jid - The Jabber ID of the parent entity - * @param { string } data.name - * @param { object } [options] - Additional options - * @param { boolean } [options.ignore_cache] + * @param {object} data + * @param {string} data.jid - The Jabber ID of the entity + * @param {string} data.parent_jid - The Jabber ID of the parent entity + * @param {string} data.name + * @param {object} [options] - Additional options + * @param {boolean} [options.ignore_cache] * If true, fetch all features from the XMPP server instead of restoring them from cache * @example _converse.api.disco.entities.create({ jid }, {'ignore_cache': true}); */ - create (data, options) { - const disco_entities = /** @type {DiscoEntities} */(_converse.state.disco_entities); + create(data, options) { + const disco_entities = /** @type {DiscoEntities} */ (_converse.state.disco_entities); return disco_entities.create(data, options); - } + }, }, /** @@ -262,37 +265,40 @@ export default { * Return a given feature of a disco entity * * @method api.disco.features.get - * @param { string } feature The feature that might be + * @param {string} feature The feature that might be * supported. In the XML stanza, this is the `var` * attribute of the `` element. For * example: `http://jabber.org/protocol/muc` - * @param { string } jid The JID of the entity + * @param {string} jid The JID of the entity * (and its associated items) which should be queried - * @returns {promise} A promise which resolves with a list containing + * @returns {Promise} + * A promise which resolves with a list containing * _converse.Entity instances representing the entity * itself or those items associated with the entity if * they support the given feature. * @example * api.disco.features.get(Strophe.NS.MAM, _converse.bare_jid); */ - async get (feature, jid) { - if (!jid) throw new TypeError('You need to provide an entity JID'); + async get(feature, jid) { + if (!jid) throw new TypeError('api.disco.features.get: You need to provide an entity JID'); const entity = await api.disco.entities.get(jid, true); if (_converse.state.disco_entities === undefined && !api.connection.connected()) { // Happens during tests when disco lookups happen asynchronously after teardown. - log.warn(`Tried to get feature ${feature} for ${jid} but `+ - `_converse.disco_entities has been torn down`); + log.warn( + `Tried to get feature ${feature} for ${jid} but ` + + `_converse.disco_entities has been torn down` + ); return []; } const promises = [ entity.getFeature(feature), - ...api.disco.entities.items(jid).map(i => i.getFeature(feature)) + ...api.disco.entities.items(jid).map((i) => i.getFeature(feature)), ]; const result = await Promise.all(promises); - return result.filter(f => (f instanceof Object)); + return result.filter((f) => f instanceof Object); }, /** @@ -300,18 +306,18 @@ export default { * associated items, supports a given feature. * * @method api.disco.features.has - * @param { string } feature The feature that might be + * @param {string} feature The feature that might be * supported. In the XML stanza, this is the `var` * attribute of the `` element. For * example: `http://jabber.org/protocol/muc` - * @param { string } jid The JID of the entity + * @param {string} jid The JID of the entity * (and its associated items) which should be queried - * @returns {Promise} A promise which resolves with a boolean + * @returns {Promise} A promise which resolves with a boolean * @example * api.disco.features.has(Strophe.NS.MAM, _converse.bare_jid); */ - async has (feature, jid) { - if (!jid) throw new TypeError('You need to provide an entity JID'); + async has(feature, jid) { + if (!jid) throw new TypeError('api.disco.feature.has: You need to provide an entity JID'); const entity = await api.disco.entities.get(jid, true); @@ -325,22 +331,22 @@ export default { return true; } - const result = await Promise.all(api.disco.entities.items(jid).map(i => i.getFeature(feature))); - return result.map(f => (f instanceof Object)).includes(true); - } + const result = await Promise.all(api.disco.entities.items(jid).map((i) => i.getFeature(feature))); + return result.map((f) => f instanceof Object).includes(true); + }, }, /** * Used to determine whether an entity supports a given feature. * * @method api.disco.supports - * @param { string } feature The feature that might be + * @param {string} feature The feature that might be * supported. In the XML stanza, this is the `var` * attribute of the `` element. For * example: `http://jabber.org/protocol/muc` - * @param { string } jid The JID of the entity + * @param {string} jid The JID of the entity * (and its associated items) which should be queried - * @returns {promise} A promise which resolves with `true` or `false`. + * @returns {Promise|boolean} A promise which resolves with `true` or `false`. * @example * if (await api.disco.supports(Strophe.NS.MAM, _converse.bare_jid)) { * // The feature is supported @@ -348,23 +354,28 @@ export default { * // The feature is not supported * } */ - supports (feature, jid) { - return api.disco.features.has(feature, jid); + supports(feature, jid) { + try { + return api.disco.features.has(feature, jid); + } catch (e) { + log.error(e); + debugger; + return false; + } }, /** * Refresh the features, fields and identities associated with a * disco entity by refetching them from the server * @method api.disco.refresh - * @param { string } jid The JID of the entity whose features are refreshed. - * @returns {promise} A promise which resolves once the features have been refreshed + * @param {string} jid The JID of the entity whose features are refreshed. + * @returns {Promise} A promise which resolves once the features have been refreshed * @example * await api.disco.refresh('room@conference.example.org'); */ - async refresh (jid) { - if (!jid) { - throw new TypeError('api.disco.refresh: You need to provide an entity JID'); - } + async refresh(jid) { + if (!jid) throw new TypeError('api.disco.refresh: You need to provide an entity JID'); + await api.waitUntil('discoInitialized'); let entity = await api.disco.entities.get(jid); if (entity) { @@ -372,12 +383,12 @@ export default { entity.fields.reset(); entity.identities.reset(); if (!entity.waitUntilFeaturesDiscovered.isPending) { - entity.waitUntilFeaturesDiscovered = getOpenPromise() + entity.waitUntilFeaturesDiscovered = getOpenPromise(); } entity.queryInfo(); } else { // Create it if it doesn't exist - entity = await api.disco.entities.create({ jid }, {'ignore_cache': true}); + entity = await api.disco.entities.create({ jid }, { 'ignore_cache': true }); } return entity.waitUntilFeaturesDiscovered; }, @@ -391,10 +402,9 @@ export default { * @example * const features = await api.disco.getFeatures('room@conference.example.org'); */ - async getFeatures (jid) { - if (!jid) { - throw new TypeError('api.disco.getFeatures: You need to provide an entity JID'); - } + async getFeatures(jid) { + if (!jid) throw new TypeError('api.disco.getFeatures: You need to provide an entity JID'); + await api.waitUntil('discoInitialized'); let entity = await api.disco.entities.get(jid, true); entity = await entity.waitUntilFeaturesDiscovered; @@ -412,10 +422,9 @@ export default { * @example * const fields = await api.disco.getFields('room@conference.example.org'); */ - async getFields (jid) { - if (!jid) { - throw new TypeError('api.disco.getFields: You need to provide an entity JID'); - } + async getFields(jid) { + if (!jid) throw new TypeError('api.disco.getFields: You need to provide an entity JID'); + await api.waitUntil('discoInitialized'); let entity = await api.disco.entities.get(jid, true); entity = await entity.waitUntilFeaturesDiscovered; @@ -453,16 +462,17 @@ export default { * } * ).catch(e => log.error(e)); */ - async getIdentity (category, type, jid) { + async getIdentity(category, type, jid) { const e = await api.disco.entities.get(jid, true); if (e === undefined && !api.connection.connected()) { // Happens during tests when disco lookups happen asynchronously after teardown. - const msg = `Tried to look up category ${category} for ${jid} `+ + const msg = + `Tried to look up category ${category} for ${jid} ` + `but _converse.disco_entities has been torn down`; log.warn(msg); return; } return e.getIdentity(category, type); - } - } -} + }, + }, +}; diff --git a/src/headless/plugins/muc/occupant.js b/src/headless/plugins/muc/occupant.js index c25a9b19b9..3a4eb00a5e 100644 --- a/src/headless/plugins/muc/occupant.js +++ b/src/headless/plugins/muc/occupant.js @@ -2,6 +2,7 @@ import { Model } from '@converse/skeletor'; import log from '../../log'; import api from '../../shared/api/index.js'; import _converse from '../../shared/_converse.js'; +import converse from '../../shared/api/public.js'; import ColorAwareModel from '../../shared/color.js'; import ModelWithMessages from '../../shared/model-with-messages.js'; import { AFFILIATIONS, ROLES } from './constants.js'; @@ -10,6 +11,8 @@ import u from '../../utils/index.js'; import { shouldCreateGroupchatMessage } from './utils'; import { sendChatState } from '../../shared/actions'; +const { Strophe, stx } = converse.env; + /** * Represents a participant in a MUC */ @@ -39,6 +42,7 @@ class MUCOccupant extends ModelWithMessages(ColorAwareModel(Model)) { states: [], hidden: true, num_unread: 0, + message_type: 'chat', }; } @@ -188,6 +192,15 @@ class MUCOccupant extends ModelWithMessages(ColorAwareModel(Model)) { attrs = await api.hook('getOutgoingMessageAttributes', this, attrs); return attrs; } + + /** + * @param {import('../chat/message').default} message - The message object + */ + async createMessageStanza(message) { + const stanza = await super.createMessageStanza(message); + stanza.cnode(stx``).root(); + return stanza; + } } export default MUCOccupant; diff --git a/src/headless/plugins/muc/tests/messages.js b/src/headless/plugins/muc/tests/messages.js index 8edbe2e190..24edd9e72d 100644 --- a/src/headless/plugins/muc/tests/messages.js +++ b/src/headless/plugins/muc/tests/messages.js @@ -128,8 +128,8 @@ describe("A MUC message", function () { const muc_jid = 'lounge@montague.lit'; const model = await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); - const received_stanza = u.toStanza(` - + const received_stanza = stx` + @@ -139,7 +139,7 @@ describe("A MUC message", function () { pong - `); + `; await model.handleMessageStanza(received_stanza); await u.waitUntil(() => model.messages.last()); expect(model.messages.last().get('body')).toBe('> ping\n pong'); diff --git a/src/headless/plugins/roster/contact.js b/src/headless/plugins/roster/contact.js index 5da9ffdf4a..275f215a57 100644 --- a/src/headless/plugins/roster/contact.js +++ b/src/headless/plugins/roster/contact.js @@ -146,7 +146,9 @@ class RosterContact extends ColorAwareModel(Model) { /** * Remove this contact from the roster - * @param {boolean} unauthorize - Whether to also unauthorize the + * @async + * @param {boolean} [unauthorize] - Whether to also unauthorize the + * @returns {Promise} */ remove (unauthorize) { const subscription = this.get('subscription'); @@ -154,21 +156,13 @@ class RosterContact extends ColorAwareModel(Model) { this.destroy(); return; } - - if (unauthorize) { - if (subscription === 'from') { - this.unauthorize(); - } else if (subscription === 'both') { - this.unauthorize(); - } + if (unauthorize && ['from', 'both'].includes(subscription)) { + this.unauthorize(); } + const promise = this.sendRosterRemoveStanza(); + if (this.collection) this.destroy(); - this.sendRosterRemoveStanza(); - if (this.collection) { - // The model might have already been removed as - // result of a roster push. - this.destroy(); - } + return promise; } /** diff --git a/src/headless/plugins/roster/utils.js b/src/headless/plugins/roster/utils.js index cf0660bd43..a2bac6aca4 100644 --- a/src/headless/plugins/roster/utils.js +++ b/src/headless/plugins/roster/utils.js @@ -212,8 +212,8 @@ export function onRosterContactsFetched () { /** * Reject or cancel another user's subscription to our presence updates. * @function rejectPresenceSubscription - * @param { String } jid - The Jabber ID of the user whose subscription is being canceled - * @param { String } message - An optional message to the user + * @param {String} jid - The Jabber ID of the user whose subscription is being canceled + * @param {String} message - An optional message to the user */ export function rejectPresenceSubscription (jid, message) { const pres = $pres({to: jid, type: "unsubscribed"}); diff --git a/src/headless/shared/model-with-contact.js b/src/headless/shared/model-with-contact.js index 991c6a081f..edcbfb984c 100644 --- a/src/headless/shared/model-with-contact.js +++ b/src/headless/shared/model-with-contact.js @@ -8,33 +8,32 @@ import api from './api/index.js'; * @param {T} BaseModel */ export default function ModelWithContact(BaseModel) { - return class ModelWithContact extends BaseModel { /** - * @typedef {import('../plugins/vcard/vcard').default} VCard - * @typedef {import('../plugins/roster/contact').default} RosterContact - * @typedef {import('./_converse.js').XMPPStatus} XMPPStatus - */ + * @typedef {import('../plugins/vcard/vcard').default} VCard + * @typedef {import('../plugins/roster/contact').default} RosterContact + * @typedef {import('./_converse.js').XMPPStatus} XMPPStatus + */ initialize() { super.initialize(); this.rosterContactAdded = getOpenPromise(); /** - * @public - * @type {RosterContact|XMPPStatus} - */ + * @public + * @type {RosterContact|XMPPStatus} + */ this.contact = null; /** - * @public - * @type {VCard} - */ + * @public + * @type {VCard} + */ this.vcard = null; } /** - * @param {string} jid - */ + * @param {string} jid + */ async setModelContact(jid) { if (this.contact?.get('jid') === jid) return; @@ -44,10 +43,10 @@ export default function ModelWithContact(BaseModel) { if (Strophe.getBareJidFromJid(jid) === session.get('bare_jid')) { contact = state.xmppstatus; } else { - contact = await api.contacts.get(jid) || await api.contacts.add({ - jid, - subscription: 'none', - }, false, false); + contact = await api.contacts.get(jid); + if (!contact && !(await api.blocklist.get()).get(jid)) { + await api.contacts.add({ jid, subscription: 'none' }, false, false); + } } if (contact) { @@ -65,5 +64,5 @@ export default function ModelWithContact(BaseModel) { this.trigger('contactAdded', this.contact); } } - } + }; } diff --git a/src/headless/shared/model-with-messages.js b/src/headless/shared/model-with-messages.js index 05cd169558..6201f63818 100644 --- a/src/headless/shared/model-with-messages.js +++ b/src/headless/shared/model-with-messages.js @@ -15,7 +15,7 @@ import { MethodNotImplementedError } from './errors.js'; import { sendMarker, sendReceiptStanza, sendRetractionMessage } from './actions.js'; import { parseMessage } from '../plugins/chat/parsers'; -const { Strophe, $msg, u } = converse.env; +const { Strophe, stx, u } = converse.env; /** * Adds a messages collection to a model and various methods related to sending @@ -155,9 +155,9 @@ export default function ModelWithMessages(BaseModel) { } /** - * @param {MessageAttributes|Error} attrs_or_error + * @param {MessageAttributes|Error} _attrs_or_error */ - async onMessage(attrs_or_error) { + async onMessage(_attrs_or_error) { throw new MethodNotImplementedError('onMessage is not implemented'); } @@ -889,64 +889,48 @@ export default function ModelWithMessages(BaseModel) { /** * Given a {@link Message} return the XML stanza that represents it. * @method ChatBox#createMessageStanza - * @param { Message } message - The message object + * @param {Message} message - The message object */ async createMessageStanza(message) { - const stanza = $msg({ - 'from': message.get('from') || api.connection.get().jid, - 'to': message.get('to') || this.get('jid'), - 'type': this.get('message_type'), - 'id': (message.get('edited') && u.getUniqueId()) || message.get('msgid'), - }) - .c('body') - .t(message.get('body')) - .up() - .c(constants.ACTIVE, { 'xmlns': Strophe.NS.CHATSTATES }) - .root(); - - if (message.get('type') === 'chat') { - stanza.c('request', { 'xmlns': Strophe.NS.RECEIPTS }).root(); - } - - if (!message.get('is_encrypted')) { - if (message.get('is_spoiler')) { - if (message.get('spoiler_hint')) { - stanza.c('spoiler', { 'xmlns': Strophe.NS.SPOILER }, message.get('spoiler_hint')).root(); - } else { - stanza.c('spoiler', { 'xmlns': Strophe.NS.SPOILER }).root(); - } - } - (message.get('references') || []).forEach((reference) => { - const attrs = { - 'xmlns': Strophe.NS.REFERENCE, - 'begin': reference.begin, - 'end': reference.end, - 'type': reference.type, - }; - if (reference.uri) { - attrs.uri = reference.uri; + const { + body, + edited, + is_encrypted, + is_spoiler, + msgid, + oob_url, + origin_id, + references, + spoiler_hint, + type, + } = message.attributes; + + const stanza = stx` + + ${body ? stx`${body}` : ''} + + ${type === 'chat' ? stx`` : ''} + ${!is_encrypted && oob_url ? stx`${oob_url}` : ''} + ${!is_encrypted && is_spoiler ? stx`${spoiler_hint ?? ''}` : ''} + ${ + !is_encrypted + ? references?.map( + (ref) => stx`` + ) + : '' } - stanza.c('reference', attrs).root(); - }); - - if (message.get('oob_url')) { - stanza.c('x', { 'xmlns': Strophe.NS.OUTOFBAND }).c('url').t(message.get('oob_url')).root(); - } - } + ${edited ? stx`` : ''} + ${origin_id ? stx`` : ''} + `; - if (message.get('edited')) { - stanza - .c('replace', { - 'xmlns': Strophe.NS.MESSAGE_CORRECT, - 'id': message.get('msgid'), - }) - .root(); - } - - if (message.get('origin_id')) { - stanza.c('origin-id', { 'xmlns': Strophe.NS.SID, 'id': message.get('origin_id') }).root(); - } - stanza.root(); /** * *Hook* which allows plugins to update an outgoing message stanza * @event _converse#createMessageStanza diff --git a/src/headless/types/plugins/chat/message.d.ts b/src/headless/types/plugins/chat/message.d.ts index 7fa0961b7f..21fed80571 100644 --- a/src/headless/types/plugins/chat/message.d.ts +++ b/src/headless/types/plugins/chat/message.d.ts @@ -157,6 +157,8 @@ declare class Message extends Message_base { is_ephemeral: boolean; }; file: any; + /** @type {import('./types').MessageAttributes} */ + attributes: import("./types").MessageAttributes; initialize(): Promise; chatbox: any; initialized: any; @@ -204,7 +206,10 @@ declare class Message extends Message_base { * @method _converse.Message#sendSlotRequestStanza */ private sendSlotRequestStanza; - getUploadRequestMetadata(stanza: any): { + /** + * @param {Element} stanza + */ + getUploadRequestMetadata(stanza: Element): { headers: { name: string; value: string; diff --git a/src/headless/types/plugins/chat/model.d.ts b/src/headless/types/plugins/chat/model.d.ts index 2fcd6b6737..f96dcd45b1 100644 --- a/src/headless/types/plugins/chat/model.d.ts +++ b/src/headless/types/plugins/chat/model.d.ts @@ -17,7 +17,7 @@ declare const ChatBox_base: { messages: any; fetchMessages(): any; afterMessagesFetched(): void; - onMessage(attrs_or_error: import("./types").MessageAttributes | Error): Promise; + onMessage(_attrs_or_error: import("./types").MessageAttributes | Error): Promise; getUpdatedMessageAttributes(message: import("./message.js").default, attrs: import("./types").MessageAttributes): object; updateMessage(message: import("./message.js").default, attrs: import("./types").MessageAttributes): void; handleCorrection(attrs: import("./types").MessageAttributes | import("../muc/types.js").MUCMessageAttributes): Promise; diff --git a/src/headless/types/plugins/chat/types.d.ts b/src/headless/types/plugins/chat/types.d.ts index 0b1a2adca9..08d7229d09 100644 --- a/src/headless/types/plugins/chat/types.d.ts +++ b/src/headless/types/plugins/chat/types.d.ts @@ -1,4 +1,10 @@ import { EncryptionAttrs } from "../../shared/types"; +export type Reference = { + begin: number; + end: number; + type: string; + uri: string; +}; export type MessageErrorAttributes = { is_error: boolean; error: string; @@ -42,7 +48,7 @@ export type MessageAttributes = EncryptionAttrs & MessageErrorAttributes & { plaintext: string; receipt_id: string; received: string; - references: Array; + references: Array; replace_id: string; retracted: string; retracted_id: string; diff --git a/src/headless/types/plugins/disco/api.d.ts b/src/headless/types/plugins/disco/api.d.ts index 91ec0a0101..ca5d52d0c1 100644 --- a/src/headless/types/plugins/disco/api.d.ts +++ b/src/headless/types/plugins/disco/api.d.ts @@ -15,10 +15,10 @@ declare namespace _default { * Lets you add new identities for this client (i.e. instance of Converse) * @method api.disco.own.identities.add * - * @param { String } category - server, client, gateway, directory, etc. - * @param { String } type - phone, pc, web, etc. - * @param { String } name - "Converse" - * @param { String } lang - en, el, de, etc. + * @param {String} category - server, client, gateway, directory, etc. + * @param {String} type - phone, pc, web, etc. + * @param {String} name - "Converse" + * @param {String} lang - en, el, de, etc. * * @example _converse.api.disco.own.identities.clear(); */ @@ -63,8 +63,8 @@ declare namespace _default { * Query for information about an XMPP entity * * @method api.disco.info - * @param { string } jid The Jabber ID of the entity to query - * @param { string } [node] A specific node identifier associated with the JID + * @param {string} jid The Jabber ID of the entity to query + * @param {string} [node] A specific node identifier associated with the JID * @returns {promise} Promise which resolves once we have a result from the server. */ export function info(jid: string, node?: string): Promise; @@ -72,8 +72,8 @@ declare namespace _default { * Query for items associated with an XMPP entity * * @method api.disco.items - * @param { string } jid The Jabber ID of the entity to query for items - * @param { string } [node] A specific node identifier associated with the JID + * @param {string} jid The Jabber ID of the entity to query for items + * @param {string} [node] A specific node identifier associated with the JID * @returns {promise} Promise which resolves once we have a result from the server. */ export function items(jid: string, node?: string): Promise; @@ -82,8 +82,8 @@ declare namespace _default { * Get the corresponding `DiscoEntity` instance. * * @method api.disco.entities.get - * @param { string } jid The Jabber ID of the entity - * @param { boolean } [create] Whether the entity should be created if it doesn't exist. + * @param {string} jid The Jabber ID of the entity + * @param {boolean} [create] Whether the entity should be created if it doesn't exist. * @example _converse.api.disco.entities.get(jid); */ function get(jid: string, create?: boolean): Promise; @@ -91,7 +91,7 @@ declare namespace _default { * Return any disco items advertised on this entity * * @method api.disco.entities.items - * @param { string } jid - The Jabber ID of the entity for which we want to fetch items + * @param {string} jid - The Jabber ID of the entity for which we want to fetch items * @example api.disco.entities.items(jid); */ function items(jid: string): any; @@ -104,12 +104,12 @@ declare namespace _default { * `ignore_cache: true` in the options parameter. * * @method api.disco.entities.create - * @param { object } data - * @param { string } data.jid - The Jabber ID of the entity - * @param { string } data.parent_jid - The Jabber ID of the parent entity - * @param { string } data.name - * @param { object } [options] - Additional options - * @param { boolean } [options.ignore_cache] + * @param {object} data + * @param {string} data.jid - The Jabber ID of the entity + * @param {string} data.parent_jid - The Jabber ID of the parent entity + * @param {string} data.name + * @param {object} [options] - Additional options + * @param {boolean} [options.ignore_cache] * If true, fetch all features from the XMPP server instead of restoring them from cache * @example _converse.api.disco.entities.create({ jid }, {'ignore_cache': true}); */ @@ -132,49 +132,50 @@ declare namespace _default { * Return a given feature of a disco entity * * @method api.disco.features.get - * @param { string } feature The feature that might be + * @param {string} feature The feature that might be * supported. In the XML stanza, this is the `var` * attribute of the `` element. For * example: `http://jabber.org/protocol/muc` - * @param { string } jid The JID of the entity + * @param {string} jid The JID of the entity * (and its associated items) which should be queried - * @returns {promise} A promise which resolves with a list containing + * @returns {Promise} + * A promise which resolves with a list containing * _converse.Entity instances representing the entity * itself or those items associated with the entity if * they support the given feature. * @example * api.disco.features.get(Strophe.NS.MAM, _converse.bare_jid); */ - function get(feature: string, jid: string): Promise; + function get(feature: string, jid: string): Promise; /** * Returns true if an entity with the given JID, or if one of its * associated items, supports a given feature. * * @method api.disco.features.has - * @param { string } feature The feature that might be + * @param {string} feature The feature that might be * supported. In the XML stanza, this is the `var` * attribute of the `` element. For * example: `http://jabber.org/protocol/muc` - * @param { string } jid The JID of the entity + * @param {string} jid The JID of the entity * (and its associated items) which should be queried - * @returns {Promise} A promise which resolves with a boolean + * @returns {Promise} A promise which resolves with a boolean * @example * api.disco.features.has(Strophe.NS.MAM, _converse.bare_jid); */ - function has(feature: string, jid: string): Promise; + function has(feature: string, jid: string): Promise; } export { features_1 as features }; /** * Used to determine whether an entity supports a given feature. * * @method api.disco.supports - * @param { string } feature The feature that might be + * @param {string} feature The feature that might be * supported. In the XML stanza, this is the `var` * attribute of the `` element. For * example: `http://jabber.org/protocol/muc` - * @param { string } jid The JID of the entity + * @param {string} jid The JID of the entity * (and its associated items) which should be queried - * @returns {promise} A promise which resolves with `true` or `false`. + * @returns {Promise|boolean} A promise which resolves with `true` or `false`. * @example * if (await api.disco.supports(Strophe.NS.MAM, _converse.bare_jid)) { * // The feature is supported @@ -182,13 +183,13 @@ declare namespace _default { * // The feature is not supported * } */ - export function supports(feature: string, jid: string): Promise; + export function supports(feature: string, jid: string): Promise | boolean; /** * Refresh the features, fields and identities associated with a * disco entity by refetching them from the server * @method api.disco.refresh - * @param { string } jid The JID of the entity whose features are refreshed. - * @returns {promise} A promise which resolves once the features have been refreshed + * @param {string} jid The JID of the entity whose features are refreshed. + * @returns {Promise} A promise which resolves once the features have been refreshed * @example * await api.disco.refresh('room@conference.example.org'); */ diff --git a/src/headless/types/plugins/muc/muc.d.ts b/src/headless/types/plugins/muc/muc.d.ts index 21f1359463..bd0e980200 100644 --- a/src/headless/types/plugins/muc/muc.d.ts +++ b/src/headless/types/plugins/muc/muc.d.ts @@ -17,7 +17,7 @@ declare const MUC_base: { messages: any; fetchMessages(): any; afterMessagesFetched(): void; - onMessage(attrs_or_error: import("../chat/types").MessageAttributes | Error): Promise; + onMessage(_attrs_or_error: import("../chat/types").MessageAttributes | Error): Promise; getUpdatedMessageAttributes(message: import("../chat/message.js").default, attrs: import("../chat/types").MessageAttributes): object; updateMessage(message: import("../chat/message.js").default, attrs: import("../chat/types").MessageAttributes): void; handleCorrection(attrs: import("../chat/types").MessageAttributes | import("./types").MUCMessageAttributes): Promise; diff --git a/src/headless/types/plugins/muc/occupant.d.ts b/src/headless/types/plugins/muc/occupant.d.ts index da366175bc..c915aacdf4 100644 --- a/src/headless/types/plugins/muc/occupant.d.ts +++ b/src/headless/types/plugins/muc/occupant.d.ts @@ -17,21 +17,21 @@ declare const MUCOccupant_base: { messages: any; fetchMessages(): any; afterMessagesFetched(): void; - onMessage(attrs_or_error: import("../chat/types").MessageAttributes | Error): Promise; - getUpdatedMessageAttributes(message: import("../chat").Message, attrs: import("../chat/types").MessageAttributes): object; - updateMessage(message: import("../chat").Message, attrs: import("../chat/types").MessageAttributes): void; - handleCorrection(attrs: import("../chat/types").MessageAttributes | import("./types").MUCMessageAttributes): Promise; + onMessage(_attrs_or_error: import("../chat/types").MessageAttributes | Error): Promise; + getUpdatedMessageAttributes(message: import("../chat/message").default, attrs: import("../chat/types").MessageAttributes): object; + updateMessage(message: import("../chat/message").default, attrs: import("../chat/types").MessageAttributes): void; + handleCorrection(attrs: import("../chat/types").MessageAttributes | import("./types").MUCMessageAttributes): Promise; queueMessage(attrs: import("../chat/types").MessageAttributes): any; msg_chain: any; getOutgoingMessageAttributes(_attrs?: import("../chat/types").MessageAttributes): Promise; - sendMessage(attrs?: any): Promise; - retractOwnMessage(message: import("../chat").Message): void; + sendMessage(attrs?: any): Promise; + retractOwnMessage(message: import("../chat/message").default): void; sendFiles(files: File[]): Promise; setEditable(attrs: any, send_time: string): void; setChatState(state: string, options?: object): any; chat_state_timeout: NodeJS.Timeout; - onMessageAdded(message: import("../chat").Message): void; - onMessageUploadChanged(message: import("../chat").Message): Promise; + onMessageAdded(message: import("../chat/message").default): void; + onMessageUploadChanged(message: import("../chat/message").default): Promise; onScrolledChanged(): void; pruneHistoryWhenScrolledDown(): void; shouldShowErrorMessage(attrs: import("../chat/types").MessageAttributes): Promise; @@ -41,8 +41,8 @@ declare const MUCOccupant_base: { getOldestMessage(): any; getMostRecentMessage(): any; getMessageReferencedByError(attrs: object): any; - findDanglingRetraction(attrs: object): import("../chat").Message | null; - getDuplicateMessage(attrs: object): import("../chat").Message; + findDanglingRetraction(attrs: object): import("../chat/message").default | null; + getDuplicateMessage(attrs: object): import("../chat/message").default; getOriginIdQueryAttrs(attrs: object): { origin_id: any; from: any; @@ -52,15 +52,15 @@ declare const MUCOccupant_base: { from: any; msgid: any; }; - sendMarkerForMessage(msg: import("../chat").Message, type?: ("received" | "displayed" | "acknowledged"), force?: boolean): Promise; - handleUnreadMessage(message: import("../chat").Message): void; - getErrorAttributesForMessage(message: import("../chat").Message, attrs: import("../chat/types").MessageAttributes): Promise; + sendMarkerForMessage(msg: import("../chat/message").default, type?: ("received" | "displayed" | "acknowledged"), force?: boolean): Promise; + handleUnreadMessage(message: import("../chat/message").default): void; + getErrorAttributesForMessage(message: import("../chat/message").default, attrs: import("../chat/types").MessageAttributes): Promise; handleErrorMessageStanza(stanza: Element): Promise; - incrementUnreadMsgsCounter(message: import("../chat").Message): void; + incrementUnreadMsgsCounter(message: import("../chat/message").default): void; clearUnreadMsgCounter(): void; handleRetraction(attrs: import("../chat/types").MessageAttributes): Promise; handleReceipt(attrs: import("../chat/types").MessageAttributes): boolean; - createMessageStanza(message: import("../chat").Message): Promise; + createMessageStanza(message: import("../chat/message").default): Promise; pruneHistory(): void; debouncedPruneHistory: import("lodash").DebouncedFunc<() => void>; isScrolledUp(): any; @@ -214,6 +214,7 @@ declare class MUCOccupant extends MUCOccupant_base { states: any[]; hidden: boolean; num_unread: number; + message_type: string; }; save(key: any, val: any, options: any): any; getMessagesCollection(): MUCMessages; diff --git a/src/headless/types/plugins/roster/contact.d.ts b/src/headless/types/plugins/roster/contact.d.ts index 02afff4b96..334afe96b6 100644 --- a/src/headless/types/plugins/roster/contact.d.ts +++ b/src/headless/types/plugins/roster/contact.d.ts @@ -118,9 +118,11 @@ declare class RosterContact extends RosterContact_base { authorize(message: string): this; /** * Remove this contact from the roster - * @param {boolean} unauthorize - Whether to also unauthorize the + * @async + * @param {boolean} [unauthorize] - Whether to also unauthorize the + * @returns {Promise} */ - remove(unauthorize: boolean): void; + remove(unauthorize?: boolean): Promise; /** * Instruct the XMPP server to remove this contact from our roster * @async diff --git a/src/headless/types/plugins/roster/utils.d.ts b/src/headless/types/plugins/roster/utils.d.ts index 1ccb97557a..37ef78f0d7 100644 --- a/src/headless/types/plugins/roster/utils.d.ts +++ b/src/headless/types/plugins/roster/utils.d.ts @@ -24,8 +24,8 @@ export function onRosterContactsFetched(): void; /** * Reject or cancel another user's subscription to our presence updates. * @function rejectPresenceSubscription - * @param { String } jid - The Jabber ID of the user whose subscription is being canceled - * @param { String } message - An optional message to the user + * @param {String} jid - The Jabber ID of the user whose subscription is being canceled + * @param {String} message - An optional message to the user */ export function rejectPresenceSubscription(jid: string, message: string): void; export type RosterContacts = import("./contacts").default; diff --git a/src/headless/types/shared/chatbox.d.ts b/src/headless/types/shared/chatbox.d.ts index 6466bd79b1..3424913c9a 100644 --- a/src/headless/types/shared/chatbox.d.ts +++ b/src/headless/types/shared/chatbox.d.ts @@ -16,7 +16,7 @@ declare const ChatBoxBase_base: { messages: any; fetchMessages(): any; afterMessagesFetched(): void; - onMessage(attrs_or_error: import("../plugins/chat/types.js").MessageAttributes | Error): Promise; + onMessage(_attrs_or_error: import("../plugins/chat/types.js").MessageAttributes | Error): Promise; getUpdatedMessageAttributes(message: import("../index.js").Message, attrs: import("../plugins/chat/types.js").MessageAttributes): object; updateMessage(message: import("../index.js").Message, attrs: import("../plugins/chat/types.js").MessageAttributes): void; handleCorrection(attrs: import("../plugins/chat/types.js").MessageAttributes | import("../plugins/muc/types.js").MUCMessageAttributes): Promise; diff --git a/src/headless/types/shared/model-with-contact.d.ts b/src/headless/types/shared/model-with-contact.d.ts index 77affa555c..a83f8772e4 100644 --- a/src/headless/types/shared/model-with-contact.d.ts +++ b/src/headless/types/shared/model-with-contact.d.ts @@ -5,25 +5,25 @@ export default function ModelWithContact(BaseModel: T): { new (...args: any[]): { /** - * @typedef {import('../plugins/vcard/vcard').default} VCard - * @typedef {import('../plugins/roster/contact').default} RosterContact - * @typedef {import('./_converse.js').XMPPStatus} XMPPStatus - */ + * @typedef {import('../plugins/vcard/vcard').default} VCard + * @typedef {import('../plugins/roster/contact').default} RosterContact + * @typedef {import('./_converse.js').XMPPStatus} XMPPStatus + */ initialize(): void; rosterContactAdded: any; /** - * @public - * @type {RosterContact|XMPPStatus} - */ + * @public + * @type {RosterContact|XMPPStatus} + */ contact: import("../plugins/roster/contact").default | import("../index.js").XMPPStatus; /** - * @public - * @type {VCard} - */ + * @public + * @type {VCard} + */ vcard: import("../plugins/vcard/vcard").default; /** - * @param {string} jid - */ + * @param {string} jid + */ setModelContact(jid: string): Promise; cid: any; attributes: {}; diff --git a/src/headless/types/shared/model-with-messages.d.ts b/src/headless/types/shared/model-with-messages.d.ts index ecbc086a46..07087d5486 100644 --- a/src/headless/types/shared/model-with-messages.d.ts +++ b/src/headless/types/shared/model-with-messages.d.ts @@ -38,9 +38,9 @@ export default function ModelWithMessages; + onMessage(_attrs_or_error: import("../plugins/chat/types.ts").MessageAttributes | Error): Promise; /** * @param {Message} message * @param {MessageAttributes} attrs @@ -216,7 +216,7 @@ export default function ModelWithMessages; /** diff --git a/src/plugins/adhoc-views/tests/adhoc.js b/src/plugins/adhoc-views/tests/adhoc.js index 1f96e6fd66..817ed6da53 100644 --- a/src/plugins/adhoc-views/tests/adhoc.js +++ b/src/plugins/adhoc-views/tests/adhoc.js @@ -4,6 +4,8 @@ const { Strophe, sizzle, u, stx } = converse.env; describe("Ad-hoc commands", function () { + beforeAll(() => jasmine.addMatchers({ toEqualStanza: jasmine.toEqualStanza })); + it("can be queried for via a modal", mock.initConverse([], {}, async (_converse) => { const { api } = _converse; const entity_jid = 'muc.montague.lit'; diff --git a/src/plugins/chatview/tests/chatbox.js b/src/plugins/chatview/tests/chatbox.js index bde4d98934..4ee1f57829 100644 --- a/src/plugins/chatview/tests/chatbox.js +++ b/src/plugins/chatview/tests/chatbox.js @@ -102,6 +102,7 @@ describe("Chatboxes", function () { async function (_converse) { const { api } = _converse; + await mock.waitUntilBlocklistInitialized(_converse); await mock.waitForRoster(_converse, 'current', 0); const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; const stanza = u.toStanza(` diff --git a/src/plugins/chatview/tests/corrections.js b/src/plugins/chatview/tests/corrections.js index fe3e9108cf..89b0131734 100644 --- a/src/plugins/chatview/tests/corrections.js +++ b/src/plugins/chatview/tests/corrections.js @@ -4,6 +4,8 @@ const { Promise, $msg, Strophe, sizzle, u } = converse.env; describe("A Chat Message", function () { + beforeAll(() => jasmine.addMatchers({ toEqualStanza: jasmine.toEqualStanza })); + it("can be sent as a correction by using the up arrow", mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) { @@ -56,16 +58,16 @@ describe("A Chat Message", function () { expect(api.connection.get().send).toHaveBeenCalled(); const msg = api.connection.get().send.calls.all()[0].args[0]; - expect(Strophe.serialize(msg)) - .toBe(``+ - `But soft, what light through yonder window breaks?`+ - ``+ - ``+ - ``+ - ``+ - ``); + expect(msg).toEqualStanza( + stx` + But soft, what light through yonder window breaks? + + + + + `); expect(view.model.messages.models.length).toBe(1); const corrected_message = view.model.messages.at(0); expect(corrected_message.get('msgid')).toBe(first_msg.get('msgid')); diff --git a/src/plugins/chatview/tests/markers.js b/src/plugins/chatview/tests/markers.js index 2f95567688..4c4b96323d 100644 --- a/src/plugins/chatview/tests/markers.js +++ b/src/plugins/chatview/tests/markers.js @@ -35,9 +35,10 @@ describe("A XEP-0333 Chat Marker", function () { })); it("is not sent when a markable message is received from someone not on the roster", - mock.initConverse([], {'allow_non_roster_messaging': true}, async function (_converse) { + mock.initConverse([], { allow_non_roster_messaging: true }, async function (_converse) { await mock.waitForRoster(_converse, 'current', 0); + await mock.waitUntilBlocklistInitialized(_converse); const contact_jid = 'someone@montague.lit'; const msgid = u.getUniqueId(); const stanza = stx` @@ -58,7 +59,7 @@ describe("A XEP-0333 Chat Marker", function () { .map(s => s?.nodeTree ?? s) .filter(e => e.nodeName === 'message'); - await u.waitUntil(() => sent_messages.length === 2); + await u.waitUntil(() => sent_messages.length === 1); expect(Strophe.serialize(sent_messages[0])).toBe( ``+ ``+ diff --git a/src/plugins/chatview/tests/messages.js b/src/plugins/chatview/tests/messages.js index 877e640725..d69bd35dfa 100644 --- a/src/plugins/chatview/tests/messages.js +++ b/src/plugins/chatview/tests/messages.js @@ -972,6 +972,7 @@ describe("A Chat Message", function () { async function (_converse) { const { api } = _converse; + await mock.waitUntilBlocklistInitialized(_converse); await mock.waitForRoster(_converse, 'current', 0); spyOn(_converse.api, "trigger").and.callThrough(); @@ -1017,7 +1018,6 @@ describe("A Chat Message", function () { })); }); - describe("who is not on the roster", function () { it("will open a chatbox and be displayed inside it if allow_non_roster_messaging is true", @@ -1025,6 +1025,7 @@ describe("A Chat Message", function () { [], {'allow_non_roster_messaging': false}, async function (_converse) { + await mock.waitUntilBlocklistInitialized(_converse); await mock.waitForRoster(_converse, 'current', 0); const { api } = _converse; diff --git a/src/plugins/chatview/tests/spoilers.js b/src/plugins/chatview/tests/spoilers.js index 4dc3f04154..b966f9bd83 100644 --- a/src/plugins/chatview/tests/spoilers.js +++ b/src/plugins/chatview/tests/spoilers.js @@ -1,5 +1,6 @@ /* global mock, converse */ +const { Strophe, sizzle, $msg, u, stx } = converse.env; const original_timeout = jasmine.DEFAULT_TIMEOUT_INTERVAL; const settings = { @@ -11,6 +12,7 @@ const settings = { describe("A spoiler message", function () { + beforeAll(() => jasmine.addMatchers({ toEqualStanza: jasmine.toEqualStanza })); beforeEach(() => (jasmine.DEFAULT_TIMEOUT_INTERVAL = 7000)); afterEach(() => (jasmine.DEFAULT_TIMEOUT_INTERVAL = original_timeout)); @@ -20,26 +22,13 @@ describe("A spoiler message", function () { const { api } = _converse; await mock.waitForRoster(_converse, 'current'); const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - - /* - * And at the end of the story, both of them die! It is so tragic! - * Love story end - * - */ const spoiler_hint = "Love story end" const spoiler = "And at the end of the story, both of them die! It is so tragic!"; - const $msg = converse.env.$msg; - const u = converse.env.utils; - const msg = $msg({ - 'xmlns': 'jabber:client', - 'to': _converse.bare_jid, - 'from': sender_jid, - 'type': 'chat' - }).c('body').t(spoiler).up() - .c('spoiler', { - 'xmlns': 'urn:xmpp:spoiler:0', - }).t(spoiler_hint) - .tree(); + const msg = stx` + + ${spoiler} + ${spoiler_hint} + `; api.connection.get()._dataRecv(mock.createRequest(msg)); await new Promise(resolve => _converse.api.listen.once('chatBoxViewInitialized', resolve)); const view = _converse.chatboxviews.get(sender_jid); @@ -58,23 +47,12 @@ describe("A spoiler message", function () { const { api } = _converse; await mock.waitForRoster(_converse, 'current'); const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - /* - * And at the end of the story, both of them die! It is so tragic! - * Love story end - * - */ - const $msg = converse.env.$msg; - const u = converse.env.utils; const spoiler = "And at the end of the story, both of them die! It is so tragic!"; - const msg = $msg({ - 'xmlns': 'jabber:client', - 'to': _converse.bare_jid, - 'from': sender_jid, - 'type': 'chat' - }).c('body').t(spoiler).up() - .c('spoiler', { - 'xmlns': 'urn:xmpp:spoiler:0', - }).tree(); + const msg = stx` + + ${spoiler} + + `; api.connection.get()._dataRecv(mock.createRequest(msg)); await new Promise(resolve => _converse.api.listen.once('chatBoxViewInitialized', resolve)); const view = _converse.chatboxviews.get(sender_jid); @@ -97,17 +75,11 @@ describe("A spoiler message", function () { mock.openControlBox(_converse); const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - const { $pres, Strophe} = converse.env; - const u = converse.env.utils; - // XXX: We need to send a presence from the contact, so that we // have a resource, that resource is then queried to see // whether Strophe.NS.SPOILER is supported, in which case // the spoiler button will appear. - const presence = $pres({ - 'from': contact_jid+'/phone', - 'to': 'romeo@montague.lit' - }); + const presence = stx``; api.connection.get()._dataRecv(mock.createRequest(presence)); await mock.openChatBoxFor(_converse, contact_jid); await mock.waitUntilDiscoConfirmed(_converse, contact_jid+'/phone', [], [Strophe.NS.SPOILER]); @@ -128,21 +100,9 @@ describe("A spoiler message", function () { }); await new Promise(resolve => api.listen.on('sendMessage', resolve)); - /* Test the XML stanza - * - * - * This is the spoiler - * - * - * " - */ const stanza = api.connection.get().send.calls.argsFor(0)[0]; - const spoiler_el = await u.waitUntil(() => stanza.querySelector('spoiler[xmlns="urn:xmpp:spoiler:0"]')); - expect(spoiler_el.textContent).toBe(''); + const spoiler_el = sizzle('spoiler[xmlns="urn:xmpp:spoiler:0"]', stanza).pop(); + expect(spoiler_el.textContent).toBe('') const spoiler = 'This is the spoiler'; const body_el = stanza.querySelector('body'); @@ -174,17 +134,11 @@ describe("A spoiler message", function () { mock.openControlBox(_converse); const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - const { $pres, Strophe} = converse.env; - const u = converse.env.utils; - // XXX: We need to send a presence from the contact, so that we // have a resource, that resource is then queried to see // whether Strophe.NS.SPOILER is supported, in which case // the spoiler button will appear. - const presence = $pres({ - 'from': contact_jid+'/phone', - 'to': 'romeo@montague.lit' - }); + const presence = stx``; api.connection.get()._dataRecv(mock.createRequest(presence)); await mock.openChatBoxFor(_converse, contact_jid); await mock.waitUntilDiscoConfirmed(_converse, contact_jid+'/phone', [], [Strophe.NS.SPOILER]); @@ -210,21 +164,22 @@ describe("A spoiler message", function () { await new Promise(resolve => api.listen.on('sendMessage', resolve)); const stanza = api.connection.get().send.calls.argsFor(0)[0]; - expect(Strophe.serialize(stanza)).toBe( - ``+ - `This is the spoiler`+ - ``+ - ``+ - `This is the hint`+ - ``+ - `` + expect(stanza).toEqualStanza( + stx` + This is the spoiler + + + This is the hint + + ` ); - await u.waitUntil(() => stanza.querySelector('spoiler[xmlns="urn:xmpp:spoiler:0"]')?.textContent === 'This is the hint'); + const spoiler_el = sizzle('spoiler[xmlns="urn:xmpp:spoiler:0"]', stanza).pop(); + expect(spoiler_el?.textContent).toBe('This is the hint'); const spoiler = 'This is the spoiler' const body_el = stanza.querySelector('body'); diff --git a/src/plugins/muc-views/tests/muc-private-messages.js b/src/plugins/muc-views/tests/muc-private-messages.js index b304ae6681..8a4218cf2b 100644 --- a/src/plugins/muc-views/tests/muc-private-messages.js +++ b/src/plugins/muc-views/tests/muc-private-messages.js @@ -130,15 +130,17 @@ describe('MUC Private Messages', () => { const sent_stanza = api.connection.get().sent_stanzas.pop(); expect(sent_stanza).toEqualStanza(stx` - - hello - - - - `); + + hello + + + + + `); }) ); diff --git a/src/plugins/omemo/index.js b/src/plugins/omemo/index.js index 60296180f3..08f89cefd2 100644 --- a/src/plugins/omemo/index.js +++ b/src/plugins/omemo/index.js @@ -32,6 +32,8 @@ import { setEncryptedFileURL, } from './utils.js'; +import './styles/omemo.scss'; + const { Strophe } = converse.env; const { shouldClearCache } = u; diff --git a/src/plugins/omemo/styles/omemo.scss b/src/plugins/omemo/styles/omemo.scss new file mode 100644 index 0000000000..6470a2eb45 --- /dev/null +++ b/src/plugins/omemo/styles/omemo.scss @@ -0,0 +1,6 @@ +converse-omemo-fingerprints { + .fingerprint { + margin-top: 0.5em; + display: block; + } +} diff --git a/src/plugins/omemo/templates/fingerprints.js b/src/plugins/omemo/templates/fingerprints.js index c30493dda4..008a2b3ddd 100644 --- a/src/plugins/omemo/templates/fingerprints.js +++ b/src/plugins/omemo/templates/fingerprints.js @@ -1,25 +1,37 @@ import { __ } from 'i18n'; import { html } from 'lit'; import { formatFingerprint } from '../utils.js'; +import { converse } from '@converse/headless'; +const { u } = converse.env; + +/** + * @param {import('../fingerprints').Fingerprints} el + * @param {import('../device').default} device + */ const device_fingerprint = (el, device) => { const i18n_trusted = __('Trusted'); const i18n_untrusted = __('Untrusted'); + const i18n_toggle_trusted_devices = __('Choose which devices you trust for OMEMO encrypted communication'); + + const id1 = u.getUniqueId(); + const id2 = u.getUniqueId(); + const is_trusted = device.get('trusted') !== -1; + if (device.get('bundle') && device.get('bundle').fingerprint) { return html`
  • -
    - - +
    + + + + +
    ${formatFingerprint(device.get('bundle').fingerprint)} @@ -30,6 +42,9 @@ const device_fingerprint = (el, device) => { } } +/** + * @param {import('../fingerprints').Fingerprints} el + */ export default (el) => { const i18n_fingerprints = __('OMEMO Fingerprints'); const i18n_no_devices = __("No OMEMO-enabled devices found"); diff --git a/src/plugins/omemo/utils.js b/src/plugins/omemo/utils.js index 7f2f8f8f01..3cd2556941 100644 --- a/src/plugins/omemo/utils.js +++ b/src/plugins/omemo/utils.js @@ -17,7 +17,7 @@ import { MIMETYPES_MAP } from 'utils/file.js'; import { IQError, UserFacingError } from 'shared/errors.js'; import DeviceLists from './devicelists.js'; -const { Strophe, URI, sizzle } = converse.env; +const { Strophe, URI, sizzle, stx } = converse.env; const { CHATROOMS_TYPE, PRIVATE_CHAT_TYPE } = constants; const { appendArrayBuffer, @@ -467,24 +467,6 @@ async function decryptWhisperMessage (attrs) { } } -export function addKeysToMessageStanza (stanza, dicts, iv) { - for (const i in dicts) { - if (Object.prototype.hasOwnProperty.call(dicts, i)) { - const payload = dicts[i].payload; - const device = dicts[i].device; - const prekey = 3 == parseInt(payload.type, 10); - - stanza.c('key', { 'rid': device.get('id') }).t(btoa(payload.body)); - if (prekey) { - stanza.attrs({ 'prekey': prekey }); - } - stanza.up(); - } - } - stanza.c('iv').t(iv).up().up(); - return Promise.resolve(stanza); -} - /** * Given an XML element representing a user's OMEMO bundle, parse it * and return a map. @@ -873,16 +855,6 @@ export async function createOMEMOMessageStanza (chat, data) { throw new Error('No message body to encrypt!'); } const devices = await getBundlesAndBuildSessions(chat); - - // An encrypted header is added to the message for - // each device that is supposed to receive it. - // These headers simply contain the key that the - // payload message is encrypted with, - // and they are separately encrypted using the - // session corresponding to the counterpart device. - stanza.c('encrypted', { 'xmlns': Strophe.NS.OMEMO }) - .c('header', { 'sid': _converse.state.omemo_store.get('device_id') }); - const { key_and_tag, iv, payload } = await omemo.encryptMessage(message.get('plaintext')); // The 16 bytes key and the GCM authentication tag (The tag @@ -892,13 +864,33 @@ export async function createOMEMOMessageStanza (chat, data) { // concatenation is encrypted using the corresponding // long-standing SignalProtocol session. const dicts = await Promise.all(devices - .filter(device => device.get('trusted') != UNTRUSTED && device.get('active')) - .map(device => encryptKey(key_and_tag, device))); + .filter((device) => device.get('trusted') != UNTRUSTED && device.get('active')) + .map((device) => encryptKey(key_and_tag, device))); - stanza = await addKeysToMessageStanza(stanza, dicts, iv); - stanza.c('payload').t(payload).up().up(); - stanza.c('store', { 'xmlns': Strophe.NS.HINTS }).up(); - stanza.c('encryption', { 'xmlns': Strophe.NS.EME, namespace: Strophe.NS.OMEMO }); + // An encrypted header is added to the message for + // each device that is supposed to receive it. + // These headers simply contain the key that the + // payload message is encrypted with, + // and they are separately encrypted using the + // session corresponding to the counterpart device. + stanza.cnode( + stx` +
    + ${dicts.map(({ payload, device }) => { + const prekey = 3 == parseInt(payload.type, 10); + if (prekey) { + return stx`${btoa(payload.body)}`; + } + return stx`${btoa(payload.body)}`; + })} + ${iv} +
    + ${payload} +
    ` + ).root(); + + stanza.cnode(stx``).root(); + stanza.cnode(stx``).root(); return { message, stanza }; } diff --git a/src/plugins/rosterview/contactview.js b/src/plugins/rosterview/contactview.js index c831f9da6c..87eb18687a 100644 --- a/src/plugins/rosterview/contactview.js +++ b/src/plugins/rosterview/contactview.js @@ -1,28 +1,27 @@ import { Model } from '@converse/skeletor'; -import { _converse, converse, api, log } from "@converse/headless"; +import { _converse, converse, api } 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 tplRequestingContact from './templates/requesting_contact.js'; +import tplRosterItem from './templates/roster_item.js'; +import tplUnsavedContact from './templates/unsaved_contact.js'; import { __ } from 'i18n'; +import { blockContact, removeContact } from './utils.js'; const { Strophe } = converse.env; - export default class RosterContact extends CustomElement { - - static get properties () { + static get properties() { return { - model: { type: Object } - } + model: { type: Object }, + }; } - constructor () { + constructor() { super(); this.model = null; } - initialize () { + initialize() { this.listenTo(this.model, 'change', () => this.requestUpdate()); this.listenTo(this.model, 'highlight', () => this.requestUpdate()); this.listenTo(this.model, 'vcard:add', () => this.requestUpdate()); @@ -30,7 +29,7 @@ export default class RosterContact extends CustomElement { this.listenTo(this.model, 'presenceChanged', () => this.requestUpdate()); } - render () { + render() { if (this.model.get('requesting') === true) { return tplRequestingContact(this); } else if (this.model.get('subscription') === 'none') { @@ -43,7 +42,7 @@ export default class RosterContact extends CustomElement { /** * @param {MouseEvent} ev */ - openChat (ev) { + openChat(ev) { ev?.preventDefault?.(); api.chats.open(this.model.get('jid'), this.model.attributes, true); } @@ -53,43 +52,37 @@ export default class RosterContact extends CustomElement { */ addContact(ev) { ev?.preventDefault?.(); - api.modal.show('converse-add-contact-modal', {'model': new Model()}, ev); + api.modal.show('converse-add-contact-modal', { 'model': new Model() }, ev); } /** * @param {MouseEvent} ev */ - async removeContact (ev) { + async removeContact(ev) { ev?.preventDefault?.(); - if (!api.settings.get('allow_contact_removal')) { return; } - - const result = await api.confirm(__("Are you sure you want to remove this contact?")); - if (!result) return; - - const chat = await api.chats.get(this.model.get('jid')); - chat?.close(); - try { - // TODO: ask user whether they want to unauthorize the contact's - // presence request as well. - this.model.remove(); - } catch (e) { - log.error(e); - api.alert('error', __('Error'), - [__('Sorry, there was an error while trying to remove %1$s as a contact.', this.model.getDisplayName())] - ); - } + // TODO: ask user whether they want to unauthorize the contact's + // presence request as well. + await removeContact(this.model); + } + + /** + * @param {MouseEvent} ev + */ + async blockContact(ev) { + ev?.preventDefault?.(); + await blockContact(this.model); } /** * @param {MouseEvent} ev */ - async acceptRequest (ev) { + async acceptRequest(ev) { ev?.preventDefault?.(); await _converse.state.roster.sendContactAddIQ({ jid: this.model.get('jid'), name: this.model.getFullname(), - groups: [] + groups: [], }); this.model.authorize().subscribe(); } @@ -97,20 +90,23 @@ export default class RosterContact extends CustomElement { /** * @param {MouseEvent} ev */ - async declineRequest (ev) { - if (ev && ev.preventDefault) { ev.preventDefault(); } - + async declineRequest(ev) { + ev?.preventDefault?.(); 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' - }] : [] + blocking_supported + ? [ + { + label: __('Block this user from sending you further messages'), + name: 'block', + type: 'checkbox', + }, + ] + : [] ); if (result) { diff --git a/src/plugins/rosterview/templates/roster_item.js b/src/plugins/rosterview/templates/roster_item.js index 0f5b8b8a28..92158b7391 100644 --- a/src/plugins/rosterview/templates/roster_item.js +++ b/src/plugins/rosterview/templates/roster_item.js @@ -11,7 +11,7 @@ export const tplRemoveLink = (el) => { const display_name = el.model.getDisplayName(); const i18n_remove = __('Click to remove %1$s as a contact', display_name); return html` - + `; diff --git a/src/plugins/rosterview/tests/roster.js b/src/plugins/rosterview/tests/roster.js index edbce2497a..f735eb8d31 100644 --- a/src/plugins/rosterview/tests/roster.js +++ b/src/plugins/rosterview/tests/roster.js @@ -763,17 +763,33 @@ describe("The Contacts Roster", function () { })); it("can be removed by the user", - mock.initConverse([], {'roster_groups': false}, async function (_converse) { + mock.initConverse( + [], + {'roster_groups': false}, + async function (_converse) { + spyOn(_converse.api, 'confirm').and.callFake(() => Promise.resolve(true)); await mock.openControlBox(_converse); - await mock.waitForRoster(_converse, 'all'); + await mock.waitForRoster(_converse, 'pending'); 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(_converse.api, 'confirm').and.returnValue(Promise.resolve(true)); + + const sent_IQs = _converse.api.connection.get().IQ_stanzas; + for (let i=0; i sent_IQs.find(iq => iq.querySelector('iq item[subscription="remove"]'))); + expect(stanza).toEqualStanza( + stx` + + `); + _converse.api.connection.get()._dataRecv(mock.createRequest( + stx``)); + while (sent_IQs.length) sent_IQs.pop(); } await u.waitUntil(() => rosterview.querySelector(`ul[data-group="Pending contacts"]`) === null); })); @@ -1361,10 +1377,11 @@ describe("The Contacts Roster", function () { async function (_converse) { const { api } = _converse; - const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await mock.waitUntilBlocklistInitialized(_converse); await mock.waitForRoster(_converse, "current", 0); await mock.openControlBox(_converse); + const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; const msg = stx` } */ -export async function removeContact (contact) { +export async function removeContact(contact, unauthorize = false) { + if (!api.settings.get('allow_contact_removal')) return; + + const result = await api.confirm(__('Are you sure you want to remove this contact?')); + if (!result) return false; + + const chat = await api.chats.get(contact.get('jid')); + chat?.close(); try { - await contact.sendRosterRemoveStanza(); + await contact.remove(unauthorize); } catch (e) { log.error(e); api.alert('error', __('Error'), [ - __('Sorry, there was an error while trying to remove %1$s as a contact.', - contact.getDisplayName()) + __('Sorry, an error occurred while trying to remove %1$s as a contact', contact.getDisplayName()), + ]); + } + return true; +} + +/** + * @param {RosterContact} contact + * @returns {Promise} + */ +export async function blockContact(contact) { + const domain = _converse.session.get('domain'); + if (!(await api.disco.supports(Strophe.NS.BLOCKING, domain))) return false; + + const i18n_confirm = __('Do you want to block this contact, so they cannot send you messages?'); + if (!(await api.confirm(i18n_confirm))) return false; + + (await api.chats.get(contact.get('jid')))?.close(); + + try { + await Promise.all([ + api.blocklist.add(contact.get('jid')), + contact.remove(true) + ]); + } catch (e) { + log.error(e); + api.alert('error', __('Error'), [ + __('Sorry, an error occurred while trying to block %1$s', contact.getDisplayName()), + ]); + } +} + +/** + * @param {RosterContact} contact + * @returns {Promise} + */ +export async function unblockContact(contact) { + const domain = _converse.session.get('domain'); + if (!(await api.disco.supports(Strophe.NS.BLOCKING, domain))) return false; + + const i18n_confirm = __('Do you want to unblock this contact, so they can send you messages?'); + if (!(await api.confirm(i18n_confirm))) return false; + + try { + await api.blocklist.remove(contact.get('jid')); + } catch (e) { + log.error(e); + api.alert('error', __('Error'), [ + __('Sorry, an error occurred while trying to unblock %1$s', contact.getDisplayName()), ]); - } finally { - contact.destroy(); } } /** * @param {string} jid */ -export function highlightRosterItem (jid) { +export function highlightRosterItem(jid) { _converse.state.roster?.get(jid)?.trigger('highlight'); } @@ -37,12 +91,15 @@ export function highlightRosterItem (jid) { * @param {Event} ev * @param {string} name */ -export function toggleGroup (ev, name) { +export function toggleGroup(ev, name) { ev?.preventDefault?.(); const { roster } = _converse.state; const collapsed = roster.state.get('collapsed_groups'); if (collapsed.includes(name)) { - roster.state.save('collapsed_groups', collapsed.filter(n => n !== name)); + roster.state.save( + 'collapsed_groups', + collapsed.filter((n) => n !== name) + ); } else { roster.state.save('collapsed_groups', [...collapsed, name]); } @@ -71,12 +128,10 @@ function getFilterCriteria(contact) { * @param {string} groupname * @returns {boolean} */ -export function isContactFiltered (contact, groupname) { +export function isContactFiltered(contact, groupname) { const filter = _converse.state.roster_filter; const type = filter.get('type'); - const q = (type === 'state') ? - filter.get('state').toLowerCase() : - filter.get('text').toLowerCase(); + const q = type === 'state' ? filter.get('state').toLowerCase() : filter.get('text').toLowerCase(); if (!q) return false; @@ -90,11 +145,11 @@ export function isContactFiltered (contact, groupname) { } else if (q === 'unread_messages') { return contact.get('num_unread') === 0; } else if (q === 'online') { - return ["offline", "unavailable", "dnd", "away", "xa"].includes(contact.getStatus()); + return ['offline', 'unavailable', 'dnd', 'away', 'xa'].includes(contact.getStatus()); } else { return !contact.getStatus().includes(q); } - } else if (type === 'items') { + } else if (type === 'items') { return !getFilterCriteria(contact).includes(q); } } @@ -105,15 +160,17 @@ export function isContactFiltered (contact, groupname) { * @param {Model} model * @returns {boolean} */ -export function shouldShowContact (contact, groupname, model) { +export function shouldShowContact(contact, groupname, model) { if (!model.get('filter_visible')) return true; const chat_status = contact.getStatus(); if (api.settings.get('hide_offline_users') && chat_status === 'offline') { // If pending or requesting, show - if ((contact.get('ask') === 'subscribe') || - (contact.get('subscription') === 'from') || - (contact.get('requesting') === true)) { + if ( + contact.get('ask') === 'subscribe' || + contact.get('subscription') === 'from' || + contact.get('requesting') === true + ) { return !isContactFiltered(contact, groupname); } return false; @@ -125,7 +182,7 @@ export function shouldShowContact (contact, groupname, model) { * @param {string} group * @param {Model} model */ -export function shouldShowGroup (group, model) { +export function shouldShowGroup(group, model) { if (!model.get('filter_visible')) return true; const filter = _converse.state.roster_filter; @@ -148,21 +205,21 @@ export function shouldShowGroup (group, model) { * @param {RosterContact} contact * @returns {import('./types').ContactsMap} */ -export function populateContactsMap (contacts_map, contact) { +export function populateContactsMap(contacts_map, contact) { const { labels } = _converse; - const contact_groups = /** @type {string[]} */(u.unique(contact.get('groups') ?? [])); + const contact_groups = /** @type {string[]} */ (u.unique(contact.get('groups') ?? [])); if (contact.get('requesting')) { - contact_groups.push(/** @type {string} */(labels.HEADER_REQUESTING_CONTACTS)); + contact_groups.push(/** @type {string} */ (labels.HEADER_REQUESTING_CONTACTS)); } else if (contact.get('ask') === 'subscribe') { - contact_groups.push(/** @type {string} */(labels.HEADER_PENDING_CONTACTS)); + contact_groups.push(/** @type {string} */ (labels.HEADER_PENDING_CONTACTS)); } else if (contact.get('subscription') === 'none') { - contact_groups.push(/** @type {string} */(labels.HEADER_UNSAVED_CONTACTS)); + contact_groups.push(/** @type {string} */ (labels.HEADER_UNSAVED_CONTACTS)); } else if (!api.settings.get('roster_groups')) { - contact_groups.push(/** @type {string} */(labels.HEADER_CURRENT_CONTACTS)); + contact_groups.push(/** @type {string} */ (labels.HEADER_CURRENT_CONTACTS)); } else if (!contact_groups.length) { - contact_groups.push(/** @type {string} */(labels.HEADER_UNGROUPED)); + contact_groups.push(/** @type {string} */ (labels.HEADER_UNGROUPED)); } for (const name of contact_groups) { @@ -173,7 +230,7 @@ export function populateContactsMap (contacts_map, contact) { } if (contact.get('num_unread')) { - const name = /** @type {string} */(labels.HEADER_UNREAD); + const name = /** @type {string} */ (labels.HEADER_UNREAD); contacts_map[name] ? contacts_map[name].push(contact) : (contacts_map[name] = [contact]); } return contacts_map; @@ -184,14 +241,14 @@ export function populateContactsMap (contacts_map, contact) { * @param {RosterContact|XMPPStatus} contact2 * @returns {(-1|0|1)} */ -export function contactsComparator (contact1, contact2) { +export function contactsComparator(contact1, contact2) { const status1 = contact1.getStatus(); const status2 = contact2.getStatus(); if (STATUS_WEIGHTS[status1] === STATUS_WEIGHTS[status2]) { - const name1 = (contact1.getDisplayName()).toLowerCase(); - const name2 = (contact2.getDisplayName()).toLowerCase(); - return name1 < name2 ? -1 : (name1 > name2? 1 : 0); - } else { + const name1 = contact1.getDisplayName().toLowerCase(); + const name2 = contact2.getDisplayName().toLowerCase(); + return name1 < name2 ? -1 : name1 > name2 ? 1 : 0; + } else { return STATUS_WEIGHTS[status1] < STATUS_WEIGHTS[status2] ? -1 : 1; } } @@ -200,7 +257,7 @@ export function contactsComparator (contact1, contact2) { * @param {string} a * @param {string} b */ -export function groupsComparator (a, b) { +export function groupsComparator(a, b) { const HEADER_WEIGHTS = {}; const { HEADER_UNREAD, @@ -212,47 +269,47 @@ export function groupsComparator (a, b) { HEADER_WEIGHTS[HEADER_UNREAD] = 0; HEADER_WEIGHTS[HEADER_REQUESTING_CONTACTS] = 1; - HEADER_WEIGHTS[HEADER_CURRENT_CONTACTS] = 2; - HEADER_WEIGHTS[HEADER_UNGROUPED] = 3; - HEADER_WEIGHTS[HEADER_PENDING_CONTACTS] = 4; + HEADER_WEIGHTS[HEADER_CURRENT_CONTACTS] = 2; + HEADER_WEIGHTS[HEADER_UNGROUPED] = 3; + HEADER_WEIGHTS[HEADER_PENDING_CONTACTS] = 4; - const WEIGHTS = HEADER_WEIGHTS; + const WEIGHTS = HEADER_WEIGHTS; const special_groups = Object.keys(HEADER_WEIGHTS); const a_is_special = special_groups.includes(a); const b_is_special = special_groups.includes(b); - if (!a_is_special && !b_is_special ) { - return a.toLowerCase() < b.toLowerCase() ? -1 : (a.toLowerCase() > b.toLowerCase() ? 1 : 0); + if (!a_is_special && !b_is_special) { + return a.toLowerCase() < b.toLowerCase() ? -1 : a.toLowerCase() > b.toLowerCase() ? 1 : 0; } else if (a_is_special && b_is_special) { - return WEIGHTS[a] < WEIGHTS[b] ? -1 : (WEIGHTS[a] > WEIGHTS[b] ? 1 : 0); + return WEIGHTS[a] < WEIGHTS[b] ? -1 : WEIGHTS[a] > WEIGHTS[b] ? 1 : 0; } else if (!a_is_special && b_is_special) { const a_header = HEADER_CURRENT_CONTACTS; - return WEIGHTS[a_header] < WEIGHTS[b] ? -1 : (WEIGHTS[a_header] > WEIGHTS[b] ? 1 : 0); + return WEIGHTS[a_header] < WEIGHTS[b] ? -1 : WEIGHTS[a_header] > WEIGHTS[b] ? 1 : 0; } else if (a_is_special && !b_is_special) { const b_header = HEADER_CURRENT_CONTACTS; - return WEIGHTS[a] < WEIGHTS[b_header] ? -1 : (WEIGHTS[a] > WEIGHTS[b_header] ? 1 : 0); + return WEIGHTS[a] < WEIGHTS[b_header] ? -1 : WEIGHTS[a] > WEIGHTS[b_header] ? 1 : 0; } } -export function getGroupsAutoCompleteList () { - const roster = /** @type {RosterContacts} */(_converse.state.roster); +export function getGroupsAutoCompleteList() { + const roster = /** @type {RosterContacts} */ (_converse.state.roster); const groups = roster.reduce((groups, contact) => groups.concat(contact.get('groups')), []); - return [...new Set(groups.filter(i => i))]; + return [...new Set(groups.filter((i) => i))]; } -export function getJIDsAutoCompleteList () { - const roster = /** @type {RosterContacts} */(_converse.state.roster); - return [...new Set(roster.map(item => Strophe.getDomainFromJid(item.get('jid'))))]; +export function getJIDsAutoCompleteList() { + const roster = /** @type {RosterContacts} */ (_converse.state.roster); + return [...new Set(roster.map((item) => Strophe.getDomainFromJid(item.get('jid'))))]; } /** * @param {string} query */ -export async function getNamesAutoCompleteList (query) { +export async function getNamesAutoCompleteList(query) { const options = { - 'mode': /** @type {RequestMode} */('cors'), + 'mode': /** @type {RequestMode} */ ('cors'), 'headers': { - 'Accept': 'text/json' - } + 'Accept': 'text/json', + }, }; const url = `${api.settings.get('xhr_user_search_url')}q=${encodeURIComponent(query)}`; let response; @@ -269,5 +326,5 @@ export async function getNamesAutoCompleteList (query) { log.error(`Invalid JSON returned"`); return []; } - return json.map(i => ({'label': i.fullname || i.jid, 'value': i.jid})); + return json.map((i) => ({ 'label': i.fullname || i.jid, 'value': i.jid })); } diff --git a/src/shared/chat/styles/message-body.scss b/src/shared/chat/styles/message-body.scss index 3007371df2..79e0adde2f 100644 --- a/src/shared/chat/styles/message-body.scss +++ b/src/shared/chat/styles/message-body.scss @@ -13,19 +13,13 @@ converse-chat-message-body { video { display: block; - max-height: 25em; + max-height: 30em; @include media-breakpoint-down(sm) { max-width: 95%; } @include media-breakpoint-up(md) { max-width: 70%; } - @include media-breakpoint-up(lg) { - max-width: 50%; - } - @include media-breakpoint-up(xl) { - max-width: 40%; - } } } diff --git a/src/shared/chat/templates/message-text.js b/src/shared/chat/templates/message-text.js index ba5deabe8c..63428e607a 100644 --- a/src/shared/chat/templates/message-text.js +++ b/src/shared/chat/templates/message-text.js @@ -2,19 +2,33 @@ import { __ } from 'i18n/index.js'; import { getOOBURLMarkup } from 'utils/html.js'; import { html } from 'lit'; -const tplEditedIcon = (el) => { +/** + * @param {import('../message').default} el + */ +function tplEditedIcon(el) { const i18n_edited = __('This message has been edited'); - return html``; + return html``; } -const tplCheckmark = () => { - return html`` +function tplCheckmark() { + return html``; } - +/** + * @param {import('../message').default} el + */ export default (el) => { const i18n_show = __('Show more'); - const is_groupchat_message = (el.model.get('type') === 'groupchat'); + const is_groupchat_message = el.model.get('type') === 'groupchat'; const i18n_show_less = __('Show less'); const error_text = el.model.get('error_text') || el.model.get('error'); const i18n_error = `${__('Message delivery failed.')}\n${error_text}`; @@ -23,30 +37,39 @@ export default (el) => { `; - const spoiler_classes = el.model.get('is_spoiler') ? `spoiler ${el.model.get('is_spoiler_visible') ? '' : 'hidden'}` : ''; + const spoiler_classes = el.model.get('is_spoiler') + ? `spoiler ${el.model.get('is_spoiler_visible') ? '' : 'hidden'}` + : ''; const text = el.model.getMessageText(); const show_oob = el.model.get('oob_url') && text !== el.model.get('oob_url'); return html` - ${ el.model.get('is_spoiler') ? tplSpoilerHint : '' } - ${ el.model.get('subject') ? html`
    ${el.model.get('subject')}
    ` : '' } + ${el.model.get('is_spoiler') ? tplSpoilerHint : ''} + ${el.model.get('subject') ? html`
    ${el.model.get('subject')}
    ` : ''} - ${ (el.model.get('received') && !el.model.isMeCommand() && !is_groupchat_message) ? tplCheckmark() : '' } - ${ (el.model.get('edited')) ? tplEditedIcon(el) : '' } + text="${text}" + > + ${el.model.get('received') && !el.model.isMeCommand() && !is_groupchat_message ? tplCheckmark() : ''} + ${el.model.get('edited') ? tplEditedIcon(el) : ''} - ${ show_oob ? html`
    ${getOOBURLMarkup(el.model.get('oob_url'))}
    ` : '' } - ${ error_text ? html`
    ${ i18n_error }
    ` : '' } + ${show_oob ? html`
    ${getOOBURLMarkup(el.model.get('oob_url'))}
    ` : ''} + ${error_text ? html`
    ${i18n_error}
    ` : ''} `; -} +}; diff --git a/src/shared/chat/templates/message.js b/src/shared/chat/templates/message.js index 9ce404bb56..0a3b309980 100644 --- a/src/shared/chat/templates/message.js +++ b/src/shared/chat/templates/message.js @@ -1,11 +1,11 @@ -import 'shared/avatar/avatar.js'; -import 'shared/chat/unfurl.js'; -import { __ } from 'i18n'; import { html } from 'lit'; import { api, converse } from '@converse/headless'; import { shouldRenderMediaFromURL } from '../../../utils/url.js'; import { getAuthorStyle } from '../../../utils/color.js'; import { getHats } from '../utils.js'; +import { __ } from 'i18n'; +import 'shared/avatar/avatar.js'; +import 'shared/chat/unfurl.js'; const { dayjs } = converse.env; diff --git a/src/shared/modals/styles/user-details.scss b/src/shared/modals/styles/user-details.scss new file mode 100644 index 0000000000..8c8eba89f0 --- /dev/null +++ b/src/shared/modals/styles/user-details.scss @@ -0,0 +1,5 @@ +.conversejs { + .remove-contact { + margin-inline-end: 0.5em; + } +} diff --git a/src/shared/modals/templates/user-details.js b/src/shared/modals/templates/user-details.js index 63a2b72af6..7f766981e4 100644 --- a/src/shared/modals/templates/user-details.js +++ b/src/shared/modals/templates/user-details.js @@ -1,71 +1,115 @@ -import avatar from 'shared/avatar/templates/avatar.js'; -import { __ } from 'i18n'; -import { api } from "@converse/headless"; import { html } from 'lit'; -import { modal_close_button } from "plugins/modal/templates/buttons.js"; +import { until } from 'lit/directives/until.js'; +import { api, converse, _converse } from '@converse/headless'; +import { __ } from 'i18n'; +import avatar from 'shared/avatar/templates/avatar.js'; -const remove_button = (el) => { - const i18n_remove_contact = __('Remove as contact'); +const { Strophe } = converse.env; + +/** + * @param {import('../user-details').default} el + */ +function tplUnblockButton(el) { + const i18n_block = __('Remove from blocklist'); return html` - `; } -export const tplFooter = (el) => { - const is_roster_contact = el.model.contact !== undefined; - const i18n_refresh = __('Refresh'); - const allow_contact_removal = api.settings.get('allow_contact_removal'); +/** + * @param {import('../user-details').default} el + */ +function tplBlockButton(el) { + const i18n_block = __('Add to blocklist'); return html` - + `; } +/** + * @param {import('../user-details').default} el + */ +function tplRemoveButton(el) { + const i18n_remove_contact = __('Remove as contact'); + return html` + + `; +} -export const tplUserDetailsModal = (el) => { +/** + * @param {import('../user-details').default} el + */ +export function tplUserDetailsModal(el) { const vcard = el.model?.vcard; const vcard_json = vcard ? vcard.toJSON() : {}; const o = { ...el.model.toJSON(), ...vcard_json }; + const is_roster_contact = el.model.contact !== undefined; + const allow_contact_removal = api.settings.get('allow_contact_removal'); + + const domain = _converse.session.get('domain'); + const blocking_supported = api.disco.supports(Strophe.NS.BLOCKING, domain).then( + /** @param {boolean} supported */ + async (supported) => { + const blocklist = await api.blocklist.get(); + if (supported) { + if (blocklist.get(el.model.get('jid'))) { + tplUnblockButton(el); + } else { + tplBlockButton(el); + } + } + } + ); + const i18n_address = __('XMPP Address'); const i18n_email = __('Email'); const i18n_full_name = __('Full Name'); const i18n_nickname = __('Nickname'); - const i18n_profile = __('The User\'s Profile Image'); + const i18n_profile = __("The User's Profile Image"); const i18n_role = __('Role'); const i18n_url = __('URL'); + const avatar_data = { - 'alt_text': i18n_profile, - 'extra_classes': 'mb-3', - 'height': '120', - 'width': '120' - } + alt_text: i18n_profile, + extra_classes: 'mb-3', + height: '120', + width: '120', + }; return html` - + `; } diff --git a/src/shared/modals/user-details.js b/src/shared/modals/user-details.js index fa34c63dd5..593cdc036e 100644 --- a/src/shared/modals/user-details.js +++ b/src/shared/modals/user-details.js @@ -1,26 +1,32 @@ -/** - * @typedef {import('@converse/headless').ChatBox} ChatBox - */ +import { api } from "@converse/headless"; +import { blockContact, removeContact, unblockContact } from 'plugins/rosterview/utils.js'; import BaseModal from "plugins/modal/modal.js"; -import { tplUserDetailsModal, tplFooter } from "./templates/user-details.js"; import { __ } from 'i18n'; -import { api, converse, log } from "@converse/headless"; -import { removeContact } from 'plugins/rosterview/utils.js'; +import { tplUserDetailsModal } from "./templates/user-details.js"; -const u = converse.env.utils; +import './styles/user-details.scss'; export default class UserDetailsModal extends BaseModal { initialize () { super.initialize(); - this.model.rosterContactAdded.then(() => this.registerContactEventHandlers()); + this.model.rosterContactAdded.then(() => { + this.registerContactEventHandlers(); + api.vcard.update(this.model.contact.vcard, true); + }); this.listenTo(this.model, 'change', this.render); - this.registerContactEventHandlers(); + + if (this.model.contact !== undefined) { + this.registerContactEventHandlers(); + // Refresh the vcard + api.vcard.update(this.model.contact.vcard, true); + } + /** * Triggered once the UserDetailsModal has been initialized * @event _converse#userDetailsModalInitialized - * @type {ChatBox} + * @type {import('@converse/headless').ChatBox} * @example _converse.api.listen.on('userDetailsModalInitialized', (chatbox) => { ... }); */ api.trigger('userDetailsModalInitialized', this.model); @@ -30,46 +36,47 @@ export default class UserDetailsModal extends BaseModal { return tplUserDetailsModal(this); } - renderModalFooter () { - return tplFooter(this); - } - getModalTitle () { return this.model.getDisplayName(); } registerContactEventHandlers () { - if (this.model.contact !== undefined) { - this.listenTo(this.model.contact, 'change', this.render); - this.listenTo(this.model.contact.vcard, 'change', this.render); - this.model.contact.on('destroy', () => { - delete this.model.contact; - this.render(); - }); - } - } + this.listenTo(this.model.contact, 'change', this.render); + this.listenTo(this.model.contact.vcard, 'change', this.render); + this.model.contact.on('destroy', () => { + delete this.model.contact; + this.close(); + }); - async refreshContact (ev) { - if (ev && ev.preventDefault) { ev.preventDefault(); } - const refresh_icon = this.querySelector('.fa-refresh'); - u.addClass('fa-spin', refresh_icon); - try { - await api.vcard.update(this.model.contact.vcard, true); - } catch (e) { - log.fatal(e); - this.alert(__('Sorry, something went wrong while trying to refresh'), 'danger'); - } - u.removeClass('fa-spin', refresh_icon); + // Refresh the vcard + api.vcard.update(this.model.contact.vcard, true); } + /** + * @param {MouseEvent} ev + */ async removeContact (ev) { ev?.preventDefault?.(); - if (!api.settings.get('allow_contact_removal')) { return; } - const result = await api.confirm(__("Are you sure you want to remove this contact?")); - if (result) { - setTimeout(() => removeContact(this.model.contact), 1); - this.modal.hide(); - } + setTimeout(() => removeContact(this.model.contact), 1); + this.modal.hide(); + } + + /** + * @param {MouseEvent} ev + */ + async blockContact(ev) { + ev?.preventDefault?.(); + setTimeout(() => blockContact(this.model.contact), 1); + this.modal.hide(); + } + + /** + * @param {MouseEvent} ev + */ + async unblockContact(ev) { + ev?.preventDefault?.(); + setTimeout(() => unblockContact(this.model.contact), 1); + this.modal.hide(); } } diff --git a/src/shared/styles/buttons.scss b/src/shared/styles/buttons.scss index 0829200684..d4e76a36e0 100644 --- a/src/shared/styles/buttons.scss +++ b/src/shared/styles/buttons.scss @@ -2,6 +2,8 @@ --button-text-color: var(--background-color); .btn { + --converse-btn-active-color: var(--background-color) !important; + &.fa { color: var(--button-text-color) !important; } @@ -25,86 +27,126 @@ .btn-primary, .btn-outline-primary { - --converse-btn-color: var(--button-text-color); - --converse-btn-bg: var(--primary-color); - --converse-btn-disabled-bg: var(--disabled-color); - --converse-btn-border-color: var(--primary-color); - --converse-btn-hover-bg: var(--primary-color); - --converse-btn-hover-border-color: var(--primary-color-hover); --converse-btn-active-bg: var(--primary-color); --converse-btn-active-border-color: var(--primary-color); - --converse-btn-disabled-color: var(--primary-color); + --converse-btn-border-color: var(--primary-color); + --converse-btn-disabled-bg: var(--disabled-color); --converse-btn-disabled-border-color: var(--primary-color); + --converse-btn-disabled-color: var(--primary-color); + --converse-btn-hover-bg: var(--primary-color); + --converse-btn-hover-border-color: var(--primary-color-hover); + --converse-btn-hover-color: var(--background-color); + } + .btn-primary { + --converse-btn-color: var(--button-text-color); + --converse-btn-bg: var(--primary-color); + } + .btn-outline-primary { + --converse-btn-bg: var(--background); + --converse-btn-color: var(--primary-color); } .btn-secondary, .btn-outline-secondary { - --converse-btn-color: var(--button-text-color); - --converse-btn-bg: var(--secondary-color); - --converse-btn-disabled-bg: var(--disabled-color); - --converse-btn-border-color: var(--secondary-color); - --converse-btn-hover-bg: var(--secondary-color); - --converse-btn-hover-border-color: var(--secondary-color-hover); --converse-btn-active-bg: var(--secondary-color); --converse-btn-active-border-color: var(--secondary-color); - --converse-btn-disabled-color: var(--secondary-color); + --converse-btn-border-color: var(--secondary-color); + --converse-btn-disabled-bg: var(--disabled-color); --converse-btn-disabled-border-color: var(--secondary-color); + --converse-btn-disabled-color: var(--secondary-color); + --converse-btn-hover-bg: var(--secondary-color); + --converse-btn-hover-border-color: var(--secondary-color-hover); + --converse-btn-hover-color: var(--background-color); + } + .btn-secondary { + --converse-btn-color: var(--button-text-color); + --converse-btn-bg: var(--secondary-color); + } + .btn-outline-secondary { + --converse-btn-color: var(--secondary-color); + --converse-btn-bg: var(--background); } .btn-success, .btn-outline-success { - --converse-btn-color: var(--button-text-color); - --converse-btn-bg: var(--success-color); - --converse-btn-disabled-bg: var(--disabled-color); - --converse-btn-border-color: var(--success-color); - --converse-btn-hover-bg: var(--success-color-hover); - --converse-btn-hover-border-color: var(--success-color-hover); --converse-btn-active-bg: var(--success-color); --converse-btn-active-border-color: var(--success-color); - --converse-btn-disabled-color: var(--success-color); + --converse-btn-border-color: var(--success-color); + --converse-btn-disabled-bg: var(--disabled-color); --converse-btn-disabled-border-color: var(--success-color); + --converse-btn-disabled-color: var(--success-color); + --converse-btn-hover-bg: var(--success-color-hover); + --converse-btn-hover-border-color: var(--success-color-hover); + } + .btn-success { + --converse-btn-color: var(--button-text-color); + --converse-btn-bg: var(--success-color); + } + .btn-outline-success { + --converse-btn-color: var(--success-color); + --converse-btn-bg: var(--background); } .btn-warning, .btn-outline-warning { - --converse-btn-color: var(--button-text-color); - --converse-btn-bg: var(--warning-color); - --converse-btn-disabled-bg: var(--disabled-color); - --converse-btn-border-color: var(--warning-color); - --converse-btn-hover-bg: var(--warning-color-hover); - --converse-btn-hover-border-color: var(--warning-color-hover); --converse-btn-active-bg: var(--warning-color); --converse-btn-active-border-color: var(--warning-color); - --converse-btn-disabled-color: var(--warning-color); + --converse-btn-border-color: var(--warning-color); + --converse-btn-disabled-bg: var(--disabled-color); --converse-btn-disabled-border-color: var(--warning-color); + --converse-btn-disabled-color: var(--warning-color); + --converse-btn-hover-bg: var(--warning-color-hover); + --converse-btn-hover-border-color: var(--warning-color-hover); + } + .btn-warning { + --converse-btn-color: var(--button-text-color); + --converse-btn-bg: var(--warning-color); + } + .btn-outline-warning { + --converse-btn-color: var(--warning-color); + --converse-btn-bg: var(--background); } .btn-danger, .btn-outline-danger { - --converse-btn-color: var(--button-text-color); - --converse-btn-bg: var(--danger-color); - --converse-btn-disabled-bg: var(--disabled-color); - --converse-btn-border-color: var(--danger-color); - --converse-btn-hover-bg: var(--danger-color-hover); - --converse-btn-hover-border-color: var(--danger-color-hover); --converse-btn-active-bg: var(--danger-color); --converse-btn-active-border-color: var(--danger-color); - --converse-btn-disabled-color: var(--danger-color); + --converse-btn-border-color: var(--danger-color); + --converse-btn-disabled-bg: var(--disabled-color); --converse-btn-disabled-border-color: var(--danger-color); + --converse-btn-disabled-color: var(--danger-color); + --converse-btn-hover-bg: var(--danger-color-hover); + --converse-btn-hover-border-color: var(--danger-color-hover); + --converse-btn-hover-color: var(--background-color); + } + .btn-danger { + --converse-btn-color: var(--button-text-color); + --converse-btn-bg: var(--danger-color); + } + .btn-outline-danger { + --converse-btn-color: var(--danger-color); + --converse-btn-bg: var(--background); } .btn-info, .btn-outline-info { - --converse-btn-color: var(--button-text-color); - --converse-btn-bg: var(--info-color); - --converse-btn-disabled-bg: var(--disabled-color); - --converse-btn-border-color: var(--info-color); - --converse-btn-hover-bg: var(--info-color-dark); - --converse-btn-hover-border-color: var(--info-color-dark); --converse-btn-active-bg: var(--info-color); --converse-btn-active-border-color: var(--info-color); - --converse-btn-disabled-color: var(--info-color); + --converse-btn-border-color: var(--info-color); + --converse-btn-disabled-bg: var(--disabled-color); --converse-btn-disabled-border-color: var(--info-color); + --converse-btn-disabled-color: var(--info-color); + --converse-btn-hover-bg: var(--info-color-dark); + --converse-btn-hover-border-color: var(--danger-color-hover); + --converse-btn-hover-border-color: var(--info-color-dark); + } + .btn-info { + --converse-btn-color: var(--button-text-color); + --converse-btn-bg: var(--info-color); + } + .btn-outline-info { + --converse-btn-color: var(--info-color); + --converse-btn-bg: var(--background); } .btn--transparent { diff --git a/src/shared/styles/themes/dracula.scss b/src/shared/styles/themes/dracula.scss index f5ff6c6fe7..c5ff900d58 100644 --- a/src/shared/styles/themes/dracula.scss +++ b/src/shared/styles/themes/dracula.scss @@ -23,10 +23,16 @@ --primary-color: var(--purple) !important; --secondary-color: var(--pink) !important; --success-color: var(--green); - --danger-color: var(--pink); + --danger-color: var(--red); --warning-color: var(--orange); --info-color: var(--yellow); - --converse-highlight-color: var(--yellow); + --converse-highlight-color: var(--yellow) !important; + .list-group-item { + --converse-list-group-color: var(--foreground-color); + &.active { + --converse-list-group-active-color: var(--background-color) !important; + } + } // Online status indicators --chat-status-away: var(--orange); diff --git a/src/shared/texture/utils.js b/src/shared/texture/utils.js index 129ef25a63..c70901cbfb 100644 --- a/src/shared/texture/utils.js +++ b/src/shared/texture/utils.js @@ -37,6 +37,7 @@ export async function getHeaders(url) { } } + /** * We don't render more than two line-breaks, replace extra line-breaks with * the zero-width whitespace character diff --git a/src/templates/video.js b/src/templates/video.js index caa68eb65f..d8ce9973a9 100644 --- a/src/templates/video.js +++ b/src/templates/video.js @@ -8,6 +8,6 @@ export default (url, hide_url) => { const { hostname } = new URL(url); return html`
    - ${hide_url ? '' : html`${hostname}`} + ${hide_url || !hostname ? '' : html`${hostname}`}
    `; } diff --git a/src/types/plugins/omemo/templates/fingerprints.d.ts b/src/types/plugins/omemo/templates/fingerprints.d.ts index 341c6f3dad..4d13842b45 100644 --- a/src/types/plugins/omemo/templates/fingerprints.d.ts +++ b/src/types/plugins/omemo/templates/fingerprints.d.ts @@ -1,3 +1,3 @@ -declare function _default(el: any): import("lit").TemplateResult<1>; +declare function _default(el: import("../fingerprints").Fingerprints): import("lit").TemplateResult<1>; export default _default; //# sourceMappingURL=fingerprints.d.ts.map \ No newline at end of file diff --git a/src/types/plugins/omemo/utils.d.ts b/src/types/plugins/omemo/utils.d.ts index ac4cccc70b..f4aa3148c0 100644 --- a/src/types/plugins/omemo/utils.d.ts +++ b/src/types/plugins/omemo/utils.d.ts @@ -25,7 +25,6 @@ export function parseEncryptedMessage(stanza: Element, attrs: (MUCMessageAttribu export function onChatBoxesInitialized(): void; export function onChatInitialized(el: any): void; export function getSessionCipher(jid: any, id: any): any; -export function addKeysToMessageStanza(stanza: any, dicts: any, iv: any): Promise; /** * Given an XML element representing a user's OMEMO bundle, parse it * and return a map. diff --git a/src/types/plugins/rosterview/contactview.d.ts b/src/types/plugins/rosterview/contactview.d.ts index ecf7a01df4..488372ebb9 100644 --- a/src/types/plugins/rosterview/contactview.d.ts +++ b/src/types/plugins/rosterview/contactview.d.ts @@ -19,6 +19,10 @@ export default class RosterContact extends CustomElement { * @param {MouseEvent} ev */ removeContact(ev: MouseEvent): Promise; + /** + * @param {MouseEvent} ev + */ + blockContact(ev: MouseEvent): Promise; /** * @param {MouseEvent} ev */ diff --git a/src/types/plugins/rosterview/utils.d.ts b/src/types/plugins/rosterview/utils.d.ts index 09fb299d5a..361afcae5e 100644 --- a/src/types/plugins/rosterview/utils.d.ts +++ b/src/types/plugins/rosterview/utils.d.ts @@ -1,7 +1,19 @@ /** * @param {RosterContact} contact + * @param {boolean} [unauthorize] + * @returns {Promise} */ -export function removeContact(contact: RosterContact): Promise; +export function removeContact(contact: RosterContact, unauthorize?: boolean): Promise; +/** + * @param {RosterContact} contact + * @returns {Promise} + */ +export function blockContact(contact: RosterContact): Promise; +/** + * @param {RosterContact} contact + * @returns {Promise} + */ +export function unblockContact(contact: RosterContact): Promise; /** * @param {string} jid */ @@ -59,5 +71,5 @@ export function getNamesAutoCompleteList(query: string): Promise<{ export type Model = import("@converse/skeletor").Model; export type RosterContact = import("@converse/headless").RosterContact; export type RosterContacts = import("@converse/headless").RosterContacts; -import { XMPPStatus } from "@converse/headless"; +import { XMPPStatus } from '@converse/headless'; //# sourceMappingURL=utils.d.ts.map \ No newline at end of file diff --git a/src/types/shared/chat/templates/message-text.d.ts b/src/types/shared/chat/templates/message-text.d.ts index 53e34c0f2d..cc828f7e60 100644 --- a/src/types/shared/chat/templates/message-text.d.ts +++ b/src/types/shared/chat/templates/message-text.d.ts @@ -1,3 +1,3 @@ -declare function _default(el: any): import("lit").TemplateResult<1>; +declare function _default(el: import("../message").default): import("lit").TemplateResult<1>; export default _default; //# sourceMappingURL=message-text.d.ts.map \ No newline at end of file diff --git a/src/types/shared/modals/templates/user-details.d.ts b/src/types/shared/modals/templates/user-details.d.ts index daaca7571a..1e8edcf2fa 100644 --- a/src/types/shared/modals/templates/user-details.d.ts +++ b/src/types/shared/modals/templates/user-details.d.ts @@ -1,3 +1,5 @@ -export function tplFooter(el: any): import("lit").TemplateResult<1>; -export function tplUserDetailsModal(el: any): import("lit").TemplateResult<1>; +/** + * @param {import('../user-details').default} el + */ +export function tplUserDetailsModal(el: import("../user-details").default): import("lit").TemplateResult<1>; //# sourceMappingURL=user-details.d.ts.map \ No newline at end of file diff --git a/src/types/shared/modals/user-details.d.ts b/src/types/shared/modals/user-details.d.ts index f25a835e62..0dffa3a3a0 100644 --- a/src/types/shared/modals/user-details.d.ts +++ b/src/types/shared/modals/user-details.d.ts @@ -1,11 +1,19 @@ export default class UserDetailsModal extends BaseModal { renderModal(): import("lit").TemplateResult<1>; - renderModalFooter(): import("lit").TemplateResult<1>; getModalTitle(): any; registerContactEventHandlers(): void; - refreshContact(ev: any): Promise; - removeContact(ev: any): Promise; + /** + * @param {MouseEvent} ev + */ + removeContact(ev: MouseEvent): Promise; + /** + * @param {MouseEvent} ev + */ + blockContact(ev: MouseEvent): Promise; + /** + * @param {MouseEvent} ev + */ + unblockContact(ev: MouseEvent): Promise; } -export type ChatBox = import("@converse/headless").ChatBox; import BaseModal from "plugins/modal/modal.js"; //# sourceMappingURL=user-details.d.ts.map \ No newline at end of file