Переглянути джерело

Updates #1896: Properly identify archived one-on-one messages

Also, rename attribute from `is_receipt_request` to `is_valid_receipt_request` to avoid confusion.
JC Brand 4 роки тому
батько
коміт
09371712b0

+ 1 - 0
karma.conf.js

@@ -46,6 +46,7 @@ module.exports = function(config) {
       { pattern: "spec/chatbox.js", type: 'module' },
       { pattern: "spec/user-details-modal.js", type: 'module' },
       { pattern: "spec/messages.js", type: 'module' },
+      { pattern: "spec/receipts.js", type: 'module' },
       { pattern: "spec/muc_messages.js", type: 'module' },
       { pattern: "spec/mentions.js", type: 'module' },
       { pattern: "spec/retractions.js", type: 'module' },

+ 64 - 72
package-lock.json

@@ -5069,9 +5069,9 @@
 			}
 		},
 		"@octokit/types": {
-			"version": "5.1.2",
-			"resolved": "https://registry.npmjs.org/@octokit/types/-/types-5.1.2.tgz",
-			"integrity": "sha512-+zuMnja97vuZmWa+HdUY+0KB9MLwcEHueSSyKu0G/HqZaFYCVdLpBkavb0xyDlH7eoBdvAvSX/+Y8+4FOEZkrQ==",
+			"version": "5.4.0",
+			"resolved": "https://registry.npmjs.org/@octokit/types/-/types-5.4.0.tgz",
+			"integrity": "sha512-D/uotqF69M50OIlwMqgyIg9PuLT2daOiBAYF0P40I2ekFA2ESwwBY5dxZe/UhXdPvIbNKDzuZmQrO7rMpuFbcg==",
 			"dev": true,
 			"requires": {
 				"@types/node": ">= 8"
@@ -5532,9 +5532,9 @@
 			}
 		},
 		"abab": {
-			"version": "2.0.3",
-			"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.3.tgz",
-			"integrity": "sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg=="
+			"version": "2.0.4",
+			"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.4.tgz",
+			"integrity": "sha512-Eu9ELJWCz/c1e9gTiCY+FceWxcqzjYEbqMgtndnuSqZSUCOL73TWNK2mHfIj4Cw2E/ongOp+JISVNCmovt2KYQ=="
 		},
 		"abbrev": {
 			"version": "1.1.1",
@@ -7297,23 +7297,29 @@
 			"dev": true
 		},
 		"compare-func": {
-			"version": "1.3.4",
-			"resolved": "https://registry.npmjs.org/compare-func/-/compare-func-1.3.4.tgz",
-			"integrity": "sha512-sq2sWtrqKPkEXAC8tEJA1+BqAH9GbFkGBtUOqrUX57VSfwp8xyktctk+uLoRy5eccTdxzDcVIztlYDpKs3Jv1Q==",
+			"version": "2.0.0",
+			"resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz",
+			"integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==",
 			"dev": true,
 			"requires": {
 				"array-ify": "^1.0.0",
-				"dot-prop": "^3.0.0"
+				"dot-prop": "^5.1.0"
 			},
 			"dependencies": {
 				"dot-prop": {
-					"version": "3.0.0",
-					"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-3.0.0.tgz",
-					"integrity": "sha1-G3CK8JSknJoOfbyteQq6U52sEXc=",
+					"version": "5.2.0",
+					"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.2.0.tgz",
+					"integrity": "sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A==",
 					"dev": true,
 					"requires": {
-						"is-obj": "^1.0.0"
+						"is-obj": "^2.0.0"
 					}
+				},
+				"is-obj": {
+					"version": "2.0.0",
+					"resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz",
+					"integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==",
+					"dev": true
 				}
 			}
 		},
@@ -7483,12 +7489,12 @@
 			"dev": true
 		},
 		"conventional-changelog-angular": {
-			"version": "5.0.10",
-			"resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-5.0.10.tgz",
-			"integrity": "sha512-k7RPPRs0vp8+BtPsM9uDxRl6KcgqtCJmzRD1wRtgqmhQ96g8ifBGo9O/TZBG23jqlXS/rg8BKRDELxfnQQGiaA==",
+			"version": "5.0.11",
+			"resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-5.0.11.tgz",
+			"integrity": "sha512-nSLypht/1yEflhuTogC03i7DX7sOrXGsRn14g131Potqi6cbGbGEE9PSDEHKldabB6N76HiSyw9Ph+kLmC04Qw==",
 			"dev": true,
 			"requires": {
-				"compare-func": "^1.3.1",
+				"compare-func": "^2.0.0",
 				"q": "^1.5.1"
 			}
 		},
@@ -7538,12 +7544,12 @@
 			"dev": true
 		},
 		"conventional-changelog-writer": {
-			"version": "4.0.16",
-			"resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-4.0.16.tgz",
-			"integrity": "sha512-jmU1sDJDZpm/dkuFxBeRXvyNcJQeKhGtVcFFkwTphUAzyYWcwz2j36Wcv+Mv2hU3tpvLMkysOPXJTLO55AUrYQ==",
+			"version": "4.0.17",
+			"resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-4.0.17.tgz",
+			"integrity": "sha512-IKQuK3bib/n032KWaSb8YlBFds+aLmzENtnKtxJy3+HqDq5kohu3g/UdNbIHeJWygfnEbZjnCKFxAW0y7ArZAw==",
 			"dev": true,
 			"requires": {
-				"compare-func": "^1.3.1",
+				"compare-func": "^2.0.0",
 				"conventional-commits-filter": "^2.0.6",
 				"dateformat": "^3.0.0",
 				"handlebars": "^4.7.6",
@@ -8883,9 +8889,9 @@
 			}
 		},
 		"duplexer": {
-			"version": "0.1.1",
-			"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz",
-			"integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=",
+			"version": "0.1.2",
+			"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
+			"integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==",
 			"dev": true
 		},
 		"duplexify": {
@@ -11635,9 +11641,9 @@
 			}
 		},
 		"git-up": {
-			"version": "4.0.1",
-			"resolved": "https://registry.npmjs.org/git-up/-/git-up-4.0.1.tgz",
-			"integrity": "sha512-LFTZZrBlrCrGCG07/dm1aCjjpL1z9L3+5aEeI9SBhAqSc+kiA9Or1bgZhQFNppJX6h/f5McrvJt1mQXTFm6Qrw==",
+			"version": "4.0.2",
+			"resolved": "https://registry.npmjs.org/git-up/-/git-up-4.0.2.tgz",
+			"integrity": "sha512-kbuvus1dWQB2sSW4cbfTeGpCMd8ge9jx9RKnhXhuJ7tnvT+NIrTVfYZxjtflZddQYcmdOTlkAcjmx7bor+15AQ==",
 			"dev": true,
 			"requires": {
 				"is-ssh": "^1.3.0",
@@ -11645,9 +11651,9 @@
 			}
 		},
 		"git-url-parse": {
-			"version": "11.1.2",
-			"resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-11.1.2.tgz",
-			"integrity": "sha512-gZeLVGY8QVKMIkckncX+iCq2/L8PlwncvDFKiWkBn9EtCfYDbliRTTp6qzyQ1VMdITUfq7293zDzfpjdiGASSQ==",
+			"version": "11.1.3",
+			"resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-11.1.3.tgz",
+			"integrity": "sha512-GPsfwticcu52WQ+eHp0IYkAyaOASgYdtsQDIt4rUp6GbiNt1P9ddrh3O0kQB0eD4UJZszVqNT3+9Zwcg40fywA==",
 			"dev": true,
 			"requires": {
 				"git-up": "^4.0.0"
@@ -12873,9 +12879,9 @@
 			}
 		},
 		"is-ssh": {
-			"version": "1.3.1",
-			"resolved": "https://registry.npmjs.org/is-ssh/-/is-ssh-1.3.1.tgz",
-			"integrity": "sha512-0eRIASHZt1E68/ixClI8bp2YK2wmBPVWEismTs6M+M099jKgrzl/3E976zIbImSIob48N2/XGe9y7ZiYdImSlg==",
+			"version": "1.3.2",
+			"resolved": "https://registry.npmjs.org/is-ssh/-/is-ssh-1.3.2.tgz",
+			"integrity": "sha512-elEw0/0c2UscLrNG+OAorbP539E3rhliKPg+hDMWN9VwrDXfYK+4PBEykDPfxlYYtQvl84TascnQyobfQLHEhQ==",
 			"dev": true,
 			"requires": {
 				"protocols": "^1.1.0"
@@ -13735,9 +13741,9 @@
 			}
 		},
 		"localforage": {
-			"version": "1.8.1",
-			"resolved": "https://registry.npmjs.org/localforage/-/localforage-1.8.1.tgz",
-			"integrity": "sha512-azSSJJfc7h4bVpi0PGi+SmLQKJl2/8NErI+LhJsrORNikMZnhaQ7rv9fHj+ofwgSHrKRlsDCL/639a6nECIKuQ==",
+			"version": "1.9.0",
+			"resolved": "https://registry.npmjs.org/localforage/-/localforage-1.9.0.tgz",
+			"integrity": "sha512-rR1oyNrKulpe+VM9cYmcFn6tsHuokyVHFaCM3+osEmxaHTbEk8oQu6eGDfS6DQLWi/N67XRmB8ECG37OES368g==",
 			"requires": {
 				"lie": "3.1.1"
 			}
@@ -14112,18 +14118,16 @@
 			}
 		},
 		"meow": {
-			"version": "7.0.1",
-			"resolved": "https://registry.npmjs.org/meow/-/meow-7.0.1.tgz",
-			"integrity": "sha512-tBKIQqVrAHqwit0vfuFPY3LlzJYkEOFyKa3bPgxzNl6q/RtN8KQ+ALYEASYuFayzSAsjlhXj/JZ10rH85Q6TUw==",
+			"version": "7.1.0",
+			"resolved": "https://registry.npmjs.org/meow/-/meow-7.1.0.tgz",
+			"integrity": "sha512-kq5F0KVteskZ3JdfyQFivJEj2RaA8NFsS4+r9DaMKLcUHpk5OcHS3Q0XkCXONB1mZRPsu/Y/qImKri0nwSEZog==",
 			"dev": true,
 			"requires": {
 				"@types/minimist": "^1.2.0",
-				"arrify": "^2.0.1",
-				"camelcase": "^6.0.0",
 				"camelcase-keys": "^6.2.2",
 				"decamelize-keys": "^1.1.0",
 				"hard-rejection": "^2.1.0",
-				"minimist-options": "^4.0.2",
+				"minimist-options": "4.1.0",
 				"normalize-package-data": "^2.5.0",
 				"read-pkg-up": "^7.0.1",
 				"redent": "^3.0.0",
@@ -14132,18 +14136,6 @@
 				"yargs-parser": "^18.1.3"
 			},
 			"dependencies": {
-				"arrify": {
-					"version": "2.0.1",
-					"resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz",
-					"integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==",
-					"dev": true
-				},
-				"camelcase": {
-					"version": "6.0.0",
-					"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.0.0.tgz",
-					"integrity": "sha512-8KMDF1Vz2gzOq54ONPJS65IvTUaB1cHJ2DMM7MbPmLZljDH1qpzzLsWdiN9pHh6qvkRVDTi/07+eNGch/oLU4w==",
-					"dev": true
-				},
 				"find-up": {
 					"version": "4.1.0",
 					"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
@@ -19385,9 +19377,9 @@
 			"dev": true
 		},
 		"parse-path": {
-			"version": "4.0.1",
-			"resolved": "https://registry.npmjs.org/parse-path/-/parse-path-4.0.1.tgz",
-			"integrity": "sha512-d7yhga0Oc+PwNXDvQ0Jv1BuWkLVPXcAoQ/WREgd6vNNoKYaW52KI+RdOFjI63wjkmps9yUE8VS4veP+AgpQ/hA==",
+			"version": "4.0.2",
+			"resolved": "https://registry.npmjs.org/parse-path/-/parse-path-4.0.2.tgz",
+			"integrity": "sha512-HSqVz6iuXSiL8C1ku5Gl1Z5cwDd9Wo0q8CoffdAghP6bz8pJa1tcMC+m4N+z6VAS8QdksnIGq1TB6EgR4vPR6w==",
 			"dev": true,
 			"requires": {
 				"is-ssh": "^1.3.0",
@@ -19401,9 +19393,9 @@
 			"dev": true
 		},
 		"parse-url": {
-			"version": "5.0.1",
-			"resolved": "https://registry.npmjs.org/parse-url/-/parse-url-5.0.1.tgz",
-			"integrity": "sha512-flNUPP27r3vJpROi0/R3/2efgKkyXqnXwyP1KQ2U0SfFRgdizOdWfvrrvJg1LuOoxs7GQhmxJlq23IpQ/BkByg==",
+			"version": "5.0.2",
+			"resolved": "https://registry.npmjs.org/parse-url/-/parse-url-5.0.2.tgz",
+			"integrity": "sha512-Czj+GIit4cdWtxo3ISZCvLiUjErSo0iI3wJ+q9Oi3QuMYTI6OZu+7cewMWZ+C1YAnKhYTk6/TLuhIgCypLthPA==",
 			"dev": true,
 			"requires": {
 				"is-ssh": "^1.3.0",
@@ -20435,9 +20427,9 @@
 			"dev": true
 		},
 		"protocols": {
-			"version": "1.4.7",
-			"resolved": "https://registry.npmjs.org/protocols/-/protocols-1.4.7.tgz",
-			"integrity": "sha512-Fx65lf9/YDn3hUX08XUc0J8rSux36rEsyiv21ZGUC1mOyeM3lTRpZLcrm8aAolzS4itwVfm7TAPyxC2E5zd6xg==",
+			"version": "1.4.8",
+			"resolved": "https://registry.npmjs.org/protocols/-/protocols-1.4.8.tgz",
+			"integrity": "sha512-IgjKyaUSjsROSO8/D49Ab7hP8mJgTYcqApOqdPhLoPxAplXmkp+zRvsrSQjFn5by0rhm4VH0GAUELIPpx7B1yg==",
 			"dev": true
 		},
 		"protoduck": {
@@ -21281,9 +21273,9 @@
 			}
 		},
 		"rxjs": {
-			"version": "6.6.0",
-			"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.0.tgz",
-			"integrity": "sha512-3HMA8z/Oz61DUHe+SdOiQyzIf4tOx5oQHmMir7IZEu6TMqCLHT4LRcmNaUS0NwOz8VLvmmBduMsoaUvMaIiqzg==",
+			"version": "6.6.2",
+			"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.2.tgz",
+			"integrity": "sha512-BHdBMVoWC2sL26w//BCu3YzKT4s2jip/WhwsGEDmeKYBhKDZeYezVUnHatYB7L85v5xs0BAQmg6BEYJEKxBabg==",
 			"dev": true,
 			"requires": {
 				"tslib": "^1.9.0"
@@ -22852,8 +22844,8 @@
 			}
 		},
 		"strophe.js": {
-			"version": "github:strophe/strophejs#c4a94e59877c06dc2395f4ccbd26f3fee67a4c9f",
-			"from": "github:strophe/strophejs#c4a94e59877c06dc2395f4ccbd26f3fee67a4c9f",
+			"version": "github:strophe/strophejs#ceb5640786dc60de4a13b18c9a263f2ef112874c",
+			"from": "github:strophe/strophejs#ceb5640786dc60de4a13b18c9a263f2ef112874c",
 			"requires": {
 				"abab": "^2.0.3",
 				"ws": "^7.0.0",
@@ -24351,9 +24343,9 @@
 			"dev": true
 		},
 		"windows-release": {
-			"version": "3.3.1",
-			"resolved": "https://registry.npmjs.org/windows-release/-/windows-release-3.3.1.tgz",
-			"integrity": "sha512-Pngk/RDCaI/DkuHPlGTdIkDiTAnAkyMjoQMZqRsxydNl1qGXNIoZrB7RK8g53F2tEgQBMqQJHQdYZuQEEAu54A==",
+			"version": "3.3.3",
+			"resolved": "https://registry.npmjs.org/windows-release/-/windows-release-3.3.3.tgz",
+			"integrity": "sha512-OSOGH1QYiW5yVor9TtmXKQvt2vjQqbYS+DqmsZw+r7xDwLXEeT3JGW0ZppFmHx4diyXmxt238KFR3N9jzevBRg==",
 			"dev": true,
 			"requires": {
 				"execa": "^1.0.0"

+ 0 - 108
spec/messages.js

@@ -1188,117 +1188,9 @@ describe("A Chat Message", function () {
         done();
     }));
 
-    it("received may emit a message delivery receipt",
-        mock.initConverse(
-            ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-            async function (done, _converse) {
-
-        await mock.waitForRoster(_converse, 'current');
-        const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
-        const msg_id = u.getUniqueId();
-        const sent_stanzas = [];
-        spyOn(_converse.connection, 'send').and.callFake(stanza => sent_stanzas.push(stanza));
-        const msg = $msg({
-                'from': sender_jid,
-                'to': _converse.connection.jid,
-                'type': 'chat',
-                'id': msg_id,
-            }).c('body').t('Message!').up()
-            .c('request', {'xmlns': Strophe.NS.RECEIPTS}).tree();
-        await _converse.handleMessageStanza(msg);
-        const sent_messages = sent_stanzas.map(s => _.isElement(s) ? s : s.nodeTree).filter(s => s.nodeName === 'message');
-        // A chat state message is also included
-        expect(sent_messages.length).toBe(2);
-        const receipt = sizzle(`received[xmlns="${Strophe.NS.RECEIPTS}"]`, sent_messages[1]).pop();
-        expect(Strophe.serialize(receipt)).toBe(`<received id="${msg_id}" xmlns="${Strophe.NS.RECEIPTS}"/>`);
-        done();
-    }));
-
-    it("carbon received does not emit a message delivery receipt",
-        mock.initConverse(
-            ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-            async function (done, _converse) {
-        await mock.waitForRoster(_converse, 'current', 1);
-        const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
-        const msg_id = u.getUniqueId();
-        const view = await mock.openChatBoxFor(_converse, sender_jid);
-        spyOn(view.model, 'sendReceiptStanza').and.callThrough();
-        const msg = $msg({
-                'from': sender_jid,
-                'to': _converse.connection.jid,
-                'type': 'chat',
-                'id': u.getUniqueId(),
-            }).c('received', {'xmlns': 'urn:xmpp:carbons:2'})
-            .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'})
-            .c('message', {
-                    'xmlns': 'jabber:client',
-                    'from': sender_jid,
-                    'to': _converse.bare_jid+'/another-resource',
-                    'type': 'chat',
-                    'id': msg_id
-            }).c('body').t('Message!').up()
-            .c('request', {'xmlns': Strophe.NS.RECEIPTS}).tree();
-        await _converse.handleMessageStanza(msg);
-        expect(view.model.sendReceiptStanza).not.toHaveBeenCalled();
-        done();
-    }));
 
     describe("when sent", function () {
 
-        it("can have its delivery acknowledged by a receipt",
-            mock.initConverse(
-                ['rosterGroupsFetched', 'chatBoxesFetched'], {},
-                async function (done, _converse) {
-
-            await mock.waitForRoster(_converse, 'current', 1);
-            const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
-            await mock.openChatBoxFor(_converse, contact_jid);
-            const view = _converse.chatboxviews.get(contact_jid);
-            const textarea = view.el.querySelector('textarea.chat-textarea');
-            textarea.value = 'But soft, what light through yonder airlock breaks?';
-            view.onKeyDown({
-                target: textarea,
-                preventDefault: function preventDefault () {},
-                keyCode: 13 // Enter
-            });
-            const chatbox = _converse.chatboxes.get(contact_jid);
-            expect(chatbox).toBeDefined();
-            await new Promise(resolve => view.model.messages.once('rendered', resolve));
-            let msg_obj = chatbox.messages.models[0];
-            let msg_id = msg_obj.get('msgid');
-            let msg = $msg({
-                    'from': contact_jid,
-                    'to': _converse.connection.jid,
-                    'id': u.getUniqueId(),
-                }).c('received', {'id': msg_id, xmlns: Strophe.NS.RECEIPTS}).up().tree();
-            _converse.connection._dataRecv(mock.createRequest(msg));
-            await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__receipt').length === 1);
-
-            // Also handle receipts with type 'chat'. See #1353
-            spyOn(_converse, 'handleMessageStanza').and.callThrough();
-            textarea.value = 'Another message';
-            view.onKeyDown({
-                target: textarea,
-                preventDefault: function preventDefault () {},
-                keyCode: 13 // Enter
-            });
-            await new Promise(resolve => view.model.messages.once('rendered', resolve));
-
-            msg_obj = chatbox.messages.models[1];
-            msg_id = msg_obj.get('msgid');
-            msg = $msg({
-                    'from': contact_jid,
-                    'type': 'chat',
-                    'to': _converse.connection.jid,
-                    'id': u.getUniqueId(),
-                }).c('received', {'id': msg_id, xmlns: Strophe.NS.RECEIPTS}).up().tree();
-            _converse.connection._dataRecv(mock.createRequest(msg));
-            await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__receipt').length === 2);
-            expect(_converse.handleMessageStanza.calls.count()).toBe(1);
-            done();
-        }));
-
-
         it("will appear inside the chatbox it was sent from",
             mock.initConverse(
                 ['rosterGroupsFetched', 'chatBoxesFetched'], {},

+ 154 - 0
spec/receipts.js

@@ -0,0 +1,154 @@
+/*global mock, converse */
+
+const { Promise, Strophe, $msg, sizzle, _ } = converse.env;
+const u = converse.env.utils;
+
+
+describe("A delivery receipt", function () {
+
+    it("is emitted for a received message which requests it",
+        mock.initConverse(
+            ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+            async function (done, _converse) {
+
+        await mock.waitForRoster(_converse, 'current');
+        const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
+        const msg_id = u.getUniqueId();
+        const sent_stanzas = [];
+        spyOn(_converse.connection, 'send').and.callFake(stanza => sent_stanzas.push(stanza));
+        const msg = $msg({
+                'from': sender_jid,
+                'to': _converse.connection.jid,
+                'type': 'chat',
+                'id': msg_id,
+            }).c('body').t('Message!').up()
+            .c('request', {'xmlns': Strophe.NS.RECEIPTS}).tree();
+        await _converse.handleMessageStanza(msg);
+        const sent_messages = sent_stanzas.map(s => _.isElement(s) ? s : s.nodeTree).filter(s => s.nodeName === 'message');
+        // A chat state message is also included
+        expect(sent_messages.length).toBe(2);
+        const receipt = sizzle(`received[xmlns="${Strophe.NS.RECEIPTS}"]`, sent_messages[1]).pop();
+        expect(Strophe.serialize(receipt)).toBe(`<received id="${msg_id}" xmlns="${Strophe.NS.RECEIPTS}"/>`);
+        done();
+    }));
+
+    it("is not emitted for a carbon message",
+        mock.initConverse(
+            ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+            async function (done, _converse) {
+
+        await mock.waitForRoster(_converse, 'current', 1);
+        const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
+        const msg_id = u.getUniqueId();
+        const view = await mock.openChatBoxFor(_converse, sender_jid);
+        spyOn(view.model, 'sendReceiptStanza').and.callThrough();
+        const msg = $msg({
+                'from': sender_jid,
+                'to': _converse.connection.jid,
+                'type': 'chat',
+                'id': u.getUniqueId(),
+            }).c('received', {'xmlns': 'urn:xmpp:carbons:2'})
+            .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'})
+            .c('message', {
+                    'xmlns': 'jabber:client',
+                    'from': sender_jid,
+                    'to': _converse.bare_jid+'/another-resource',
+                    'type': 'chat',
+                    'id': msg_id
+            }).c('body').t('Message!').up()
+            .c('request', {'xmlns': Strophe.NS.RECEIPTS}).tree();
+        await _converse.handleMessageStanza(msg);
+        expect(view.model.sendReceiptStanza).not.toHaveBeenCalled();
+        done();
+    }));
+
+    it("is not emitted for an archived message",
+        mock.initConverse(
+            ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+            async function (done, _converse) {
+
+        await mock.waitForRoster(_converse, 'current', 1);
+        const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
+        const view = await mock.openChatBoxFor(_converse, sender_jid);
+        spyOn(view.model, 'sendReceiptStanza').and.callThrough();
+
+        const stanza = u.toStanza(
+            `<message xmlns="jabber:client" to="${_converse.jid}">
+                <result xmlns="urn:xmpp:mam:2" id="9ZWxmXMR8SVor-tC" queryid="f543c5f9-55e7-400e-860a-56baac121e6a">
+                    <forwarded xmlns="urn:xmpp:forward:0">
+                        <delay xmlns="urn:xmpp:delay" stamp="2020-01-10T22:19:30Z"/>
+                        <message xmlns="jabber:client" type="chat" to="${_converse.jid}" from="${sender_jid}" id="id8b6426b4-40fe-4151-941e-4c64e380acb9">
+                            <body>Please confirm receipt</body>
+                            <request xmlns="urn:xmpp:receipts"/>
+                            <origin-id xmlns="urn:xmpp:sid:0" id="id8b6426b4-40fe-4151-941e-4c64e380acb9"/>
+                        </message>
+                    </forwarded>
+                </result>
+            </message>`);
+
+        spyOn(view.model, 'getDuplicateMessage').and.callThrough();
+        view.model.handleMAMResult({ 'messages': [stanza] });
+        let message_attrs;
+        _converse.api.listen.on('MAMResult', async data => {
+            message_attrs = await data.messages[0];
+        });
+        await u.waitUntil(() => view.model.getDuplicateMessage.calls.count());
+        expect(message_attrs.is_archived).toBe(true);
+        expect(message_attrs.is_valid_receipt_request).toBe(false);
+        expect(view.model.sendReceiptStanza).not.toHaveBeenCalled();
+        done();
+    }));
+
+    it("can be received for a sent message",
+        mock.initConverse(
+            ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+            async function (done, _converse) {
+
+        await mock.waitForRoster(_converse, 'current', 1);
+        const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
+        await mock.openChatBoxFor(_converse, contact_jid);
+        const view = _converse.chatboxviews.get(contact_jid);
+        const textarea = view.el.querySelector('textarea.chat-textarea');
+        textarea.value = 'But soft, what light through yonder airlock breaks?';
+        view.onKeyDown({
+            target: textarea,
+            preventDefault: function preventDefault () {},
+            keyCode: 13 // Enter
+        });
+        const chatbox = _converse.chatboxes.get(contact_jid);
+        expect(chatbox).toBeDefined();
+        await new Promise(resolve => view.model.messages.once('rendered', resolve));
+        let msg_obj = chatbox.messages.models[0];
+        let msg_id = msg_obj.get('msgid');
+        let msg = $msg({
+                'from': contact_jid,
+                'to': _converse.connection.jid,
+                'id': u.getUniqueId(),
+            }).c('received', {'id': msg_id, xmlns: Strophe.NS.RECEIPTS}).up().tree();
+        _converse.connection._dataRecv(mock.createRequest(msg));
+        await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__receipt').length === 1);
+
+        // Also handle receipts with type 'chat'. See #1353
+        spyOn(_converse, 'handleMessageStanza').and.callThrough();
+        textarea.value = 'Another message';
+        view.onKeyDown({
+            target: textarea,
+            preventDefault: function preventDefault () {},
+            keyCode: 13 // Enter
+        });
+        await new Promise(resolve => view.model.messages.once('rendered', resolve));
+
+        msg_obj = chatbox.messages.models[1];
+        msg_id = msg_obj.get('msgid');
+        msg = $msg({
+                'from': contact_jid,
+                'type': 'chat',
+                'to': _converse.connection.jid,
+                'id': u.getUniqueId(),
+            }).c('received', {'id': msg_id, xmlns: Strophe.NS.RECEIPTS}).up().tree();
+        _converse.connection._dataRecv(mock.createRequest(msg));
+        await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__receipt').length === 2);
+        expect(_converse.handleMessageStanza.calls.count()).toBe(1);
+        done();
+    }));
+});

+ 1 - 1
src/headless/converse-chat.js

@@ -889,7 +889,7 @@ converse.plugins.add('converse-chat', {
 
             handleReceipt (attrs) {
                 if (attrs.sender === 'them') {
-                    if (attrs.is_receipt_request) {
+                    if (attrs.is_valid_receipt_request) {
                         this.sendReceiptStanza(attrs.from, attrs.msgid);
                     } else if (attrs.receipt_id) {
                         const message = this.messages.findWhere({'msgid': attrs.receipt_id});

+ 1 - 1
src/headless/converse-muc.js

@@ -1998,7 +1998,7 @@ converse.plugins.add('converse-muc', {
                 const message = this.getDuplicateMessage(attrs);
                 if (message) {
                     return this.updateMessage(message, attrs);
-                } else if (attrs.is_receipt_request || attrs.is_marker || this.ignorableCSN(attrs)) {
+                } else if (attrs.is_valid_receipt_request || attrs.is_marker || this.ignorableCSN(attrs)) {
                     return;
                 }
                 if (await this.handleRetraction(attrs) ||

+ 5 - 5
src/headless/package-lock.json

@@ -14,9 +14,9 @@
 			}
 		},
 		"abab": {
-			"version": "2.0.3",
-			"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.3.tgz",
-			"integrity": "sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg==",
+			"version": "2.0.4",
+			"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.4.tgz",
+			"integrity": "sha512-Eu9ELJWCz/c1e9gTiCY+FceWxcqzjYEbqMgtndnuSqZSUCOL73TWNK2mHfIj4Cw2E/ongOp+JISVNCmovt2KYQ==",
 			"dev": true
 		},
 		"filesize": {
@@ -83,8 +83,8 @@
 			}
 		},
 		"strophe.js": {
-			"version": "github:strophe/strophejs#c4a94e59877c06dc2395f4ccbd26f3fee67a4c9f",
-			"from": "github:strophe/strophejs#c4a94e59877c06dc2395f4ccbd26f3fee67a4c9f",
+			"version": "github:strophe/strophejs#ceb5640786dc60de4a13b18c9a263f2ef112874c",
+			"from": "github:strophe/strophejs#ceb5640786dc60de4a13b18c9a263f2ef112874c",
 			"dev": true,
 			"requires": {
 				"abab": "^2.0.3",

+ 1 - 1
src/headless/package.json

@@ -42,6 +42,6 @@
     "localforage": "^1.7.3",
     "lodash-es": "^4.17.15",
     "pluggable.js": "2.0.1",
-    "strophe.js": "strophe/strophejs#c4a94e59877c06dc2395f4ccbd26f3fee67a4c9f"
+    "strophe.js": "strophe/strophejs#ceb5640786dc60de4a13b18c9a263f2ef112874c"
   }
 }

+ 9 - 8
src/headless/utils/stanza.js

@@ -72,7 +72,7 @@ function getEncryptionAttributes (stanza, _converse) {
 }
 
 
-function isReceiptRequest (stanza, attrs) {
+function isValidReceiptRequest (stanza, attrs) {
     return (
         attrs.sender !== 'me' &&
         !attrs.is_carbon &&
@@ -382,6 +382,7 @@ const st = {
             return new StanzaParseError(`Ignoring incoming message intended for a different resource: ${to_jid}`, stanza);
         }
 
+        const original_stanza = stanza;
         let from_jid = stanza.getAttribute('from') || _converse.bare_jid;
         if (isCarbon(stanza)) {
             if (from_jid === _converse.bare_jid) {
@@ -396,7 +397,8 @@ const st = {
             }
         }
 
-        if (st.isArchived(stanza)) {
+        const is_archived = st.isArchived(stanza);
+        if (is_archived) {
             if (from_jid === _converse.bare_jid) {
                 const selector = `[xmlns="${Strophe.NS.MAM}"] > forwarded[xmlns="${Strophe.NS.FORWARD}"] > message`;
                 stanza = sizzle(selector, stanza).pop();
@@ -446,7 +448,7 @@ const st = {
          * @property { Boolean } is_markable - Can this message be marked with a XEP-0333 chat marker?
          * @property { Boolean } is_marker - Is this message a XEP-0333 Chat Marker?
          * @property { Boolean } is_only_emojis - Does the message body contain only emojis?
-         * @property { Boolean } is_receipt_request - Does this message request a XEP-0184 receipt?
+         * @property { Boolean } is_valid_receipt_request - Does this message request a XEP-0184 receipt (and is not from us or a carbon or archived message)
          * @property { Boolean } is_spoiler - Is this a XEP-0382 spoiler message?
          * @property { Boolean } is_tombstone - Is this a XEP-0424 tombstone?
          * @property { Object } encrypted -  XEP-0384 encryption payload attributes
@@ -479,18 +481,17 @@ const st = {
          * @property { String } to - The recipient JID
          * @property { String } type - The type of message
          */
-        const original_stanza = stanza;
         const delay = sizzle(`delay[xmlns="${Strophe.NS.DELAY}"]`, original_stanza).pop();
         const marker = st.getChatMarker(stanza);
         const now =  (new Date()).toISOString();
         let attrs = Object.assign({
                 contact_jid,
+                is_archived,
                 is_headline,
                 is_server_message,
                 'body': stanza.querySelector('body')?.textContent?.trim(),
                 'chat_state': getChatState(stanza),
                 'from': Strophe.getBareJidFromJid(stanza.getAttribute('from')),
-                'is_archived': st.isArchived(original_stanza),
                 'is_carbon': isCarbon(original_stanza),
                 'is_delayed': !!delay,
                 'is_markable': !!sizzle(`markable[xmlns="${Strophe.NS.MARKERS}"]`, stanza).length,
@@ -527,7 +528,7 @@ const st = {
         attrs = Object.assign({
             'message': attrs.body || attrs.error, // TODO: Remove and use body and error attributes instead
             'is_only_emojis': attrs.body ? u.isOnlyEmojis(attrs.body) : false,
-            'is_receipt_request': isReceiptRequest(stanza, attrs)
+            'is_valid_receipt_request': isValidReceiptRequest(stanza, attrs)
         }, attrs);
 
         // We prefer to use one of the XEP-0359 unique and stable stanza IDs
@@ -581,7 +582,7 @@ const st = {
          * @property { Boolean } is_markable - Can this message be marked with a XEP-0333 chat marker?
          * @property { Boolean } is_marker - Is this message a XEP-0333 Chat Marker?
          * @property { Boolean } is_only_emojis - Does the message body contain only emojis?
-         * @property { Boolean } is_receipt_request - Does this message request a XEP-0184 receipt?
+         * @property { Boolean } is_valid_receipt_request - Does this message request a XEP-0184 receipt (and is not from us or a carbon or archived message)
          * @property { Boolean } is_spoiler - Is this a XEP-0382 spoiler message?
          * @property { Boolean } is_tombstone - Is this a XEP-0424 tombstone?
          * @property { Object } encrypted -  XEP-0384 encryption payload attributes
@@ -654,7 +655,7 @@ const st = {
         await api.emojis.initialize();
         attrs = Object.assign({
             'is_only_emojis': attrs.body ? u.isOnlyEmojis(attrs.body) : false,
-            'is_receipt_request': isReceiptRequest(stanza, attrs),
+            'is_valid_receipt_request': isValidReceiptRequest(stanza, attrs),
             'message': attrs.body || attrs.error, // TODO: Remove and use body and error attributes instead
             'sender': attrs.nick === chatbox.get('nick') ? 'me': 'them',
         }, attrs);