From cb1f9290453c6ec6ad757f5b94398f01cad0a24a Mon Sep 17 00:00:00 2001 From: JC Brand Date: Fri, 9 Jun 2023 20:37:27 +0200 Subject: [PATCH] Fixes #3123: Contacts do not show up online until chat is opened with them. The issue was that nothing was listening to the new `presenceChanged` event. --- CHANGES.md | 1 + Makefile | 4 ++++ src/headless/plugins/roster/contacts.js | 10 ++++---- src/headless/plugins/roster/presence.js | 28 +++++++++++----------- src/headless/shared/api/presence.js | 4 ++-- src/headless/utils/init.js | 11 +++++++-- src/plugins/rosterview/contactview.js | 5 ++-- src/plugins/rosterview/tests/roster.js | 31 +++++++++++++++++++++++++ webpack.html | 3 ++- webpack/webpack.serve.js | 7 +++++- 10 files changed, 75 insertions(+), 29 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 47321504c..92f55843f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -9,6 +9,7 @@ - Generate TypeScript declaration files into `dist/types` - Removed documentation about the no longer implemented `fullname` option. - Updated translations +- #3123: Contacts do not show up online until chat is opened with them. - #3156: Add function to prevent drag stutter effect over iframes when resize is called in overlay mode - #3165: Use configured nickname in profile view in the control box diff --git a/Makefile b/Makefile index c2a932659..124856299 100644 --- a/Makefile +++ b/Makefile @@ -58,6 +58,10 @@ serve: node_modules dist serve_bg: node_modules $(HTTPSERVE) -p $(HTTPSERVE_PORT) -c-1 -s & +certs: + mkdir certs + cd certs && openssl req -newkey rsa:4096 -x509 -sha256 -days 365 -nodes -out chat.example.org.crt -keyout chat.example.org.key + ######################################################################## ## Translation machinery diff --git a/src/headless/plugins/roster/contacts.js b/src/headless/plugins/roster/contacts.js index 61559ba18..b1d48d909 100644 --- a/src/headless/plugins/roster/contacts.js +++ b/src/headless/plugins/roster/contacts.js @@ -377,9 +377,7 @@ const RosterContacts = Collection.extend({ _converse.xmppstatus.save({'status': show}, {'silent': true}); const status_message = presence.querySelector('status')?.textContent; - if (status_message) { - _converse.xmppstatus.save({'status_message': status_message}); - } + if (status_message) _converse.xmppstatus.save({ status_message }); } if (_converse.jid === jid && presence_type === 'unavailable') { // XXX: We've received an "unavailable" presence from our @@ -412,11 +410,11 @@ const RosterContacts = Collection.extend({ return; // Ignore MUC } - const status_message = presence.querySelector('status')?.textContent; const contact = this.get(bare_jid); - if (contact && (status_message !== contact.get('status'))) { - contact.save({'status': status_message}); + if (contact) { + const status = presence.querySelector('status')?.textContent; + if (contact.get('status') !== status) contact.save({status}); } if (presence_type === 'subscribed' && contact) { diff --git a/src/headless/plugins/roster/presence.js b/src/headless/plugins/roster/presence.js index 02232d160..05b987bd7 100644 --- a/src/headless/plugins/roster/presence.js +++ b/src/headless/plugins/roster/presence.js @@ -30,7 +30,7 @@ export const Presence = Model.extend({ const hpr = this.getHighestPriorityResource(); const show = hpr?.attributes?.show || 'offline'; if (this.get('show') !== show) { - this.save({'show': show}); + this.save({ show }); } }, @@ -51,17 +51,17 @@ export const Presence = Model.extend({ * @param { Element } presence: The presence stanza */ addResource (presence) { - const jid = presence.getAttribute('from'), - name = Strophe.getResourceFromJid(jid), - delay = sizzle(`delay[xmlns="${Strophe.NS.DELAY}"]`, presence).pop(), - priority = presence.querySelector('priority')?.textContent ?? 0, - resource = this.resources.get(name), - settings = { - 'name': name, - 'priority': isNaN(parseInt(priority, 10)) ? 0 : parseInt(priority, 10), - 'show': presence.querySelector('show')?.textContent ?? 'online', - 'timestamp': delay ? dayjs(delay.getAttribute('stamp')).toISOString() : (new Date()).toISOString() - }; + const jid = presence.getAttribute('from'); + const name = Strophe.getResourceFromJid(jid); + const delay = sizzle(`delay[xmlns="${Strophe.NS.DELAY}"]`, presence).pop(); + const priority = presence.querySelector('priority')?.textContent; + const resource = this.resources.get(name); + const settings = { + name, + 'priority': isNaN(parseInt(priority, 10)) ? 0 : parseInt(priority, 10), + 'show': presence.querySelector('show')?.textContent ?? 'online', + 'timestamp': delay ? dayjs(delay.getAttribute('stamp')).toISOString() : (new Date()).toISOString() + }; if (resource) { resource.save(settings); } else { @@ -78,9 +78,7 @@ export const Presence = Model.extend({ */ removeResource (name) { const resource = this.resources.get(name); - if (resource) { - resource.destroy(); - } + resource?.destroy(); } }); diff --git a/src/headless/shared/api/presence.js b/src/headless/shared/api/presence.js index 8d465e222..e9180fad7 100644 --- a/src/headless/shared/api/presence.js +++ b/src/headless/shared/api/presence.js @@ -10,8 +10,8 @@ export default { /** * Send out a presence stanza * @method _converse.api.user.presence.send - * @param { String } type - * @param { String } to + * @param { String } [type] + * @param { String } [to] * @param { String } [status] - An optional status message * @param { Array|Array|Element|Strophe.Builder } [child_nodes] * Nodes(s) to be added as child nodes of the `presence` XML element. diff --git a/src/headless/utils/init.js b/src/headless/utils/init.js index 700f00aeb..8577f5c0a 100644 --- a/src/headless/utils/init.js +++ b/src/headless/utils/init.js @@ -357,7 +357,7 @@ async function getLoginCredentialsFromBrowser () { if (!jid) return null; try { - const creds = await navigator.credentials.get({'password': true}); + const creds = await navigator.credentials.get({ password: true}); if (creds && creds.type == 'password' && isValidJID(creds.id)) { // XXX: We don't actually compare `creds.id` with `jid` because // the user might have been presented a list of credentials with @@ -431,7 +431,7 @@ export async function attemptNonPreboundSession (credentials, automatic) { * The user's plaintext password is not stored, nor any material from which * the user's plaintext password could be recovered. * - * @param { String } JID - The XMPP address for which to fetch the SCRAM keys + * @param { String } jid - The XMPP address for which to fetch the SCRAM keys * @returns { Promise } A promise which resolves once we've fetched the previously * used login keys. */ @@ -444,6 +444,13 @@ export async function savedLoginInfo (jid) { } +/** + * @param { Object } [credentials] + * @param { string } credentials.password + * @param { Object } credentials.password + * @param { string } credentials.password.ck + * @returns { Promise } + */ async function connect (credentials) { const { api } = _converse; if ([ANONYMOUS, EXTERNAL].includes(api.settings.get("authentication"))) { diff --git a/src/plugins/rosterview/contactview.js b/src/plugins/rosterview/contactview.js index c44619308..b4ac5fef9 100644 --- a/src/plugins/rosterview/contactview.js +++ b/src/plugins/rosterview/contactview.js @@ -15,10 +15,11 @@ export default class RosterContact extends CustomElement { } initialize () { - this.listenTo(this.model, "change", () => this.requestUpdate()); - this.listenTo(this.model, "highlight", () => this.requestUpdate()); + this.listenTo(this.model, 'change', () => this.requestUpdate()); + this.listenTo(this.model, 'highlight', () => this.requestUpdate()); this.listenTo(this.model, 'vcard:add', () => this.requestUpdate()); this.listenTo(this.model, 'vcard:change', () => this.requestUpdate()); + this.listenTo(this.model, 'presenceChanged', () => this.requestUpdate()); } render () { diff --git a/src/plugins/rosterview/tests/roster.js b/src/plugins/rosterview/tests/roster.js index 11544acc8..e52c833ce 100644 --- a/src/plugins/rosterview/tests/roster.js +++ b/src/plugins/rosterview/tests/roster.js @@ -812,6 +812,37 @@ describe("The Contacts Roster", function () { expect(true).toBe(true); })); + it("will have their online statuses shown correctly", + mock.initConverse( + [], {}, + async function (_converse) { + + await mock.waitForRoster(_converse, 'current', 1); + await mock.openControlBox(_converse); + const icon_el = document.querySelector('converse-roster-contact converse-icon'); + expect(icon_el.getAttribute('color')).toBe('var(--subdued-color)'); + + let pres = $pres({from: 'mercutio@montague.lit/resource'}); + _converse.connection._dataRecv(mock.createRequest(pres)); + await u.waitUntil(() => icon_el.getAttribute('color') === 'var(--chat-status-online)'); + + pres = $pres({from: 'mercutio@montague.lit/resource'}).c('show', 'away'); + _converse.connection._dataRecv(mock.createRequest(pres)); + await u.waitUntil(() => icon_el.getAttribute('color') === 'var(--chat-status-away)'); + + pres = $pres({from: 'mercutio@montague.lit/resource'}).c('show', 'xa'); + _converse.connection._dataRecv(mock.createRequest(pres)); + await u.waitUntil(() => icon_el.getAttribute('color') === 'var(--subdued-color)'); + + pres = $pres({from: 'mercutio@montague.lit/resource'}).c('show', 'dnd'); + _converse.connection._dataRecv(mock.createRequest(pres)); + await u.waitUntil(() => icon_el.getAttribute('color') === 'var(--chat-status-busy)'); + + pres = $pres({from: 'mercutio@montague.lit/resource', type: 'unavailable'}); + _converse.connection._dataRecv(mock.createRequest(pres)); + await u.waitUntil(() => icon_el.getAttribute('color') === 'var(--subdued-color)'); + })); + it("can be added to the roster and they will be sorted alphabetically", mock.initConverse( [], {}, diff --git a/webpack.html b/webpack.html index 7989d58ce..c046efbb9 100644 --- a/webpack.html +++ b/webpack.html @@ -40,7 +40,8 @@ // muc_domain: 'conference.chat.example.org', muc_respect_autojoin: true, view_mode: 'fullscreen', - websocket_url: 'ws://chat.example.org:5380/xmpp-websocket', + websocket_url: 'ws://chat.example.org:5381/xmpp-websocket', + // websocket_url: 'wss://chat.example.org:5381/xmpp-websocket', // websocket_url: 'wss://conversejs.org/xmpp-websocket', // bosh_service_url: 'http://chat.example.org:5280/http-bind', allow_user_defined_connection_url: true, diff --git a/webpack/webpack.serve.js b/webpack/webpack.serve.js index 6e3994f58..086c1da43 100644 --- a/webpack/webpack.serve.js +++ b/webpack/webpack.serve.js @@ -12,7 +12,12 @@ module.exports = merge(common, { devtool: "inline-source-map", devServer: { static: [ path.resolve(__dirname, '../') ], - port: 3003 + port: 3003, + // https: { + // key: './certs/chat.example.org.key', + // cert: './certs/chat.example.org.crt', + // requestCert: true, + // }, }, plugins: [ new HTMLWebpackPlugin({