Merge branch 'master' into muppdev

This commit is contained in:
muppeth 2021-07-11 22:33:47 +02:00
commit da940b0752
151 changed files with 45344 additions and 26561 deletions

View File

@ -3,6 +3,7 @@
## 8.0.0 (Unreleased) ## 8.0.0 (Unreleased)
- #1083: Add support for XEP-0393 Message Styling - #1083: Add support for XEP-0393 Message Styling
- #1182: Add support for XEP-0454 OMEMO Media sharing
- #2275: Allow punctuation to immediately precede a mention - #2275: Allow punctuation to immediately precede a mention
- #2348: `auto_join_room` not showing the room in `fullscreen` `view_mode`. - #2348: `auto_join_room` not showing the room in `fullscreen` `view_mode`.
- #2400: Fixes infinite loop bug when appending .png to allowed image urls - #2400: Fixes infinite loop bug when appending .png to allowed image urls

View File

@ -73,6 +73,7 @@ GETTEXT = $(XGETTEXT) --from-code=UTF-8 --language=JavaScript --keyword=__ --key
src/i18n/converse.pot: dist/converse-no-dependencies.js src/i18n/converse.pot: dist/converse-no-dependencies.js
$(GETTEXT) 2>&1 > /dev/null; exit $$?; $(GETTEXT) 2>&1 > /dev/null; exit $$?;
rm dist/converse-no-dependencies.js rm dist/converse-no-dependencies.js
rm dist/tmp.css
.PHONY: pot .PHONY: pot
pot: src/i18n/converse.pot pot: src/i18n/converse.pot

View File

@ -108,7 +108,7 @@ In embedded mode, Converse can be embedded into an element in the DOM.
- [XEP-0424](https://xmpp.org/extensions/xep-0424.html) Message Retractions - [XEP-0424](https://xmpp.org/extensions/xep-0424.html) Message Retractions
- [XEP-0425](https://xmpp.org/extensions/xep-0425.html) Message Moderation - [XEP-0425](https://xmpp.org/extensions/xep-0425.html) Message Moderation
- [XEP-0437](https://xmpp.org/extensions/xep-0437.html) Room Activity Indicators - [XEP-0437](https://xmpp.org/extensions/xep-0437.html) Room Activity Indicators
- [XEP-0454](https://xmpp.org/extensions/xep-0454.html) OMEMO Media sharing
## Integration into other servers and frameworks ## Integration into other servers and frameworks

View File

@ -800,7 +800,7 @@ embed_audio
If set to ``false``, audio files won't be embedded in chats, instead only their links will be shown. If set to ``false``, audio files won't be embedded in chats, instead only their links will be shown.
It also accepts an array strings of whitelisted domain names to only render videos that belong to those domains. It also accepts an array strings of whitelisted domain names to only render audio files that belong to those domains.
E.g. ``['conversejs.org']`` E.g. ``['conversejs.org']``
@ -1513,7 +1513,7 @@ muc_show_ogp_unfurls
Supports showing extra metadata (picture and description) for URLs contained in Supports showing extra metadata (picture and description) for URLs contained in
groupchat messages. groupchat messages.
The metadat must come from the MUC itself, metadata sent from participants The metadata must come from the MUC itself, metadata sent from participants
themselves will not be shown. themselves will not be shown.
For Prosody XMPP server, `mod_ogp <https://modules.prosody.im/mod_ogp.html>`_ can be used. For Prosody XMPP server, `mod_ogp <https://modules.prosody.im/mod_ogp.html>`_ can be used.

View File

@ -39,7 +39,6 @@ module.exports = function(config) {
{ pattern: "src/headless/plugins/status/tests/status.js", type: 'module' }, { pattern: "src/headless/plugins/status/tests/status.js", type: 'module' },
{ pattern: "src/headless/tests/converse.js", type: 'module' }, { pattern: "src/headless/tests/converse.js", type: 'module' },
{ pattern: "src/headless/tests/eventemitter.js", type: 'module' }, { pattern: "src/headless/tests/eventemitter.js", type: 'module' },
{ pattern: "src/headless/tests/persistence.js", type: 'module' },
{ pattern: "src/plugins/bookmark-views/tests/bookmarks.js", type: 'module' }, { pattern: "src/plugins/bookmark-views/tests/bookmarks.js", type: 'module' },
{ pattern: "src/plugins/chatview/tests/chatbox.js", type: 'module' }, { pattern: "src/plugins/chatview/tests/chatbox.js", type: 'module' },
{ pattern: "src/plugins/chatview/tests/corrections.js", type: 'module' }, { pattern: "src/plugins/chatview/tests/corrections.js", type: 'module' },
@ -54,6 +53,8 @@ module.exports = function(config) {
{ pattern: "src/plugins/chatview/tests/oob.js", type: 'module' }, { pattern: "src/plugins/chatview/tests/oob.js", type: 'module' },
{ pattern: "src/plugins/chatview/tests/receipts.js", type: 'module' }, { pattern: "src/plugins/chatview/tests/receipts.js", type: 'module' },
{ pattern: "src/plugins/chatview/tests/spoilers.js", type: 'module' }, { pattern: "src/plugins/chatview/tests/spoilers.js", type: 'module' },
{ pattern: "src/plugins/chatview/tests/styling.js", type: 'module' },
{ pattern: "src/plugins/chatview/tests/unreads.js", type: 'module' },
{ pattern: "src/plugins/chatview/tests/xss.js", type: 'module' }, { pattern: "src/plugins/chatview/tests/xss.js", type: 'module' },
{ pattern: "src/plugins/controlbox/tests/controlbox.js", type: 'module' }, { pattern: "src/plugins/controlbox/tests/controlbox.js", type: 'module' },
{ pattern: "src/plugins/controlbox/tests/login.js", type: 'module' }, { pattern: "src/plugins/controlbox/tests/login.js", type: 'module' },
@ -68,6 +69,7 @@ module.exports = function(config) {
{ pattern: "src/plugins/muc-views/tests/hats.js", type: 'module' }, { pattern: "src/plugins/muc-views/tests/hats.js", type: 'module' },
{ pattern: "src/plugins/muc-views/tests/http-file-upload.js", type: 'module' }, { pattern: "src/plugins/muc-views/tests/http-file-upload.js", type: 'module' },
{ pattern: "src/plugins/muc-views/tests/markers.js", type: 'module' }, { pattern: "src/plugins/muc-views/tests/markers.js", type: 'module' },
{ pattern: "src/plugins/muc-views/tests/me-messages.js", type: 'module' },
{ pattern: "src/plugins/muc-views/tests/mentions.js", type: 'module' }, { pattern: "src/plugins/muc-views/tests/mentions.js", type: 'module' },
{ pattern: "src/plugins/muc-views/tests/modtools.js", type: 'module' }, { pattern: "src/plugins/muc-views/tests/modtools.js", type: 'module' },
{ pattern: "src/plugins/muc-views/tests/muc-api.js", type: 'module' }, { pattern: "src/plugins/muc-views/tests/muc-api.js", type: 'module' },
@ -79,16 +81,20 @@ module.exports = function(config) {
{ pattern: "src/plugins/muc-views/tests/rai.js", type: 'module' }, { pattern: "src/plugins/muc-views/tests/rai.js", type: 'module' },
{ pattern: "src/plugins/muc-views/tests/retractions.js", type: 'module' }, { pattern: "src/plugins/muc-views/tests/retractions.js", type: 'module' },
{ pattern: "src/plugins/muc-views/tests/styling.js", type: 'module' }, { pattern: "src/plugins/muc-views/tests/styling.js", type: 'module' },
{ pattern: "src/plugins/muc-views/tests/toolbar.js", type: 'module' },
{ pattern: "src/plugins/muc-views/tests/unfurls.js", type: 'module' }, { pattern: "src/plugins/muc-views/tests/unfurls.js", type: 'module' },
{ pattern: "src/plugins/muc-views/tests/xss.js", type: 'module' }, { pattern: "src/plugins/muc-views/tests/xss.js", type: 'module' },
{ pattern: "src/plugins/notifications/tests/notification.js", type: 'module' }, { pattern: "src/plugins/notifications/tests/notification.js", type: 'module' },
{ pattern: "src/plugins/omemo/tests/media-sharing.js", type: 'module' },
{ pattern: "src/plugins/omemo/tests/omemo.js", type: 'module' }, { pattern: "src/plugins/omemo/tests/omemo.js", type: 'module' },
{ pattern: "src/plugins/register/tests/register.js", type: 'module' }, { pattern: "src/plugins/register/tests/register.js", type: 'module' },
{ pattern: "src/plugins/rootview/tests/root.js", type: 'module' }, { pattern: "src/plugins/rootview/tests/root.js", type: 'module' },
{ pattern: "src/plugins/rosterview/tests/presence.js", type: 'module' }, { pattern: "src/plugins/rosterview/tests/presence.js", type: 'module' },
{ pattern: "src/plugins/rosterview/tests/protocol.js", type: 'module' }, { pattern: "src/plugins/rosterview/tests/protocol.js", type: 'module' },
{ pattern: "src/plugins/rosterview/tests/roster.js", type: 'module' }, { pattern: "src/plugins/rosterview/tests/roster.js", type: 'module' },
{ pattern: "src/shared/chat/tests/styling.js", type: 'module' },
// For some reason this test causes issues when its run earlier
{ pattern: "src/headless/tests/persistence.js", type: 'module' },
], ],
proxies: { proxies: {

64169
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -63,56 +63,50 @@
"browser": "*" "browser": "*"
}, },
"devDependencies": { "devDependencies": {
"@babel/cli": "^7.10.3", "@babel/cli": "^7.14.5",
"@babel/core": "^7.10.5", "@babel/core": "^7.14.6",
"@babel/plugin-proposal-class-properties": "^7.13.0", "@babel/preset-env": "^7.14.7",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.13.8",
"@babel/plugin-proposal-optional-chaining": "^7.12.1",
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/preset-env": "^7.12.7",
"@converse/headless": "file:src/headless", "@converse/headless": "file:src/headless",
"autoprefixer": "^9.8.6", "autoprefixer": "^9.8.6",
"babel-eslint": "^10.1.0", "babel-eslint": "^10.1.0",
"babel-loader": "^8.2.2", "babel-loader": "^8.2.2",
"babel-plugin-lodash": "^3.3.4",
"bootstrap.native-loader": "2.0.0", "bootstrap.native-loader": "2.0.0",
"clean-css-cli": "^4.3.0", "clean-css-cli": "^4.3.0",
"copy-webpack-plugin": "^6.3.2", "copy-webpack-plugin": "^9.0.1",
"css-loader": "^3.5.3", "css-loader": "^3.5.3",
"eslint": "^7.3.0", "eslint": "^7.3.0",
"exports-loader": "^0.7.0", "exports-loader": "^0.7.0",
"fast-text-encoding": "^1.0.3", "fast-text-encoding": "^1.0.3",
"file-loader": "^6.0.0", "file-loader": "^6.2.0",
"html-webpack-plugin": "^5.3.1", "html-webpack-plugin": "^5.3.2",
"http-server": "^0.12.3", "http-server": "^0.12.3",
"imports-loader": "^0.8.0", "imports-loader": "^0.8.0",
"install": "^0.13.0", "install": "^0.13.0",
"jasmine": "^3.5.0", "jasmine": "^3.7.0",
"jsdoc": "^3.6.6", "jsdoc": "^3.6.7",
"karma": "^6.3.2", "karma": "^6.3.4",
"karma-chrome-launcher": "^3.1.0", "karma-chrome-launcher": "^3.1.0",
"karma-cli": "^2.0.0", "karma-cli": "^2.0.0",
"karma-jasmine": "^3.1.1", "karma-jasmine": "^4.0.1",
"karma-jasmine-html-reporter": "^1.5.4", "karma-jasmine-html-reporter": "^1.5.4",
"karma-webpack": "^5.0.0", "karma-webpack": "^5.0.0",
"lerna": "^3.22.1", "lerna": "^4.0.0",
"mini-css-extract-plugin": "^1.5.1", "mini-css-extract-plugin": "^1.6.2",
"minimist": "^1.2.3", "minimist": "^1.2.3",
"npm": "^6.14.9", "npm": "^7.19.0",
"po-loader": "^0.5.0", "po-loader": "^0.5.0",
"po2json": "^1.0.0-beta", "po2json": "^1.0.0-beta",
"postcss-clean": "^1.1.0", "postcss-clean": "^1.2.2",
"postcss-loader": "^3.0.0", "postcss-loader": "^3.0.0",
"prettierx": "^0.12.1", "prettierx": "^0.18.2",
"run-headless-chromium": "^0.1.1", "run-headless-chromium": "^0.1.1",
"sass": "^1.32.12", "sass": "^1.32.12",
"sass-loader": "^11.0.1", "sass-loader": "^11.0.1",
"sinon": "^9.2.4",
"style-loader": "^0.23.1", "style-loader": "^0.23.1",
"webpack": "^5.36.1", "webpack": "^5.36.1",
"webpack-cli": "^4.6.0", "webpack-cli": "^4.7.2",
"webpack-dev-server": "^4.0.0-beta.2", "webpack-dev-server": "^4.0.0-beta.3",
"webpack-merge": "^5.7.3" "webpack-merge": "^5.8.0"
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "5.14.0", "@fortawesome/fontawesome-free": "5.14.0",
@ -120,8 +114,7 @@
"bootstrap.native": "^2.0.27", "bootstrap.native": "^2.0.27",
"favico.js-slevomat": "^0.3.11", "favico.js-slevomat": "^0.3.11",
"jed": "1.1.1", "jed": "1.1.1",
"lit": "^2.0.0-rc.1", "lit": "^2.0.0-rc.2",
"urijs": "^1.19.6",
"xss": "^1.0.8" "xss": "^1.0.8"
} }
} }

View File

@ -15,7 +15,7 @@ mock.initConverse = function (promise_names=[], settings=null, func) {
settings = null; settings = null;
} }
return async done => { return async () => {
if (_converse && _converse.api.connection.connected()) { if (_converse && _converse.api.connection.connected()) {
await _converse.api.user.logout(); await _converse.api.user.logout();
} }
@ -28,11 +28,10 @@ mock.initConverse = function (promise_names=[], settings=null, func) {
await initConverse(settings); await initConverse(settings);
await Promise.all((promise_names || []).map(_converse.api.waitUntil)); await Promise.all((promise_names || []).map(_converse.api.waitUntil));
try { try {
await func(done, _converse); await func(_converse);
} catch(e) { } catch(e) {
console.error(e); console.error(e);
fail(e); fail(e);
await done();
} }
} }
}; };
@ -673,3 +672,68 @@ const initConverse = async (settings) => {
window.converse_disable_effects = true; window.converse_disable_effects = true;
return _converse; return _converse;
} }
mock.deviceListFetched = async function deviceListFetched (_converse, jid) {
const selector = `iq[to="${jid}"] items[node="eu.siacs.conversations.axolotl.devicelist"]`;
const stanza = await u.waitUntil(
() => Array.from(_converse.connection.IQ_stanzas).filter(iq => iq.querySelector(selector)).pop()
);
await u.waitUntil(() => _converse.devicelists.get(jid));
return stanza;
}
mock.ownDeviceHasBeenPublished = function ownDeviceHasBeenPublished (_converse) {
return Array.from(_converse.connection.IQ_stanzas).filter(
iq => iq.querySelector('iq[from="'+_converse.bare_jid+'"] publish[node="eu.siacs.conversations.axolotl.devicelist"]')
).pop();
}
mock.bundleHasBeenPublished = function bundleHasBeenPublished (_converse) {
const selector = 'publish[node="eu.siacs.conversations.axolotl.bundles:123456789"]';
return Array.from(_converse.connection.IQ_stanzas).filter(iq => iq.querySelector(selector)).pop();
}
mock.bundleFetched = function bundleFetched (_converse, jid, device_id) {
return Array.from(_converse.connection.IQ_stanzas).filter(
iq => iq.querySelector(`iq[to="${jid}"] items[node="eu.siacs.conversations.axolotl.bundles:${device_id}"]`)
).pop();
}
mock.initializedOMEMO = async function initializedOMEMO (_converse) {
await mock.waitUntilDiscoConfirmed(
_converse, _converse.bare_jid,
[{'category': 'pubsub', 'type': 'pep'}],
['http://jabber.org/protocol/pubsub#publish-options']
);
let iq_stanza = await u.waitUntil(() => mock.deviceListFetched(_converse, _converse.bare_jid));
let stanza = $iq({
'from': _converse.bare_jid,
'id': iq_stanza.getAttribute('id'),
'to': _converse.bare_jid,
'type': 'result',
}).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"})
.c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"})
.c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute
.c('list', {'xmlns': "eu.siacs.conversations.axolotl"})
.c('device', {'id': '482886413b977930064a5888b92134fe'});
_converse.connection._dataRecv(mock.createRequest(stanza));
iq_stanza = await u.waitUntil(() => mock.ownDeviceHasBeenPublished(_converse))
stanza = $iq({
'from': _converse.bare_jid,
'id': iq_stanza.getAttribute('id'),
'to': _converse.bare_jid,
'type': 'result'});
_converse.connection._dataRecv(mock.createRequest(stanza));
iq_stanza = await u.waitUntil(() => mock.bundleHasBeenPublished(_converse))
stanza = $iq({
'from': _converse.bare_jid,
'id': iq_stanza.getAttribute('id'),
'to': _converse.bare_jid,
'type': 'result'});
_converse.connection._dataRecv(mock.createRequest(stanza));
await _converse.api.waitUntil('OMEMOInitialized');
}

View File

@ -1,4 +1,4 @@
/*global mock, converse, _ */ /*global mock, converse */
const $iq = converse.env.$iq; const $iq = converse.env.$iq;
const Strophe = converse.env.Strophe; const Strophe = converse.env.Strophe;
@ -18,7 +18,7 @@ describe("XEP-0357 Push Notifications", function () {
'jid': 'push-5@client.example', 'jid': 'push-5@client.example',
'node': 'yxs32uqsflafdk3iuqo' 'node': 'yxs32uqsflafdk3iuqo'
}] }]
}, async function (done, _converse) { }, async function (_converse) {
const IQ_stanzas = _converse.connection.IQ_stanzas; const IQ_stanzas = _converse.connection.IQ_stanzas;
expect(_converse.session.get('push_enabled')).toBeFalsy(); expect(_converse.session.get('push_enabled')).toBeFalsy();
@ -33,7 +33,7 @@ describe("XEP-0357 Push Notifications", function () {
[{'category': 'account', 'type':'registered'}], [{'category': 'account', 'type':'registered'}],
['urn:xmpp:push:0'], [], 'info'); ['urn:xmpp:push:0'], [], 'info');
const stanza = await u.waitUntil(() => const stanza = await u.waitUntil(() =>
_.filter(IQ_stanzas, iq => iq.querySelector('iq[type="set"] enable[xmlns="urn:xmpp:push:0"]')).pop() IQ_stanzas.filter(iq => iq.querySelector('iq[type="set"] enable[xmlns="urn:xmpp:push:0"]')).pop()
); );
expect(Strophe.serialize(stanza)).toEqual( expect(Strophe.serialize(stanza)).toEqual(
`<iq id="${stanza.getAttribute('id')}" type="set" xmlns="jabber:client">`+ `<iq id="${stanza.getAttribute('id')}" type="set" xmlns="jabber:client">`+
@ -46,7 +46,6 @@ describe("XEP-0357 Push Notifications", function () {
'id': stanza.getAttribute('id') 'id': stanza.getAttribute('id')
}))); })));
await u.waitUntil(() => _converse.session.get('push_enabled')); await u.waitUntil(() => _converse.session.get('push_enabled'));
done();
})); }));
it("can be enabled for a MUC domain", it("can be enabled for a MUC domain",
@ -57,7 +56,7 @@ describe("XEP-0357 Push Notifications", function () {
'jid': 'push-5@client.example', 'jid': 'push-5@client.example',
'node': 'yxs32uqsflafdk3iuqo' 'node': 'yxs32uqsflafdk3iuqo'
}] }]
}, async function (done, _converse) { }, async function (_converse) {
const IQ_stanzas = _converse.connection.IQ_stanzas; const IQ_stanzas = _converse.connection.IQ_stanzas;
await mock.waitUntilDiscoConfirmed( await mock.waitUntilDiscoConfirmed(
@ -80,16 +79,14 @@ describe("XEP-0357 Push Notifications", function () {
await u.waitUntil(() => _converse.session.get('push_enabled')); await u.waitUntil(() => _converse.session.get('push_enabled'));
expect(_converse.session.get('push_enabled').length).toBe(1); expect(_converse.session.get('push_enabled').length).toBe(1);
expect(_.includes(_converse.session.get('push_enabled'), 'romeo@montague.lit')).toBe(true); expect(_converse.session.get('push_enabled').includes('romeo@montague.lit')).toBe(true);
mock.openAndEnterChatRoom(_converse, 'coven@chat.shakespeare.lit', 'oldhag'); mock.openAndEnterChatRoom(_converse, 'coven@chat.shakespeare.lit', 'oldhag');
await mock.waitUntilDiscoConfirmed( await mock.waitUntilDiscoConfirmed(
_converse, 'chat.shakespeare.lit', _converse, 'chat.shakespeare.lit',
[{'category': 'account', 'type':'registered'}], [{'category': 'account', 'type':'registered'}],
['urn:xmpp:push:0'], [], 'info'); ['urn:xmpp:push:0'], [], 'info');
iq = await u.waitUntil(() => _.filter( iq = await u.waitUntil(() => IQ_stanzas.filter(iq => sizzle(`iq[type="set"][to="chat.shakespeare.lit"] enable[xmlns="${Strophe.NS.PUSH}"]`, iq).length
IQ_stanzas,
iq => sizzle(`iq[type="set"][to="chat.shakespeare.lit"] enable[xmlns="${Strophe.NS.PUSH}"]`, iq).length
).pop()); ).pop());
expect(Strophe.serialize(iq)).toEqual( expect(Strophe.serialize(iq)).toEqual(
@ -102,8 +99,7 @@ describe("XEP-0357 Push Notifications", function () {
'type': 'result', 'type': 'result',
'id': iq.getAttribute('id') 'id': iq.getAttribute('id')
}))); })));
await u.waitUntil(() => _.includes(_converse.session.get('push_enabled'), 'chat.shakespeare.lit')); await u.waitUntil(() => _converse.session.get('push_enabled').includes('chat.shakespeare.lit'));
done();
})); }));
it("can be disabled", it("can be disabled",
@ -114,7 +110,7 @@ describe("XEP-0357 Push Notifications", function () {
'node': 'yxs32uqsflafdk3iuqo', 'node': 'yxs32uqsflafdk3iuqo',
'disable': true 'disable': true
}] }]
}, async function (done, _converse) { }, async function (_converse) {
const IQ_stanzas = _converse.connection.IQ_stanzas; const IQ_stanzas = _converse.connection.IQ_stanzas;
expect(_converse.session.get('push_enabled')).toBeFalsy(); expect(_converse.session.get('push_enabled')).toBeFalsy();
@ -136,7 +132,6 @@ describe("XEP-0357 Push Notifications", function () {
'id': stanza.getAttribute('id') 'id': stanza.getAttribute('id')
}))); })));
await u.waitUntil(() => _converse.session.get('push_enabled')) await u.waitUntil(() => _converse.session.get('push_enabled'))
done();
})); }));
@ -147,7 +142,7 @@ describe("XEP-0357 Push Notifications", function () {
'node': 'yxs32uqsflafdk3iuqo', 'node': 'yxs32uqsflafdk3iuqo',
'secret': 'eruio234vzxc2kla-91' 'secret': 'eruio234vzxc2kla-91'
}] }]
}, async function (done, _converse) { }, async function (_converse) {
const IQ_stanzas = _converse.connection.IQ_stanzas; const IQ_stanzas = _converse.connection.IQ_stanzas;
expect(_converse.session.get('push_enabled')).toBeFalsy(); expect(_converse.session.get('push_enabled')).toBeFalsy();
@ -179,6 +174,5 @@ describe("XEP-0357 Push Notifications", function () {
'id': stanza.getAttribute('id') 'id': stanza.getAttribute('id')
}))); })));
await u.waitUntil(() => _converse.session.get('push_enabled')) await u.waitUntil(() => _converse.session.get('push_enabled'))
done();
})); }));
}); });

View File

@ -5,7 +5,7 @@ const u = converse.env.utils;
describe("The User Details Modal", function () { describe("The User Details Modal", function () {
it("can be used to remove a contact", it("can be used to remove a contact",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1); await mock.waitForRoster(_converse, 'current', 1);
_converse.api.trigger('rosterContactsFetched'); _converse.api.trigger('rosterContactsFetched');
@ -30,11 +30,10 @@ describe("The User Details Modal", function () {
show_modal_button.click(); show_modal_button.click();
remove_contact_button = modal.el.querySelector('button.remove-contact'); remove_contact_button = modal.el.querySelector('button.remove-contact');
expect(remove_contact_button === null).toBeTruthy(); expect(remove_contact_button === null).toBeTruthy();
done();
})); }));
it("shows an alert when an error happened while removing the contact", it("shows an alert when an error happened while removing the contact",
mock.initConverse([], {}, async function (done, _converse) { mock.initConverse([], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1); await mock.waitForRoster(_converse, 'current', 1);
_converse.api.trigger('rosterContactsFetched'); _converse.api.trigger('rosterContactsFetched');
@ -71,6 +70,5 @@ describe("The User Details Modal", function () {
remove_contact_button = modal.el.querySelector('button.remove-contact'); remove_contact_button = modal.el.querySelector('button.remove-contact');
expect(u.isVisible(remove_contact_button)).toBeTruthy(); expect(u.isVisible(remove_contact_button)).toBeTruthy();
done();
})); }));
}); });

View File

@ -3,6 +3,7 @@
* @license Mozilla Public License (MPLv2) * @license Mozilla Public License (MPLv2)
*/ */
import Storage from '@converse/skeletor/src/storage.js'; import Storage from '@converse/skeletor/src/storage.js';
import URI from 'urijs';
import _converse from '@converse/headless/shared/_converse'; import _converse from '@converse/headless/shared/_converse';
import advancedFormat from 'dayjs/plugin/advancedFormat'; import advancedFormat from 'dayjs/plugin/advancedFormat';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
@ -48,6 +49,7 @@ Strophe.addNamespace('CARBONS', 'urn:xmpp:carbons:2');
Strophe.addNamespace('CHATSTATES', 'http://jabber.org/protocol/chatstates'); Strophe.addNamespace('CHATSTATES', 'http://jabber.org/protocol/chatstates');
Strophe.addNamespace('CSI', 'urn:xmpp:csi:0'); Strophe.addNamespace('CSI', 'urn:xmpp:csi:0');
Strophe.addNamespace('DELAY', 'urn:xmpp:delay'); Strophe.addNamespace('DELAY', 'urn:xmpp:delay');
Strophe.addNamespace('EME', 'urn:xmpp:eme:0');
Strophe.addNamespace('FASTEN', 'urn:xmpp:fasten:0'); Strophe.addNamespace('FASTEN', 'urn:xmpp:fasten:0');
Strophe.addNamespace('FORWARD', 'urn:xmpp:forward:0'); Strophe.addNamespace('FORWARD', 'urn:xmpp:forward:0');
Strophe.addNamespace('HINTS', 'urn:xmpp:hints'); Strophe.addNamespace('HINTS', 'urn:xmpp:hints');
@ -928,7 +930,7 @@ _converse.shouldClearCache = () => (
); );
export function clearSession () { export function clearSession () {
_converse.session?.destroy(); _converse.session?.destroy();
delete _converse.session; delete _converse.session;
_converse.shouldClearCache() && _converse.api.user.settings.clear(); _converse.shouldClearCache() && _converse.api.user.settings.clear();
@ -1415,6 +1417,7 @@ Object.assign(converse, {
Model, Model,
Promise, Promise,
Strophe, Strophe,
URI,
dayjs, dayjs,
html, html,
log, log,

View File

@ -1,77 +1,178 @@
{ {
"name": "@converse/headless", "name": "@converse/headless",
"version": "6.0.0", "version": "8.0.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
"@converse/openpromise": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/@converse/openpromise/-/openpromise-0.0.1.tgz",
"integrity": "sha512-oA1TKrm6H838isYZJxMWXpXyOUezkD49eMJ6bkI+FfL2MsVuOV3ZbhBV+c07mLSknKXO7pUbWTVa5f7bXJXYjQ=="
},
"@converse/skeletor": {
"version": "github:conversejs/skeletor#f354bc530493a17d031f6f9c524cc34e073908e3",
"from": "github:conversejs/skeletor#f354bc530493a17d031f6f9c524cc34e073908e3",
"requires": {
"lit-html": "^2.0.0-rc.2",
"lodash-es": "^4.17.21",
"mergebounce": "0.0.2"
}
},
"@types/trusted-types": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-1.0.6.tgz",
"integrity": "sha512-230RC8sFeHoT6sSUlRO6a8cAnclO06eeiq1QDfiv2FGCLWFvvERWgwIQD4FWqD9A69BN7Lzee4OXwoMVnnsWDw=="
},
"abab": { "abab": {
"version": "2.0.5", "version": "2.0.5",
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz",
"integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==", "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q=="
"dev": true },
"babel-runtime": {
"version": "6.26.0",
"resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
"integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=",
"requires": {
"core-js": "^2.4.0",
"regenerator-runtime": "^0.11.0"
}
},
"core-js": {
"version": "2.6.12",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz",
"integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ=="
},
"dayjs": {
"version": "1.10.5",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.5.tgz",
"integrity": "sha512-BUFis41ikLz+65iH6LHQCDm4YPMj5r1YFLdupPIyM4SGcXMmtiLQ7U37i+hGS8urIuqe7I/ou3IS1jVc4nbN4g=="
}, },
"filesize": { "filesize": {
"version": "6.1.0", "version": "6.3.0",
"resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz", "resolved": "https://registry.npmjs.org/filesize/-/filesize-6.3.0.tgz",
"integrity": "sha512-LpCHtPQ3sFx67z+uh2HnSyWSLLu5Jxo21795uRDuar/EOuYWXib5EmPaGIBuSnRqH2IODiKA2k5re/K9OnN/Yg==", "integrity": "sha512-ytx0ruGpDHKWVoiui6+BY/QMNngtDQ/pJaFwfBpQif0J63+E8DLdFyqS3NkKQn7vIruUEpoGD9JUJSg7Kp+I0g=="
"dev": true
}, },
"immediate": { "immediate": {
"version": "3.0.6", "version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=", "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps="
"dev": true
}, },
"lie": { "lie": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
"integrity": "sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=", "integrity": "sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=",
"dev": true,
"requires": { "requires": {
"immediate": "~3.0.5" "immediate": "~3.0.5"
} }
}, },
"lit-html": {
"version": "2.0.0-rc.3",
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.0.0-rc.3.tgz",
"integrity": "sha512-Y6P8LlAyQuqvzq6l/Nc4z5/P5M/rVLYKQIRxcNwSuGajK0g4kbcBFQqZmgvqKG+ak+dHZjfm2HUw9TF5N/pkCw==",
"requires": {
"@types/trusted-types": "^1.0.1"
}
},
"localforage": { "localforage": {
"version": "1.7.3", "version": "1.9.0",
"resolved": "https://registry.npmjs.org/localforage/-/localforage-1.7.3.tgz", "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.9.0.tgz",
"integrity": "sha512-1TulyYfc4udS7ECSBT2vwJksWbkwwTX8BzeUIiq8Y07Riy7bDAAnxDaPU/tWyOVmQAcWJIEIFP9lPfBGqVoPgQ==", "integrity": "sha512-rR1oyNrKulpe+VM9cYmcFn6tsHuokyVHFaCM3+osEmxaHTbEk8oQu6eGDfS6DQLWi/N67XRmB8ECG37OES368g==",
"dev": true,
"requires": { "requires": {
"lie": "3.1.1" "lie": "3.1.1"
} }
}, },
"lodash": { "localforage-driver-commons": {
"version": "4.17.20", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "resolved": "https://registry.npmjs.org/localforage-driver-commons/-/localforage-driver-commons-1.0.3.tgz",
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", "integrity": "sha512-K9PiNNXcyX98lQVyCADjv+QKxFD71y0DtVUhqMjwCkFY/d/g7GdJLPN9U92M7RUvfkL8mzPhC+mWEKo9tur5oQ=="
"dev": true },
"localforage-driver-memory": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/localforage-driver-memory/-/localforage-driver-memory-1.0.5.tgz",
"integrity": "sha512-m4v478ixdT3hA7gKv+pAxDIWgMKiUV2GuYem5jnpOBQFVJbrHU7jmNlrj8a0MfD9qff3i48E3Yfip5Eu1AN6Qg==",
"requires": {
"localforage-driver-commons": "^1.0.1",
"tslib": "^1.6.0"
}
},
"localforage-setitems": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/localforage-setitems/-/localforage-setitems-1.4.0.tgz",
"integrity": "sha1-NrhZDVB9+1yAQDPih+zljYiZbV8=",
"requires": {
"localforage": ">=1.4.0"
}
},
"localforage-webextensionstorage-driver": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/localforage-webextensionstorage-driver/-/localforage-webextensionstorage-driver-2.0.0.tgz",
"integrity": "sha512-gB9q+NOn3D62x8Akn7nykh2H0ArNehYflZ3sgGZNc8eB6Yf0HnK30vwpe0xXTLYMIe15XeRNiiZd8qwTFnGYSw==",
"requires": {
"babel-runtime": "^6.22.0"
}
}, },
"lodash-es": { "lodash-es": {
"version": "4.17.15", "version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.15.tgz", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ==", "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
"dev": true },
"mergebounce": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/mergebounce/-/mergebounce-0.0.2.tgz",
"integrity": "sha512-1nxx6ljFJkx26WlwQLzbaBQc6lDg7mqdHPhIDixpOW+7Idx6DdPBrUZCwinihWbw33B1/YhZbdLU7dAf1vyC6w==",
"requires": {
"@converse/openpromise": "0.0.1",
"lodash-es": "^4.17.21"
}
}, },
"pluggable.js": { "pluggable.js": {
"version": "2.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/pluggable.js/-/pluggable.js-2.0.1.tgz", "resolved": "https://registry.npmjs.org/pluggable.js/-/pluggable.js-3.0.1.tgz",
"integrity": "sha512-SBt6v6Tbp20Jf8hU0cpcc/+HBHGMY8/Q+yA6Ih0tBQE8tfdZ6U4PRG0iNvUUjLx/hVyOP53n0UfGBymlfaaXCg==", "integrity": "sha512-DQC51A6aKLk6anvyvQfukNcVzGHOI5B04DerHioqLSF7ptI+Nla2hHzG4PGxq8tKqOGwQHnXnj9qxcFM3VViEQ==",
"dev": true,
"requires": { "requires": {
"lodash": "^4.17.11" "lodash-es": "^4.17.21"
} }
}, },
"regenerator-runtime": {
"version": "0.11.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz",
"integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg=="
},
"sizzle": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/sizzle/-/sizzle-2.3.6.tgz",
"integrity": "sha512-abtd95IkbcMAaYk1Lux4k9Xz6wnQqyLy2aco9HGJ8jVaCDEcc+ug0hW8RdV6aIre3ycWXxPdcX0u7QL/1UaSoA=="
},
"sprintf-js": { "sprintf-js": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz",
"integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug=="
"dev": true },
"strophe.js": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/strophe.js/-/strophe.js-1.4.2.tgz",
"integrity": "sha512-jkyZQCZLm7Zgmra0zJKxpHPNIUncYj/e/eYfgxFoc5gwrWeHWigNBs0q7wtqhCiqG6Qxcf22PUpcyBq8cK+9ew==",
"requires": {
"abab": "^2.0.3",
"ws": "^7.0.0",
"xmldom": "0.5.0"
}
},
"tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
}, },
"ws": { "ws": {
"version": "7.3.1", "version": "7.3.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz",
"integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==", "integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==",
"dev": true, "optional": true
},
"xmldom": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.5.0.tgz",
"integrity": "sha512-Foaj5FXVzgn7xFzsKeNIde9g6aFBxTPi37iwsno8QvApmtg7KYrr+OPyRHcJF7dud2a5nGRBXK3n0dL62Gf7PA==",
"optional": true "optional": true
} }
} }

View File

@ -37,9 +37,9 @@
"gitHead": "9641dcdc820e029b05930479c242d2b707bbe8e2", "gitHead": "9641dcdc820e029b05930479c242d2b707bbe8e2",
"devDependencies": {}, "devDependencies": {},
"dependencies": { "dependencies": {
"@converse/skeletor": "conversejs/skeletor#f354bc530493a17d031f6f9c524cc34e073908e3", "@converse/skeletor": "0.0.5",
"dayjs": "1.10.4", "dayjs": "1.10.5",
"filesize": "^6.1.0", "filesize": "^6.3.0",
"localforage": "^1.9.0", "localforage": "^1.9.0",
"localforage-driver-memory": "^1.0.5", "localforage-driver-memory": "^1.0.5",
"localforage-setitems": "^1.4.0", "localforage-setitems": "^1.4.0",
@ -48,6 +48,7 @@
"pluggable.js": "3.0.1", "pluggable.js": "3.0.1",
"sizzle": "^2.3.5", "sizzle": "^2.3.5",
"sprintf-js": "^1.1.2", "sprintf-js": "^1.1.2",
"strophe.js": "1.4.2" "strophe.js": "1.4.2",
"urijs": "^1.19.6"
} }
} }

View File

@ -9,7 +9,7 @@ describe("A sent presence stanza", function () {
afterEach(() => (jasmine.DEFAULT_TIMEOUT_INTERVAL = original_timeout)); afterEach(() => (jasmine.DEFAULT_TIMEOUT_INTERVAL = original_timeout));
it("includes a entity capabilities node", it("includes a entity capabilities node",
mock.initConverse([], {}, async (done, _converse) => { mock.initConverse([], {}, async (_converse) => {
await mock.waitForRoster(_converse, 'current', 0); await mock.waitForRoster(_converse, 'current', 0);
_converse.api.disco.own.identities.clear(); _converse.api.disco.own.identities.clear();
@ -27,10 +27,9 @@ describe("A sent presence stanza", function () {
`<priority>0</priority>`+ `<priority>0</priority>`+
`<c hash="sha-1" node="https://conversejs.org" ver="QgayPKawpkPSDYmwT/WM94uAlu0=" xmlns="http://jabber.org/protocol/caps"/>`+ `<c hash="sha-1" node="https://conversejs.org" ver="QgayPKawpkPSDYmwT/WM94uAlu0=" xmlns="http://jabber.org/protocol/caps"/>`+
`</presence>`) `</presence>`)
done();
})); }));
it("has a given priority", mock.initConverse(['statusInitialized'], {}, async (done, _converse) => { it("has a given priority", mock.initConverse(['statusInitialized'], {}, async (_converse) => {
const { api } = _converse; const { api } = _converse;
let pres = await _converse.xmppstatus.constructPresence('online', null, 'Hello world'); let pres = await _converse.xmppstatus.constructPresence('online', null, 'Hello world');
expect(pres.toLocaleString()).toBe( expect(pres.toLocaleString()).toBe(
@ -62,6 +61,5 @@ describe("A sent presence stanza", function () {
`<c hash="sha-1" node="https://conversejs.org" ver="PxXfr6uz8ClMWIga0OB/MhKNH/M=" xmlns="http://jabber.org/protocol/caps"/>`+ `<c hash="sha-1" node="https://conversejs.org" ver="PxXfr6uz8ClMWIga0OB/MhKNH/M=" xmlns="http://jabber.org/protocol/caps"/>`+
`</presence>` `</presence>`
); );
done();
})); }));
}); });

View File

@ -224,15 +224,23 @@ const MessageMixin = {
uploadFile () { uploadFile () {
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => { xhr.onreadystatechange = async () => {
if (xhr.readyState === XMLHttpRequest.DONE) { if (xhr.readyState === XMLHttpRequest.DONE) {
log.info('Status: ' + xhr.status); log.info('Status: ' + xhr.status);
if (xhr.status === 200 || xhr.status === 201) { if (xhr.status === 200 || xhr.status === 201) {
this.save({ let attrs = {
'upload': _converse.SUCCESS, 'upload': _converse.SUCCESS,
'oob_url': this.get('get'), 'oob_url': this.get('get'),
'message': this.get('get') 'message': this.get('get'),
}); 'body': this.get('get'),
};
/**
* *Hook* which allows plugins to change the attributes
* saved on the message once a file has been uploaded.
* @event _converse#afterFileUploaded
*/
attrs = await api.hook('afterFileUploaded', this, attrs);
this.save(attrs);
} else { } else {
xhr.onerror(); xhr.onerror();
} }

View File

@ -9,6 +9,7 @@ import { _converse, api, converse } from "../../core.js";
import { getOpenPromise } from '@converse/openpromise'; import { getOpenPromise } from '@converse/openpromise';
import { initStorage } from '@converse/headless/shared/utils.js'; import { initStorage } from '@converse/headless/shared/utils.js';
import { debouncedPruneHistory, pruneHistory } from '@converse/headless/shared/chat/utils.js'; import { debouncedPruneHistory, pruneHistory } from '@converse/headless/shared/chat/utils.js';
import { getMediaURLs } from '@converse/headless/shared/parsers';
import { parseMessage } from './parsers.js'; import { parseMessage } from './parsers.js';
import { sendMarker } from '@converse/headless/shared/actions'; import { sendMarker } from '@converse/headless/shared/actions';
@ -56,8 +57,8 @@ const ChatBox = ModelWithContact.extend({
} }
this.set({'box_id': `box-${jid}`}); this.set({'box_id': `box-${jid}`});
this.initNotifications(); this.initNotifications();
this.initMessages();
this.initUI(); this.initUI();
this.initMessages();
if (this.get('type') === _converse.PRIVATE_CHAT_TYPE) { if (this.get('type') === _converse.PRIVATE_CHAT_TYPE) {
this.presence = _converse.presences.findWhere({'jid': jid}) || _converse.presences.create({'jid': jid}); this.presence = _converse.presences.findWhere({'jid': jid}) || _converse.presences.create({'jid': jid});
@ -241,9 +242,16 @@ const ChatBox = ModelWithContact.extend({
} }
}, },
onMessageUploadChanged (message) { async onMessageUploadChanged (message) {
if (message.get('upload') === _converse.SUCCESS) { if (message.get('upload') === _converse.SUCCESS) {
api.send(this.createMessageStanza(message)); const attrs = {
'body': message.get('message'),
'spoiler_hint': message.get('spoiler_hint'),
'oob_url': message.get('oob_url')
}
await this.sendMessage(attrs);
message.destroy();
} }
}, },
@ -338,9 +346,10 @@ const ChatBox = ModelWithContact.extend({
}, },
pruneHistoryWhenScrolledDown () { pruneHistoryWhenScrolledDown () {
if (!this.ui.get('scrolled') && if (
api.settings.get('prune_messages_above') && api.settings.get('prune_messages_above') &&
api.settings.get('pruning_behavior') === 'unscrolled' api.settings.get('pruning_behavior') === 'unscrolled' &&
!this.ui.get('scrolled')
) { ) {
pruneHistory(this); pruneHistory(this);
} }
@ -841,11 +850,12 @@ const ChatBox = ModelWithContact.extend({
return stanza; return stanza;
}, },
getOutgoingMessageAttributes (text, spoiler_hint) { getOutgoingMessageAttributes (attrs) {
const is_spoiler = this.get('composing_spoiler'); const is_spoiler = !!this.get('composing_spoiler');
const origin_id = u.getUniqueId(); const origin_id = u.getUniqueId();
const text = attrs?.body;
const body = text ? u.httpToGeoUri(u.shortnamesToUnicode(text), _converse) : undefined; const body = text ? u.httpToGeoUri(u.shortnamesToUnicode(text), _converse) : undefined;
return { return Object.assign({}, attrs, {
'from': _converse.bare_jid, 'from': _converse.bare_jid,
'fullname': _converse.xmppstatus.get('fullname'), 'fullname': _converse.xmppstatus.get('fullname'),
'id': origin_id, 'id': origin_id,
@ -855,13 +865,12 @@ const ChatBox = ModelWithContact.extend({
'msgid': origin_id, 'msgid': origin_id,
'nickname': this.get('nickname'), 'nickname': this.get('nickname'),
'sender': 'me', 'sender': 'me',
'spoiler_hint': is_spoiler ? spoiler_hint : undefined,
'time': (new Date()).toISOString(), 'time': (new Date()).toISOString(),
'type': this.get('message_type'), 'type': this.get('message_type'),
body, body,
is_spoiler, is_spoiler,
origin_id origin_id
} }, getMediaURLs(text));
}, },
/** /**
@ -910,15 +919,14 @@ const ChatBox = ModelWithContact.extend({
* @private * @private
* @method _converse.ChatBox#sendMessage * @method _converse.ChatBox#sendMessage
* @memberOf _converse.ChatBox * @memberOf _converse.ChatBox
* @param { String } text - The chat message text * @param { Object } [attrs] - A map of attributes to be saved on the message
* @param { String } spoiler_hint - An optional hint, if the message being sent is a spoiler
* @returns { _converse.Message } * @returns { _converse.Message }
* @example * @example
* const chat = api.chats.get('buddy1@example.com'); * const chat = api.chats.get('buddy1@example.org');
* chat.sendMessage('hello world'); * chat.sendMessage({'body': 'hello world'});
*/ */
async sendMessage (text, spoiler_hint) { async sendMessage (attrs) {
const attrs = this.getOutgoingMessageAttributes(text, spoiler_hint); attrs = this.getOutgoingMessageAttributes(attrs);
let message = this.messages.findWhere('correcting') let message = this.messages.findWhere('correcting')
if (message) { if (message) {
const older_versions = message.get('older_versions') || {}; const older_versions = message.get('older_versions') || {};
@ -1001,6 +1009,13 @@ const ChatBox = ModelWithContact.extend({
return; return;
} }
Array.from(files).forEach(async file => { Array.from(files).forEach(async file => {
/**
* *Hook* which allows plugins to transform files before they'll be
* uploaded. The main use-case is to encrypt the files.
* @event _converse#beforeFileUpload
*/
file = await api.hook('beforeFileUpload', this, file);
if (!window.isNaN(max_file_size) && window.parseInt(file.size) > max_file_size) { if (!window.isNaN(max_file_size) && window.parseInt(file.size) > max_file_size) {
return this.createMessage({ return this.createMessage({
'message': __('The size of your file, %1$s, exceeds the maximum allowed by your server, which is %2$s.', 'message': __('The size of your file, %1$s, exceeds the maximum allowed by your server, which is %2$s.',

View File

@ -11,6 +11,7 @@ import {
getCorrectionAttributes, getCorrectionAttributes,
getEncryptionAttributes, getEncryptionAttributes,
getErrorAttributes, getErrorAttributes,
getMediaURLs,
getOutOfBandAttributes, getOutOfBandAttributes,
getReceiptId, getReceiptId,
getReferences, getReferences,
@ -215,5 +216,10 @@ export async function parseMessage (stanza, _converse) {
* *Hook* which allows plugins to add additional parsing * *Hook* which allows plugins to add additional parsing
* @event _converse#parseMessage * @event _converse#parseMessage
*/ */
return api.hook('parseMessage', stanza, attrs); attrs = await api.hook('parseMessage', stanza, attrs);
// We call this after the hook, to allow plugins to decrypt encrypted
// messages, since we need to parse the message text to determine whether
// there are media urls.
return Object.assign(attrs, getMediaURLs(attrs.is_encrypted ? attrs.plaintext : attrs.body));
} }

View File

@ -4,7 +4,7 @@ describe("The \"chats\" API", function() {
it("has a method 'get' which returns the promise that resolves to a chat model", mock.initConverse( it("has a method 'get' which returns the promise that resolves to a chat model", mock.initConverse(
['rosterInitialized', 'chatBoxesInitialized'], {}, ['rosterInitialized', 'chatBoxesInitialized'], {},
async (done, _converse) => { async (_converse) => {
const u = converse.env.utils; const u = converse.env.utils;
@ -28,17 +28,16 @@ describe("The \"chats\" API", function() {
expect(chat.get('box_id')).toBe(`box-${jid}`); expect(chat.get('box_id')).toBe(`box-${jid}`);
// Test for multiple JIDs // Test for multiple JIDs
mock.openChatBoxFor(_converse, jid2); await mock.openChatBoxFor(_converse, jid2);
await u.waitUntil(() => _converse.chatboxes.length == 3); await u.waitUntil(() => _converse.chatboxes.length == 3);
const list = await _converse.api.chats.get([jid, jid2]); const list = await _converse.api.chats.get([jid, jid2]);
expect(Array.isArray(list)).toBeTruthy(); expect(Array.isArray(list)).toBeTruthy();
expect(list[0].get('box_id')).toBe(`box-${jid}`); expect(list[0].get('box_id')).toBe(`box-${jid}`);
expect(list[1].get('box_id')).toBe(`box-${jid2}`); expect(list[1].get('box_id')).toBe(`box-${jid2}`);
done();
})); }));
it("has a method 'open' which opens and returns a promise that resolves to a chat model", mock.initConverse( it("has a method 'open' which opens and returns a promise that resolves to a chat model", mock.initConverse(
['chatBoxesInitialized'], {}, async (done, _converse) => { ['chatBoxesInitialized'], {}, async (_converse) => {
await mock.openControlBox(_converse); await mock.openControlBox(_converse);
await mock.waitForRoster(_converse, 'current', 2); await mock.waitForRoster(_converse, 'current', 2);
@ -62,6 +61,5 @@ describe("The \"chats\" API", function() {
expect(Array.isArray(list)).toBeTruthy(); expect(Array.isArray(list)).toBeTruthy();
expect(list[0].get('box_id')).toBe(`box-${jid}`); expect(list[0].get('box_id')).toBe(`box-${jid}`);
expect(list[1].get('box_id')).toBe(`box-${jid2}`); expect(list[1].get('box_id')).toBe(`box-${jid2}`);
done();
})); }));
}); });

View File

@ -6,7 +6,13 @@
import DiscoEntities from './entities.js'; import DiscoEntities from './entities.js';
import DiscoEntity from './entity.js'; import DiscoEntity from './entity.js';
import { _converse, api, converse } from '@converse/headless/core.js'; import { _converse, api, converse } from '@converse/headless/core.js';
import { initializeDisco, initStreamFeatures, notifyStreamFeaturesAdded, populateStreamFeatures } from './utils.js'; import {
clearSession,
initStreamFeatures,
initializeDisco,
notifyStreamFeaturesAdded,
populateStreamFeatures
} from './utils.js';
import disco_api from './api.js'; import disco_api from './api.js';
const { Strophe } = converse.env; const { Strophe } = converse.env;
@ -46,15 +52,10 @@ converse.plugins.add('converse-disco', {
} }
}); });
api.listen.on('clearSession', () => { // All disco entities stored in sessionStorage and are refetched
if (_converse.shouldClearCache() && _converse.disco_entities) { // upon login or reconnection and then stored with new ids, so to
Array.from(_converse.disco_entities.models).forEach(e => e.features.clearStore()); // avoid sessionStorage filling up, we remove them.
Array.from(_converse.disco_entities.models).forEach(e => e.identities.clearStore()); api.listen.on('will-reconnect', clearSession);
Array.from(_converse.disco_entities.models).forEach(e => e.dataforms.clearStore()); api.listen.on('clearSession', clearSession);
Array.from(_converse.disco_entities.models).forEach(e => e.fields.clearStore());
_converse.disco_entities.clearStore();
delete _converse.disco_entities;
}
});
} }
}); });

View File

@ -7,7 +7,7 @@ describe("Service Discovery", function () {
it("stores the features it receives", it("stores the features it receives",
mock.initConverse( mock.initConverse(
['discoInitialized'], {}, ['discoInitialized'], {},
async function (done, _converse) { async function (_converse) {
const { u, $iq } = converse.env; const { u, $iq } = converse.env;
const IQ_stanzas = _converse.connection.IQ_stanzas; const IQ_stanzas = _converse.connection.IQ_stanzas;
@ -161,7 +161,6 @@ describe("Service Discovery", function () {
expect(entities.get(_converse.domain).items.pluck('jid').includes('words.shakespeare.lit')).toBeTruthy(); expect(entities.get(_converse.domain).items.pluck('jid').includes('words.shakespeare.lit')).toBeTruthy();
expect(entities.get(_converse.domain).identities.where({'category': 'conference'}).length).toBe(1); expect(entities.get(_converse.domain).identities.where({'category': 'conference'}).length).toBe(1);
expect(entities.get(_converse.domain).identities.where({'category': 'directory'}).length).toBe(1); expect(entities.get(_converse.domain).identities.where({'category': 'directory'}).length).toBe(1);
done();
})); }));
}); });
@ -169,15 +168,15 @@ describe("Service Discovery", function () {
it("emits the serviceDiscovered event", it("emits the serviceDiscovered event",
mock.initConverse( mock.initConverse(
['discoInitialized'], {}, ['discoInitialized'], {},
function (done, _converse) { function (_converse) {
const { Strophe } = converse.env; const { Strophe } = converse.env;
sinon.spy(_converse.api, "trigger"); spyOn(_converse.api, "trigger").and.callThrough();
_converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM}); _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM});
expect(_converse.api.trigger.called).toBe(true); expect(_converse.api.trigger).toHaveBeenCalled();
expect(_converse.api.trigger.args[0][0]).toBe('serviceDiscovered'); const last_call = _converse.api.trigger.calls.all().pop();
expect(_converse.api.trigger.args[0][1].get('var')).toBe(Strophe.NS.MAM); expect(last_call.args[0]).toBe('serviceDiscovered');
done(); expect(last_call.args[1].get('var')).toBe(Strophe.NS.MAM);
})); }));
}); });
}); });

View File

@ -112,7 +112,7 @@ export function populateStreamFeatures () {
// Strophe.js sets the <stream:features> element on the // Strophe.js sets the <stream:features> element on the
// Strophe.Connection instance (_converse.connection). // Strophe.Connection instance (_converse.connection).
// //
// Once this is done, we populate the _converse.stream_features collection // Once this is we populate the _converse.stream_features collection
// and trigger streamFeaturesAdded. // and trigger streamFeaturesAdded.
initStreamFeatures(); initStreamFeatures();
Array.from(_converse.connection.features.childNodes).forEach(feature => { Array.from(_converse.connection.features.childNodes).forEach(feature => {
@ -123,3 +123,12 @@ export function populateStreamFeatures () {
}); });
notifyStreamFeaturesAdded(); notifyStreamFeaturesAdded();
} }
export function clearSession () {
_converse.disco_entities?.forEach(e => e.features.clearStore());
_converse.disco_entities?.forEach(e => e.identities.clearStore());
_converse.disco_entities?.forEach(e => e.dataforms.clearStore());
_converse.disco_entities?.forEach(e => e.fields.clearStore());
_converse.disco_entities.clearStore();
delete _converse.disco_entities;
}

View File

@ -66,6 +66,7 @@ converse.plugins.add('converse-headlines', {
async initialize () { async initialize () {
this.set({'box_id': `box-${this.get('jid')}`}); this.set({'box_id': `box-${this.get('jid')}`});
this.initUI();
this.initMessages(); this.initMessages();
await this.fetchMessages(); await this.fetchMessages();
/** /**

View File

@ -13,7 +13,7 @@ import { _converse, api, converse } from '../../core.js';
import { computeAffiliationsDelta, setAffiliations, getAffiliationList } from './affiliations/utils.js'; import { computeAffiliationsDelta, setAffiliations, getAffiliationList } from './affiliations/utils.js';
import { getOpenPromise } from '@converse/openpromise'; import { getOpenPromise } from '@converse/openpromise';
import { initStorage } from '@converse/headless/shared/utils.js'; import { initStorage } from '@converse/headless/shared/utils.js';
import { isArchived } from '@converse/headless/shared/parsers'; import { isArchived, getMediaURLs } from '@converse/headless/shared/parsers';
import { parseMUCMessage, parseMUCPresence } from './parsers.js'; import { parseMUCMessage, parseMUCPresence } from './parsers.js';
import { sendMarker } from '@converse/headless/shared/actions'; import { sendMarker } from '@converse/headless/shared/actions';
@ -438,10 +438,14 @@ const ChatRoomMixin = {
}; };
if (attrs.msgid === message.get('retraction_id')) { if (attrs.msgid === message.get('retraction_id')) {
// The error message refers to a retraction // The error message refers to a retraction
new_attrs.retracted = undefined;
new_attrs.retraction_id = undefined; new_attrs.retraction_id = undefined;
new_attrs.retracted_id = undefined;
if (!attrs.error) { if (!attrs.error) {
if (attrs.error_condition === 'forbidden') { if (attrs.error_condition === 'forbidden') {
new_attrs.error = __("You're not allowed to retract your message."); new_attrs.error = __("You're not allowed to retract your message.");
} else if (attrs.error_condition === 'not-acceptable') { } else if (attrs.error_condition === 'not-acceptable') {
new_attrs.error = __( new_attrs.error = __(
"Your retraction was not delivered because you're not present in the groupchat." "Your retraction was not delivered because you're not present in the groupchat."
@ -665,8 +669,7 @@ const ChatRoomMixin = {
* @method _converse.ChatRoom#sendTimedMessage * @method _converse.ChatRoom#sendTimedMessage
* @param { _converse.Message|XMLElement } message * @param { _converse.Message|XMLElement } message
* @returns { Promise<XMLElement>|Promise<_converse.TimeoutError> } Returns a promise * @returns { Promise<XMLElement>|Promise<_converse.TimeoutError> } Returns a promise
* which resolves with the reflected message stanza or rejects * which resolves with the reflected message stanza or with an error stanza or {@link _converse.TimeoutError}.
* with an error stanza or with a {@link _converse.TimeoutError}.
*/ */
sendTimedMessage (el) { sendTimedMessage (el) {
if (typeof el.tree === 'function') { if (typeof el.tree === 'function') {
@ -681,23 +684,15 @@ const ChatRoomMixin = {
const promise = getOpenPromise(); const promise = getOpenPromise();
const timeoutHandler = _converse.connection.addTimedHandler(_converse.STANZA_TIMEOUT, () => { const timeoutHandler = _converse.connection.addTimedHandler(_converse.STANZA_TIMEOUT, () => {
_converse.connection.deleteHandler(handler); _converse.connection.deleteHandler(handler);
promise.reject(new _converse.TimeoutError('Timeout Error: No response from server')); const err = new _converse.TimeoutError('Timeout Error: No response from server');
promise.resolve(err);
return false; return false;
}); });
const handler = _converse.connection.addHandler( const handler = _converse.connection.addHandler(
stanza => { stanza => {
timeoutHandler && _converse.connection.deleteTimedHandler(timeoutHandler); timeoutHandler && _converse.connection.deleteTimedHandler(timeoutHandler);
if (stanza.getAttribute('type') === 'groupchat') { promise.resolve(stanza);
promise.resolve(stanza); }, null, 'message', ['error', 'groupchat'], id);
} else {
promise.reject(stanza);
}
},
null,
'message',
['error', 'groupchat'],
id
);
api.send(el); api.send(el);
return promise; return promise;
}, },
@ -735,17 +730,20 @@ const ChatRoomMixin = {
'retraction_id': stanza.nodeTree.getAttribute('id'), 'retraction_id': stanza.nodeTree.getAttribute('id'),
'editable': false 'editable': false
}); });
try { const result = await this.sendTimedMessage(stanza);
await this.sendTimedMessage(stanza);
} catch (e) { if (u.isErrorStanza(result)) {
log.error(result);
} else if (result instanceof _converse.TimeoutError) {
log.error(result);
message.save({ message.save({
editable, editable,
'error_type': 'timeout', 'error_type': 'timeout',
'error': __('A timeout happened while while trying to retract your message.'), 'error': __('A timeout happened while while trying to retract your message.'),
'retracted': undefined, 'retracted': undefined,
'retracted_id': undefined 'retracted_id': undefined,
'retraction_id': undefined
}); });
throw e;
} }
}, },
@ -961,12 +959,15 @@ const ChatRoomMixin = {
return [updated_message, updated_references]; return [updated_message, updated_references];
}, },
getOutgoingMessageAttributes (original_message, spoiler_hint) { getOutgoingMessageAttributes (attrs) {
const is_spoiler = this.get('composing_spoiler'); const is_spoiler = this.get('composing_spoiler');
const [text, references] = this.parseTextForReferences(original_message); let text = '', references;
if (attrs?.body) {
[text, references] = this.parseTextForReferences(attrs.body);
}
const origin_id = u.getUniqueId(); const origin_id = u.getUniqueId();
const body = text ? u.httpToGeoUri(u.shortnamesToUnicode(text), _converse) : undefined; const body = text ? u.httpToGeoUri(u.shortnamesToUnicode(text), _converse) : undefined;
return { return Object.assign({}, attrs, {
body, body,
is_spoiler, is_spoiler,
origin_id, origin_id,
@ -979,9 +980,8 @@ const ChatRoomMixin = {
'message': body, 'message': body,
'nick': this.get('nick'), 'nick': this.get('nick'),
'sender': 'me', 'sender': 'me',
'spoiler_hint': is_spoiler ? spoiler_hint : undefined,
'type': 'groupchat' 'type': 'groupchat'
}; }, getMediaURLs(text));
}, },
/** /**

View File

@ -6,6 +6,7 @@ import {
getCorrectionAttributes, getCorrectionAttributes,
getEncryptionAttributes, getEncryptionAttributes,
getErrorAttributes, getErrorAttributes,
getMediaURLs,
getOpenGraphMetadata, getOpenGraphMetadata,
getOutOfBandAttributes, getOutOfBandAttributes,
getReceiptId, getReceiptId,
@ -184,9 +185,10 @@ export async function parseMUCMessage (stanza, chatbox, _converse) {
getOpenGraphMetadata(stanza), getOpenGraphMetadata(stanza),
getRetractionAttributes(stanza, original_stanza), getRetractionAttributes(stanza, original_stanza),
getModerationAttributes(stanza), getModerationAttributes(stanza),
getEncryptionAttributes(stanza, _converse) getEncryptionAttributes(stanza, _converse),
); );
await api.emojis.initialize(); await api.emojis.initialize();
attrs = Object.assign( attrs = Object.assign(
{ {
@ -213,11 +215,17 @@ export async function parseMUCMessage (stanza, chatbox, _converse) {
} }
// We prefer to use one of the XEP-0359 unique and stable stanza IDs as the Model id, to avoid duplicates. // We prefer to use one of the XEP-0359 unique and stable stanza IDs as the Model id, to avoid duplicates.
attrs['id'] = attrs['origin_id'] || attrs[`stanza_id ${attrs.from_muc || attrs.from}`] || u.getUniqueId(); attrs['id'] = attrs['origin_id'] || attrs[`stanza_id ${attrs.from_muc || attrs.from}`] || u.getUniqueId();
/** /**
* *Hook* which allows plugins to add additional parsing * *Hook* which allows plugins to add additional parsing
* @event _converse#parseMUCMessage * @event _converse#parseMUCMessage
*/ */
return api.hook('parseMUCMessage', stanza, attrs); attrs = await api.hook('parseMUCMessage', stanza, attrs);
// We call this after the hook, to allow plugins to decrypt encrypted
// messages, since we need to parse the message text to determine whether
// there are media urls.
return Object.assign(attrs, getMediaURLs(attrs.is_encrypted ? attrs.plaintext : attrs.body));
} }
/** /**

View File

@ -6,7 +6,7 @@ const Strophe = converse.env.Strophe;
describe('The MUC Affiliations API', function () { describe('The MUC Affiliations API', function () {
it('can be used to set affiliations in MUCs without having to join them first', it('can be used to set affiliations in MUCs without having to join them first',
mock.initConverse([], {}, async function (done, _converse) { mock.initConverse([], {}, async function (_converse) {
const { api } = _converse; const { api } = _converse;
const user_jid = 'annoyingguy@montague.lit'; const user_jid = 'annoyingguy@montague.lit';
const muc_jid = 'lounge@montague.lit'; const muc_jid = 'lounge@montague.lit';
@ -36,7 +36,6 @@ describe('The MUC Affiliations API', function () {
`</query>` + `</query>` +
`</iq>`); `</iq>`);
done();
}) })
); );
}); });

View File

@ -8,26 +8,26 @@ describe("A Groupchat Message", function () {
mock.initConverse( mock.initConverse(
['chatBoxesFetched'], ['chatBoxesFetched'],
{'prune_messages_above': 3}, {'prune_messages_above': 3},
async function (done, _converse) { async function (_converse) {
const muc_jid = 'lounge@montague.lit'; const muc_jid = 'lounge@montague.lit';
const model = await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); const model = await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
expect(model.ui.get('scrolled')).toBeFalsy(); expect(model.ui.get('scrolled')).toBeFalsy();
model.sendMessage('1st message'); model.sendMessage({'body': '1st message'});
model.sendMessage('2nd message'); model.sendMessage({'body': '2nd message'});
model.sendMessage('3rd message'); model.sendMessage({'body': '3rd message'});
await u.waitUntil(() => model.messages.length === 3); await u.waitUntil(() => model.messages.length === 3);
// Make sure pruneHistory fires // Make sure pruneHistory fires
await new Promise(resolve => setTimeout(resolve, 550)); await new Promise(resolve => setTimeout(resolve, 550));
model.sendMessage('4th message'); model.sendMessage({'body': '4th message'});
await u.waitUntil(() => model.messages.length === 4); await u.waitUntil(() => model.messages.length === 4);
await u.waitUntil(() => model.messages.length === 3, 550); await u.waitUntil(() => model.messages.length === 3, 550);
model.ui.set('scrolled', true); model.ui.set('scrolled', true);
model.sendMessage('5th message'); model.sendMessage({'body': '5th message'});
model.sendMessage('6th message'); model.sendMessage({'body': '6th message'});
await u.waitUntil(() => model.messages.length === 5); await u.waitUntil(() => model.messages.length === 5);
// Wait long enough to be sure the debounced pruneHistory method didn't fire. // Wait long enough to be sure the debounced pruneHistory method didn't fire.
@ -47,6 +47,5 @@ describe("A Groupchat Message", function () {
_converse.connection._dataRecv(mock.createRequest(stanza)); _converse.connection._dataRecv(mock.createRequest(stanza));
await u.waitUntil(() => model.messages.length === 4); await u.waitUntil(() => model.messages.length === 4);
await u.waitUntil(() => model.messages.length === 3, 550); await u.waitUntil(() => model.messages.length === 3, 550);
done();
})); }));
}); });

View File

@ -8,7 +8,7 @@ describe("Chatrooms", function () {
it("allows you to automatically register your nickname when joining a room", it("allows you to automatically register your nickname when joining a room",
mock.initConverse(['chatBoxesFetched'], {'auto_register_muc_nickname': true}, mock.initConverse(['chatBoxesFetched'], {'auto_register_muc_nickname': true},
async function (done, _converse) { async function (_converse) {
const muc_jid = 'coven@chat.shakespeare.lit'; const muc_jid = 'coven@chat.shakespeare.lit';
const room = await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); const room = await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
@ -47,12 +47,11 @@ describe("Chatrooms", function () {
`</x>`+ `</x>`+
`</query>`+ `</query>`+
`</iq>`); `</iq>`);
done();
})); }));
it("allows you to automatically deregister your nickname when closing a room", it("allows you to automatically deregister your nickname when closing a room",
mock.initConverse(['chatBoxesFetched'], {'auto_register_muc_nickname': 'unregister'}, mock.initConverse(['chatBoxesFetched'], {'auto_register_muc_nickname': 'unregister'},
async function (done, _converse) { async function (_converse) {
const muc_jid = 'coven@chat.shakespeare.lit'; const muc_jid = 'coven@chat.shakespeare.lit';
const room = await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); const room = await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
@ -96,7 +95,6 @@ describe("Chatrooms", function () {
}).c('query', {'xmlns': 'jabber:iq:register'}); }).c('query', {'xmlns': 'jabber:iq:register'});
_converse.connection._dataRecv(mock.createRequest(result)); _converse.connection._dataRecv(mock.createRequest(result));
done();
})); }));
}); });
}); });

View File

@ -9,7 +9,7 @@ describe("XMPP Ping", function () {
describe("An IQ stanza", function () { describe("An IQ stanza", function () {
it("is returned when converse.js gets pinged", it("is returned when converse.js gets pinged",
mock.initConverse(['statusInitialized'], {}, (done, _converse) => { mock.initConverse(['statusInitialized'], {}, (_converse) => {
const ping = u.toStanza(` const ping = u.toStanza(`
<iq from="${_converse.domain}" <iq from="${_converse.domain}"
to="${_converse.jid}" id="s2c1" type="get"> to="${_converse.jid}" id="s2c1" type="get">
@ -19,17 +19,15 @@ describe("XMPP Ping", function () {
const sent_stanza = _converse.connection.IQ_stanzas.pop(); const sent_stanza = _converse.connection.IQ_stanzas.pop();
expect(Strophe.serialize(sent_stanza)).toBe( expect(Strophe.serialize(sent_stanza)).toBe(
`<iq id="s2c1" to="${_converse.domain}" type="result" xmlns="jabber:client"/>`); `<iq id="s2c1" to="${_converse.domain}" type="result" xmlns="jabber:client"/>`);
done();
})); }));
it("is sent out when converse.js pings a server", mock.initConverse((done, _converse) => { it("is sent out when converse.js pings a server", mock.initConverse((_converse) => {
_converse.api.ping(); _converse.api.ping();
const sent_stanza = _converse.connection.IQ_stanzas.pop(); const sent_stanza = _converse.connection.IQ_stanzas.pop();
expect(Strophe.serialize(sent_stanza)).toBe( expect(Strophe.serialize(sent_stanza)).toBe(
`<iq id="${sent_stanza.getAttribute('id')}" to="montague.lit" type="get" xmlns="jabber:client">`+ `<iq id="${sent_stanza.getAttribute('id')}" to="montague.lit" type="get" xmlns="jabber:client">`+
`<ping xmlns="urn:xmpp:ping"/>`+ `<ping xmlns="urn:xmpp:ping"/>`+
`</iq>`); `</iq>`);
done();
})); }));
}); });
}); });

View File

@ -1,6 +1,7 @@
import { Model } from '@converse/skeletor/src/model.js'; import { Model } from '@converse/skeletor/src/model.js';
import { _converse, api, converse } from "@converse/headless/core"; import { _converse, api, converse } from "@converse/headless/core";
import { getOpenPromise } from '@converse/openpromise'; import { getOpenPromise } from '@converse/openpromise';
import { rejectPresenceSubscription } from './utils.js';
const { Strophe, $iq, $pres } = converse.env; const { Strophe, $iq, $pres } = converse.env;
@ -11,6 +12,7 @@ const { Strophe, $iq, $pres } = converse.env;
const RosterContact = Model.extend({ const RosterContact = Model.extend({
defaults: { defaults: {
'chat_state': undefined, 'chat_state': undefined,
'groups': [],
'image': _converse.DEFAULT_IMAGE, 'image': _converse.DEFAULT_IMAGE,
'image_type': _converse.DEFAULT_IMAGE_TYPE, 'image_type': _converse.DEFAULT_IMAGE_TYPE,
'num_unread': 0, 'num_unread': 0,
@ -24,7 +26,6 @@ const RosterContact = Model.extend({
const bare_jid = Strophe.getBareJidFromJid(jid).toLowerCase(); const bare_jid = Strophe.getBareJidFromJid(jid).toLowerCase();
attributes.jid = bare_jid; attributes.jid = bare_jid;
this.set(Object.assign({ this.set(Object.assign({
'groups': [],
'id': bare_jid, 'id': bare_jid,
'jid': bare_jid, 'jid': bare_jid,
'user_id': Strophe.getNodeFromJid(jid) 'user_id': Strophe.getNodeFromJid(jid)
@ -147,7 +148,7 @@ const RosterContact = Model.extend({
* @param { String } message - Optional message to send to the person being unauthorized * @param { String } message - Optional message to send to the person being unauthorized
*/ */
unauthorize (message) { unauthorize (message) {
_converse.rejectPresenceSubscription(this.get('jid'), message); rejectPresenceSubscription(this.get('jid'), message);
return this; return this;
}, },

View File

@ -6,9 +6,9 @@ import { Model } from "@converse/skeletor/src/model";
import { __ } from 'i18n'; import { __ } from 'i18n';
import { _converse, api, converse } from "@converse/headless/core"; import { _converse, api, converse } from "@converse/headless/core";
import { initStorage } from '@converse/headless/shared/utils.js'; import { initStorage } from '@converse/headless/shared/utils.js';
import { rejectPresenceSubscription } from './utils.js';
const { Strophe, $iq, sizzle } = converse.env; const { Strophe, $iq, sizzle, u } = converse.env;
const u = converse.env.utils;
const RosterContacts = Collection.extend({ const RosterContacts = Collection.extend({
@ -299,7 +299,7 @@ const RosterContacts = Collection.extend({
const contact = this.get(jid); const contact = this.get(jid);
const subscription = item.getAttribute("subscription"); const subscription = item.getAttribute("subscription");
const ask = item.getAttribute("ask"); const ask = item.getAttribute("ask");
const groups = Array.from(item.getElementsByTagName('group')).map(e => e.textContent); const groups = [...new Set(sizzle('group', item).map(e => e.textContent))];
if (!contact) { if (!contact) {
if ((subscription === "none" && ask === null) || (subscription === "remove")) { if ((subscription === "none" && ask === null) || (subscription === "remove")) {
return; // We're lazy when adding contacts. return; // We're lazy when adding contacts.
@ -355,7 +355,7 @@ const RosterContacts = Collection.extend({
contact = this.get(bare_jid); contact = this.get(bare_jid);
if (!api.settings.get('allow_contact_requests')) { if (!api.settings.get('allow_contact_requests')) {
_converse.rejectPresenceSubscription( rejectPresenceSubscription(
jid, jid,
__("This client does not allow presence subscriptions") __("This client does not allow presence subscriptions")
); );

View File

@ -2,206 +2,56 @@
* @copyright The Converse.js contributors * @copyright The Converse.js contributors
* @license Mozilla Public License (MPLv2) * @license Mozilla Public License (MPLv2)
*/ */
import "@converse/headless/plugins/status"; import '@converse/headless/plugins/status';
import RosterContact from './contact.js'; import RosterContact from './contact.js';
import RosterContacts from './contacts.js'; import RosterContacts from './contacts.js';
import invoke from 'lodash-es/invoke';
import log from "@converse/headless/log";
import roster_api from './api.js'; import roster_api from './api.js';
import { Presence, Presences } from './presence.js'; import { Presence, Presences } from './presence.js';
import { __ } from 'i18n'; import { __ } from 'i18n';
import { _converse, api, converse } from "@converse/headless/core"; import { _converse, api, converse } from '@converse/headless/core';
import { clearPresences, initRoster, updateUnreadCounter } from './utils.js'; import {
import { initStorage } from '@converse/headless/shared/utils.js'; onChatBoxesInitialized,
onClearSession,
const { $pres } = converse.env; onPresencesInitialized,
onRosterContactsFetched,
onStatusInitialized,
unregisterPresenceHandler,
} from './utils.js';
converse.plugins.add('converse-roster', { converse.plugins.add('converse-roster', {
dependencies: ['converse-status'], dependencies: ['converse-status'],
initialize () { initialize () {
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
api.settings.extend({ api.settings.extend({
'allow_contact_requests': true, 'allow_contact_requests': true,
'auto_subscribe': false, 'auto_subscribe': false,
'synchronize_availability': true, 'synchronize_availability': true
}); });
api.promises.add([ api.promises.add(['cachedRoster', 'roster', 'rosterContactsFetched', 'rosterInitialized']);
'cachedRoster',
'roster',
'rosterContactsFetched',
'rosterInitialized',
]);
// API methods only available to plugins // API methods only available to plugins
Object.assign(_converse.api, roster_api); Object.assign(_converse.api, roster_api);
_converse.HEADER_CURRENT_CONTACTS = __('My contacts'); _converse.HEADER_CURRENT_CONTACTS = __('My contacts');
_converse.HEADER_PENDING_CONTACTS = __('Pending contacts'); _converse.HEADER_PENDING_CONTACTS = __('Pending contacts');
_converse.HEADER_REQUESTING_CONTACTS = __('Contact requests'); _converse.HEADER_REQUESTING_CONTACTS = __('Contact requests');
_converse.HEADER_UNGROUPED = __('Ungrouped'); _converse.HEADER_UNGROUPED = __('Ungrouped');
_converse.HEADER_UNREAD = __('New messages'); _converse.HEADER_UNREAD = __('New messages');
_converse.registerPresenceHandler = function () {
_converse.unregisterPresenceHandler();
_converse.presence_ref = _converse.connection.addHandler(presence => {
_converse.roster.presenceHandler(presence);
return true;
}, null, 'presence', null);
};
/**
* Reject or cancel another user's subscription to our presence updates.
* @method rejectPresenceSubscription
* @private
* @memberOf _converse
* @param { String } jid - The Jabber ID of the user whose subscription is being canceled
* @param { String } message - An optional message to the user
*/
_converse.rejectPresenceSubscription = function (jid, message) {
const pres = $pres({to: jid, type: "unsubscribed"});
if (message && message !== "") { pres.c("status").t(message); }
api.send(pres);
};
_converse.sendInitialPresence = function () {
if (_converse.send_initial_presence) {
api.user.presence.send();
}
};
/**
* Fetch all the roster groups, and then the roster contacts.
* Emit an event after fetching is done in each case.
* @private
* @method _converse.populateRoster
* @param { Bool } ignore_cache - If set to to true, the local cache
* will be ignored it's guaranteed that the XMPP server
* will be queried for the roster.
*/
_converse.populateRoster = async function (ignore_cache=false) {
if (ignore_cache) {
_converse.send_initial_presence = true;
}
try {
await _converse.roster.fetchRosterContacts();
api.trigger('rosterContactsFetched');
} catch (reason) {
log.error(reason);
} finally {
_converse.sendInitialPresence();
}
};
_converse.Presence = Presence; _converse.Presence = Presence;
_converse.Presences = Presences; _converse.Presences = Presences;
_converse.RosterContact = RosterContact; _converse.RosterContact = RosterContact;
_converse.RosterContacts = RosterContacts; _converse.RosterContacts = RosterContacts;
_converse.unregisterPresenceHandler = function () { api.listen.on('beforeTearDown', () => unregisterPresenceHandler());
if (_converse.presence_ref !== undefined) { api.listen.on('chatBoxesInitialized', onChatBoxesInitialized);
_converse.connection.deleteHandler(_converse.presence_ref); api.listen.on('clearSession', onClearSession);
delete _converse.presence_ref; api.listen.on('presencesInitialized', onPresencesInitialized);
} api.listen.on('statusInitialized', onStatusInitialized);
};
/******************** Event Handlers ********************/
api.listen.on('chatBoxesInitialized', () => {
_converse.chatboxes.on('change:num_unread', updateUnreadCounter);
_converse.chatboxes.on('add', chatbox => {
if (chatbox.get('type') === _converse.PRIVATE_CHAT_TYPE) {
chatbox.setRosterContact(chatbox.get('jid'));
}
});
});
api.listen.on('beforeTearDown', () => _converse.unregisterPresenceHandler());
api.waitUntil('rosterContactsFetched').then(() => {
_converse.roster.on('add', (contact) => {
/* When a new contact is added, check if we already have a
* chatbox open for it, and if so attach it to the chatbox.
*/
const chatbox = _converse.chatboxes.findWhere({'jid': contact.get('jid')});
if (chatbox) {
chatbox.setRosterContact(contact.get('jid'));
}
});
});
api.listen.on('streamResumptionFailed', () => _converse.session.set('roster_cached', false)); api.listen.on('streamResumptionFailed', () => _converse.session.set('roster_cached', false));
api.listen.on('clearSession', async () => { api.waitUntil('rosterContactsFetched').then(onRosterContactsFetched);
await clearPresences();
if (_converse.shouldClearCache()) {
if (_converse.rostergroups) {
await _converse.rostergroups.clearStore();
delete _converse.rostergroups;
}
if (_converse.roster) {
invoke(_converse, 'roster.data.destroy');
await _converse.roster.clearStore();
delete _converse.roster;
}
}
});
api.listen.on('statusInitialized', async reconnecting => {
if (reconnecting) {
// When reconnecting and not resuming a previous session,
// we clear all cached presence data, since it might be stale
// and we'll receive new presence updates
!_converse.connection.hasResumed() && await clearPresences();
} else {
_converse.presences = new _converse.Presences();
const id = `converse.presences-${_converse.bare_jid}`;
initStorage(_converse.presences, id, 'session');
// We might be continuing an existing session, so we fetch
// cached presence data.
_converse.presences.fetch();
}
/**
* Triggered once the _converse.Presences collection has been
* initialized and its cached data fetched.
* Returns a boolean indicating whether this event has fired due to
* Converse having reconnected.
* @event _converse#presencesInitialized
* @type { bool }
* @example _converse.api.listen.on('presencesInitialized', reconnecting => { ... });
*/
api.trigger('presencesInitialized', reconnecting);
});
api.listen.on('presencesInitialized', async (reconnecting) => {
if (reconnecting) {
/**
* Similar to `rosterInitialized`, but instead pertaining to reconnection.
* This event indicates that the roster and its groups are now again
* available after Converse.js has reconnected.
* @event _converse#rosterReadyAfterReconnection
* @example _converse.api.listen.on('rosterReadyAfterReconnection', () => { ... });
*/
api.trigger('rosterReadyAfterReconnection');
} else {
await initRoster();
}
_converse.roster.onConnected();
_converse.registerPresenceHandler();
_converse.populateRoster(!_converse.connection.restored);
});
} }
}); });

View File

@ -5,7 +5,7 @@
describe("A received presence stanza", function () { describe("A received presence stanza", function () {
it("has its priority taken into account", it("has its priority taken into account",
mock.initConverse([], {}, async (done, _converse) => { mock.initConverse([], {}, async (_converse) => {
const u = converse.env.utils; const u = converse.env.utils;
mock.openControlBox(_converse); mock.openControlBox(_converse);
@ -177,6 +177,5 @@ describe("A received presence stanza", function () {
_converse.connection._dataRecv(mock.createRequest(stanza)); _converse.connection._dataRecv(mock.createRequest(stanza));
expect(_converse.roster.get(contact_jid).presence.get('show')).toBe('offline'); expect(_converse.roster.get(contact_jid).presence.get('show')).toBe('offline');
expect(contact.presence.resources.length).toBe(0); expect(contact.presence.resources.length).toBe(0);
done();
})); }));
}); });

View File

@ -1,9 +1,12 @@
import { _converse, api } from "@converse/headless/core"; import log from "@converse/headless/log";
import { Model } from '@converse/skeletor/src/model.js'; import { Model } from '@converse/skeletor/src/model.js';
import { _converse, api, converse } from "@converse/headless/core";
import { initStorage } from '@converse/headless/shared/utils.js'; import { initStorage } from '@converse/headless/shared/utils.js';
const { $pres } = converse.env;
export async function initRoster () {
async function initRoster () {
// Initialize the Bakcbone collections that represent the contats // Initialize the Bakcbone collections that represent the contats
// roster and the roster groups. // roster and the roster groups.
await api.waitUntil('VCardsInitialized'); await api.waitUntil('VCardsInitialized');
@ -28,19 +31,167 @@ export async function initRoster () {
} }
export function updateUnreadCounter (chatbox) { /**
const contact = _converse.roster && _converse.roster.findWhere({'jid': chatbox.get('jid')}); * Fetch all the roster groups, and then the roster contacts.
if (contact !== undefined) { * Emit an event after fetching is done in each case.
contact.save({'num_unread': chatbox.get('num_unread')}); * @private
* @param { Bool } ignore_cache - If set to to true, the local cache
* will be ignored it's guaranteed that the XMPP server
* will be queried for the roster.
*/
async function populateRoster (ignore_cache=false) {
if (ignore_cache) {
_converse.send_initial_presence = true;
}
try {
await _converse.roster.fetchRosterContacts();
api.trigger('rosterContactsFetched');
} catch (reason) {
log.error(reason);
} finally {
_converse.send_initial_presence && api.user.presence.send();
} }
} }
export async function clearPresences () { function updateUnreadCounter (chatbox) {
const contact = _converse.roster?.findWhere({'jid': chatbox.get('jid')});
contact?.save({'num_unread': chatbox.get('num_unread')});
}
function registerPresenceHandler () {
unregisterPresenceHandler();
_converse.presence_ref = _converse.connection.addHandler(presence => {
_converse.roster.presenceHandler(presence);
return true;
}, null, 'presence', null);
}
export function unregisterPresenceHandler () {
if (_converse.presence_ref !== undefined) {
_converse.connection.deleteHandler(_converse.presence_ref);
delete _converse.presence_ref;
}
}
async function clearPresences () {
await _converse.presences?.clearStore(); await _converse.presences?.clearStore();
} }
/**
* Roster specific event handler for the clearSession event
*/
export async function onClearSession () {
await clearPresences();
if (_converse.shouldClearCache()) {
if (_converse.rostergroups) {
await _converse.rostergroups.clearStore();
delete _converse.rostergroups;
}
if (_converse.roster) {
_converse.roster.data?.destroy();
await _converse.roster.clearStore();
delete _converse.roster;
}
}
}
/**
* Roster specific event handler for the presencesInitialized event
* @param { Boolean } reconnecting
*/
export async function onPresencesInitialized (reconnecting) {
if (reconnecting) {
/**
* Similar to `rosterInitialized`, but instead pertaining to reconnection.
* This event indicates that the roster and its groups are now again
* available after Converse.js has reconnected.
* @event _converse#rosterReadyAfterReconnection
* @example _converse.api.listen.on('rosterReadyAfterReconnection', () => { ... });
*/
api.trigger('rosterReadyAfterReconnection');
} else {
await initRoster();
}
_converse.roster.onConnected();
registerPresenceHandler();
populateRoster(!_converse.connection.restored);
}
/**
* Roster specific event handler for the statusInitialized event
* @param { Boolean } reconnecting
*/
export async function onStatusInitialized (reconnecting) {
if (reconnecting) {
// When reconnecting and not resuming a previous session,
// we clear all cached presence data, since it might be stale
// and we'll receive new presence updates
!_converse.connection.hasResumed() && (await clearPresences());
} else {
_converse.presences = new _converse.Presences();
const id = `converse.presences-${_converse.bare_jid}`;
initStorage(_converse.presences, id, 'session');
// We might be continuing an existing session, so we fetch
// cached presence data.
_converse.presences.fetch();
}
/**
* Triggered once the _converse.Presences collection has been
* initialized and its cached data fetched.
* Returns a boolean indicating whether this event has fired due to
* Converse having reconnected.
* @event _converse#presencesInitialized
* @type { bool }
* @example _converse.api.listen.on('presencesInitialized', reconnecting => { ... });
*/
api.trigger('presencesInitialized', reconnecting);
}
/**
* Roster specific event handler for the chatBoxesInitialized event
*/
export function onChatBoxesInitialized () {
_converse.chatboxes.on('change:num_unread', updateUnreadCounter);
_converse.chatboxes.on('add', chatbox => {
if (chatbox.get('type') === _converse.PRIVATE_CHAT_TYPE) {
chatbox.setRosterContact(chatbox.get('jid'));
}
});
}
/**
* Roster specific handler for the rosterContactsFetched promise
*/
export function onRosterContactsFetched () {
_converse.roster.on('add', contact => {
// When a new contact is added, check if we already have a
// chatbox open for it, and if so attach it to the chatbox.
const chatbox = _converse.chatboxes.findWhere({ 'jid': contact.get('jid') });
chatbox?.setRosterContact(contact.get('jid'));
});
}
/**
* 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
*/
export function rejectPresenceSubscription (jid, message) {
const pres = $pres({to: jid, type: "unsubscribed"});
if (message && message !== "") { pres.c("status").t(message); }
api.send(pres);
}
export function contactsComparator (contact1, contact2) { export function contactsComparator (contact1, contact2) {
const status1 = contact1.presence.get('show') || 'offline'; const status1 = contact1.presence.get('show') || 'offline';
const status2 = contact2.presence.get('show') || 'offline'; const status2 = contact2.presence.get('show') || 'offline';

View File

@ -16,7 +16,7 @@ describe("XEP-0198 Stream Management", function () {
'show_controlbox_by_default': true, 'show_controlbox_by_default': true,
'smacks_max_unacked_stanzas': 2 'smacks_max_unacked_stanzas': 2
}, },
async function (done, _converse) { async function (_converse) {
await _converse.api.user.login('romeo@montague.lit/orchard', 'secret'); await _converse.api.user.login('romeo@montague.lit/orchard', 'secret');
const sent_stanzas = _converse.connection.sent_stanzas; const sent_stanzas = _converse.connection.sent_stanzas;
@ -119,7 +119,6 @@ describe("XEP-0198 Stream Management", function () {
expect(Strophe.serialize(iq)).toBe(`<iq id="${iq.getAttribute('id')}" type="get" xmlns="jabber:client"><query xmlns="jabber:iq:roster"/></iq>`); expect(Strophe.serialize(iq)).toBe(`<iq id="${iq.getAttribute('id')}" type="get" xmlns="jabber:client"><query xmlns="jabber:iq:roster"/></iq>`);
expect(IQ_stanzas.filter(iq => sizzle('query[xmlns="jabber:iq:roster"]', iq).pop()).length).toBe(0); expect(IQ_stanzas.filter(iq => sizzle('query[xmlns="jabber:iq:roster"]', iq).pop()).length).toBe(0);
done();
})); }));
@ -131,7 +130,7 @@ describe("XEP-0198 Stream Management", function () {
'show_controlbox_by_default': true, 'show_controlbox_by_default': true,
'smacks_max_unacked_stanzas': 2 'smacks_max_unacked_stanzas': 2
}, },
async function (done, _converse) { async function (_converse) {
await _converse.api.user.login('romeo@montague.lit/orchard', 'secret'); await _converse.api.user.login('romeo@montague.lit/orchard', 'secret');
const sent_stanzas = _converse.connection.sent_stanzas; const sent_stanzas = _converse.connection.sent_stanzas;
@ -173,7 +172,6 @@ describe("XEP-0198 Stream Management", function () {
// Check that the roster gets fetched // Check that the roster gets fetched
await mock.waitForRoster(_converse, 'current', 1); await mock.waitForRoster(_converse, 'current', 1);
await new Promise(resolve => _converse.api.listen.once('reconnected', resolve)); await new Promise(resolve => _converse.api.listen.once('reconnected', resolve));
done();
})); }));
@ -187,7 +185,7 @@ describe("XEP-0198 Stream Management", function () {
'show_controlbox_by_default': true, 'show_controlbox_by_default': true,
'smacks_max_unacked_stanzas': 2 'smacks_max_unacked_stanzas': 2
}, },
async function (done, _converse) { async function (_converse) {
const key = "converse-test-session/converse.session-romeo@montague.lit-converse.session-romeo@montague.lit"; const key = "converse-test-session/converse.session-romeo@montague.lit-converse.session-romeo@montague.lit";
sessionStorage.setItem( sessionStorage.setItem(
@ -266,6 +264,5 @@ describe("XEP-0198 Stream Management", function () {
await u.waitUntil(() => muc.messages.length); await u.waitUntil(() => muc.messages.length);
expect(muc.messages.at(0).get('message')).toBe('First message') expect(muc.messages.at(0).get('message')).toBe('First message')
delete _converse.no_connection_on_bind; delete _converse.no_connection_on_bind;
done();
})); }));
}); });

View File

@ -5,7 +5,7 @@ const u = converse.env.utils;
describe("The XMPPStatus model", function () { describe("The XMPPStatus model", function () {
it("won't send <show>online</show> when setting a custom status message", it("won't send <show>online</show> when setting a custom status message",
mock.initConverse(async (done, _converse) => { mock.initConverse(async (_converse) => {
const sent_stanzas = _converse.connection.sent_stanzas; const sent_stanzas = _converse.connection.sent_stanzas;
await _converse.api.user.status.set('online'); await _converse.api.user.status.set('online');
@ -18,6 +18,5 @@ describe("The XMPPStatus model", function () {
expect(stanza.querySelectorAll('show').length).toBe(0); expect(stanza.querySelectorAll('show').length).toBe(0);
expect(stanza.querySelectorAll('priority').length).toBe(1); expect(stanza.querySelectorAll('priority').length).toBe(1);
expect(stanza.querySelector('priority').textContent).toBe('0'); expect(stanza.querySelector('priority').textContent).toBe('0');
done();
})); }));
}); });

View File

@ -1,9 +1,19 @@
import URI from 'urijs';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import log from '@converse/headless/log';
import sizzle from 'sizzle'; import sizzle from 'sizzle';
import { Strophe } from 'strophe.js/src/strophe'; import { Strophe } from 'strophe.js/src/strophe';
import { _converse, api } from '@converse/headless/core'; import { _converse, api } from '@converse/headless/core';
import { decodeHTMLEntities } from '@converse/headless/shared/utils'; import { decodeHTMLEntities } from '@converse/headless/shared/utils';
import { rejectMessage } from '@converse/headless/shared/actions'; import { rejectMessage } from '@converse/headless/shared/actions';
import {
isAudioDomainAllowed,
isAudioURL,
isImageDomainAllowed,
isImageURL,
isVideoDomainAllowed,
isVideoURL
} from '@converse/headless/utils/url.js';
const { NS } = Strophe; const { NS } = Strophe;
@ -48,13 +58,29 @@ export function getStanzaIDs (stanza, original_stanza) {
} }
export function getEncryptionAttributes (stanza, _converse) { export function getEncryptionAttributes (stanza, _converse) {
const eme_tag = sizzle(`encryption[xmlns="${Strophe.NS.EME}"]`, stanza).pop();
const namespace = eme_tag?.getAttribute('namespace');
const attrs = {};
if (namespace) {
attrs.is_encrypted = true;
attrs.encryption_namespace = namespace;
if (namespace !== Strophe.NS.OMEMO) {
// Found an encrypted message, but it's not OMEMO
return attrs;
}
}
const encrypted = sizzle(`encrypted[xmlns="${Strophe.NS.OMEMO}"]`, stanza).pop(); const encrypted = sizzle(`encrypted[xmlns="${Strophe.NS.OMEMO}"]`, stanza).pop();
const attrs = { 'is_encrypted': !!encrypted }; if (!eme_tag) {
attrs.is_encrypted = !!encrypted;
}
if (!encrypted || api.settings.get('clear_cache_on_logout')) { if (!encrypted || api.settings.get('clear_cache_on_logout')) {
return attrs; return attrs;
} }
const header = encrypted.querySelector('header'); const header = encrypted.querySelector('header');
attrs['encrypted'] = { 'device_id': header.getAttribute('sid') }; attrs.encrypted = { 'device_id': header.getAttribute('sid') };
const device_id = _converse.omemo_store?.get('device_id'); const device_id = _converse.omemo_store?.get('device_id');
const key = device_id && sizzle(`key[rid="${device_id}"]`, encrypted).pop(); const key = device_id && sizzle(`key[rid="${device_id}"]`, encrypted).pop();
@ -150,6 +176,34 @@ export function getOpenGraphMetadata (stanza) {
return {}; return {};
} }
export function getMediaURLs (text) {
const objs = [];
if (!text) {
return {};
}
const parse_options = { 'start': /\b(?:([a-z][a-z0-9.+-]*:\/\/)|xmpp:|mailto:|www\.)/gi };
try {
URI.withinString(
text,
(url, start, end) => {
objs.push({ url, start, end });
return url;
},
parse_options
);
} catch (error) {
log.debug(error);
}
const media_urls = objs.filter(o => {
return (isImageURL(o.url) && isImageDomainAllowed(o.url)) ||
(isVideoURL(o.url) && isVideoDomainAllowed(o.url)) ||
(isAudioURL(o.url) && isAudioDomainAllowed(o.url));
}).map(o => ({ 'start': o.start, 'end': o.end }));
return media_urls.length ? { media_urls } : {};
}
export function getSpoilerAttributes (stanza) { export function getSpoilerAttributes (stanza) {
const spoiler = sizzle(`spoiler[xmlns="${Strophe.NS.SPOILER}"]`, stanza).pop(); const spoiler = sizzle(`spoiler[xmlns="${Strophe.NS.SPOILER}"]`, stanza).pop();
return { return {

View File

@ -13,19 +13,23 @@ export function getDefaultStore () {
} }
} }
function storeUsesIndexedDB (store) {
return store === 'persistent' && api.settings.get('persistent_store') === 'IndexedDB';
}
export function createStore (id, store) { export function createStore (id, store) {
const name = store || getDefaultStore(); const name = store || getDefaultStore();
const s = _converse.storage[name]; const s = _converse.storage[name];
if (typeof s === 'undefined') { if (typeof s === 'undefined') {
throw new TypeError(`createStore: Could not find store for ${id}`); throw new TypeError(`createStore: Could not find store for ${id}`);
} }
return new Storage(id, s, api.settings.get('persistent_store') === 'IndexedDB'); return new Storage(id, s, storeUsesIndexedDB(store));
} }
export function initStorage (model, id, type) { export function initStorage (model, id, type) {
const store = type || getDefaultStore(); const store = type || getDefaultStore();
model.browserStorage = _converse.createStore(id, store); model.browserStorage = _converse.createStore(id, store);
if (store === 'persistent' && api.settings.get('persistent_store') === 'IndexedDB') { if (storeUsesIndexedDB(store)) {
const flush = () => model.browserStorage.flush(); const flush = () => model.browserStorage.flush();
window.addEventListener(_converse.unloadevent, flush); window.addEventListener(_converse.unloadevent, flush);
model.on('destroy', () => window.removeEventListener(_converse.unloadevent, flush)); model.on('destroy', () => window.removeEventListener(_converse.unloadevent, flush));

View File

@ -6,7 +6,7 @@ describe("Converse", function() {
describe("Authentication", function () { describe("Authentication", function () {
it("needs either a bosh_service_url a websocket_url or both", mock.initConverse(async (done, _converse) => { it("needs either a bosh_service_url a websocket_url or both", mock.initConverse(async (_converse) => {
const url = _converse.bosh_service_url; const url = _converse.bosh_service_url;
const connection = _converse.connection; const connection = _converse.connection;
_converse.api.settings.set('bosh_service_url', undefined); _converse.api.settings.set('bosh_service_url', undefined);
@ -17,7 +17,6 @@ describe("Converse", function() {
_converse.api.settings.set('bosh_service_url', url); _converse.api.settings.set('bosh_service_url', url);
_converse.connection = connection; _converse.connection = connection;
expect(e.message).toBe("initConnection: you must supply a value for either the bosh_service_url or websocket_url or both."); expect(e.message).toBe("initConnection: you must supply a value for either the bosh_service_url or websocket_url or both.");
done();
} }
})); }));
}); });
@ -25,7 +24,7 @@ describe("Converse", function() {
describe("A chat state indication", function () { describe("A chat state indication", function () {
it("are sent out when the client becomes or stops being idle", it("are sent out when the client becomes or stops being idle",
mock.initConverse(['discoInitialized'], {}, (done, _converse) => { mock.initConverse(['discoInitialized'], {}, (_converse) => {
spyOn(_converse, 'sendCSI').and.callThrough(); spyOn(_converse, 'sendCSI').and.callThrough();
let sent_stanza; let sent_stanza;
@ -47,14 +46,13 @@ describe("Converse", function() {
_converse.onUserActivity(); _converse.onUserActivity();
expect(_converse.sendCSI).toHaveBeenCalledWith('active'); expect(_converse.sendCSI).toHaveBeenCalledWith('active');
expect(Strophe.serialize(sent_stanza)).toBe('<active xmlns="urn:xmpp:csi:0"/>'); expect(Strophe.serialize(sent_stanza)).toBe('<active xmlns="urn:xmpp:csi:0"/>');
done();
})); }));
}); });
describe("Automatic status change", function () { describe("Automatic status change", function () {
it("happens when the client is idle for long enough", it("happens when the client is idle for long enough",
mock.initConverse(['initialized'], {}, async (done, _converse) => { mock.initConverse(['initialized'], {}, async (_converse) => {
let i = 0; let i = 0;
// Usually initialized by registerIntervalHandler // Usually initialized by registerIntervalHandler
_converse.idle_seconds = 0; _converse.idle_seconds = 0;
@ -120,7 +118,6 @@ describe("Converse", function() {
_converse.onUserActivity(); _converse.onUserActivity();
expect(await _converse.api.user.status.get()).toBe('dnd'); expect(await _converse.api.user.status.get()).toBe('dnd');
expect(_converse.auto_changed_status).toBe(false); expect(_converse.auto_changed_status).toBe(false);
done();
})); }));
}); });
@ -129,15 +126,14 @@ describe("Converse", function() {
describe("The \"status\" API", function () { describe("The \"status\" API", function () {
it("has a method for getting the user's availability", it("has a method for getting the user's availability",
mock.initConverse(['statusInitialized'], {}, async(done, _converse) => { mock.initConverse(['statusInitialized'], {}, async(_converse) => {
_converse.xmppstatus.set('status', 'online'); _converse.xmppstatus.set('status', 'online');
expect(await _converse.api.user.status.get()).toBe('online'); expect(await _converse.api.user.status.get()).toBe('online');
_converse.xmppstatus.set('status', 'dnd'); _converse.xmppstatus.set('status', 'dnd');
expect(await _converse.api.user.status.get()).toBe('dnd'); expect(await _converse.api.user.status.get()).toBe('dnd');
done();
})); }));
it("has a method for setting the user's availability", mock.initConverse(async (done, _converse) => { it("has a method for setting the user's availability", mock.initConverse(async (_converse) => {
await _converse.api.user.status.set('away'); await _converse.api.user.status.set('away');
expect(await _converse.xmppstatus.get('status')).toBe('away'); expect(await _converse.xmppstatus.get('status')).toBe('away');
await _converse.api.user.status.set('dnd'); await _converse.api.user.status.set('dnd');
@ -149,39 +145,35 @@ describe("Converse", function() {
const promise = _converse.api.user.status.set('invalid') const promise = _converse.api.user.status.set('invalid')
promise.catch(e => { promise.catch(e => {
expect(e.message).toBe('Invalid availability value. See https://xmpp.org/rfcs/rfc3921.html#rfc.section.2.2.2.1'); expect(e.message).toBe('Invalid availability value. See https://xmpp.org/rfcs/rfc3921.html#rfc.section.2.2.2.1');
done();
}); });
})); }));
it("allows setting the status message as well", mock.initConverse(async (done, _converse) => { it("allows setting the status message as well", mock.initConverse(async (_converse) => {
await _converse.api.user.status.set('away', "I'm in a meeting"); await _converse.api.user.status.set('away', "I'm in a meeting");
expect(_converse.xmppstatus.get('status')).toBe('away'); expect(_converse.xmppstatus.get('status')).toBe('away');
expect(_converse.xmppstatus.get('status_message')).toBe("I'm in a meeting"); expect(_converse.xmppstatus.get('status_message')).toBe("I'm in a meeting");
done();
})); }));
it("has a method for getting the user's status message", it("has a method for getting the user's status message",
mock.initConverse(['statusInitialized'], {}, async (done, _converse) => { mock.initConverse(['statusInitialized'], {}, async (_converse) => {
await _converse.xmppstatus.set('status_message', undefined); await _converse.xmppstatus.set('status_message', undefined);
expect(await _converse.api.user.status.message.get()).toBe(undefined); expect(await _converse.api.user.status.message.get()).toBe(undefined);
await _converse.xmppstatus.set('status_message', "I'm in a meeting"); await _converse.xmppstatus.set('status_message', "I'm in a meeting");
expect(await _converse.api.user.status.message.get()).toBe("I'm in a meeting"); expect(await _converse.api.user.status.message.get()).toBe("I'm in a meeting");
done();
})); }));
it("has a method for setting the user's status message", it("has a method for setting the user's status message",
mock.initConverse(['statusInitialized'], {}, async (done, _converse) => { mock.initConverse(['statusInitialized'], {}, async (_converse) => {
_converse.xmppstatus.set('status_message', undefined); _converse.xmppstatus.set('status_message', undefined);
await _converse.api.user.status.message.set("I'm in a meeting"); await _converse.api.user.status.message.set("I'm in a meeting");
expect(_converse.xmppstatus.get('status_message')).toBe("I'm in a meeting"); expect(_converse.xmppstatus.get('status_message')).toBe("I'm in a meeting");
done();
})); }));
}); });
}); });
describe("The \"tokens\" API", function () { describe("The \"tokens\" API", function () {
it("has a method for retrieving the next RID", mock.initConverse((done, _converse) => { it("has a method for retrieving the next RID", mock.initConverse((_converse) => {
mock.createContacts(_converse, 'current'); mock.createContacts(_converse, 'current');
const old_connection = _converse.connection; const old_connection = _converse.connection;
_converse.connection._proto.rid = '1234'; _converse.connection._proto.rid = '1234';
@ -190,10 +182,9 @@ describe("Converse", function() {
expect(_converse.api.tokens.get('rid')).toBe(null); expect(_converse.api.tokens.get('rid')).toBe(null);
// Restore the connection // Restore the connection
_converse.connection = old_connection; _converse.connection = old_connection;
done();
})); }));
it("has a method for retrieving the SID", mock.initConverse((done, _converse) => { it("has a method for retrieving the SID", mock.initConverse((_converse) => {
mock.createContacts(_converse, 'current'); mock.createContacts(_converse, 'current');
const old_connection = _converse.connection; const old_connection = _converse.connection;
_converse.connection._proto.sid = '1234'; _converse.connection._proto.sid = '1234';
@ -202,14 +193,13 @@ describe("Converse", function() {
expect(_converse.api.tokens.get('sid')).toBe(null); expect(_converse.api.tokens.get('sid')).toBe(null);
// Restore the connection // Restore the connection
_converse.connection = old_connection; _converse.connection = old_connection;
done();
})); }));
}); });
describe("The \"contacts\" API", function () { describe("The \"contacts\" API", function () {
it("has a method 'get' which returns wrapped contacts", it("has a method 'get' which returns wrapped contacts",
mock.initConverse([], {}, async function (done, _converse) { mock.initConverse([], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
let contact = await _converse.api.contacts.get('non-existing@jabber.org'); let contact = await _converse.api.contacts.get('non-existing@jabber.org');
@ -228,11 +218,10 @@ describe("Converse", function() {
// Check that all JIDs are returned if you call without any parameters // Check that all JIDs are returned if you call without any parameters
list = await _converse.api.contacts.get(); list = await _converse.api.contacts.get();
expect(list.length).toBe(mock.cur_names.length); expect(list.length).toBe(mock.cur_names.length);
done();
})); }));
it("has a method 'add' with which contacts can be added", it("has a method 'add' with which contacts can be added",
mock.initConverse(['rosterInitialized'], {}, async (done, _converse) => { mock.initConverse(['rosterInitialized'], {}, async (_converse) => {
await mock.waitForRoster(_converse, 'current', 0); await mock.waitForRoster(_converse, 'current', 0);
try { try {
@ -251,13 +240,12 @@ describe("Converse", function() {
spyOn(_converse.roster, 'addAndSubscribe'); spyOn(_converse.roster, 'addAndSubscribe');
await _converse.api.contacts.add("newcontact@example.org"); await _converse.api.contacts.add("newcontact@example.org");
expect(_converse.roster.addAndSubscribe).toHaveBeenCalled(); expect(_converse.roster.addAndSubscribe).toHaveBeenCalled();
done();
})); }));
}); });
describe("The \"settings\" API", function() { describe("The \"settings\" API", function() {
it("has methods 'get' and 'set' to set configuration settings", it("has methods 'get' and 'set' to set configuration settings",
mock.initConverse(null, {'play_sounds': true}, (done, _converse) => { mock.initConverse(null, {'play_sounds': true}, (_converse) => {
expect(Object.keys(_converse.api.settings)).toEqual(["extend", "update", "get", "set"]); expect(Object.keys(_converse.api.settings)).toEqual(["extend", "update", "get", "set"]);
expect(_converse.api.settings.get("play_sounds")).toBe(true); expect(_converse.api.settings.get("play_sounds")).toBe(true);
@ -269,11 +257,10 @@ describe("Converse", function() {
expect(typeof _converse.api.settings.get("non_existing")).toBe("undefined"); expect(typeof _converse.api.settings.get("non_existing")).toBe("undefined");
_converse.api.settings.set("non_existing", true); _converse.api.settings.set("non_existing", true);
expect(typeof _converse.api.settings.get("non_existing")).toBe("undefined"); expect(typeof _converse.api.settings.get("non_existing")).toBe("undefined");
done();
})); }));
it("extended via settings.extend don't override settings passed in via converse.initialize", it("extended via settings.extend don't override settings passed in via converse.initialize",
mock.initConverse([], {'emoji_categories': {"travel": ":rocket:"}}, (done, _converse) => { mock.initConverse([], {'emoji_categories': {"travel": ":rocket:"}}, (_converse) => {
expect(_converse.api.settings.get('emoji_categories')?.travel).toBe(':rocket:'); expect(_converse.api.settings.get('emoji_categories')?.travel).toBe(':rocket:');
@ -283,7 +270,6 @@ describe("Converse", function() {
expect(_converse.api.settings.get('emoji_categories')?.travel).toBe(':rocket:'); expect(_converse.api.settings.get('emoji_categories')?.travel).toBe(':rocket:');
expect(_converse.api.settings.get('emoji_categories')?.food).toBe(undefined); expect(_converse.api.settings.get('emoji_categories')?.food).toBe(undefined);
done();
})); }));
it("only overrides the passed in properties", it("only overrides the passed in properties",
@ -292,7 +278,7 @@ describe("Converse", function() {
'root': document.createElement('div').attachShadow({ 'mode': 'open' }), 'root': document.createElement('div').attachShadow({ 'mode': 'open' }),
'emoji_categories': { 'travel': ':rocket:' }, 'emoji_categories': { 'travel': ':rocket:' },
}, },
(done, _converse) => { (_converse) => {
expect(_converse.api.settings.get('emoji_categories')?.travel).toBe(':rocket:'); expect(_converse.api.settings.get('emoji_categories')?.travel).toBe(':rocket:');
// Test that the extend command doesn't override user-provided site // Test that the extend command doesn't override user-provided site
@ -303,7 +289,6 @@ describe("Converse", function() {
expect(_converse.api.settings.get('emoji_categories').travel).toBe(':rocket:'); expect(_converse.api.settings.get('emoji_categories').travel).toBe(':rocket:');
expect(_converse.api.settings.get('emoji_categories').food).toBe(undefined); expect(_converse.api.settings.get('emoji_categories').food).toBe(undefined);
done();
} }
) )
); );
@ -311,7 +296,7 @@ describe("Converse", function() {
}); });
describe("The \"plugins\" API", function() { describe("The \"plugins\" API", function() {
it("only has a method 'add' for registering plugins", mock.initConverse((done, _converse) => { it("only has a method 'add' for registering plugins", mock.initConverse((_converse) => {
expect(Object.keys(converse.plugins)).toEqual(["add"]); expect(Object.keys(converse.plugins)).toEqual(["add"]);
// Cheating a little bit. We clear the plugins to test more easily. // Cheating a little bit. We clear the plugins to test more easily.
const _old_plugins = _converse.pluggable.plugins; const _old_plugins = _converse.pluggable.plugins;
@ -321,17 +306,15 @@ describe("Converse", function() {
converse.plugins.add('plugin2', {}); converse.plugins.add('plugin2', {});
expect(Object.keys(_converse.pluggable.plugins)).toEqual(['plugin1', 'plugin2']); expect(Object.keys(_converse.pluggable.plugins)).toEqual(['plugin1', 'plugin2']);
_converse.pluggable.plugins = _old_plugins; _converse.pluggable.plugins = _old_plugins;
done();
})); }));
describe("The \"plugins.add\" method", function() { describe("The \"plugins.add\" method", function() {
it("throws an error when multiple plugins attempt to register with the same name", it("throws an error when multiple plugins attempt to register with the same name",
mock.initConverse((done, _converse) => { // eslint-disable-line no-unused-vars mock.initConverse((_converse) => { // eslint-disable-line no-unused-vars
converse.plugins.add('myplugin', {}); converse.plugins.add('myplugin', {});
const error = new TypeError('Error: plugin with name "myplugin" has already been registered!'); const error = new TypeError('Error: plugin with name "myplugin" has already been registered!');
expect(() => converse.plugins.add('myplugin', {})).toThrow(error); expect(() => converse.plugins.add('myplugin', {})).toThrow(error);
done();
})); }));
}); });
}); });

View File

@ -2,7 +2,7 @@
describe("The _converse Event Emitter", function() { describe("The _converse Event Emitter", function() {
it("allows you to subscribe to emitted events", mock.initConverse((done, _converse) => { it("allows you to subscribe to emitted events", mock.initConverse((_converse) => {
this.callback = function () {}; this.callback = function () {};
spyOn(this, 'callback'); spyOn(this, 'callback');
_converse.on('connected', this.callback); _converse.on('connected', this.callback);
@ -12,10 +12,9 @@ describe("The _converse Event Emitter", function() {
expect(this.callback.calls.count(), 2); expect(this.callback.calls.count(), 2);
_converse.api.trigger('connected'); _converse.api.trigger('connected');
expect(this.callback.calls.count(), 3); expect(this.callback.calls.count(), 3);
done();
})); }));
it("allows you to listen once for an emitted event", mock.initConverse((done, _converse) => { it("allows you to listen once for an emitted event", mock.initConverse((_converse) => {
this.callback = function () {}; this.callback = function () {};
spyOn(this, 'callback'); spyOn(this, 'callback');
_converse.once('connected', this.callback); _converse.once('connected', this.callback);
@ -25,10 +24,9 @@ describe("The _converse Event Emitter", function() {
expect(this.callback.calls.count(), 1); expect(this.callback.calls.count(), 1);
_converse.api.trigger('connected'); _converse.api.trigger('connected');
expect(this.callback.calls.count(), 1); expect(this.callback.calls.count(), 1);
done();
})); }));
it("allows you to stop listening or subscribing to an event", mock.initConverse((done, _converse) => { it("allows you to stop listening or subscribing to an event", mock.initConverse((_converse) => {
this.callback = function () {}; this.callback = function () {};
this.anotherCallback = function () {}; this.anotherCallback = function () {};
this.neverCalled = function () {}; this.neverCalled = function () {};
@ -56,6 +54,5 @@ describe("The _converse Event Emitter", function() {
expect(this.callback.calls.count(), 1); expect(this.callback.calls.count(), 1);
expect(this.anotherCallback.calls.count(), 3); expect(this.anotherCallback.calls.count(), 3);
expect(this.neverCalled).not.toHaveBeenCalled(); expect(this.neverCalled).not.toHaveBeenCalled();
done();
})); }));
}); });

View File

@ -3,10 +3,9 @@
describe("The persistent store", function() { describe("The persistent store", function() {
it("is unique to the user based on their JID", it("is unique to the user based on their JID",
mock.initConverse(['discoInitialized'], {'persistent_store': 'IndexedDB'}, (done, _converse) => { mock.initConverse([], {'persistent_store': 'IndexedDB'}, (_converse) => {
expect(_converse.storage.persistent.config().storeName).toBe(_converse.bare_jid); expect(_converse.storage.persistent.config().storeName).toBe(_converse.bare_jid);
expect(_converse.storage.persistent.config().description).toBe('indexedDB instance'); expect(_converse.storage.persistent.config().description).toBe('indexedDB instance');
done();
})); }));
}); });

View File

@ -0,0 +1,48 @@
import { converse } from '@converse/headless/core.js';
const { u } = converse.env;
export function appendArrayBuffer (buffer1, buffer2) {
const tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength);
tmp.set(new Uint8Array(buffer1), 0);
tmp.set(new Uint8Array(buffer2), buffer1.byteLength);
return tmp.buffer;
}
export function arrayBufferToHex (ab) {
// https://stackoverflow.com/questions/40031688/javascript-arraybuffer-to-hex#40031979
return Array.prototype.map.call(new Uint8Array(ab), x => ('00' + x.toString(16)).slice(-2)).join('');
}
export function arrayBufferToString (ab) {
return new TextDecoder("utf-8").decode(ab);
}
export function stringToArrayBuffer (string) {
const bytes = new TextEncoder("utf-8").encode(string);
return bytes.buffer;
}
export function arrayBufferToBase64 (ab) {
return btoa((new Uint8Array(ab)).reduce((data, byte) => data + String.fromCharCode(byte), ''));
}
export function base64ToArrayBuffer (b64) {
const binary_string = window.atob(b64),
len = binary_string.length,
bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binary_string.charCodeAt(i)
}
return bytes.buffer
}
export function hexToArrayBuffer (hex) {
const typedArray = new Uint8Array(hex.match(/[\da-f]{2}/gi).map(h => parseInt(h, 16)))
return typedArray.buffer
}
Object.assign(u, { arrayBufferToHex, arrayBufferToString, stringToArrayBuffer, arrayBufferToBase64, base64ToArrayBuffer });

View File

@ -439,42 +439,6 @@ u.formatFingerprint = function (fp) {
return fp; return fp;
}; };
u.appendArrayBuffer = function (buffer1, buffer2) {
const tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength);
tmp.set(new Uint8Array(buffer1), 0);
tmp.set(new Uint8Array(buffer2), buffer1.byteLength);
return tmp.buffer;
};
u.arrayBufferToHex = function (ab) {
// https://stackoverflow.com/questions/40031688/javascript-arraybuffer-to-hex#40031979
return Array.prototype.map.call(new Uint8Array(ab), x => ('00' + x.toString(16)).slice(-2)).join('');
};
u.arrayBufferToString = function (ab) {
return new TextDecoder("utf-8").decode(ab);
};
u.stringToArrayBuffer = function (string) {
const bytes = new TextEncoder("utf-8").encode(string);
return bytes.buffer;
};
u.arrayBufferToBase64 = function (ab) {
return btoa((new Uint8Array(ab)).reduce((data, byte) => data + String.fromCharCode(byte), ''));
};
u.base64ToArrayBuffer = function (b64) {
const binary_string = window.atob(b64),
len = binary_string.length,
bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binary_string.charCodeAt(i)
}
return bytes.buffer
};
u.getRandomInt = function (max) { u.getRandomInt = function (max) {
return Math.floor(Math.random() * Math.floor(max)); return Math.floor(Math.random() * Math.floor(max));
}; };

103
src/headless/utils/url.js Normal file
View File

@ -0,0 +1,103 @@
import URI from 'urijs';
import log from '@converse/headless/log';
import { api } from '@converse/headless/core';
function checkTLS (uri) {
return (
window.location.protocol === 'http:' ||
(window.location.protocol === 'https:' && uri.protocol().toLowerCase() === 'https')
);
}
export function getURI (url) {
try {
return url instanceof URI ? url : new URI(url);
} catch (error) {
log.debug(error);
return null;
}
}
function checkFileTypes (types, url) {
const uri = getURI(url);
if (uri === null || !checkTLS(uri)) {
return false;
}
const filename = uri.filename().toLowerCase();
return !!types.filter(ext => filename.endsWith(ext)).length;
}
function isDomainAllowed (whitelist, url) {
const uri = getURI(url);
const subdomain = uri.subdomain();
const domain = uri.domain();
const fulldomain = `${subdomain ? `${subdomain}.` : ''}${domain}`;
return whitelist.includes(domain) || whitelist.includes(fulldomain);
}
export function filterQueryParamsFromURL (url) {
const paramsArray = api.settings.get('filter_url_query_params');
if (!paramsArray) return url;
const parsed_uri = getURI(url);
return parsed_uri.removeQuery(paramsArray).toString();
}
export function isAudioDomainAllowed (url) {
const embed_audio = api.settings.get('embed_audio');
if (!Array.isArray(embed_audio)) {
return embed_audio;
}
try {
return isDomainAllowed(embed_audio, url);
} catch (error) {
log.debug(error);
return false;
}
}
export function isVideoDomainAllowed (url) {
const embed_videos = api.settings.get('embed_videos');
if (!Array.isArray(embed_videos)) {
return embed_videos;
}
try {
return isDomainAllowed(embed_videos, url);
} catch (error) {
log.debug(error);
return false;
}
}
export function isImageDomainAllowed (url) {
const show_images_inline = api.settings.get('show_images_inline');
if (!Array.isArray(show_images_inline)) {
return show_images_inline;
}
try {
return isDomainAllowed(show_images_inline, url);
} catch (error) {
log.debug(error);
return false;
}
}
export function isURLWithImageExtension (url) {
return checkFileTypes(['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.svg'], url);
}
export function isAudioURL (url) {
return checkFileTypes(['.ogg', '.mp3', '.m4a'], url);
}
export function isVideoURL (url) {
return checkFileTypes(['.mp4', '.webm'], url);
}
export function isImageURL (url) {
const regex = api.settings.get('image_urls_regex');
return regex?.test(url) || isURLWithImageExtension(url);
}
export function isEncryptedFileURL (url) {
return url.startsWith('aesgcm://');
}

File diff suppressed because it is too large Load Diff

View File

@ -7,8 +7,8 @@ msgstr ""
"Project-Id-Version: Converse.js 0.4\n" "Project-Id-Version: Converse.js 0.4\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-03-17 12:35+0100\n" "POT-Creation-Date: 2021-03-17 12:35+0100\n"
"PO-Revision-Date: 2021-01-20 09:32+0000\n" "PO-Revision-Date: 2021-05-23 22:31+0000\n"
"Last-Translator: Juanro49 <juanrobertogarciasanchez@gmail.com>\n" "Last-Translator: juliojulian <majuga@publicar.uy>\n"
"Language-Team: Spanish <https://hosted.weblate.org/projects/conversejs/" "Language-Team: Spanish <https://hosted.weblate.org/projects/conversejs/"
"translations/es/>\n" "translations/es/>\n"
"Language: es\n" "Language: es\n"
@ -16,7 +16,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n" "Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.5-dev\n" "X-Generator: Weblate 4.7-dev\n"
"plural_forms: nplurals=2; plural=(n != 1);\n" "plural_forms: nplurals=2; plural=(n != 1);\n"
"lang: es\n" "lang: es\n"
"Language-Code: es\n" "Language-Code: es\n"
@ -77,9 +77,8 @@ msgstr "%1$s se ha marchado"
#: dist/converse-no-dependencies.js:51675 #: dist/converse-no-dependencies.js:51675
#: dist/converse-no-dependencies.js:58125 #: dist/converse-no-dependencies.js:58125
#, fuzzy
msgid "You're not allowed to retract your message." msgid "You're not allowed to retract your message."
msgstr "Lo sentimos, no tienes permisos para retractarte de este mensaje." msgstr "No tienes permisos para editar este mensaje."
#: dist/converse-no-dependencies.js:51677 #: dist/converse-no-dependencies.js:51677
#: dist/converse-no-dependencies.js:58129 #: dist/converse-no-dependencies.js:58129

View File

@ -7,8 +7,8 @@ msgstr ""
"Project-Id-Version: Converse.js 0.4\n" "Project-Id-Version: Converse.js 0.4\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-03-17 12:35+0100\n" "POT-Creation-Date: 2021-03-17 12:35+0100\n"
"PO-Revision-Date: 2021-01-26 23:32+0000\n" "PO-Revision-Date: 2021-05-20 18:33+0000\n"
"Last-Translator: Vincent Finance <linuxmario@linuxmario.net>\n" "Last-Translator: lilim <lionel@les-miquelots.net>\n"
"Language-Team: French <https://hosted.weblate.org/projects/conversejs/" "Language-Team: French <https://hosted.weblate.org/projects/conversejs/"
"translations/fr/>\n" "translations/fr/>\n"
"Language: fr\n" "Language: fr\n"
@ -16,7 +16,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n > 1;\n" "Plural-Forms: nplurals=2; plural=n > 1;\n"
"X-Generator: Weblate 4.5-dev\n" "X-Generator: Weblate 4.7-dev\n"
"plural_forms: nplurals=2; plural=(n != 1);\n" "plural_forms: nplurals=2; plural=(n != 1);\n"
"lang: fr\n" "lang: fr\n"
"Language-Code: fr\n" "Language-Code: fr\n"
@ -762,7 +762,6 @@ msgstr ""
#: dist/converse-no-dependencies.js:76031 #: dist/converse-no-dependencies.js:76031
#: dist/converse-no-dependencies.js:76122 #: dist/converse-no-dependencies.js:76122
#, fuzzy
msgid "" msgid ""
"Be aware that other XMPP/Jabber clients (and servers) may not yet support " "Be aware that other XMPP/Jabber clients (and servers) may not yet support "
"retractions and that this message may not be removed everywhere." "retractions and that this message may not be removed everywhere."
@ -774,7 +773,6 @@ msgstr ""
#: dist/converse-no-dependencies.js:76032 #: dist/converse-no-dependencies.js:76032
#: dist/converse-no-dependencies.js:76129 #: dist/converse-no-dependencies.js:76129
#: dist/converse-no-dependencies.js:76166 #: dist/converse-no-dependencies.js:76166
#, fuzzy
msgid "Are you sure you want to retract this message?" msgid "Are you sure you want to retract this message?"
msgstr "Voulez-vous vraiment retirer ce message ?" msgstr "Voulez-vous vraiment retirer ce message ?"
@ -797,12 +795,10 @@ msgid "Sorry, you're not allowed to retract this message."
msgstr "Désolé, vous n'êtes pas autorisé a retirer ce message." msgstr "Désolé, vous n'êtes pas autorisé a retirer ce message."
#: dist/converse-no-dependencies.js:76190 #: dist/converse-no-dependencies.js:76190
#, fuzzy
msgid "You are about to retract this message." msgid "You are about to retract this message."
msgstr "Vous êtes sur le point de retirer ce message." msgstr "Vous êtes sur le point de retirer ce message."
#: dist/converse-no-dependencies.js:76190 #: dist/converse-no-dependencies.js:76190
#, fuzzy
msgid "" msgid ""
"You may optionally include a message, explaining the reason for the " "You may optionally include a message, explaining the reason for the "
"retraction." "retraction."
@ -811,7 +807,6 @@ msgstr ""
"du retrait." "du retrait."
#: dist/converse-no-dependencies.js:76197 #: dist/converse-no-dependencies.js:76197
#, fuzzy
msgid "Message Retraction" msgid "Message Retraction"
msgstr "Versions du message" msgstr "Versions du message"
@ -820,9 +815,8 @@ msgid "Optional reason"
msgstr "Raison facultative" msgstr "Raison facultative"
#: dist/converse-no-dependencies.js:76208 #: dist/converse-no-dependencies.js:76208
#, fuzzy
msgid "Sorry, you're not allowed to retract this message" msgid "Sorry, you're not allowed to retract this message"
msgstr "Désolé, vous n'êtes pas autorisé a retirer ce message." msgstr "Désolé, vous n'êtes pas autorisé a retirer ce message"
#: dist/converse-no-dependencies.js:76270 #: dist/converse-no-dependencies.js:76270
msgid "Cancel Editing" msgid "Cancel Editing"
@ -838,19 +832,19 @@ msgstr "Retirer"
#: dist/converse-no-dependencies.js:76316 #: dist/converse-no-dependencies.js:76316
msgid "Show URL previews" msgid "Show URL previews"
msgstr "" msgstr "Montrer les prévisualisations d'URL"
#: dist/converse-no-dependencies.js:76316 #: dist/converse-no-dependencies.js:76316
msgid "Hide URL previews" msgid "Hide URL previews"
msgstr "" msgstr "Cacher les prévisualisations d'URL"
#: dist/converse-no-dependencies.js:76318 #: dist/converse-no-dependencies.js:76318
msgid "Show URL preview" msgid "Show URL preview"
msgstr "" msgstr "Montrer la prévisualisation de l'URL"
#: dist/converse-no-dependencies.js:76318 #: dist/converse-no-dependencies.js:76318
msgid "Hide URL preview" msgid "Hide URL preview"
msgstr "" msgstr "Cacher la prévisualisation de l'URL"
#. harmony default export #. harmony default export
#: dist/converse-no-dependencies.js:76429 #: dist/converse-no-dependencies.js:76429
@ -1248,7 +1242,7 @@ msgstr ""
#: dist/converse-no-dependencies.js:94078 #: dist/converse-no-dependencies.js:94078
msgid "Loading configuration form" msgid "Loading configuration form"
msgstr "" msgstr "Chargement du formulaire de configuration"
#: dist/converse-no-dependencies.js:94373 #: dist/converse-no-dependencies.js:94373
msgid "Sorry, an error occurred while trying to submit the config form." msgid "Sorry, an error occurred while trying to submit the config form."
@ -1506,7 +1500,6 @@ msgid "Allow muted user to post messages"
msgstr "Autoriser les utilisateurs muets à poster des messages" msgstr "Autoriser les utilisateurs muets à poster des messages"
#: dist/converse-no-dependencies.js:96345 #: dist/converse-no-dependencies.js:96345
#, fuzzy
msgid "" msgid ""
"The conversation has moved to a new address. Click the link below to enter." "The conversation has moved to a new address. Click the link below to enter."
msgstr "" msgstr ""
@ -1518,9 +1511,9 @@ msgid "This groupchat no longer exists"
msgstr "Ce salon nexiste plus" msgstr "Ce salon nexiste plus"
#: dist/converse-no-dependencies.js:96355 #: dist/converse-no-dependencies.js:96355
#, fuzzy, javascript-format #, javascript-format
msgid "The following reason was given: \"%1$s\"" msgid "The following reason was given: \"%1$s\""
msgstr "La raison indiquée est: « %1$s»." msgstr "La raison indiquée est: « %1$s»"
#: dist/converse-no-dependencies.js:96851 #: dist/converse-no-dependencies.js:96851
#, javascript-format #, javascript-format

View File

@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: Converse.js 4.0.4\n" "Project-Id-Version: Converse.js 4.0.4\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-03-17 10:11+0100\n" "POT-Creation-Date: 2021-03-17 10:11+0100\n"
"PO-Revision-Date: 2021-04-09 13:31+0200\n" "PO-Revision-Date: 2021-04-25 05:32+0000\n"
"Last-Translator: Xosé M <xosem@disroot.org>\n" "Last-Translator: Xosé M <xosem@disroot.org>\n"
"Language-Team: Galician <https://hosted.weblate.org/projects/conversejs/" "Language-Team: Galician <https://hosted.weblate.org/projects/conversejs/"
"translations/gl/>\n" "translations/gl/>\n"
@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n" "Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.5.2-dev\n" "X-Generator: Weblate 4.7-dev\n"
#. Strophe #. Strophe
#: dist/converse-no-dependencies.js:42989 #: dist/converse-no-dependencies.js:42989
@ -778,7 +778,7 @@ msgstr "Razón (optativo)"
#: dist/converse-no-dependencies.js:76184 #: dist/converse-no-dependencies.js:76184
msgid "Sorry, you're not allowed to retract this message" msgid "Sorry, you're not allowed to retract this message"
msgstr "Desculpa, pero non tes permiso para eliminar esta mensaxe." msgstr "Desculpa, pero non tes permiso para eliminar esta mensaxe"
#: dist/converse-no-dependencies.js:76270 #: dist/converse-no-dependencies.js:76270
msgid "Cancel Editing" msgid "Cancel Editing"
@ -1461,7 +1461,7 @@ msgstr "Esta conversa en grupo xa non existe"
#: dist/converse-no-dependencies.js:96194 #: dist/converse-no-dependencies.js:96194
#, javascript-format #, javascript-format
msgid "The following reason was given: \"%1$s\"" msgid "The following reason was given: \"%1$s\""
msgstr "A razón do cambio é: \"%1$s\"." msgstr "A razón do cambio é: \"%1$s\""
#: dist/converse-no-dependencies.js:96851 #: dist/converse-no-dependencies.js:96851
#, javascript-format #, javascript-format

View File

@ -9,8 +9,8 @@ msgstr ""
"Project-Id-Version: Converse.js 0.9.6\n" "Project-Id-Version: Converse.js 0.9.6\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-03-17 12:35+0100\n" "POT-Creation-Date: 2021-03-17 12:35+0100\n"
"PO-Revision-Date: 2020-11-18 18:41+0100\n" "PO-Revision-Date: 2021-05-20 18:33+0000\n"
"Last-Translator: Michal Biesiada <blade-14@o2.pl>\n" "Last-Translator: G <emgrzegorz@interia.pl>\n"
"Language-Team: Polish <https://hosted.weblate.org/projects/conversejs/" "Language-Team: Polish <https://hosted.weblate.org/projects/conversejs/"
"translations/pl/>\n" "translations/pl/>\n"
"Language: pl\n" "Language: pl\n"
@ -19,7 +19,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " "Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
"|| n%100>=20) ? 1 : 2;\n" "|| n%100>=20) ? 1 : 2;\n"
"X-Generator: Weblate 4.4-dev\n" "X-Generator: Weblate 4.7-dev\n"
#. Strophe #. Strophe
#: dist/converse-no-dependencies.js:42989 #: dist/converse-no-dependencies.js:42989
@ -32,9 +32,8 @@ msgid "An error occurred while connecting to the chat server."
msgstr "Wystąpił błąd w czasie łączenia się z serwerem." msgstr "Wystąpił błąd w czasie łączenia się z serwerem."
#: dist/converse-no-dependencies.js:43246 #: dist/converse-no-dependencies.js:43246
#, fuzzy
msgid "Your XMPP address and/or password is incorrect. Please try again." msgid "Your XMPP address and/or password is incorrect. Please try again."
msgstr "Twój Jabber ID i/lub hasło jest nieprawidłowe. Spróbuj ponownie." msgstr "Twój XMPP adres i/lub hasło jest nieprawidłowe. Spróbuj ponownie."
#: dist/converse-no-dependencies.js:43258 #: dist/converse-no-dependencies.js:43258
#, javascript-format #, javascript-format
@ -69,26 +68,22 @@ msgstr "%1$s odszedł od klawiatury"
#: dist/converse-no-dependencies.js:51675 #: dist/converse-no-dependencies.js:51675
#: dist/converse-no-dependencies.js:58125 #: dist/converse-no-dependencies.js:58125
#, fuzzy
msgid "You're not allowed to retract your message." msgid "You're not allowed to retract your message."
msgstr "Przepraszam, nie wolno ci wycofać tej wiadomości." msgstr "Nie wolno tobie wycofać tej wiadomości."
#: dist/converse-no-dependencies.js:51677 #: dist/converse-no-dependencies.js:51677
#: dist/converse-no-dependencies.js:58129 #: dist/converse-no-dependencies.js:58129
#, fuzzy
msgid "Sorry, an error occurred while trying to retract your message." msgid "Sorry, an error occurred while trying to retract your message."
msgstr "Przepraszam, coś poszło nie tak podczas próby cofnięcia wiadomości." msgstr "Przepraszam, pojawił się błąd podczas próby cofnięcia wiadomości."
#: dist/converse-no-dependencies.js:51682 #: dist/converse-no-dependencies.js:51682
#, fuzzy
msgid "You're not allowed to send a message." msgid "You're not allowed to send a message."
msgstr "Nie możesz wysyłać wiadomości w tym pokoju" msgstr "Nie możesz wysyłać wiadomości"
#: dist/converse-no-dependencies.js:51684 #: dist/converse-no-dependencies.js:51684
#: dist/converse-no-dependencies.js:58138 #: dist/converse-no-dependencies.js:58138
#, fuzzy
msgid "Sorry, an error occurred while trying to send your message." msgid "Sorry, an error occurred while trying to send your message."
msgstr "Wystąpił błąd w czasie próby usunięcia urządzenia." msgstr "Przepraszam, wystąpił błąd podczas próby wysłania twojej wiadomości."
#: dist/converse-no-dependencies.js:51955 #: dist/converse-no-dependencies.js:51955
#, javascript-format #, javascript-format
@ -122,9 +117,8 @@ msgstr ""
"serwer, który wynosi %2$s." "serwer, który wynosi %2$s."
#: dist/converse-no-dependencies.js:53208 #: dist/converse-no-dependencies.js:53208
#, fuzzy
msgid "Undecryptable OMEMO message" msgid "Undecryptable OMEMO message"
msgstr "Nieszyfrowalny komunikat OMEMO" msgstr "Nierozszyfrowalna wiadomość OMEMO"
#: dist/converse-no-dependencies.js:53272 #: dist/converse-no-dependencies.js:53272
msgid "Sorry, could not determine upload URL." msgid "Sorry, could not determine upload URL."
@ -213,15 +207,13 @@ msgid "A timeout happened while while trying to retract your message."
msgstr "Podczas próby cofnięcia komunikatu pojawił się limit czasu" msgstr "Podczas próby cofnięcia komunikatu pojawił się limit czasu"
#: dist/converse-no-dependencies.js:59253 #: dist/converse-no-dependencies.js:59253
#, fuzzy
msgid "Sorry, an error happened while running the command." msgid "Sorry, an error happened while running the command."
msgstr "Wystąpił błąd w trakcie próby zapisania Twoich danych profilowych." msgstr "Przepraszam, ale wystąpił błąd w trakcie uruchomienia polecenia."
#: dist/converse-no-dependencies.js:59253 #: dist/converse-no-dependencies.js:59253
#: dist/converse-no-dependencies.js:94373 #: dist/converse-no-dependencies.js:94373
#, fuzzy
msgid "Check your browser's developer console for details." msgid "Check your browser's developer console for details."
msgstr "Możesz sprawdzić konsolę dewelopera przeglądarki pod kątem błędów." msgstr "Sprawdzić konsolę dewelopera przeglądarki po szczegóły."
#: dist/converse-no-dependencies.js:59278 #: dist/converse-no-dependencies.js:59278
#: dist/converse-no-dependencies.js:59301 #: dist/converse-no-dependencies.js:59301
@ -340,19 +332,19 @@ msgid "%1$s have stopped typing"
msgstr "%1$s przestał pisać" msgstr "%1$s przestał pisać"
#: dist/converse-no-dependencies.js:60543 #: dist/converse-no-dependencies.js:60543
#, fuzzy, javascript-format #, javascript-format
msgid "%1$s have gone away" msgid "%1$s have gone away"
msgstr "%1$s odszedł od klawiatury" msgstr "%1$s oddalił się"
#: dist/converse-no-dependencies.js:60545 #: dist/converse-no-dependencies.js:60545
#, fuzzy, javascript-format #, javascript-format
msgid "%1$s have entered the groupchat" msgid "%1$s have entered the groupchat"
msgstr "%1$s wszedł do pokoju" msgstr "%1$s wszedł na czat groupowy"
#: dist/converse-no-dependencies.js:60547 #: dist/converse-no-dependencies.js:60547
#, fuzzy, javascript-format #, javascript-format
msgid "%1$s have left the groupchat" msgid "%1$s have left the groupchat"
msgstr "%1$s opuścił pokój" msgstr "%1$s opuścił czat groupowy"
#: dist/converse-no-dependencies.js:60549 #: dist/converse-no-dependencies.js:60549
#, fuzzy, javascript-format #, fuzzy, javascript-format
@ -375,9 +367,9 @@ msgid "%1$s have been muted"
msgstr "%1$s został wyciszony" msgstr "%1$s został wyciszony"
#: dist/converse-no-dependencies.js:60859 #: dist/converse-no-dependencies.js:60859
#, fuzzy, javascript-format #, javascript-format
msgid "%1$s has been banned by %2$s" msgid "%1$s has been banned by %2$s"
msgstr "%1$s został zbanowany" msgstr "%1$s został zbanowany przez %2$s"
#: dist/converse-no-dependencies.js:60859 #: dist/converse-no-dependencies.js:60859
#, javascript-format #, javascript-format
@ -390,9 +382,9 @@ msgid "%1$s's nickname has changed"
msgstr "%1$s zmienił pseudonim" msgstr "%1$s zmienił pseudonim"
#: dist/converse-no-dependencies.js:60863 #: dist/converse-no-dependencies.js:60863
#, fuzzy, javascript-format #, javascript-format
msgid "%1$s has been kicked out by %2$s" msgid "%1$s has been kicked out by %2$s"
msgstr "%1$s został wyrzucony" msgstr "%1$s został wykopany przez %2$s"
#: dist/converse-no-dependencies.js:60863 #: dist/converse-no-dependencies.js:60863
#, javascript-format #, javascript-format
@ -539,10 +531,8 @@ msgid "You have been banned from this groupchat"
msgstr "Zostałeś zablokowany na tym czacie grupowym" msgstr "Zostałeś zablokowany na tym czacie grupowym"
#: dist/converse-no-dependencies.js:62490 #: dist/converse-no-dependencies.js:62490
#, fuzzy
msgid "You have exited this groupchat due to a technical problem" msgid "You have exited this groupchat due to a technical problem"
msgstr "" msgstr "Wyszedłeś z tego czatu grupowego z powodu problemu technicznego"
"Zostałeś usunięty z tego czatu grupowego z powodu zmiany przynależności"
#: dist/converse-no-dependencies.js:62491 #: dist/converse-no-dependencies.js:62491
msgid "You have been kicked from this groupchat" msgid "You have been kicked from this groupchat"
@ -616,9 +606,8 @@ msgstr ""
"aby poprosić o nie ponownie." "aby poprosić o nie ponownie."
#: dist/converse-no-dependencies.js:65180 #: dist/converse-no-dependencies.js:65180
#, fuzzy
msgid "Timeout while trying to fetch archived messages." msgid "Timeout while trying to fetch archived messages."
msgstr "Podczas próby cofnięcia komunikatu pojawił się limit czasu" msgstr "Limit czasu podczas próby pobrania archiwalnych wiadomości."
#: dist/converse-no-dependencies.js:65195 #: dist/converse-no-dependencies.js:65195
#, fuzzy #, fuzzy
@ -742,9 +731,9 @@ msgid "Download audio file \"%1$s\""
msgstr "Pobierz plik audio \"%1$s\"" msgstr "Pobierz plik audio \"%1$s\""
#: dist/converse-no-dependencies.js:74156 #: dist/converse-no-dependencies.js:74156
#, fuzzy, javascript-format #, javascript-format
msgid "Download image file \"%1$s\"" msgid "Download image file \"%1$s\""
msgstr "Pobierz zdjęcie \"%1$s\"" msgstr "Pobierz obraz pliku \"%1$s\""
#: dist/converse-no-dependencies.js:74164 #: dist/converse-no-dependencies.js:74164
#, javascript-format #, javascript-format
@ -771,7 +760,6 @@ msgstr ""
#: dist/converse-no-dependencies.js:76032 #: dist/converse-no-dependencies.js:76032
#: dist/converse-no-dependencies.js:76129 #: dist/converse-no-dependencies.js:76129
#: dist/converse-no-dependencies.js:76166 #: dist/converse-no-dependencies.js:76166
#, fuzzy
msgid "Are you sure you want to retract this message?" msgid "Are you sure you want to retract this message?"
msgstr "Jesteś pewien, że chcesz wycofać tę wiadomość?" msgstr "Jesteś pewien, że chcesz wycofać tę wiadomość?"
@ -792,28 +780,24 @@ msgid "Sorry, you're not allowed to retract this message."
msgstr "Przepraszam, nie wolno ci wycofać tej wiadomości." msgstr "Przepraszam, nie wolno ci wycofać tej wiadomości."
#: dist/converse-no-dependencies.js:76190 #: dist/converse-no-dependencies.js:76190
#, fuzzy
msgid "You are about to retract this message." msgid "You are about to retract this message."
msgstr "Masz zamiar wycofać tę wiadomość." msgstr "Masz zamiar wycofać tę wiadomość."
#: dist/converse-no-dependencies.js:76190 #: dist/converse-no-dependencies.js:76190
#, fuzzy
msgid "" msgid ""
"You may optionally include a message, explaining the reason for the " "You may optionally include a message, explaining the reason for the "
"retraction." "retraction."
msgstr "Opcjonalnie możesz dołączyć wiadomość wyjaśniającą powód zwinięcia." msgstr "Opcjonalnie możesz dołączyć wiadomość wyjaśniającą powód zwinięcia."
#: dist/converse-no-dependencies.js:76197 #: dist/converse-no-dependencies.js:76197
#, fuzzy
msgid "Message Retraction" msgid "Message Retraction"
msgstr "Wersja wiadomości" msgstr "Wycofanie wiadomości"
#: dist/converse-no-dependencies.js:76197 #: dist/converse-no-dependencies.js:76197
msgid "Optional reason" msgid "Optional reason"
msgstr "Opcjonalny powód" msgstr "Opcjonalny powód"
#: dist/converse-no-dependencies.js:76208 #: dist/converse-no-dependencies.js:76208
#, fuzzy
msgid "Sorry, you're not allowed to retract this message" msgid "Sorry, you're not allowed to retract this message"
msgstr "Przepraszam, nie wolno ci wycofać tej wiadomości." msgstr "Przepraszam, nie wolno ci wycofać tej wiadomości."
@ -824,32 +808,32 @@ msgstr "Zmiana ustawień"
#: dist/converse-no-dependencies.js:76270 #: dist/converse-no-dependencies.js:76270
msgid "Edit" msgid "Edit"
msgstr "" msgstr "Edycja"
#: dist/converse-no-dependencies.js:76299 #: dist/converse-no-dependencies.js:76299
msgid "Retract" msgid "Retract"
msgstr "" msgstr "Wycofaj"
#: dist/converse-no-dependencies.js:76316 #: dist/converse-no-dependencies.js:76316
msgid "Show URL previews" msgid "Show URL previews"
msgstr "" msgstr "Pokaż podgląd URL"
#: dist/converse-no-dependencies.js:76316 #: dist/converse-no-dependencies.js:76316
msgid "Hide URL previews" msgid "Hide URL previews"
msgstr "" msgstr "Ukryj podgląd URL"
#: dist/converse-no-dependencies.js:76318 #: dist/converse-no-dependencies.js:76318
msgid "Show URL preview" msgid "Show URL preview"
msgstr "" msgstr "Pokaż podgląd URL"
#: dist/converse-no-dependencies.js:76318 #: dist/converse-no-dependencies.js:76318
msgid "Hide URL preview" msgid "Hide URL preview"
msgstr "" msgstr "Ukryj podgląd URL"
#. harmony default export #. harmony default export
#: dist/converse-no-dependencies.js:76429 #: dist/converse-no-dependencies.js:76429
msgid "Image: " msgid "Image: "
msgstr "" msgstr "Obraz: "
#. harmony default export #. harmony default export
#: dist/converse-no-dependencies.js:77991 #: dist/converse-no-dependencies.js:77991
@ -869,9 +853,8 @@ msgid "OMEMO Fingerprints"
msgstr "Odciski palców OMEMO" msgstr "Odciski palców OMEMO"
#: dist/converse-no-dependencies.js:78232 #: dist/converse-no-dependencies.js:78232
#, fuzzy
msgid "No OMEMO-enabled devices found" msgid "No OMEMO-enabled devices found"
msgstr "Pozostałe urządzenia z funkcją OMEMO" msgstr "Nie znaleziono urządzeń z włączonym OMEMO"
#: dist/converse-no-dependencies.js:78243 #: dist/converse-no-dependencies.js:78243
msgid "Remove as contact" msgid "Remove as contact"
@ -942,7 +925,7 @@ msgstr "Jesteś pewien, że chcesz usunąć ten kontakt?"
#: dist/converse-no-dependencies.js:79208 #: dist/converse-no-dependencies.js:79208
msgid "Retry" msgid "Retry"
msgstr "" msgstr "Ponów"
#: dist/converse-no-dependencies.js:79215 #: dist/converse-no-dependencies.js:79215
msgid "Uploading file:" msgid "Uploading file:"
@ -963,9 +946,8 @@ msgid "Show more"
msgstr "Pokaż więcej" msgstr "Pokaż więcej"
#: dist/converse-no-dependencies.js:79359 #: dist/converse-no-dependencies.js:79359
#, fuzzy
msgid "Show less" msgid "Show less"
msgstr "Pokaż użytkowników" msgstr "Pokaż mniej"
#: dist/converse-no-dependencies.js:80828 #: dist/converse-no-dependencies.js:80828
msgid "Search results" msgid "Search results"
@ -992,14 +974,12 @@ msgid "Message characters remaining"
msgstr "Pozostałe znaki wiadomości" msgstr "Pozostałe znaki wiadomości"
#: dist/converse-no-dependencies.js:82222 #: dist/converse-no-dependencies.js:82222
#, fuzzy
msgid "Hide participants" msgid "Hide participants"
msgstr "Uczestnicy" msgstr "Ukryj uczestników"
#: dist/converse-no-dependencies.js:82224 #: dist/converse-no-dependencies.js:82224
#, fuzzy
msgid "Show participants" msgid "Show participants"
msgstr "Uczestnicy" msgstr "Pokaż uczestników"
#: dist/converse-no-dependencies.js:82247 #: dist/converse-no-dependencies.js:82247
msgid "Choose a file to send" msgid "Choose a file to send"
@ -1020,17 +1000,15 @@ msgstr "Jesteś pewien, że chcesz wyczyścić wiadomości z tej rozmowy?"
#: dist/converse-no-dependencies.js:82962 #: dist/converse-no-dependencies.js:82962
#: dist/converse-no-dependencies.js:97658 #: dist/converse-no-dependencies.js:97658
msgid "Details" msgid "Details"
msgstr "" msgstr "Detale"
#: dist/converse-no-dependencies.js:82963 #: dist/converse-no-dependencies.js:82963
#, fuzzy
msgid "See more information about this person" msgid "See more information about this person"
msgstr "Pokaż więcej informacji o pokoju" msgstr "Pokaż więcej informacji o tej osobie"
#: dist/converse-no-dependencies.js:82976 #: dist/converse-no-dependencies.js:82976
#, fuzzy
msgid "Close and end this conversation" msgid "Close and end this conversation"
msgstr "Zakończ szyfrowaną rozmowę" msgstr "Zamknij i zakoń tę rozmowę"
#: dist/converse-no-dependencies.js:83478 #: dist/converse-no-dependencies.js:83478
msgid "Hidden message" msgid "Hidden message"
@ -1051,6 +1029,8 @@ msgstr "Masz nieprzeczytane wiadomości"
#: dist/converse-no-dependencies.js:83639 #: dist/converse-no-dependencies.js:83639
msgid "Sorry, the connection has been lost, and your message could not be sent" msgid "Sorry, the connection has been lost, and your message could not be sent"
msgstr "" msgstr ""
"Przepraszam, ale połączenie zostało utracone i twoja wiadomość nie mogła "
"zostać wysłana"
#. eslint-disable-line class-methods-use-this #. eslint-disable-line class-methods-use-this
#: dist/converse-no-dependencies.js:84684 #: dist/converse-no-dependencies.js:84684
@ -1091,9 +1071,8 @@ msgid "This is a trusted device"
msgstr "To jest zaufane urządzenie" msgstr "To jest zaufane urządzenie"
#: dist/converse-no-dependencies.js:87541 #: dist/converse-no-dependencies.js:87541
#, fuzzy
msgid "Password" msgid "Password"
msgstr "Hasło:" msgstr "Hasło"
#: dist/converse-no-dependencies.js:87547 #: dist/converse-no-dependencies.js:87547
msgid "Create an account" msgid "Create an account"
@ -1139,18 +1118,16 @@ msgid "Toggle chat"
msgstr "Przełącz czat" msgstr "Przełącz czat"
#: dist/converse-no-dependencies.js:89998 #: dist/converse-no-dependencies.js:89998
#, fuzzy
msgid "Close these announcements" msgid "Close these announcements"
msgstr "Ogłoszenia" msgstr "Zamknij ogłoszenia"
#: dist/converse-no-dependencies.js:90595 #: dist/converse-no-dependencies.js:90595
msgid "Announcements" msgid "Announcements"
msgstr "Ogłoszenia" msgstr "Ogłoszenia"
#: dist/converse-no-dependencies.js:90599 #: dist/converse-no-dependencies.js:90599
#, fuzzy
msgid "Click to open this server message" msgid "Click to open this server message"
msgstr "Kliknij, aby otworzyć ten czat grupowy" msgstr "Kliknij, aby otworzyć tę wiadomość z serwera"
#. harmony default export #. harmony default export
#: dist/converse-no-dependencies.js:90801 #: dist/converse-no-dependencies.js:90801
@ -1159,19 +1136,16 @@ msgstr "Kliknij, aby przywrócić ten czat"
#: dist/converse-no-dependencies.js:91053 #: dist/converse-no-dependencies.js:91053
#: dist/converse-no-dependencies.js:91071 #: dist/converse-no-dependencies.js:91071
#, fuzzy
msgid "Minimize" msgid "Minimize"
msgstr "Zminimalizowane" msgstr "Zminimalizuj"
#: dist/converse-no-dependencies.js:91054 #: dist/converse-no-dependencies.js:91054
#, fuzzy
msgid "Minimize this chat" msgid "Minimize this chat"
msgstr "Zminimalizuj to okno czatu" msgstr "Zminimalizuj ten czat"
#: dist/converse-no-dependencies.js:91072 #: dist/converse-no-dependencies.js:91072
#, fuzzy
msgid "Minimize this groupchat" msgid "Minimize this groupchat"
msgstr "Zminimalizuj to okno czatu" msgstr "Zminimalizuj ten czat grupowy"
#. harmony default export #. harmony default export
#: dist/converse-no-dependencies.js:91421 #: dist/converse-no-dependencies.js:91421
@ -1181,17 +1155,17 @@ msgstr "Zminimalizowane"
#. harmony default export #. harmony default export
#: dist/converse-no-dependencies.js:92962 #: dist/converse-no-dependencies.js:92962
msgid "Hide" msgid "Hide"
msgstr "" msgstr "Ukryj"
#: dist/converse-no-dependencies.js:92964 #: dist/converse-no-dependencies.js:92964
msgid "Execute" msgid "Execute"
msgstr "" msgstr "Uruchom"
#: dist/converse-no-dependencies.js:93293 #: dist/converse-no-dependencies.js:93293
msgid "" msgid ""
"Couldn't find a participant with that nickname. They might have left the " "Couldn't find a participant with that nickname. They might have left the "
"groupchat." "groupchat."
msgstr "" msgstr "Nie znaleziono uczestnika o tym nicku. Być moze opuścij czat grupowy."
#. e.g. Your nickname is "coolguy69" #. e.g. Your nickname is "coolguy69"
#: dist/converse-no-dependencies.js:93422 #: dist/converse-no-dependencies.js:93422
@ -1206,7 +1180,7 @@ msgstr "Błąd: nieprawidłowa liczba argumentów"
#. harmony default export #. harmony default export
#: dist/converse-no-dependencies.js:93540 #: dist/converse-no-dependencies.js:93540
msgid "On which entity do you want to run commands?" msgid "On which entity do you want to run commands?"
msgstr "" msgstr "Na której jednostce chcesz wykonać polecenie?"
#: dist/converse-no-dependencies.js:93542 #: dist/converse-no-dependencies.js:93542
msgid "" msgid ""
@ -1215,18 +1189,16 @@ msgid ""
msgstr "" msgstr ""
#: dist/converse-no-dependencies.js:93544 #: dist/converse-no-dependencies.js:93544
#, fuzzy
msgid "Commands found" msgid "Commands found"
msgstr "Pokoje na %1$s" msgstr "Znaleziono polecenia"
#: dist/converse-no-dependencies.js:93546 #: dist/converse-no-dependencies.js:93546
msgid "List available commands" msgid "List available commands"
msgstr "" msgstr "Lista dostępnych poleceń"
#: dist/converse-no-dependencies.js:93550 #: dist/converse-no-dependencies.js:93550
#, fuzzy
msgid "No commands found" msgid "No commands found"
msgstr "Pokoje na %1$s" msgstr "Nie znaleziono polecenia"
#: dist/converse-no-dependencies.js:93838 #: dist/converse-no-dependencies.js:93838
#, fuzzy #, fuzzy
@ -1235,20 +1207,19 @@ msgstr "Wystąpił błąd w czasie próby usunięcia urządzenia."
#: dist/converse-no-dependencies.js:93849 #: dist/converse-no-dependencies.js:93849
msgid "The specified entity doesn't support ad-hoc commands" msgid "The specified entity doesn't support ad-hoc commands"
msgstr "" msgstr "Wskazana jednostka nie obsługuje poleceń ad-hoc"
#: dist/converse-no-dependencies.js:93964 #: dist/converse-no-dependencies.js:93964
#, fuzzy
msgid "" msgid ""
"Sorry, an error occurred while trying to execute the command. See the " "Sorry, an error occurred while trying to execute the command. See the "
"developer console for details" "developer console for details"
msgstr "" msgstr ""
"Ups, podczas wykonywania polecenia wystąpił błąd. Szczegółowe informacje " "Przepraszam, podczas wykonywania polecenia wystąpił błąd. Szczegółowe "
"można znaleźć w konsoli deweloperskiej przeglądarki." "informacje można znaleźć na konsoli dewelopera."
#: dist/converse-no-dependencies.js:94078 #: dist/converse-no-dependencies.js:94078
msgid "Loading configuration form" msgid "Loading configuration form"
msgstr "" msgstr "Wczytywanie formularza konfiguracyjnego"
#: dist/converse-no-dependencies.js:94373 #: dist/converse-no-dependencies.js:94373
#, fuzzy #, fuzzy
@ -1349,7 +1320,7 @@ msgstr "Nie znaleziono użytkowników z tą rolą."
#: dist/converse-no-dependencies.js:94965 #: dist/converse-no-dependencies.js:94965
msgid "Type here to filter the search results" msgid "Type here to filter the search results"
msgstr "" msgstr "Wpisz tutaj aby odfiltrować rezultaty przeszukiwań"
#: dist/converse-no-dependencies.js:94969 #: dist/converse-no-dependencies.js:94969
msgid "Show users" msgid "Show users"
@ -1384,9 +1355,8 @@ msgid "Timeout error while trying to set the affiliation"
msgstr "Przepraszam, coś poszło nie tak podczas próby ustalenia przynależności" msgstr "Przepraszam, coś poszło nie tak podczas próby ustalenia przynależności"
#: dist/converse-no-dependencies.js:95315 #: dist/converse-no-dependencies.js:95315
#, fuzzy
msgid "Sorry, you're not allowed to make that change" msgid "Sorry, you're not allowed to make that change"
msgstr "Nie możesz dokonać tej zmiany" msgstr "Przepraszam, ale nie możesz dokonać tej zmiany"
#: dist/converse-no-dependencies.js:95317 #: dist/converse-no-dependencies.js:95317
msgid "Sorry, something went wrong while trying to set the affiliation" msgid "Sorry, something went wrong while trying to set the affiliation"
@ -1506,10 +1476,9 @@ msgid "Allow muted user to post messages"
msgstr "Pozwól uciszonemu człowiekowi na rozmowę" msgstr "Pozwól uciszonemu człowiekowi na rozmowę"
#: dist/converse-no-dependencies.js:96345 #: dist/converse-no-dependencies.js:96345
#, fuzzy
msgid "" msgid ""
"The conversation has moved to a new address. Click the link below to enter." "The conversation has moved to a new address. Click the link below to enter."
msgstr "Rozmowa została przeniesiona. Kliknij poniżej aby wejść." msgstr "Rozmowa została przeniesiona pod nowy adres. Kliknij poniżej aby wejść."
#. harmony default export #. harmony default export
#: dist/converse-no-dependencies.js:96353 #: dist/converse-no-dependencies.js:96353
@ -1517,7 +1486,7 @@ msgid "This groupchat no longer exists"
msgstr "Ten czat grupowy już nie istnieje" msgstr "Ten czat grupowy już nie istnieje"
#: dist/converse-no-dependencies.js:96355 #: dist/converse-no-dependencies.js:96355
#, fuzzy, javascript-format #, javascript-format
msgid "The following reason was given: \"%1$s\"" msgid "The following reason was given: \"%1$s\""
msgstr "Podany powód to: \"%1$s\"." msgstr "Podany powód to: \"%1$s\"."
@ -1538,19 +1507,16 @@ msgid "Invite"
msgstr "Zaproś" msgstr "Zaproś"
#: dist/converse-no-dependencies.js:96916 #: dist/converse-no-dependencies.js:96916
#, fuzzy
msgid "Invite someone to this groupchat" msgid "Invite someone to this groupchat"
msgstr "Usuń ten czat grupowy" msgstr "Zaproś kogoś na ten czat grupowy"
#: dist/converse-no-dependencies.js:96918 #: dist/converse-no-dependencies.js:96918
#, fuzzy
msgid "user@example.org" msgid "user@example.org"
msgstr "np. użytkownik@przykładowa-domena.pl" msgstr "użytkownik@example.org"
#: dist/converse-no-dependencies.js:96924 #: dist/converse-no-dependencies.js:96924
#, fuzzy
msgid "Optional reason for the invitation" msgid "Optional reason for the invitation"
msgstr "Opcjonalny powód" msgstr "Opcjonalny powód zaproszenia"
#: dist/converse-no-dependencies.js:97156 #: dist/converse-no-dependencies.js:97156
msgid "Topic" msgid "Topic"
@ -1701,42 +1667,36 @@ msgstr "Informacje o czacie grupowym dla %1$s"
#. harmony default export #. harmony default export
#: dist/converse-no-dependencies.js:97327 #: dist/converse-no-dependencies.js:97327
#, fuzzy
msgid "Hide the groupchat topic" msgid "Hide the groupchat topic"
msgstr "Wejdź do pokoju" msgstr "Ukryj temat czatu grupowego"
#: dist/converse-no-dependencies.js:97329 #: dist/converse-no-dependencies.js:97329
#, fuzzy
msgid "This groupchat is bookmarked" msgid "This groupchat is bookmarked"
msgstr "Ten człowiek jest moderatorem" msgstr "Ten czat grupowy został zapamiętany"
#: dist/converse-no-dependencies.js:97659 #: dist/converse-no-dependencies.js:97659
#, fuzzy
msgid "Show more information about this groupchat" msgid "Show more information about this groupchat"
msgstr "Pokaż więcej informacji na temat tego czatu grupowego" msgstr "Pokaż więcej informacji na temat tego czatu grupowego"
#: dist/converse-no-dependencies.js:97670 #: dist/converse-no-dependencies.js:97670
#, fuzzy
msgid "Configure" msgid "Configure"
msgstr "Potwierdź" msgstr "Konfiguracja"
#: dist/converse-no-dependencies.js:97671 #: dist/converse-no-dependencies.js:97671
msgid "Configure this groupchat" msgid "Configure this groupchat"
msgstr "Skonfiguruj ten pokój" msgstr "Skonfiguruj ten pokój"
#: dist/converse-no-dependencies.js:97684 #: dist/converse-no-dependencies.js:97684
#, fuzzy
msgid "Invite someone to join this groupchat" msgid "Invite someone to join this groupchat"
msgstr "Każdy może dołączyć do tego czatu grupowego" msgstr "Zaproś kogoś aby przyłączył się do czatu grupowego"
#: dist/converse-no-dependencies.js:97698 #: dist/converse-no-dependencies.js:97698
#, fuzzy
msgid "Show topic" msgid "Show topic"
msgstr "Pokaż pokoje" msgstr "Pokaż temat"
#: dist/converse-no-dependencies.js:97698 #: dist/converse-no-dependencies.js:97698
msgid "Hide topic" msgid "Hide topic"
msgstr "" msgstr "Ukryj temat"
#: dist/converse-no-dependencies.js:97699 #: dist/converse-no-dependencies.js:97699
msgid "Show the topic message in the heading" msgid "Show the topic message in the heading"
@ -1757,23 +1717,20 @@ msgid "Moderate this groupchat"
msgstr "Opuść ten pokój" msgstr "Opuść ten pokój"
#: dist/converse-no-dependencies.js:97729 #: dist/converse-no-dependencies.js:97729
#, fuzzy
msgid "Destroy" msgid "Destroy"
msgstr "Zniszcz pokój" msgstr "Zniszcz"
#: dist/converse-no-dependencies.js:97743 #: dist/converse-no-dependencies.js:97743
msgid "Leave" msgid "Leave"
msgstr "" msgstr "Opuść"
#: dist/converse-no-dependencies.js:97744 #: dist/converse-no-dependencies.js:97744
#, fuzzy
msgid "Leave and close this groupchat" msgid "Leave and close this groupchat"
msgstr "Opuść ten pokój" msgstr "Opuść i zamknij ten czat grupowy"
#: dist/converse-no-dependencies.js:97753 #: dist/converse-no-dependencies.js:97753
#, fuzzy
msgid "Are you sure you want to leave this groupchat?" msgid "Are you sure you want to leave this groupchat?"
msgstr "Jesteś pewny, że chcesz wyjść z grupowego czatu %1$s?" msgstr "Jesteś pewny, że chcesz opuścić ten czat grupowy?"
#: dist/converse-no-dependencies.js:98120 #: dist/converse-no-dependencies.js:98120
msgid "This user is a moderator." msgid "This user is a moderator."
@ -1818,14 +1775,12 @@ msgid "Participants"
msgstr "Uczestnicy" msgstr "Uczestnicy"
#: dist/converse-no-dependencies.js:99003 #: dist/converse-no-dependencies.js:99003
#, fuzzy
msgid "Are you sure you want to destroy this groupchat?" msgid "Are you sure you want to destroy this groupchat?"
msgstr "Jesteś pewny, że chcesz wyjść z grupowego czatu %1$s?" msgstr "Jesteś pewny, że chcesz usunąć ten czat grupowy?"
#: dist/converse-no-dependencies.js:99006 #: dist/converse-no-dependencies.js:99006
#, fuzzy
msgid "Please enter the XMPP address of this groupchat to confirm" msgid "Please enter the XMPP address of this groupchat to confirm"
msgstr "Proszę wprowadzić dostawcę XMPP, aby zarejestrować się:" msgstr "Proszę wprowadzić adres XMPP tego czatu grupowego aby potwierdzić"
#. harmony default export #. harmony default export
#: dist/converse-no-dependencies.js:99008 #: dist/converse-no-dependencies.js:99008
@ -1834,9 +1789,8 @@ msgid "name@example.org"
msgstr "np. użytkownik@przykładowa-domena.pl" msgstr "np. użytkownik@przykładowa-domena.pl"
#: dist/converse-no-dependencies.js:99012 #: dist/converse-no-dependencies.js:99012
#, fuzzy
msgid "Optional reason for destroying this groupchat" msgid "Optional reason for destroying this groupchat"
msgstr "Jesteś pewny, że chcesz wyjść z grupowego czatu %1$s?" msgstr "Dodatkowy powód do usunięcia tego czatu grupowego"
#: dist/converse-no-dependencies.js:99016 #: dist/converse-no-dependencies.js:99016
msgid "Optional XMPP address for a new groupchat that replaces this one" msgid "Optional XMPP address for a new groupchat that replaces this one"

File diff suppressed because it is too large Load Diff

View File

@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: Converse.js 0.6.3\n" "Project-Id-Version: Converse.js 0.6.3\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-03-17 12:35+0100\n" "POT-Creation-Date: 2021-03-17 12:35+0100\n"
"PO-Revision-Date: 2020-12-19 10:29+0000\n" "PO-Revision-Date: 2021-05-04 18:32+0000\n"
"Last-Translator: LL Magical <lolayami2004@gmail.com>\n" "Last-Translator: bashl <esqueleto777@disroot.org>\n"
"Language-Team: Portuguese (Brazil) <https://hosted.weblate.org/projects/" "Language-Team: Portuguese (Brazil) <https://hosted.weblate.org/projects/"
"conversejs/translations/pt_BR/>\n" "conversejs/translations/pt_BR/>\n"
"Language: pt_BR\n" "Language: pt_BR\n"
@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n > 1;\n" "Plural-Forms: nplurals=2; plural=n > 1;\n"
"X-Generator: Weblate 4.4-dev\n" "X-Generator: Weblate 4.7-dev\n"
"domain: converse\n" "domain: converse\n"
"lang: pt_BR\n" "lang: pt_BR\n"
"plural_forms: nplurals=2; plural=(n != 1);\n" "plural_forms: nplurals=2; plural=(n != 1);\n"
@ -770,9 +770,8 @@ msgid "Sorry, you're not allowed to retract this message."
msgstr "Desculpe, você não está autorizado a retrair esta mensagem." msgstr "Desculpe, você não está autorizado a retrair esta mensagem."
#: dist/converse-no-dependencies.js:76190 #: dist/converse-no-dependencies.js:76190
#, fuzzy
msgid "You are about to retract this message." msgid "You are about to retract this message."
msgstr "Você está a ponto de retirar esta mensagem." msgstr "Você está a prestes a retirar esta mensagem."
#: dist/converse-no-dependencies.js:76190 #: dist/converse-no-dependencies.js:76190
#, fuzzy #, fuzzy
@ -811,19 +810,19 @@ msgstr "Retirar"
#: dist/converse-no-dependencies.js:76316 #: dist/converse-no-dependencies.js:76316
msgid "Show URL previews" msgid "Show URL previews"
msgstr "" msgstr "Mostrar pré-visualizações da URL"
#: dist/converse-no-dependencies.js:76316 #: dist/converse-no-dependencies.js:76316
msgid "Hide URL previews" msgid "Hide URL previews"
msgstr "" msgstr "Ocultar pré-visualizações da URL"
#: dist/converse-no-dependencies.js:76318 #: dist/converse-no-dependencies.js:76318
msgid "Show URL preview" msgid "Show URL preview"
msgstr "" msgstr "Mostrar pré-visualização da URL"
#: dist/converse-no-dependencies.js:76318 #: dist/converse-no-dependencies.js:76318
msgid "Hide URL preview" msgid "Hide URL preview"
msgstr "" msgstr "Ocultar pré-visualização da URL"
#. harmony default export #. harmony default export
#: dist/converse-no-dependencies.js:76429 #: dist/converse-no-dependencies.js:76429
@ -1215,7 +1214,7 @@ msgstr ""
#: dist/converse-no-dependencies.js:94078 #: dist/converse-no-dependencies.js:94078
msgid "Loading configuration form" msgid "Loading configuration form"
msgstr "" msgstr "Carregando configuração de formulário"
#: dist/converse-no-dependencies.js:94373 #: dist/converse-no-dependencies.js:94373
msgid "Sorry, an error occurred while trying to submit the config form." msgid "Sorry, an error occurred while trying to submit the config form."
@ -1470,10 +1469,11 @@ msgid "Allow muted user to post messages"
msgstr "Permitir que o usuário mudo publique mensagens" msgstr "Permitir que o usuário mudo publique mensagens"
#: dist/converse-no-dependencies.js:96345 #: dist/converse-no-dependencies.js:96345
#, fuzzy
msgid "" msgid ""
"The conversation has moved to a new address. Click the link below to enter." "The conversation has moved to a new address. Click the link below to enter."
msgstr "Esta conversa foi movida. Clique abaixo para entrar de novo." msgstr ""
"Esta conversa foi movida para um novo endereço. Clique abaixo para entrar "
"novamente."
#. harmony default export #. harmony default export
#: dist/converse-no-dependencies.js:96353 #: dist/converse-no-dependencies.js:96353
@ -1499,7 +1499,7 @@ msgstr "A razão dada é: \"%1$s\"."
#: dist/converse-no-dependencies.js:96914 #: dist/converse-no-dependencies.js:96914
#: dist/converse-no-dependencies.js:97683 #: dist/converse-no-dependencies.js:97683
msgid "Invite" msgid "Invite"
msgstr "convite" msgstr "Convite"
#: dist/converse-no-dependencies.js:96916 #: dist/converse-no-dependencies.js:96916
msgid "Invite someone to this groupchat" msgid "Invite someone to this groupchat"

View File

@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: Converse.js 3.3.4\n" "Project-Id-Version: Converse.js 3.3.4\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-03-17 12:35+0100\n" "POT-Creation-Date: 2021-03-17 12:35+0100\n"
"PO-Revision-Date: 2020-12-14 01:29+0000\n" "PO-Revision-Date: 2021-06-19 21:33+0000\n"
"Last-Translator: Sergiu <adinfinitvm@wail.ch>\n" "Last-Translator: dhruva dhruva <dhruva.gurmukhi@slmail.me>\n"
"Language-Team: Romanian <https://hosted.weblate.org/projects/conversejs/" "Language-Team: Romanian <https://hosted.weblate.org/projects/conversejs/"
"translations/ro/>\n" "translations/ro/>\n"
"Language: ro\n" "Language: ro\n"
@ -18,7 +18,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < " "Plural-Forms: nplurals=3; plural=n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < "
"20)) ? 1 : 2;\n" "20)) ? 1 : 2;\n"
"X-Generator: Weblate 4.4-dev\n" "X-Generator: Weblate 4.7\n"
#. Strophe #. Strophe
#: dist/converse-no-dependencies.js:42989 #: dist/converse-no-dependencies.js:42989
@ -27,16 +27,14 @@ msgstr "Conexiunea s-a întrerupt, se încearcă reconectarea."
#. Strophe #. Strophe
#: dist/converse-no-dependencies.js:43239 #: dist/converse-no-dependencies.js:43239
#, fuzzy
msgid "An error occurred while connecting to the chat server." msgid "An error occurred while connecting to the chat server."
msgstr "S-a produs o eroare în timpul conexiunii la serverul de discuții." msgstr "S-a produs o eroare în timpul conectării la serverul de discuții."
#: dist/converse-no-dependencies.js:43246 #: dist/converse-no-dependencies.js:43246
#, fuzzy
msgid "Your XMPP address and/or password is incorrect. Please try again." msgid "Your XMPP address and/or password is incorrect. Please try again."
msgstr "" msgstr ""
"ID-ul dumneavoastră Jabber sau parola sunt incorecte. Vă rugăm să încercați " "Adresa dumneavoastră de XMPP sau parola sunt incorecte. Vă rugăm să "
"din nou." "încercați din nou."
#: dist/converse-no-dependencies.js:43258 #: dist/converse-no-dependencies.js:43258
#, javascript-format #, javascript-format
@ -72,9 +70,8 @@ msgstr "%1$s a plecat"
#: dist/converse-no-dependencies.js:51675 #: dist/converse-no-dependencies.js:51675
#: dist/converse-no-dependencies.js:58125 #: dist/converse-no-dependencies.js:58125
#, fuzzy
msgid "You're not allowed to retract your message." msgid "You're not allowed to retract your message."
msgstr "Nu aveți permisiunea de a crea noi grupuri de discuții." msgstr "Nu aveți permisiunea să vă retrageți mesajul."
#: dist/converse-no-dependencies.js:51677 #: dist/converse-no-dependencies.js:51677
#: dist/converse-no-dependencies.js:58129 #: dist/converse-no-dependencies.js:58129
@ -125,9 +122,8 @@ msgstr ""
"permisă de server, care este %2$s." "permisă de server, care este %2$s."
#: dist/converse-no-dependencies.js:53208 #: dist/converse-no-dependencies.js:53208
#, fuzzy
msgid "Undecryptable OMEMO message" msgid "Undecryptable OMEMO message"
msgstr "Mesajul OMEMO nu poate fi criptat" msgstr "Mesaj OMEMO nedecriptabil"
#: dist/converse-no-dependencies.js:53272 #: dist/converse-no-dependencies.js:53272
msgid "Sorry, could not determine upload URL." msgid "Sorry, could not determine upload URL."
@ -154,43 +150,43 @@ msgstr "Ne pare rău, nu am putut încărca fișierul dumneavoastră."
#: dist/converse-no-dependencies.js:55882 #: dist/converse-no-dependencies.js:55882
msgid "Smileys and emotions" msgid "Smileys and emotions"
msgstr "" msgstr "Zâmbăreți și emoticoane"
#: dist/converse-no-dependencies.js:55883 #: dist/converse-no-dependencies.js:55883
msgid "People" msgid "People"
msgstr "" msgstr "Persoane"
#: dist/converse-no-dependencies.js:55884 #: dist/converse-no-dependencies.js:55884
msgid "Activities" msgid "Activities"
msgstr "" msgstr "Activități"
#: dist/converse-no-dependencies.js:55885 #: dist/converse-no-dependencies.js:55885
msgid "Travel" msgid "Travel"
msgstr "" msgstr "Călătorie"
#: dist/converse-no-dependencies.js:55886 #: dist/converse-no-dependencies.js:55886
msgid "Objects" msgid "Objects"
msgstr "" msgstr "Obiecte"
#: dist/converse-no-dependencies.js:55887 #: dist/converse-no-dependencies.js:55887
msgid "Animals and nature" msgid "Animals and nature"
msgstr "" msgstr "Animale și natură"
#: dist/converse-no-dependencies.js:55888 #: dist/converse-no-dependencies.js:55888
msgid "Food and drink" msgid "Food and drink"
msgstr "" msgstr "Mâncăruri și băuturi"
#: dist/converse-no-dependencies.js:55889 #: dist/converse-no-dependencies.js:55889
msgid "Symbols" msgid "Symbols"
msgstr "" msgstr "Simboluri"
#: dist/converse-no-dependencies.js:55890 #: dist/converse-no-dependencies.js:55890
msgid "Flags" msgid "Flags"
msgstr "" msgstr "Steaguri"
#: dist/converse-no-dependencies.js:55891 #: dist/converse-no-dependencies.js:55891
msgid "Stickers" msgid "Stickers"
msgstr "" msgstr "Autocolante"
#: dist/converse-no-dependencies.js:58127 #: dist/converse-no-dependencies.js:58127
msgid "" msgid ""
@ -210,37 +206,33 @@ msgstr ""
msgid "" msgid ""
"Your message was not delivered because you're not present in the groupchat." "Your message was not delivered because you're not present in the groupchat."
msgstr "" msgstr ""
"Mesajul dvs. nu a fost livrat deoarece nu sunteți prezent în grupul de chat."
#: dist/converse-no-dependencies.js:58502 #: dist/converse-no-dependencies.js:58502
#, fuzzy
msgid "A timeout happened while while trying to retract your message." msgid "A timeout happened while while trying to retract your message."
msgstr "S-a produs o eroare în timpul conexiunii la serverul de discuții." msgstr "A apărut o expirare de timp când încercați să vă retrageți mesajul."
#: dist/converse-no-dependencies.js:59253 #: dist/converse-no-dependencies.js:59253
#, fuzzy
msgid "Sorry, an error happened while running the command." msgid "Sorry, an error happened while running the command."
msgstr "" msgstr "Din păcate, s-a produs o eroare în timpul rulării comenzii."
"Din păcate, a avut loc o eroare în timpul rulării acestei comenzi. Pentru "
"detalii verificați consola de dezvoltare a navigatorului dvs."
#: dist/converse-no-dependencies.js:59253 #: dist/converse-no-dependencies.js:59253
#: dist/converse-no-dependencies.js:94373 #: dist/converse-no-dependencies.js:94373
#, fuzzy
msgid "Check your browser's developer console for details." msgid "Check your browser's developer console for details."
msgstr "" msgstr "Pentru detalii verificați consola de dezvoltare a navigatorului dvs."
"Din păcate, a avut loc o eroare în timpul rulării acestei comenzi. Pentru "
"detalii verificați consola de dezvoltare a navigatorului dvs."
#: dist/converse-no-dependencies.js:59278 #: dist/converse-no-dependencies.js:59278
#: dist/converse-no-dependencies.js:59301 #: dist/converse-no-dependencies.js:59301
#, fuzzy
msgid "Error: couldn't find a groupchat participant based on your arguments" msgid "Error: couldn't find a groupchat participant based on your arguments"
msgstr "Eroare: %1$s nu se găsește în acest grup de discuții" msgstr ""
"Eroare: nu s-a putut găsi un participant în grupul de discuții pe baza "
"argumentelor dvs"
#: dist/converse-no-dependencies.js:59288 #: dist/converse-no-dependencies.js:59288
#, fuzzy
msgid "Error: found multiple groupchat participant based on your arguments" msgid "Error: found multiple groupchat participant based on your arguments"
msgstr "Eroare: %1$s nu se găsește în acest grup de discuții" msgstr ""
"Eroare: am găsit mai mulți participanți la grupul de discuții pe baza "
"argumentelor dvs"
#: dist/converse-no-dependencies.js:59316 #: dist/converse-no-dependencies.js:59316
#, javascript-format #, javascript-format
@ -263,13 +255,15 @@ msgstr "Interzis: nu aveți autorizația necesară pentru a face acest lucru."
#. Strophe #. Strophe
#: dist/converse-no-dependencies.js:59877 #: dist/converse-no-dependencies.js:59877
msgid "You're not allowed to register yourself in this groupchat." msgid "You're not allowed to register yourself in this groupchat."
msgstr "" msgstr "Nu ai permisiunea să te înregistrezi în acest grup de discuții."
#. Strophe #. Strophe
#: dist/converse-no-dependencies.js:59879 #: dist/converse-no-dependencies.js:59879
msgid "" msgid ""
"You're not allowed to register in this groupchat because it's members-only." "You're not allowed to register in this groupchat because it's members-only."
msgstr "" msgstr ""
"Nu aveți voie să vă înregistrați în acest grup de discuții, deoarece este "
"doar pentru membri."
#. Strophe #. Strophe
#: dist/converse-no-dependencies.js:59924 #: dist/converse-no-dependencies.js:59924
@ -277,12 +271,16 @@ msgid ""
"Can't register your nickname in this groupchat, it doesn't support " "Can't register your nickname in this groupchat, it doesn't support "
"registration." "registration."
msgstr "" msgstr ""
"Nu vă puteți înregistra porecla în acest grup de discuții, nu acceptă "
"înregistrarea."
#. Strophe #. Strophe
#: dist/converse-no-dependencies.js:59926 #: dist/converse-no-dependencies.js:59926
msgid "" msgid ""
"Can't register your nickname in this groupchat, invalid data form supplied." "Can't register your nickname in this groupchat, invalid data form supplied."
msgstr "" msgstr ""
"Nu vă puteți înregistra porecla în acest grup de discuții, formular invalid "
"de date furnizat."
#: dist/converse-no-dependencies.js:60130 #: dist/converse-no-dependencies.js:60130
#, javascript-format #, javascript-format

View File

@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: Converse.js 0.10\n" "Project-Id-Version: Converse.js 0.10\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-03-17 12:35+0100\n" "POT-Creation-Date: 2021-03-17 12:35+0100\n"
"PO-Revision-Date: 2020-11-18 18:28+0100\n" "PO-Revision-Date: 2021-04-14 08:39+0000\n"
"Last-Translator: Andrey <andrey@mailbox.org>\n" "Last-Translator: member7me <zegucdx5@mail.ru>\n"
"Language-Team: Russian <https://hosted.weblate.org/projects/conversejs/" "Language-Team: Russian <https://hosted.weblate.org/projects/conversejs/"
"translations/ru/>\n" "translations/ru/>\n"
"Language: ru\n" "Language: ru\n"
@ -18,7 +18,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" "Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
"X-Generator: Weblate 4.3.1\n" "X-Generator: Weblate 4.6-dev\n"
#. Strophe #. Strophe
#: dist/converse-no-dependencies.js:42989 #: dist/converse-no-dependencies.js:42989
@ -200,27 +200,27 @@ msgid "A timeout happened while while trying to retract your message."
msgstr "Истекло время ожидания при попытке отозвать ваше сообщение." msgstr "Истекло время ожидания при попытке отозвать ваше сообщение."
#: dist/converse-no-dependencies.js:59253 #: dist/converse-no-dependencies.js:59253
#, fuzzy
msgid "Sorry, an error happened while running the command." msgid "Sorry, an error happened while running the command."
msgstr "" msgstr "К сожалению, при выполнении команды произошла ошибка."
"Извините, произошла ошибка при попытке сохранить данные вашего профиля."
#: dist/converse-no-dependencies.js:59253 #: dist/converse-no-dependencies.js:59253
#: dist/converse-no-dependencies.js:94373 #: dist/converse-no-dependencies.js:94373
#, fuzzy
msgid "Check your browser's developer console for details." msgid "Check your browser's developer console for details."
msgstr "Проверьте консоль вашего браузера для деталей об ошибках." msgstr ""
"Для получения деталей ошибки, проверьте консоль разработчика в браузере."
#: dist/converse-no-dependencies.js:59278 #: dist/converse-no-dependencies.js:59278
#: dist/converse-no-dependencies.js:59301 #: dist/converse-no-dependencies.js:59301
#, fuzzy
msgid "Error: couldn't find a groupchat participant based on your arguments" msgid "Error: couldn't find a groupchat participant based on your arguments"
msgstr "Ошибка: не удалось найти участника группового чата \"%1$s\"" msgstr ""
"Ошибка: не удалось найти участника группового чата на основании ваших "
"аргументов"
#: dist/converse-no-dependencies.js:59288 #: dist/converse-no-dependencies.js:59288
#, fuzzy
msgid "Error: found multiple groupchat participant based on your arguments" msgid "Error: found multiple groupchat participant based on your arguments"
msgstr "Все участники группового чата могут видеть ваш XMPP адрес" msgstr ""
"Ошибка: на основе ваших аргументов обнаружено несколько участников "
"группового чата"
#: dist/converse-no-dependencies.js:59316 #: dist/converse-no-dependencies.js:59316
#, javascript-format #, javascript-format
@ -302,9 +302,9 @@ msgid "%1$s is no longer a moderator"
msgstr "%1$s больше не модератор" msgstr "%1$s больше не модератор"
#: dist/converse-no-dependencies.js:60524 #: dist/converse-no-dependencies.js:60524
#, fuzzy, javascript-format #, javascript-format
msgid "%1$s has been given a voice" msgid "%1$s has been given a voice"
msgstr "%1$s снова получил право голоса" msgstr "%1$s получил право голоса"
#: dist/converse-no-dependencies.js:60526 #: dist/converse-no-dependencies.js:60526
#, javascript-format #, javascript-format
@ -317,49 +317,49 @@ msgid "%1$s and %2$s"
msgstr "%1$s и %2$s" msgstr "%1$s и %2$s"
#: dist/converse-no-dependencies.js:60539 #: dist/converse-no-dependencies.js:60539
#, fuzzy, javascript-format #, javascript-format
msgid "%1$s are typing" msgid "%1$s are typing"
msgstr "%1$s набирает текст" msgstr "%1$s печатает"
#: dist/converse-no-dependencies.js:60541 #: dist/converse-no-dependencies.js:60541
#, fuzzy, javascript-format #, javascript-format
msgid "%1$s have stopped typing" msgid "%1$s have stopped typing"
msgstr "%1$s прекратил печатать" msgstr "%1$s прекратил печатать"
#: dist/converse-no-dependencies.js:60543 #: dist/converse-no-dependencies.js:60543
#, fuzzy, javascript-format #, javascript-format
msgid "%1$s have gone away" msgid "%1$s have gone away"
msgstr "%1$s отошёл" msgstr "%1$s отошёл"
#: dist/converse-no-dependencies.js:60545 #: dist/converse-no-dependencies.js:60545
#, fuzzy, javascript-format #, javascript-format
msgid "%1$s have entered the groupchat" msgid "%1$s have entered the groupchat"
msgstr "%1$s вошёл в комнату" msgstr "%1$s вошёл в комнату"
#: dist/converse-no-dependencies.js:60547 #: dist/converse-no-dependencies.js:60547
#, fuzzy, javascript-format #, javascript-format
msgid "%1$s have left the groupchat" msgid "%1$s have left the groupchat"
msgstr "%1$s покинул комнату" msgstr "%1$s покинул комнату"
#: dist/converse-no-dependencies.js:60549 #: dist/converse-no-dependencies.js:60549
#, fuzzy, javascript-format #, javascript-format
msgid "%1$s are now moderators" msgid "%1$s are now moderators"
msgstr "%1$s теперь модератор" msgstr "%1$s теперь модератор"
#: dist/converse-no-dependencies.js:60551 #: dist/converse-no-dependencies.js:60551
#, fuzzy, javascript-format #, javascript-format
msgid "%1$s are no longer moderators" msgid "%1$s are no longer moderators"
msgstr "%1$s больше не модератор" msgstr "%1$s больше не модератор"
#: dist/converse-no-dependencies.js:60553 #: dist/converse-no-dependencies.js:60553
#, fuzzy, javascript-format #, javascript-format
msgid "%1$s have been given voices" msgid "%1$s have been given voices"
msgstr "%1$s снова получил право голоса" msgstr "%1$s были даны голоса"
#: dist/converse-no-dependencies.js:60555 #: dist/converse-no-dependencies.js:60555
#, fuzzy, javascript-format #, javascript-format
msgid "%1$s have been muted" msgid "%1$s have been muted"
msgstr "%1$s был приглушён" msgstr "%1$s отключены"
#: dist/converse-no-dependencies.js:60859 #: dist/converse-no-dependencies.js:60859
#, javascript-format #, javascript-format
@ -597,14 +597,13 @@ msgstr ""
"перезагрузить страницу, чтобы запросить их снова." "перезагрузить страницу, чтобы запросить их снова."
#: dist/converse-no-dependencies.js:65180 #: dist/converse-no-dependencies.js:65180
#, fuzzy
msgid "Timeout while trying to fetch archived messages." msgid "Timeout while trying to fetch archived messages."
msgstr "При попытке отозвать сообщение произошел таймаут" msgstr "Тайм-аут при попытке получить архивные сообщения."
#: dist/converse-no-dependencies.js:65195 #: dist/converse-no-dependencies.js:65195
#, fuzzy
msgid "An error occurred while querying for archived messages." msgid "An error occurred while querying for archived messages."
msgstr "Извините, произошла ошибка при попытке удаления устройств." msgstr ""
"Извините, произошла ошибка при попытке получения заархивированных сообщений."
#: dist/converse-no-dependencies.js:67158 #: dist/converse-no-dependencies.js:67158
#, javascript-format #, javascript-format
@ -721,7 +720,7 @@ msgid "Download audio file \"%1$s\""
msgstr "Скачать аудиофайл \"%1$s\"" msgstr "Скачать аудиофайл \"%1$s\""
#: dist/converse-no-dependencies.js:74156 #: dist/converse-no-dependencies.js:74156
#, fuzzy, javascript-format #, javascript-format
msgid "Download image file \"%1$s\"" msgid "Download image file \"%1$s\""
msgstr "Скачать изображение \"%1$s\"" msgstr "Скачать изображение \"%1$s\""
@ -734,6 +733,8 @@ msgstr "Скачать файл \"%1$s\""
msgid "" msgid ""
"You have an unsent message which will be lost if you continue. Are you sure?" "You have an unsent message which will be lost if you continue. Are you sure?"
msgstr "" msgstr ""
"У вас есть неотправленное сообщение, в случае, если вы продолжите, оно будет "
"потеряно. Вы уверены?"
#: dist/converse-no-dependencies.js:76031 #: dist/converse-no-dependencies.js:76031
#: dist/converse-no-dependencies.js:76122 #: dist/converse-no-dependencies.js:76122
@ -741,13 +742,14 @@ msgid ""
"Be aware that other XMPP/Jabber clients (and servers) may not yet support " "Be aware that other XMPP/Jabber clients (and servers) may not yet support "
"retractions and that this message may not be removed everywhere." "retractions and that this message may not be removed everywhere."
msgstr "" msgstr ""
"Имейте в виду, что другие XMPP клиенты (и серверы) могут еще не поддерживать "
"удаление и что это сообщение может быть не удалено везде."
#: dist/converse-no-dependencies.js:76032 #: dist/converse-no-dependencies.js:76032
#: dist/converse-no-dependencies.js:76129 #: dist/converse-no-dependencies.js:76129
#: dist/converse-no-dependencies.js:76166 #: dist/converse-no-dependencies.js:76166
#, fuzzy
msgid "Are you sure you want to retract this message?" msgid "Are you sure you want to retract this message?"
msgstr "Вы уверены, что хотите удалить этот контакт?" msgstr "Вы уверены, что хотите удалить это сообщение?"
#: dist/converse-no-dependencies.js:76039 #: dist/converse-no-dependencies.js:76039
#: dist/converse-no-dependencies.js:76136 #: dist/converse-no-dependencies.js:76136
@ -755,45 +757,37 @@ msgstr "Вы уверены, что хотите удалить этот кон
#: dist/converse-no-dependencies.js:97755 #: dist/converse-no-dependencies.js:97755
#: dist/converse-no-dependencies.js:99021 #: dist/converse-no-dependencies.js:99021
msgid "Confirm" msgid "Confirm"
msgstr "" msgstr "Подтвердить"
#: dist/converse-no-dependencies.js:76088 #: dist/converse-no-dependencies.js:76088
msgid "A timeout occurred while trying to retract the message" msgid "A timeout occurred while trying to retract the message"
msgstr "При попытке отозвать сообщение произошел таймаут" msgstr "При попытке отозвать сообщение произошел таймаут"
#: dist/converse-no-dependencies.js:76092 #: dist/converse-no-dependencies.js:76092
#, fuzzy
msgid "Sorry, you're not allowed to retract this message." msgid "Sorry, you're not allowed to retract this message."
msgstr "Вам не разрешено создавать новые комнаты." msgstr "Извините, вы не можете удалить это сообщение."
#: dist/converse-no-dependencies.js:76190 #: dist/converse-no-dependencies.js:76190
#, fuzzy
msgid "You are about to retract this message." msgid "You are about to retract this message."
msgstr "Вы собираетесь отозвать это сообщение." msgstr "Вы собираетесь удалить это сообщение."
#: dist/converse-no-dependencies.js:76190 #: dist/converse-no-dependencies.js:76190
#, fuzzy
msgid "" msgid ""
"You may optionally include a message, explaining the reason for the " "You may optionally include a message, explaining the reason for the "
"retraction." "retraction."
msgstr "" msgstr "По желанию вы можете добавить сообщение с объяснением причины отказа."
"Вы собираетесь пригласить %1$s в комнату \"%2$s\". По желанию вы можете "
"добавить сообщение с объяснением причины приглашения."
#: dist/converse-no-dependencies.js:76197 #: dist/converse-no-dependencies.js:76197
#, fuzzy
msgid "Message Retraction" msgid "Message Retraction"
msgstr "Версии сообщения" msgstr "Удаление сообщения"
#: dist/converse-no-dependencies.js:76197 #: dist/converse-no-dependencies.js:76197
#, fuzzy
msgid "Optional reason" msgid "Optional reason"
msgstr "Опционная подсказка" msgstr "Необязательная причина"
#: dist/converse-no-dependencies.js:76208 #: dist/converse-no-dependencies.js:76208
#, fuzzy
msgid "Sorry, you're not allowed to retract this message" msgid "Sorry, you're not allowed to retract this message"
msgstr "Вам не разрешено создавать новые комнаты." msgstr "Извините, вы не можете удалить это сообщение"
#: dist/converse-no-dependencies.js:76270 #: dist/converse-no-dependencies.js:76270
msgid "Cancel Editing" msgid "Cancel Editing"
@ -809,19 +803,19 @@ msgstr "Отозвать"
#: dist/converse-no-dependencies.js:76316 #: dist/converse-no-dependencies.js:76316
msgid "Show URL previews" msgid "Show URL previews"
msgstr "" msgstr "Показать превью URL"
#: dist/converse-no-dependencies.js:76316 #: dist/converse-no-dependencies.js:76316
msgid "Hide URL previews" msgid "Hide URL previews"
msgstr "" msgstr "Скрыть превью URL"
#: dist/converse-no-dependencies.js:76318 #: dist/converse-no-dependencies.js:76318
msgid "Show URL preview" msgid "Show URL preview"
msgstr "" msgstr "Показать превью URL"
#: dist/converse-no-dependencies.js:76318 #: dist/converse-no-dependencies.js:76318
msgid "Hide URL preview" msgid "Hide URL preview"
msgstr "" msgstr "Скрыть превью URL"
#. harmony default export #. harmony default export
#: dist/converse-no-dependencies.js:76429 #: dist/converse-no-dependencies.js:76429
@ -1109,18 +1103,16 @@ msgid "Toggle chat"
msgstr "Включить чат" msgstr "Включить чат"
#: dist/converse-no-dependencies.js:89998 #: dist/converse-no-dependencies.js:89998
#, fuzzy
msgid "Close these announcements" msgid "Close these announcements"
msgstr "Покинуть эту комнату" msgstr "Закрыть эти объявления"
#: dist/converse-no-dependencies.js:90595 #: dist/converse-no-dependencies.js:90595
msgid "Announcements" msgid "Announcements"
msgstr "" msgstr "Объявления"
#: dist/converse-no-dependencies.js:90599 #: dist/converse-no-dependencies.js:90599
#, fuzzy
msgid "Click to open this server message" msgid "Click to open this server message"
msgstr "Зайти в чат" msgstr "Нажмите, чтобы открыть это сообщение сервера"
#. harmony default export #. harmony default export
#: dist/converse-no-dependencies.js:90801 #: dist/converse-no-dependencies.js:90801
@ -1129,19 +1121,16 @@ msgstr "Кликните, чтобы развернуть чат"
#: dist/converse-no-dependencies.js:91053 #: dist/converse-no-dependencies.js:91053
#: dist/converse-no-dependencies.js:91071 #: dist/converse-no-dependencies.js:91071
#, fuzzy
msgid "Minimize" msgid "Minimize"
msgstr "Свёрнуто" msgstr "Свернуть"
#: dist/converse-no-dependencies.js:91054 #: dist/converse-no-dependencies.js:91054
#, fuzzy
msgid "Minimize this chat" msgid "Minimize this chat"
msgstr "Свернуть окно чата" msgstr "Свернуть окно чата"
#: dist/converse-no-dependencies.js:91072 #: dist/converse-no-dependencies.js:91072
#, fuzzy
msgid "Minimize this groupchat" msgid "Minimize this groupchat"
msgstr "Свернуть окно чата" msgstr "Свернуть этот групповой чат"
#. harmony default export #. harmony default export
#: dist/converse-no-dependencies.js:91421 #: dist/converse-no-dependencies.js:91421
@ -1151,23 +1140,24 @@ msgstr "Свёрнуто"
#. harmony default export #. harmony default export
#: dist/converse-no-dependencies.js:92962 #: dist/converse-no-dependencies.js:92962
msgid "Hide" msgid "Hide"
msgstr "" msgstr "Скрыть"
#: dist/converse-no-dependencies.js:92964 #: dist/converse-no-dependencies.js:92964
msgid "Execute" msgid "Execute"
msgstr "" msgstr "Выполнить"
#: dist/converse-no-dependencies.js:93293 #: dist/converse-no-dependencies.js:93293
msgid "" msgid ""
"Couldn't find a participant with that nickname. They might have left the " "Couldn't find a participant with that nickname. They might have left the "
"groupchat." "groupchat."
msgstr "" msgstr ""
"Не удалось найти участника с таким ником. Он мог покинуть групповой чат."
#. e.g. Your nickname is "coolguy69" #. e.g. Your nickname is "coolguy69"
#: dist/converse-no-dependencies.js:93422 #: dist/converse-no-dependencies.js:93422
#, fuzzy, javascript-format #, javascript-format
msgid "Your nickname is \"%1$s\"" msgid "Your nickname is \"%1$s\""
msgstr "Ваш псевдоним был изменён на: %1$s" msgstr "Ваш ник был изменён на: %1$s"
#: dist/converse-no-dependencies.js:93454 #: dist/converse-no-dependencies.js:93454
msgid "Error: invalid number of arguments" msgid "Error: invalid number of arguments"
@ -1176,13 +1166,15 @@ msgstr "Ошибка: неверное количество аргументов
#. harmony default export #. harmony default export
#: dist/converse-no-dependencies.js:93540 #: dist/converse-no-dependencies.js:93540
msgid "On which entity do you want to run commands?" msgid "On which entity do you want to run commands?"
msgstr "" msgstr "На чем вы хотите запускать команды?"
#: dist/converse-no-dependencies.js:93542 #: dist/converse-no-dependencies.js:93542
msgid "" msgid ""
"Certain XMPP services and entities allow privileged users to execute ad-hoc " "Certain XMPP services and entities allow privileged users to execute ad-hoc "
"commands on them." "commands on them."
msgstr "" msgstr ""
"Некоторые XMPP службы и объекты позволяют привилегированным пользователям "
"выполнять ad-hoc команды."
#: dist/converse-no-dependencies.js:93544 #: dist/converse-no-dependencies.js:93544
msgid "Commands found" msgid "Commands found"
@ -1190,20 +1182,19 @@ msgstr "Найдены команды"
#: dist/converse-no-dependencies.js:93546 #: dist/converse-no-dependencies.js:93546
msgid "List available commands" msgid "List available commands"
msgstr "" msgstr "Список доступных команд"
#: dist/converse-no-dependencies.js:93550 #: dist/converse-no-dependencies.js:93550
msgid "No commands found" msgid "No commands found"
msgstr "Комманд не найдено" msgstr "Комманд не найдено"
#: dist/converse-no-dependencies.js:93838 #: dist/converse-no-dependencies.js:93838
#, fuzzy
msgid "Sorry, an error occurred while looking for commands on that entity." msgid "Sorry, an error occurred while looking for commands on that entity."
msgstr "Извините, произошла ошибка при попытке удаления устройств." msgstr "Извините, произошла ошибка."
#: dist/converse-no-dependencies.js:93849 #: dist/converse-no-dependencies.js:93849
msgid "The specified entity doesn't support ad-hoc commands" msgid "The specified entity doesn't support ad-hoc commands"
msgstr "" msgstr "Указанный объект не поддерживает ad-hoc команды"
#: dist/converse-no-dependencies.js:93964 #: dist/converse-no-dependencies.js:93964
msgid "" msgid ""
@ -1215,12 +1206,11 @@ msgstr ""
#: dist/converse-no-dependencies.js:94078 #: dist/converse-no-dependencies.js:94078
msgid "Loading configuration form" msgid "Loading configuration form"
msgstr "" msgstr "Загрузка формы конфигурации"
#: dist/converse-no-dependencies.js:94373 #: dist/converse-no-dependencies.js:94373
#, fuzzy
msgid "Sorry, an error occurred while trying to submit the config form." msgid "Sorry, an error occurred while trying to submit the config form."
msgstr "Извините, произошла ошибка при попытке удаления устройств." msgstr "Извините, произошла ошибка при добавлении конфиг формы."
#. harmony default export #. harmony default export
#: dist/converse-no-dependencies.js:94447 #: dist/converse-no-dependencies.js:94447
@ -1320,7 +1310,7 @@ msgstr "Пользователи с этой ролью не найдены."
#: dist/converse-no-dependencies.js:94965 #: dist/converse-no-dependencies.js:94965
msgid "Type here to filter the search results" msgid "Type here to filter the search results"
msgstr "" msgstr "Введите здесь, чтобы отфильтровать результаты поиска"
#: dist/converse-no-dependencies.js:94969 #: dist/converse-no-dependencies.js:94969
msgid "Show users" msgid "Show users"
@ -1349,46 +1339,40 @@ msgstr ""
"администраторы и владельцы автоматически получают роль модератора." "администраторы и владельцы автоматически получают роль модератора."
#: dist/converse-no-dependencies.js:95313 #: dist/converse-no-dependencies.js:95313
#, fuzzy
msgid "Timeout error while trying to set the affiliation" msgid "Timeout error while trying to set the affiliation"
msgstr "Извините, что-то пошло не так при попытке обновления" msgstr "Ошибка тайм-аута"
#: dist/converse-no-dependencies.js:95315 #: dist/converse-no-dependencies.js:95315
#, fuzzy
msgid "Sorry, you're not allowed to make that change" msgid "Sorry, you're not allowed to make that change"
msgstr "Вам не разрешено вносить это изменение" msgstr "Извините, вам не разрешено вносить это изменение"
#: dist/converse-no-dependencies.js:95317 #: dist/converse-no-dependencies.js:95317
#, fuzzy
msgid "Sorry, something went wrong while trying to set the affiliation" msgid "Sorry, something went wrong while trying to set the affiliation"
msgstr "Извините, что-то пошло не так при попытке обновления" msgstr "Извините, что-то пошло не так при попытке установить принадлежность"
#: dist/converse-no-dependencies.js:95324 #: dist/converse-no-dependencies.js:95324
msgid "Affiliation changed" msgid "Affiliation changed"
msgstr "" msgstr "Принадлежность изменена"
#: dist/converse-no-dependencies.js:95359 #: dist/converse-no-dependencies.js:95359
#, fuzzy
msgid "Role changed" msgid "Role changed"
msgstr "Включить чат" msgstr "Роль изменена"
#: dist/converse-no-dependencies.js:95372 #: dist/converse-no-dependencies.js:95372
msgid "You're not allowed to make that change" msgid "You're not allowed to make that change"
msgstr "Вам не разрешено вносить это изменение" msgstr "Вам не разрешено вносить это изменение"
#: dist/converse-no-dependencies.js:95374 #: dist/converse-no-dependencies.js:95374
#, fuzzy
msgid "Sorry, something went wrong while trying to set the role" msgid "Sorry, something went wrong while trying to set the role"
msgstr "Извините, что-то пошло не так при попытке обновления" msgstr "Извините, что-то пошло не так при попытке установить роль"
#: dist/converse-no-dependencies.js:95419 #: dist/converse-no-dependencies.js:95419
msgid "Enter groupchat" msgid "Enter groupchat"
msgstr "Войти в комнату" msgstr "Войти в комнату"
#: dist/converse-no-dependencies.js:95421 #: dist/converse-no-dependencies.js:95421
#, fuzzy
msgid "Choose a nickname to enter" msgid "Choose a nickname to enter"
msgstr "Выберите файл для отправки" msgstr "Выберите ник для входа"
#: dist/converse-no-dependencies.js:95421 #: dist/converse-no-dependencies.js:95421
msgid "Please choose your nickname" msgid "Please choose your nickname"
@ -1408,17 +1392,15 @@ msgstr "Дать права администратора"
#: dist/converse-no-dependencies.js:96121 #: dist/converse-no-dependencies.js:96121
msgid "Ban user by changing their affiliation to outcast" msgid "Ban user by changing their affiliation to outcast"
msgstr "" msgstr "Забанить пользователя, изменив его принадлежность на выбывшего"
#: dist/converse-no-dependencies.js:96121 #: dist/converse-no-dependencies.js:96121
#, fuzzy
msgid "Clear the chat area" msgid "Clear the chat area"
msgstr "Закрыть это окно чата" msgstr "Очистить окно чата"
#: dist/converse-no-dependencies.js:96121 #: dist/converse-no-dependencies.js:96121
#, fuzzy
msgid "Close this groupchat" msgid "Close this groupchat"
msgstr "Покинуть эту комнату" msgstr "Закрыть групповой чат"
#: dist/converse-no-dependencies.js:96121 #: dist/converse-no-dependencies.js:96121
msgid "Change user role to participant" msgid "Change user role to participant"
@ -1443,7 +1425,7 @@ msgstr "Сделать пользователя участником"
#: dist/converse-no-dependencies.js:96121 #: dist/converse-no-dependencies.js:96121
msgid "Opens up the moderator tools GUI" msgid "Opens up the moderator tools GUI"
msgstr "" msgstr "Открывает GUI интерфейс инструментов модератора"
#: dist/converse-no-dependencies.js:96121 #: dist/converse-no-dependencies.js:96121
msgid "Remove user's ability to post messages" msgid "Remove user's ability to post messages"
@ -1462,13 +1444,12 @@ msgid "Grant ownership of this groupchat"
msgstr "Предоставить права владельца на этот чат" msgstr "Предоставить права владельца на этот чат"
#: dist/converse-no-dependencies.js:96121 #: dist/converse-no-dependencies.js:96121
#, fuzzy
msgid "Register your nickname" msgid "Register your nickname"
msgstr "Изменить свой псевдоним" msgstr "Зарегистрируйте свой ник"
#: dist/converse-no-dependencies.js:96121 #: dist/converse-no-dependencies.js:96121
msgid "Revoke the user's current affiliation" msgid "Revoke the user's current affiliation"
msgstr "" msgstr "Отменить текущую принадлежность пользователя"
#: dist/converse-no-dependencies.js:96121 #: dist/converse-no-dependencies.js:96121
msgid "Set groupchat subject" msgid "Set groupchat subject"
@ -1483,10 +1464,9 @@ msgid "Allow muted user to post messages"
msgstr "Разрешить заглушенным пользователям отправлять сообщения" msgstr "Разрешить заглушенным пользователям отправлять сообщения"
#: dist/converse-no-dependencies.js:96345 #: dist/converse-no-dependencies.js:96345
#, fuzzy
msgid "" msgid ""
"The conversation has moved to a new address. Click the link below to enter." "The conversation has moved to a new address. Click the link below to enter."
msgstr "Беседа перемещена. Нажмите ниже чтобы войти." msgstr "Беседа перемещена в новый адрес. Нажмите ниже, чтобы войти."
#. harmony default export #. harmony default export
#: dist/converse-no-dependencies.js:96353 #: dist/converse-no-dependencies.js:96353
@ -1494,9 +1474,9 @@ msgid "This groupchat no longer exists"
msgstr "Эта комната больше не существует" msgstr "Эта комната больше не существует"
#: dist/converse-no-dependencies.js:96355 #: dist/converse-no-dependencies.js:96355
#, fuzzy, javascript-format #, javascript-format
msgid "The following reason was given: \"%1$s\"" msgid "The following reason was given: \"%1$s\""
msgstr "Причиной является: \"%1$s\"." msgstr "Была указана следующая причина: \"%1$s\""
#: dist/converse-no-dependencies.js:96851 #: dist/converse-no-dependencies.js:96851
#, javascript-format #, javascript-format
@ -1515,19 +1495,16 @@ msgid "Invite"
msgstr "Пригласить" msgstr "Пригласить"
#: dist/converse-no-dependencies.js:96916 #: dist/converse-no-dependencies.js:96916
#, fuzzy
msgid "Invite someone to this groupchat" msgid "Invite someone to this groupchat"
msgstr "Покинуть эту комнату" msgstr "Пригласите кого-нибудь в этот групповой чат"
#: dist/converse-no-dependencies.js:96918 #: dist/converse-no-dependencies.js:96918
#, fuzzy
msgid "user@example.org" msgid "user@example.org"
msgstr "например, name@example.org" msgstr "name@example.org"
#: dist/converse-no-dependencies.js:96924 #: dist/converse-no-dependencies.js:96924
#, fuzzy
msgid "Optional reason for the invitation" msgid "Optional reason for the invitation"
msgstr "Опционная подсказка" msgstr "Необязательная причина приглашения"
#: dist/converse-no-dependencies.js:97156 #: dist/converse-no-dependencies.js:97156
msgid "Topic" msgid "Topic"
@ -1678,78 +1655,68 @@ msgstr "Информация конференции от %1$s"
#. harmony default export #. harmony default export
#: dist/converse-no-dependencies.js:97327 #: dist/converse-no-dependencies.js:97327
#, fuzzy
msgid "Hide the groupchat topic" msgid "Hide the groupchat topic"
msgstr "Войти в комнату" msgstr "Скрыть тему группового чата"
#: dist/converse-no-dependencies.js:97329 #: dist/converse-no-dependencies.js:97329
#, fuzzy
msgid "This groupchat is bookmarked" msgid "This groupchat is bookmarked"
msgstr "Эта комната модерируется" msgstr "Этот групповой чат добавлен в закладки"
#: dist/converse-no-dependencies.js:97659 #: dist/converse-no-dependencies.js:97659
#, fuzzy
msgid "Show more information about this groupchat" msgid "Show more information about this groupchat"
msgstr "Показать больше информации об этом чате" msgstr "Показать дополнительную информацию об этом групповом чате"
#: dist/converse-no-dependencies.js:97670 #: dist/converse-no-dependencies.js:97670
msgid "Configure" msgid "Configure"
msgstr "" msgstr "Настроить"
#: dist/converse-no-dependencies.js:97671 #: dist/converse-no-dependencies.js:97671
msgid "Configure this groupchat" msgid "Configure this groupchat"
msgstr "Настроить комнату" msgstr "Настроить комнату"
#: dist/converse-no-dependencies.js:97684 #: dist/converse-no-dependencies.js:97684
#, fuzzy
msgid "Invite someone to join this groupchat" msgid "Invite someone to join this groupchat"
msgstr "Каждый может присоединиться к этой комнате" msgstr "Предложите кому-нибудь присоединиться к этому групповому чату"
#: dist/converse-no-dependencies.js:97698 #: dist/converse-no-dependencies.js:97698
#, fuzzy
msgid "Show topic" msgid "Show topic"
msgstr "Показать список групп" msgstr "Показать тему"
#: dist/converse-no-dependencies.js:97698 #: dist/converse-no-dependencies.js:97698
msgid "Hide topic" msgid "Hide topic"
msgstr "" msgstr "Скрыть тему"
#: dist/converse-no-dependencies.js:97699 #: dist/converse-no-dependencies.js:97699
msgid "Show the topic message in the heading" msgid "Show the topic message in the heading"
msgstr "" msgstr "Показывать темы сообщения в заголовке"
#: dist/converse-no-dependencies.js:97699 #: dist/converse-no-dependencies.js:97699
msgid "Hide the topic in the heading" msgid "Hide the topic in the heading"
msgstr "" msgstr "Скрыть тему в заголовке"
#: dist/converse-no-dependencies.js:97716 #: dist/converse-no-dependencies.js:97716
#, fuzzy
msgid "Moderate" msgid "Moderate"
msgstr "Модерируемая" msgstr "Модерируемая"
#: dist/converse-no-dependencies.js:97717 #: dist/converse-no-dependencies.js:97717
#, fuzzy
msgid "Moderate this groupchat" msgid "Moderate this groupchat"
msgstr "Покинуть эту комнату" msgstr "Модерировать этот групповой чат"
#: dist/converse-no-dependencies.js:97729 #: dist/converse-no-dependencies.js:97729
#, fuzzy
msgid "Destroy" msgid "Destroy"
msgstr "Временный чат" msgstr "Уничтожить"
#: dist/converse-no-dependencies.js:97743 #: dist/converse-no-dependencies.js:97743
msgid "Leave" msgid "Leave"
msgstr "" msgstr "Покинуть"
#: dist/converse-no-dependencies.js:97744 #: dist/converse-no-dependencies.js:97744
#, fuzzy
msgid "Leave and close this groupchat" msgid "Leave and close this groupchat"
msgstr "Покинуть эту комнату" msgstr "Выйти и закрыть этот групповой чат"
#: dist/converse-no-dependencies.js:97753 #: dist/converse-no-dependencies.js:97753
#, fuzzy
msgid "Are you sure you want to leave this groupchat?" msgid "Are you sure you want to leave this groupchat?"
msgstr "Вы уверены, что хотите покинуть комнату \"%1$s\"?" msgstr "Вы уверены, что хотите покинуть этот групповой чат?"
#: dist/converse-no-dependencies.js:98120 #: dist/converse-no-dependencies.js:98120
msgid "This user is a moderator." msgid "This user is a moderator."
@ -1794,9 +1761,8 @@ msgid "Participants"
msgstr "Участники" msgstr "Участники"
#: dist/converse-no-dependencies.js:99003 #: dist/converse-no-dependencies.js:99003
#, fuzzy
msgid "Are you sure you want to destroy this groupchat?" msgid "Are you sure you want to destroy this groupchat?"
msgstr "Вы уверены, что хотите покинуть комнату \"%1$s\"?" msgstr "Вы уверены, что хотите уничтожить этот групповой чат?"
#: dist/converse-no-dependencies.js:99006 #: dist/converse-no-dependencies.js:99006
msgid "Please enter the XMPP address of this groupchat to confirm" msgid "Please enter the XMPP address of this groupchat to confirm"
@ -1814,12 +1780,11 @@ msgstr "Необязательная причина для удаления эт
#: dist/converse-no-dependencies.js:99016 #: dist/converse-no-dependencies.js:99016
msgid "Optional XMPP address for a new groupchat that replaces this one" msgid "Optional XMPP address for a new groupchat that replaces this one"
msgstr "" msgstr "Дополнительный XMPP адрес для нового группового чата, заменяющего этот"
#: dist/converse-no-dependencies.js:99017 #: dist/converse-no-dependencies.js:99017
#, fuzzy
msgid "replacement@example.org" msgid "replacement@example.org"
msgstr "например, name@example.org" msgstr "replace@example.org"
#: dist/converse-no-dependencies.js:99575 #: dist/converse-no-dependencies.js:99575
msgid "has gone offline" msgid "has gone offline"
@ -1852,7 +1817,6 @@ msgid "%1$s says"
msgstr "%1$s говорит" msgstr "%1$s говорит"
#: dist/converse-no-dependencies.js:99649 #: dist/converse-no-dependencies.js:99649
#, fuzzy
msgid "Encrypted message received" msgid "Encrypted message received"
msgstr "Получено зашифрованное сообщение" msgstr "Получено зашифрованное сообщение"
@ -1909,9 +1873,8 @@ msgid "Device without a fingerprint"
msgstr "Устройство без отпечатка" msgstr "Устройство без отпечатка"
#: dist/converse-no-dependencies.js:100394 #: dist/converse-no-dependencies.js:100394
#, fuzzy
msgid "Checkbox for selecting the following device" msgid "Checkbox for selecting the following device"
msgstr "Флаг для выбора следующих отпечатков" msgstr "Установите флажок для выбора следующего устройства"
#: dist/converse-no-dependencies.js:100404 #: dist/converse-no-dependencies.js:100404
msgid "Other OMEMO-enabled devices" msgid "Other OMEMO-enabled devices"
@ -1960,12 +1923,11 @@ msgstr ""
#: dist/converse-no-dependencies.js:100447 #: dist/converse-no-dependencies.js:100447
msgid "OMEMO" msgid "OMEMO"
msgstr "" msgstr "OMEMO"
#: dist/converse-no-dependencies.js:100449 #: dist/converse-no-dependencies.js:100449
#, fuzzy
msgid "Profile" msgid "Profile"
msgstr "Ваш профиль" msgstr "Профиль"
#: dist/converse-no-dependencies.js:100556 #: dist/converse-no-dependencies.js:100556
msgid "Sorry, an error happened while trying to save your profile data." msgid "Sorry, an error happened while trying to save your profile data."
@ -1982,13 +1944,12 @@ msgstr "Подробнее"
#: dist/converse-no-dependencies.js:100651 #: dist/converse-no-dependencies.js:100651
msgid "Commands" msgid "Commands"
msgstr "" msgstr "Команды"
#. harmony default export #. harmony default export
#: dist/converse-no-dependencies.js:100657 #: dist/converse-no-dependencies.js:100657
#, fuzzy
msgid "Settings" msgid "Settings"
msgstr "Изменить настройки" msgstr "Настройки"
#: dist/converse-no-dependencies.js:100659 #: dist/converse-no-dependencies.js:100659
#, javascript-format #, javascript-format
@ -2051,11 +2012,12 @@ msgid "Sorry, could not decrypt a received OMEMO message due to an error."
msgstr "К сожалению, не удалось расшифровать OMEMO сообщение." msgstr "К сожалению, не удалось расшифровать OMEMO сообщение."
#: dist/converse-no-dependencies.js:101694 #: dist/converse-no-dependencies.js:101694
#, fuzzy
msgid "" msgid ""
"Sorry, could not decrypt a received OMEMO because we don't have the JID for " "Sorry, could not decrypt a received OMEMO because we don't have the JID for "
"that user." "that user."
msgstr "К сожалению, не удалось расшифровать OMEMO сообщение." msgstr ""
"К сожалению, не удалось расшифровать полученный OMEMO, потому что у нас нет "
"JID для этого пользователя."
#: dist/converse-no-dependencies.js:102242 #: dist/converse-no-dependencies.js:102242
#, javascript-format #, javascript-format
@ -2067,13 +2029,13 @@ msgstr ""
"поддерживается в данной комнате." "поддерживается в данной комнате."
#: dist/converse-no-dependencies.js:102323 #: dist/converse-no-dependencies.js:102323
#, fuzzy
msgid "" msgid ""
"Cannot use end-to-end encryption in this groupchat, either the groupchat has " "Cannot use end-to-end encryption in this groupchat, either the groupchat has "
"some anonymity or not all participants support OMEMO." "some anonymity or not all participants support OMEMO."
msgstr "" msgstr ""
"Невозможно использовать сквозное шифрование в этой комнате, комната частично " "Невозможно использовать end-to-end encryption шифрование в этой комнате, "
"анонимна или не все участники имеют поддержку шифрования OMEMO." "комната частично анонимна или не все участники имеют поддержку шифрования "
"OMEMO."
#: dist/converse-no-dependencies.js:102325 #: dist/converse-no-dependencies.js:102325
#, javascript-format #, javascript-format
@ -2089,15 +2051,16 @@ msgid "Messages are being sent in plaintext"
msgstr "Сообщения отправляются в виде открытого текста" msgstr "Сообщения отправляются в виде открытого текста"
#: dist/converse-no-dependencies.js:102346 #: dist/converse-no-dependencies.js:102346
#, fuzzy
msgid "Messages are sent encrypted" msgid "Messages are sent encrypted"
msgstr "Ваши сообщения больше не шифруются" msgstr "Сообщения отправляются в зашифрованном виде"
#: dist/converse-no-dependencies.js:102350 #: dist/converse-no-dependencies.js:102350
msgid "" msgid ""
"This groupchat needs to be members-only and non-anonymous in order to " "This groupchat needs to be members-only and non-anonymous in order to "
"support OMEMO encrypted messages" "support OMEMO encrypted messages"
msgstr "" msgstr ""
"Чтобы поддерживать зашифрованные сообщения OMEMO, этот групповой чат должен "
"быть для пользователей и не анонимным"
#: dist/converse-no-dependencies.js:102379 #: dist/converse-no-dependencies.js:102379
#, javascript-format #, javascript-format
@ -2137,12 +2100,11 @@ msgstr ""
"расшифрованы на этом девайсе." "расшифрованы на этом девайсе."
#: dist/converse-no-dependencies.js:102629 #: dist/converse-no-dependencies.js:102629
#, fuzzy
msgid "" msgid ""
"Sorry, no devices found to which we can send an OMEMO encrypted message." "Sorry, no devices found to which we can send an OMEMO encrypted message."
msgstr "" msgstr ""
"Извините, мы не смогли найти ни одно устройство которому можно отправить " "К сожалению, не найдено устройств, на которые можно отправить зашифрованное "
"зашифрованное сообщение." "сообщение OMEMO."
#: dist/converse-no-dependencies.js:102732 #: dist/converse-no-dependencies.js:102732
msgid "" msgid ""
@ -2417,9 +2379,9 @@ msgstr "Нажми что-бы удалить %1$s как контакт"
#. harmony default export #. harmony default export
#: dist/converse-no-dependencies.js:107574 #: dist/converse-no-dependencies.js:107574
#, fuzzy, javascript-format #, javascript-format
msgid "Click to chat with %1$s (XMPP address: %2$s)" msgid "Click to chat with %1$s (XMPP address: %2$s)"
msgstr "Нажмите для чата с %1$s (Идентификатор Jabber: %2$s)" msgstr "Нажмите, чтобы начать чат с %1$s (адрес XMPP: %2$s)"
#: dist/converse-no-dependencies.js:107839 #: dist/converse-no-dependencies.js:107839
#, javascript-format #, javascript-format

View File

@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: Converse.js 6.0.0\n" "Project-Id-Version: Converse.js 6.0.0\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-03-17 10:11+0100\n" "POT-Creation-Date: 2021-03-17 10:11+0100\n"
"PO-Revision-Date: 2021-04-09 13:32+0200\n" "PO-Revision-Date: 2021-06-25 10:43+0200\n"
"Last-Translator: Kim Alvefur <zash@zash.se>\n" "Last-Translator: Kim Alvefur <zash@zash.se>\n"
"Language-Team: Swedish <https://hosted.weblate.org/projects/conversejs/" "Language-Team: Swedish <https://hosted.weblate.org/projects/conversejs/"
"translations/sv/>\n" "translations/sv/>\n"
@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n" "Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.5.2-dev\n" "X-Generator: Weblate 4.7-dev\n"
#. Strophe #. Strophe
#: dist/converse-no-dependencies.js:42989 #: dist/converse-no-dependencies.js:42989
@ -83,21 +83,6 @@ msgstr "Du har inte behörighet att skicka ett meddelande."
msgid "Sorry, an error occurred while trying to send your message." msgid "Sorry, an error occurred while trying to send your message."
msgstr "Ledsen, ett fel uppstod när meddelandet skulle skickas." msgstr "Ledsen, ett fel uppstod när meddelandet skulle skickas."
#: dist/converse-no-dependencies.js:51955
#, javascript-format
msgid "%1$s has gone offline"
msgstr ""
#: dist/converse-no-dependencies.js:51959
#, javascript-format
msgid "%1$s is busy"
msgstr ""
#: dist/converse-no-dependencies.js:51961
#, javascript-format
msgid "%1$s is online"
msgstr ""
#: dist/converse-no-dependencies.js:52840 #: dist/converse-no-dependencies.js:52840
#: dist/converse-no-dependencies.js:52863 #: dist/converse-no-dependencies.js:52863
msgid "Sorry, looks like file upload is not supported by your server." msgid "Sorry, looks like file upload is not supported by your server."
@ -1045,7 +1030,7 @@ msgstr "Visa denna meny"
#: dist/converse-no-dependencies.js:84722 #: dist/converse-no-dependencies.js:84722
#, javascript-format #, javascript-format
msgid "%1$s has gone offline" msgid "%1$s has gone offline"
msgstr "%1$s kopplade ifrån" msgstr "%1$s har kopplat ner"
#: dist/converse-no-dependencies.js:84726 #: dist/converse-no-dependencies.js:84726
#, javascript-format #, javascript-format
@ -1055,7 +1040,7 @@ msgstr "%1$s är upptagen"
#: dist/converse-no-dependencies.js:84728 #: dist/converse-no-dependencies.js:84728
#, javascript-format #, javascript-format
msgid "%1$s is online" msgid "%1$s is online"
msgstr "%1$s är uppkopplad" msgstr "%1$s är online"
#: dist/converse-no-dependencies.js:87546 #: dist/converse-no-dependencies.js:87546
msgid "" msgid ""

View File

@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: Converse.js 3.3.2\n" "Project-Id-Version: Converse.js 3.3.2\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-03-17 12:35+0100\n" "POT-Creation-Date: 2021-03-17 12:35+0100\n"
"PO-Revision-Date: 2020-12-16 10:29+0000\n" "PO-Revision-Date: 2021-04-10 06:04+0000\n"
"Last-Translator: Oğuz Ersen <oguzersen@protonmail.com>\n" "Last-Translator: Oğuz Ersen <oguzersen@protonmail.com>\n"
"Language-Team: Turkish <https://hosted.weblate.org/projects/conversejs/" "Language-Team: Turkish <https://hosted.weblate.org/projects/conversejs/"
"translations/tr/>\n" "translations/tr/>\n"
@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n" "Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.4-dev\n" "X-Generator: Weblate 4.6-dev\n"
#. Strophe #. Strophe
#: dist/converse-no-dependencies.js:42989 #: dist/converse-no-dependencies.js:42989
@ -727,7 +727,6 @@ msgstr ""
#: dist/converse-no-dependencies.js:76031 #: dist/converse-no-dependencies.js:76031
#: dist/converse-no-dependencies.js:76122 #: dist/converse-no-dependencies.js:76122
#, fuzzy
msgid "" msgid ""
"Be aware that other XMPP/Jabber clients (and servers) may not yet support " "Be aware that other XMPP/Jabber clients (and servers) may not yet support "
"retractions and that this message may not be removed everywhere." "retractions and that this message may not be removed everywhere."
@ -739,7 +738,6 @@ msgstr ""
#: dist/converse-no-dependencies.js:76032 #: dist/converse-no-dependencies.js:76032
#: dist/converse-no-dependencies.js:76129 #: dist/converse-no-dependencies.js:76129
#: dist/converse-no-dependencies.js:76166 #: dist/converse-no-dependencies.js:76166
#, fuzzy
msgid "Are you sure you want to retract this message?" msgid "Are you sure you want to retract this message?"
msgstr "Bu mesajı geri çekmek istediğinizden emin misiniz?" msgstr "Bu mesajı geri çekmek istediğinizden emin misiniz?"
@ -760,12 +758,10 @@ msgid "Sorry, you're not allowed to retract this message."
msgstr "Üzgünüm, bu mesajı geri çekmenize izin verilmiyor." msgstr "Üzgünüm, bu mesajı geri çekmenize izin verilmiyor."
#: dist/converse-no-dependencies.js:76190 #: dist/converse-no-dependencies.js:76190
#, fuzzy
msgid "You are about to retract this message." msgid "You are about to retract this message."
msgstr "Bu mesajı geri çekmek üzeresiniz." msgstr "Bu mesajı geri çekmek üzeresiniz."
#: dist/converse-no-dependencies.js:76190 #: dist/converse-no-dependencies.js:76190
#, fuzzy
msgid "" msgid ""
"You may optionally include a message, explaining the reason for the " "You may optionally include a message, explaining the reason for the "
"retraction." "retraction."
@ -774,18 +770,16 @@ msgstr ""
"ekleyebilirsiniz." "ekleyebilirsiniz."
#: dist/converse-no-dependencies.js:76197 #: dist/converse-no-dependencies.js:76197
#, fuzzy
msgid "Message Retraction" msgid "Message Retraction"
msgstr "Mesaj versiyonları" msgstr "Mesaj Geri Çekme"
#: dist/converse-no-dependencies.js:76197 #: dist/converse-no-dependencies.js:76197
msgid "Optional reason" msgid "Optional reason"
msgstr "İsteğe bağlı neden" msgstr "İsteğe bağlı neden"
#: dist/converse-no-dependencies.js:76208 #: dist/converse-no-dependencies.js:76208
#, fuzzy
msgid "Sorry, you're not allowed to retract this message" msgid "Sorry, you're not allowed to retract this message"
msgstr "Üzgünüm, bu mesajı geri çekmenize izin verilmiyor." msgstr "Üzgünüm, bu mesajı geri çekmenize izin verilmiyor"
#: dist/converse-no-dependencies.js:76270 #: dist/converse-no-dependencies.js:76270
msgid "Cancel Editing" msgid "Cancel Editing"
@ -801,19 +795,19 @@ msgstr "Geri çek"
#: dist/converse-no-dependencies.js:76316 #: dist/converse-no-dependencies.js:76316
msgid "Show URL previews" msgid "Show URL previews"
msgstr "" msgstr "URL ön izlemelerini göster"
#: dist/converse-no-dependencies.js:76316 #: dist/converse-no-dependencies.js:76316
msgid "Hide URL previews" msgid "Hide URL previews"
msgstr "" msgstr "URL ön izlemelerini gizle"
#: dist/converse-no-dependencies.js:76318 #: dist/converse-no-dependencies.js:76318
msgid "Show URL preview" msgid "Show URL preview"
msgstr "" msgstr "URL ön izlemesini göster"
#: dist/converse-no-dependencies.js:76318 #: dist/converse-no-dependencies.js:76318
msgid "Hide URL preview" msgid "Hide URL preview"
msgstr "" msgstr "URL ön izlemesini gizle"
#. harmony default export #. harmony default export
#: dist/converse-no-dependencies.js:76429 #: dist/converse-no-dependencies.js:76429
@ -993,7 +987,7 @@ msgstr "Bu kişi hakkında daha fazla bilgi göster"
#: dist/converse-no-dependencies.js:82976 #: dist/converse-no-dependencies.js:82976
msgid "Close and end this conversation" msgid "Close and end this conversation"
msgstr "Bu sohbeti kapat ve sonlandır" msgstr "Bu konuşmayı kapat ve sonlandır"
#: dist/converse-no-dependencies.js:83478 #: dist/converse-no-dependencies.js:83478
msgid "Hidden message" msgid "Hidden message"
@ -1204,7 +1198,7 @@ msgstr ""
#: dist/converse-no-dependencies.js:94078 #: dist/converse-no-dependencies.js:94078
msgid "Loading configuration form" msgid "Loading configuration form"
msgstr "" msgstr "Yapılandırma formu yükleniyor"
#: dist/converse-no-dependencies.js:94373 #: dist/converse-no-dependencies.js:94373
msgid "Sorry, an error occurred while trying to submit the config form." msgid "Sorry, an error occurred while trying to submit the config form."
@ -1460,10 +1454,10 @@ msgid "Allow muted user to post messages"
msgstr "Sessiz kullanıcının mesaj göndermesine izin ver" msgstr "Sessiz kullanıcının mesaj göndermesine izin ver"
#: dist/converse-no-dependencies.js:96345 #: dist/converse-no-dependencies.js:96345
#, fuzzy
msgid "" msgid ""
"The conversation has moved to a new address. Click the link below to enter." "The conversation has moved to a new address. Click the link below to enter."
msgstr "Konuşma taşındı. Girmek için aşağıya tıklayın." msgstr ""
"Konuşma yeni bir adrese taşındı. Girmek için aşağıdaki bağlantıya tıklayın."
#. harmony default export #. harmony default export
#: dist/converse-no-dependencies.js:96353 #: dist/converse-no-dependencies.js:96353
@ -1471,9 +1465,9 @@ msgid "This groupchat no longer exists"
msgstr "Bu grup sohbeti artık mevcut değil" msgstr "Bu grup sohbeti artık mevcut değil"
#: dist/converse-no-dependencies.js:96355 #: dist/converse-no-dependencies.js:96355
#, fuzzy, javascript-format #, javascript-format
msgid "The following reason was given: \"%1$s\"" msgid "The following reason was given: \"%1$s\""
msgstr "Verilen sebep: \"%1$s\"." msgstr "Şu neden verildi: \"%1$s\""
#: dist/converse-no-dependencies.js:96851 #: dist/converse-no-dependencies.js:96851
#, javascript-format #, javascript-format

View File

@ -16,7 +16,7 @@ const subject = (o) => {
export default (o) => { export default (o) => {
const i18n_address = __('Groupchat address (JID)'); const i18n_address = __('Groupchat XMPP address');
const i18n_archiving = __('Message archiving'); const i18n_archiving = __('Message archiving');
const i18n_archiving_help = __('Messages are archived on the server'); const i18n_archiving_help = __('Messages are archived on the server');
const i18n_desc = __('Description'); const i18n_desc = __('Description');

View File

@ -91,7 +91,7 @@ export default (o) => {
const heading_profile = __('Your Profile'); const heading_profile = __('Your Profile');
const i18n_email = __('Email'); const i18n_email = __('Email');
const i18n_fullname = __('Full Name'); const i18n_fullname = __('Full Name');
const i18n_jid = __('XMPP Address (JID)'); const i18n_jid = __('XMPP Address');
const i18n_nickname = __('Nickname'); const i18n_nickname = __('Nickname');
const i18n_role = __('Role'); const i18n_role = __('Role');
const i18n_save = __('Save and close'); const i18n_save = __('Save and close');

View File

@ -6,7 +6,7 @@ const { Strophe, u, sizzle, $iq } = converse.env;
describe("A chat room", function () { describe("A chat room", function () {
it("can be bookmarked", mock.initConverse( it("can be bookmarked", mock.initConverse(
['chatBoxesFetched'], {}, async function (done, _converse) { ['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current', 0); await mock.waitForRoster(_converse, 'current', 0);
await mock.waitUntilDiscoConfirmed( await mock.waitUntilDiscoConfirmed(
@ -14,9 +14,8 @@ describe("A chat room", function () {
[{'category': 'pubsub', 'type': 'pep'}], [{'category': 'pubsub', 'type': 'pep'}],
['http://jabber.org/protocol/pubsub#publish-options'] ['http://jabber.org/protocol/pubsub#publish-options']
); );
const { u, $iq } = converse.env;
spyOn(_converse.connection, 'getUniqueId').and.callThrough();
const { u, $iq } = converse.env;
const nick = 'JC'; const nick = 'JC';
const muc_jid = 'theplay@conference.shakespeare.lit'; const muc_jid = 'theplay@conference.shakespeare.lit';
await mock.openChatRoom(_converse, 'theplay', 'conference.shakespeare.lit', 'JC'); await mock.openChatRoom(_converse, 'theplay', 'conference.shakespeare.lit', 'JC');
@ -37,6 +36,7 @@ describe("A chat room", function () {
cancel_button.click(); cancel_button.click();
await u.waitUntil(() => view.model.session.get('view') === null); await u.waitUntil(() => view.model.session.get('view') === null);
expect(u.hasClass('on-button', toggle), false); expect(u.hasClass('on-button', toggle), false);
expect(toggle.title).toBe('Bookmark this groupchat'); expect(toggle.title).toBe('Bookmark this groupchat');
@ -128,12 +128,11 @@ describe("A chat room", function () {
expect(u.hasClass('on-button', view.querySelector('.toggle-bookmark')), true); expect(u.hasClass('on-button', view.querySelector('.toggle-bookmark')), true);
// We ignore this IQ stanza... (unless it's an error stanza), so // We ignore this IQ stanza... (unless it's an error stanza), so
// nothing to test for here. // nothing to test for here.
done();
})); }));
it("will be automatically opened if 'autojoin' is set on the bookmark", mock.initConverse( it("will be automatically opened if 'autojoin' is set on the bookmark", mock.initConverse(
[], {}, async function (done, _converse) { ['chatBoxesFetched'], {}, async function (_converse) {
const { u } = converse.env; const { u } = converse.env;
await mock.waitForRoster(_converse, 'current', 0); await mock.waitForRoster(_converse, 'current', 0);
@ -172,13 +171,12 @@ describe("A chat room", function () {
'nick': ' Othello' 'nick': ' Othello'
}); });
expect(_converse.chatboxviews.get(jid) === undefined).toBe(true); expect(_converse.chatboxviews.get(jid) === undefined).toBe(true);
done();
})); }));
describe("when bookmarked", function () { describe("when bookmarked", function () {
it("will use the nickname from the bookmark", mock.initConverse([], {}, async function (done, _converse) { it("will use the nickname from the bookmark", mock.initConverse([], {}, async function (_converse) {
const { u } = converse.env; const { u } = converse.env;
await mock.waitForRoster(_converse, 'current', 0); await mock.waitForRoster(_converse, 'current', 0);
await mock.waitUntilBookmarksReturned(_converse); await mock.waitUntilBookmarksReturned(_converse);
@ -195,10 +193,9 @@ describe("A chat room", function () {
const room = await room_creation_promise; const room = await room_creation_promise;
await u.waitUntil(() => room.getAndPersistNickname.calls.count()); await u.waitUntil(() => room.getAndPersistNickname.calls.count());
expect(room.get('nick')).toBe('Othello'); expect(room.get('nick')).toBe('Othello');
done();
})); }));
it("displays that it's bookmarked through its bookmark icon", mock.initConverse([], {}, async function (done, _converse) { it("displays that it's bookmarked through its bookmark icon", mock.initConverse([], {}, async function (_converse) {
const { u } = converse.env; const { u } = converse.env;
await mock.waitForRoster(_converse, 'current', 0); await mock.waitForRoster(_converse, 'current', 0);
@ -220,10 +217,9 @@ describe("A chat room", function () {
await u.waitUntil(() => view.querySelector('.chatbox-title__text .fa-bookmark') !== null); await u.waitUntil(() => view.querySelector('.chatbox-title__text .fa-bookmark') !== null);
view.model.set('bookmarked', false); view.model.set('bookmarked', false);
await u.waitUntil(() => view.querySelector('.chatbox-title__text .fa-bookmark') === null); await u.waitUntil(() => view.querySelector('.chatbox-title__text .fa-bookmark') === null);
done();
})); }));
it("can be unbookmarked", mock.initConverse([], {}, async function (done, _converse) { it("can be unbookmarked", mock.initConverse([], {}, async function (_converse) {
const { u, Strophe } = converse.env; const { u, Strophe } = converse.env;
await mock.waitForRoster(_converse, 'current', 0); await mock.waitForRoster(_converse, 'current', 0);
@ -282,14 +278,13 @@ describe("A chat room", function () {
`</pubsub>`+ `</pubsub>`+
`</iq>` `</iq>`
); );
done();
})); }));
}); });
describe("and when autojoin is set", function () { describe("and when autojoin is set", function () {
it("will be be opened and joined automatically upon login", mock.initConverse( it("will be be opened and joined automatically upon login", mock.initConverse(
[], {}, async function (done, _converse) { [], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current', 0); await mock.waitForRoster(_converse, 'current', 0);
await mock.waitUntilBookmarksReturned(_converse); await mock.waitUntilBookmarksReturned(_converse);
@ -310,7 +305,6 @@ describe("A chat room", function () {
'nick': '' 'nick': ''
}); });
expect(_converse.api.rooms.create).toHaveBeenCalled(); expect(_converse.api.rooms.create).toHaveBeenCalled();
done();
})); }));
}); });
}); });
@ -318,7 +312,7 @@ describe("A chat room", function () {
describe("Bookmarks", function () { describe("Bookmarks", function () {
it("can be pushed from the XMPP server", mock.initConverse( it("can be pushed from the XMPP server", mock.initConverse(
['connected', 'chatBoxesFetched'], {}, async function (done, _converse) { ['connected', 'chatBoxesFetched'], {}, async function (_converse) {
const { $msg, u } = converse.env; const { $msg, u } = converse.env;
await mock.waitForRoster(_converse, 'current', 0); await mock.waitForRoster(_converse, 'current', 0);
@ -403,13 +397,12 @@ describe("Bookmarks", function () {
expect(_converse.bookmarks.map(b => b.get('name'))).toEqual(['Second bookmark', 'The Play&apos;s the Thing', 'Yet another bookmark']); expect(_converse.bookmarks.map(b => b.get('name'))).toEqual(['Second bookmark', 'The Play&apos;s the Thing', 'Yet another bookmark']);
expect(_converse.chatboxviews.get('theplay@conference.shakespeare.lit')).not.toBeUndefined(); expect(_converse.chatboxviews.get('theplay@conference.shakespeare.lit')).not.toBeUndefined();
expect(Object.keys(_converse.chatboxviews.getAll()).length).toBe(2); expect(Object.keys(_converse.chatboxviews.getAll()).length).toBe(2);
done();
})); }));
it("can be retrieved from the XMPP server", mock.initConverse( it("can be retrieved from the XMPP server", mock.initConverse(
['chatBoxesFetched'], {}, ['chatBoxesFetched'], {},
async function (done, _converse) { async function (_converse) {
const { Strophe, sizzle, u, $iq } = converse.env; const { Strophe, sizzle, u, $iq } = converse.env;
await mock.waitForRoster(_converse, 'current', 0); await mock.waitForRoster(_converse, 'current', 0);
@ -483,13 +476,12 @@ describe("Bookmarks", function () {
expect(_converse.bookmarks.models.length).toBe(2); expect(_converse.bookmarks.models.length).toBe(2);
expect(_converse.bookmarks.findWhere({'jid': 'theplay@conference.shakespeare.lit'}).get('autojoin')).toBe(true); expect(_converse.bookmarks.findWhere({'jid': 'theplay@conference.shakespeare.lit'}).get('autojoin')).toBe(true);
expect(_converse.bookmarks.findWhere({'jid': 'another@conference.shakespeare.lit'}).get('autojoin')).toBe(false); expect(_converse.bookmarks.findWhere({'jid': 'another@conference.shakespeare.lit'}).get('autojoin')).toBe(false);
done();
})); }));
describe("The bookmarks list", function () { describe("The bookmarks list", function () {
it("shows a list of bookmarks", mock.initConverse( it("shows a list of bookmarks", mock.initConverse(
[], {}, async function (done, _converse) { [], {}, async function (_converse) {
await mock.waitUntilDiscoConfirmed( await mock.waitUntilDiscoConfirmed(
_converse, _converse.bare_jid, _converse, _converse.bare_jid,
@ -560,11 +552,10 @@ describe("Bookmarks", function () {
expect(els[1].textContent).toBe("Bookmark with a very very long name that will be shortened"); expect(els[1].textContent).toBe("Bookmark with a very very long name that will be shortened");
expect(els[2].textContent).toBe("noname@conference.shakespeare.lit"); expect(els[2].textContent).toBe("noname@conference.shakespeare.lit");
expect(els[3].textContent).toBe("The Play's the Thing"); expect(els[3].textContent).toBe("The Play's the Thing");
done();
})); }));
it("can be used to open a MUC from a bookmark", mock.initConverse( it("can be used to open a MUC from a bookmark", mock.initConverse(
[], {'view_mode': 'fullscreen'}, async function (done, _converse) { [], {'view_mode': 'fullscreen'}, async function (_converse) {
const api = _converse.api; const api = _converse.api;
await mock.waitUntilDiscoConfirmed( await mock.waitUntilDiscoConfirmed(
@ -610,11 +601,10 @@ describe("Bookmarks", function () {
await u.waitUntil(() => view.querySelector('.list-item.open').getAttribute('data-room-jid') === 'first@conference.shakespeare.lit'); await u.waitUntil(() => view.querySelector('.list-item.open').getAttribute('data-room-jid') === 'first@conference.shakespeare.lit');
expect((await api.rooms.get('first@conference.shakespeare.lit')).get('hidden')).toBe(false); expect((await api.rooms.get('first@conference.shakespeare.lit')).get('hidden')).toBe(false);
expect((await api.rooms.get('theplay@conference.shakespeare.lit')).get('hidden')).toBe(true); expect((await api.rooms.get('theplay@conference.shakespeare.lit')).get('hidden')).toBe(true);
done();
})); }));
it("remembers the toggle state of the bookmarks list", mock.initConverse( it("remembers the toggle state of the bookmarks list", mock.initConverse(
[], {}, async function (done, _converse) { [], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current', 0); await mock.waitForRoster(_converse, 'current', 0);
await mock.openControlBox(_converse); await mock.openControlBox(_converse);
@ -665,7 +655,6 @@ describe("Bookmarks", function () {
expect(u.hasClass('collapsed', sizzle('#chatrooms .bookmarks.rooms-list', chats_el).pop())).toBeFalsy(); expect(u.hasClass('collapsed', sizzle('#chatrooms .bookmarks.rooms-list', chats_el).pop())).toBeFalsy();
expect(sizzle(selector, chats_el).filter(u.isVisible).length).toBe(1); expect(sizzle(selector, chats_el).filter(u.isVisible).length).toBe(1);
expect(bookmarks_el.model.get('toggle-state')).toBe(_converse.OPENED); expect(bookmarks_el.model.get('toggle-state')).toBe(_converse.OPENED);
done();
})); }));
}); });
}); });
@ -673,7 +662,7 @@ describe("Bookmarks", function () {
describe("When hide_open_bookmarks is true and a bookmarked room is opened", function () { describe("When hide_open_bookmarks is true and a bookmarked room is opened", function () {
it("can be closed", mock.initConverse( it("can be closed", mock.initConverse(
[], { hide_open_bookmarks: true }, async function (done, _converse) { [], { hide_open_bookmarks: true }, async function (_converse) {
await mock.waitForRoster(_converse, 'current', 0); await mock.waitForRoster(_converse, 'current', 0);
await mock.openControlBox(_converse); await mock.openControlBox(_converse);
@ -704,6 +693,5 @@ describe("When hide_open_bookmarks is true and a bookmarked room is opened", fun
const view = _converse.chatboxviews.get(jid); const view = _converse.chatboxviews.get(jid);
view.close(); view.close();
await u.waitUntil(() => !u.hasClass('hidden', bookmarks_el.querySelector(".available-chatroom"))); await u.waitUntil(() => !u.hasClass('hidden', bookmarks_el.querySelector(".available-chatroom")));
done();
})); }));
}); });

View File

@ -15,17 +15,26 @@ export default class ChatBottomPanel extends ElementView {
'click .toggle-clear': 'clearMessages' 'click .toggle-clear': 'clearMessages'
}; };
constructor () {
super();
this.debouncedRender = debounce(this.render, 100);
}
async connectedCallback () { async connectedCallback () {
super.connectedCallback(); super.connectedCallback();
this.debouncedRender = debounce(this.render, 100); await this.initialize();
this.model = _converse.chatboxes.get(this.getAttribute('jid')); this.render(); // don't call in initialize, since the MUCBottomPanel subclasses it
// and we want to render after it has finished as wel.
}
async initialize () {
this.model = await api.chatboxes.get(this.getAttribute('jid'));
await this.model.initialized; await this.model.initialized;
this.listenTo(this.model, 'change:num_unread', this.debouncedRender) this.listenTo(this.model, 'change:num_unread', this.debouncedRender)
this.listenTo(this.model, 'emoji-picker-autocomplete', this.autocompleteInPicker); this.listenTo(this.model, 'emoji-picker-autocomplete', this.autocompleteInPicker);
this.addEventListener('focusin', ev => this.emitFocused(ev)); this.addEventListener('focusin', ev => this.emitFocused(ev));
this.addEventListener('focusout', ev => this.emitBlurred(ev)); this.addEventListener('focusout', ev => this.emitBlurred(ev));
this.render();
} }
render () { render () {
@ -52,10 +61,6 @@ export default class ChatBottomPanel extends ElementView {
_converse.chatboxviews.get(this.getAttribute('jid'))?.emitBlurred(ev); _converse.chatboxviews.get(this.getAttribute('jid'))?.emitBlurred(ev);
} }
getToolbarOptions () { // eslint-disable-line class-methods-use-this
return {};
}
onDrop (evt) { onDrop (evt) {
if (evt.dataTransfer.files.length == 0) { if (evt.dataTransfer.files.length == 0) {
// There are no files to be dropped, so this isnt a file // There are no files to be dropped, so this isnt a file

View File

@ -61,6 +61,7 @@ export default class ChatView extends BaseChatView {
afterShown () { afterShown () {
this.model.setChatState(_converse.ACTIVE); this.model.setChatState(_converse.ACTIVE);
this.model.clearUnreadMsgCounter();
this.maybeFocus(); this.maybeFocus();
} }
} }

View File

@ -195,7 +195,7 @@ export default class MessageForm extends ElementView {
this.querySelector('converse-emoji-dropdown')?.hideMenu(); this.querySelector('converse-emoji-dropdown')?.hideMenu();
const is_command = this.parseMessageForCommands(message_text); const is_command = this.parseMessageForCommands(message_text);
const message = is_command ? null : await this.model.sendMessage(message_text, spoiler_hint); const message = is_command ? null : await this.model.sendMessage({'body': message_text, spoiler_hint});
if (is_command || message) { if (is_command || message) {
hint_el.value = ''; hint_el.value = '';
textarea.value = ''; textarea.value = '';

View File

@ -185,9 +185,6 @@
} }
} }
video {
width: 100%
}
progress { progress {
margin: 0.5em 0; margin: 0.5em 0;
width: 100% width: 100%

View File

@ -35,7 +35,7 @@ export default (o) => {
const display_name = o.model.getDisplayName(); const display_name = o.model.getDisplayName();
const tpl_dropdown_btns = () => getDropdownButtons(o.heading_buttons_promise) const tpl_dropdown_btns = () => getDropdownButtons(o.heading_buttons_promise)
.then(btns => btns.length ? html`<converse-dropdown .items=${btns}></converse-dropdown>` : ''); .then(btns => btns.length ? html`<converse-dropdown class="dropleft" .items=${btns}></converse-dropdown>` : '');
const tpl_standalone_btns = () => getStandaloneButtons(o.heading_buttons_promise) const tpl_standalone_btns = () => getStandaloneButtons(o.heading_buttons_promise)
.then(btns => btns.reverse().map(b => until(b, ''))); .then(btns => btns.reverse().map(b => until(b, '')));

View File

@ -1,4 +1,4 @@
/*global mock, converse, _ */ /*global mock, converse */
const $msg = converse.env.$msg; const $msg = converse.env.$msg;
const Strophe = converse.env.Strophe; const Strophe = converse.env.Strophe;
@ -13,7 +13,7 @@ describe("Chatboxes", function () {
describe("A Chatbox", function () { describe("A Chatbox", function () {
it("has a /help command to show the available commands", mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { it("has a /help command to show the available commands", mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1); await mock.waitForRoster(_converse, 'current', 1);
await mock.openControlBox(_converse); await mock.openControlBox(_converse);
@ -40,11 +40,10 @@ describe("Chatboxes", function () {
await u.waitUntil(() => view.querySelectorAll('.chat-msg').length); await u.waitUntil(() => view.querySelectorAll('.chat-msg').length);
const msg_txt_sel = 'converse-chat-message:last-child .chat-msg__body'; const msg_txt_sel = 'converse-chat-message:last-child .chat-msg__body';
await u.waitUntil(() => view.querySelector(msg_txt_sel).textContent.trim() === 'hello world'); await u.waitUntil(() => view.querySelector(msg_txt_sel).textContent.trim() === 'hello world');
done();
})); }));
it("has a /clear command", mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { it("has a /clear command", mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1); await mock.waitForRoster(_converse, 'current', 1);
await mock.openControlBox(_converse); await mock.openControlBox(_converse);
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@ -67,12 +66,11 @@ describe("Chatboxes", function () {
}); });
expect(window.confirm).toHaveBeenCalled(); expect(window.confirm).toHaveBeenCalled();
await u.waitUntil(() => sizzle('converse-chat-message', view).length === 0); await u.waitUntil(() => sizzle('converse-chat-message', view).length === 0);
done();
})); }));
it("is created when you click on a roster item", mock.initConverse( it("is created when you click on a roster item", mock.initConverse(
['chatBoxesFetched'], {}, async function (done, _converse) { ['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse); await mock.openControlBox(_converse);
@ -98,12 +96,11 @@ describe("Chatboxes", function () {
// Check that new chat boxes are created to the left of the // Check that new chat boxes are created to the left of the
// controlbox (but to the right of all existing chat boxes) // controlbox (but to the right of all existing chat boxes)
expect(document.querySelectorAll("#conversejs .chatbox").length).toBe(3); expect(document.querySelectorAll("#conversejs .chatbox").length).toBe(3);
done();
})); }));
it("opens when a new message is received", mock.initConverse( it("opens when a new message is received", mock.initConverse(
[], {'allow_non_roster_messaging': true}, [], {'allow_non_roster_messaging': true},
async function (done, _converse) { async function (_converse) {
await mock.waitForRoster(_converse, 'current', 0); await mock.waitForRoster(_converse, 'current', 0);
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@ -120,10 +117,9 @@ describe("Chatboxes", function () {
await u.waitUntil(() => message_promise); await u.waitUntil(() => message_promise);
expect(_converse.chatboxviews.keys().length).toBe(2); expect(_converse.chatboxviews.keys().length).toBe(2);
expect(_converse.chatboxviews.keys().pop()).toBe(sender_jid); expect(_converse.chatboxviews.keys().pop()).toBe(sender_jid);
done();
})); }));
it("doesn't open when a message without body is received", mock.initConverse([], {}, async function (done, _converse) { it("doesn't open when a message without body is received", mock.initConverse([], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1); await mock.waitForRoster(_converse, 'current', 1);
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
const stanza = u.toStanza(` const stanza = u.toStanza(`
@ -136,11 +132,10 @@ describe("Chatboxes", function () {
_converse.connection._dataRecv(mock.createRequest(stanza)); _converse.connection._dataRecv(mock.createRequest(stanza));
await u.waitUntil(() => message_promise); await u.waitUntil(() => message_promise);
expect(_converse.chatboxviews.keys().length).toBe(1); expect(_converse.chatboxviews.keys().length).toBe(1);
done();
})); }));
it("is focused if its already open and you click on its corresponding roster item", it("is focused if its already open and you click on its corresponding roster item",
mock.initConverse(['chatBoxesFetched'], {'auto_focus': true}, async function (done, _converse) { mock.initConverse(['chatBoxesFetched'], {'auto_focus': true}, async function (_converse) {
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse); await mock.openControlBox(_converse);
@ -159,11 +154,10 @@ describe("Chatboxes", function () {
await u.waitUntil(() => view.focus.calls.count(), 1000); await u.waitUntil(() => view.focus.calls.count(), 1000);
expect(view.focus).toHaveBeenCalled(); expect(view.focus).toHaveBeenCalled();
expect(_converse.chatboxes.length).toEqual(2); expect(_converse.chatboxes.length).toEqual(2);
done();
})); }));
it("can be saved to, and retrieved from, browserStorage", it("can be saved to, and retrieved from, browserStorage",
mock.initConverse([], {}, async function (done, _converse) { mock.initConverse([], {}, async function (_converse) {
spyOn(_converse.minimize, 'trimChats'); spyOn(_converse.minimize, 'trimChats');
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
@ -188,15 +182,14 @@ describe("Chatboxes", function () {
const attrs = ['id', 'box_id', 'visible']; const attrs = ['id', 'box_id', 'visible'];
let new_attrs, old_attrs; let new_attrs, old_attrs;
for (let i=0; i<attrs.length; i++) { for (let i=0; i<attrs.length; i++) {
new_attrs = _.map(_.map(newchatboxes.models, 'attributes'), attrs[i]); new_attrs = newchatboxes.models.map(m => m.attributes[i]);
old_attrs = _.map(_.map(_converse.chatboxes.models, 'attributes'), attrs[i]); old_attrs = _converse.chatboxes.models.map(m => m.attributes[i]);
expect(_.isEqual(new_attrs, old_attrs)).toEqual(true); expect(new_attrs).toEqual(old_attrs);
} }
done();
})); }));
it("can be closed by clicking a DOM element with class 'close-chatbox-button'", it("can be closed by clicking a DOM element with class 'close-chatbox-button'",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse); await mock.openControlBox(_converse);
@ -211,11 +204,10 @@ describe("Chatboxes", function () {
expect(chatview.model.close).toHaveBeenCalled(); expect(chatview.model.close).toHaveBeenCalled();
await new Promise(resolve => _converse.api.listen.once('chatBoxClosed', resolve)); await new Promise(resolve => _converse.api.listen.once('chatBoxClosed', resolve));
expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxClosed', jasmine.any(Object)); expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxClosed', jasmine.any(Object));
done();
})); }));
it("will be removed from browserStorage when closed", it("will be removed from browserStorage when closed",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse); await mock.openControlBox(_converse);
@ -248,13 +240,12 @@ describe("Chatboxes", function () {
await new Promise(resolve => _converse.api.listen.on('chatBoxesFetched', resolve)); await new Promise(resolve => _converse.api.listen.on('chatBoxesFetched', resolve));
expect(newchatboxes.length).toEqual(1); expect(newchatboxes.length).toEqual(1);
expect(newchatboxes.models[0].id).toBe("controlbox"); expect(newchatboxes.models[0].id).toBe("controlbox");
done();
})); }));
describe("A chat toolbar", function () { describe("A chat toolbar", function () {
it("shows the remaining character count if a message_limit is configured", it("shows the remaining character count if a message_limit is configured",
mock.initConverse(['chatBoxesFetched'], {'message_limit': 200}, async function (done, _converse) { mock.initConverse(['chatBoxesFetched'], {'message_limit': 200}, async function (_converse) {
await mock.waitForRoster(_converse, 'current', 3); await mock.waitForRoster(_converse, 'current', 3);
await mock.openControlBox(_converse); await mock.openControlBox(_converse);
@ -288,12 +279,11 @@ describe("Chatboxes", function () {
textarea.value = 'hello world'; textarea.value = 'hello world';
message_form.onKeyUp(ev); message_form.onKeyUp(ev);
await u.waitUntil(() => counter.textContent === '189'); await u.waitUntil(() => counter.textContent === '189');
done();
})); }));
it("does not show a remaining character count if message_limit is zero", it("does not show a remaining character count if message_limit is zero",
mock.initConverse(['chatBoxesFetched'], {'message_limit': 0}, async function (done, _converse) { mock.initConverse(['chatBoxesFetched'], {'message_limit': 0}, async function (_converse) {
await mock.waitForRoster(_converse, 'current', 3); await mock.waitForRoster(_converse, 'current', 3);
await mock.openControlBox(_converse); await mock.openControlBox(_converse);
@ -302,12 +292,11 @@ describe("Chatboxes", function () {
const view = _converse.chatboxviews.get(contact_jid); const view = _converse.chatboxviews.get(contact_jid);
const counter = view.querySelector('.chat-toolbar .message-limit'); const counter = view.querySelector('.chat-toolbar .message-limit');
expect(counter).toBe(null); expect(counter).toBe(null);
done();
})); }));
it("can contain a button for starting a call", it("can contain a button for starting a call",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse); await mock.openControlBox(_converse);
@ -333,13 +322,12 @@ describe("Chatboxes", function () {
call_button = toolbar.querySelector('.toggle-call'); call_button = toolbar.querySelector('.toggle-call');
call_button.click(); call_button.click();
expect(_converse.api.trigger).toHaveBeenCalledWith('callButtonClicked', jasmine.any(Object)); expect(_converse.api.trigger).toHaveBeenCalledWith('callButtonClicked', jasmine.any(Object));
done();
})); }));
}); });
describe("A Chat Status Notification", function () { describe("A Chat Status Notification", function () {
it("does not open a new chatbox", mock.initConverse([], {}, async function (done, _converse) { it("does not open a new chatbox", mock.initConverse([], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse); await mock.openControlBox(_converse);
@ -357,13 +345,12 @@ describe("Chatboxes", function () {
await u.waitUntil(() => _converse.api.trigger.calls.count()); await u.waitUntil(() => _converse.api.trigger.calls.count());
expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object)); expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object));
expect(_converse.chatboxviews.keys().length).toBe(1); expect(_converse.chatboxviews.keys().length).toBe(1);
done();
})); }));
describe("An active notification", function () { describe("An active notification", function () {
it("is sent when the user opens a chat box", it("is sent when the user opens a chat box",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@ -381,11 +368,10 @@ describe("Chatboxes", function () {
expect(stanza.childNodes[0].tagName).toBe('active'); expect(stanza.childNodes[0].tagName).toBe('active');
expect(stanza.childNodes[1].tagName).toBe('no-store'); expect(stanza.childNodes[1].tagName).toBe('no-store');
expect(stanza.childNodes[2].tagName).toBe('no-permanent-store'); expect(stanza.childNodes[2].tagName).toBe('no-permanent-store');
done();
})); }));
it("is sent when the user maximizes a minimized a chat box", mock.initConverse( it("is sent when the user maximizes a minimized a chat box", mock.initConverse(
['chatBoxesFetched'], {}, async function (done, _converse) { ['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1); await mock.waitForRoster(_converse, 'current', 1);
await mock.openControlBox(_converse); await mock.openControlBox(_converse);
@ -409,14 +395,13 @@ describe("Chatboxes", function () {
`<no-permanent-store xmlns="urn:xmpp:hints"/>`+ `<no-permanent-store xmlns="urn:xmpp:hints"/>`+
`</message>` `</message>`
); );
done();
})); }));
}); });
describe("A composing notification", function () { describe("A composing notification", function () {
it("is sent as soon as the user starts typing a message which is not a command", it("is sent as soon as the user starts typing a message which is not a command",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse); await mock.openControlBox(_converse);
@ -452,12 +437,11 @@ describe("Chatboxes", function () {
}); });
expect(view.model.get('chat_state')).toBe('composing'); expect(view.model.get('chat_state')).toBe('composing');
expect(_converse.api.trigger.calls.count(), 1); expect(_converse.api.trigger.calls.count(), 1);
done();
})); }));
it("is NOT sent out if send_chat_state_notifications doesn't allow it", it("is NOT sent out if send_chat_state_notifications doesn't allow it",
mock.initConverse(['chatBoxesFetched'], {'send_chat_state_notifications': []}, mock.initConverse(['chatBoxesFetched'], {'send_chat_state_notifications': []},
async function (done, _converse) { async function (_converse) {
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse); await mock.openControlBox(_converse);
@ -477,10 +461,9 @@ describe("Chatboxes", function () {
}); });
expect(view.model.get('chat_state')).toBe('composing'); expect(view.model.get('chat_state')).toBe('composing');
expect(_converse.connection.send).not.toHaveBeenCalled(); expect(_converse.connection.send).not.toHaveBeenCalled();
done();
})); }));
it("will be shown if received", mock.initConverse([], {}, async function (done, _converse) { it("will be shown if received", mock.initConverse([], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse); await mock.openControlBox(_converse);
@ -525,11 +508,10 @@ describe("Chatboxes", function () {
const msg_el = await u.waitUntil(() => view.querySelector('.chat-msg')); const msg_el = await u.waitUntil(() => view.querySelector('.chat-msg'));
await u.waitUntil( () => view.querySelector('.chat-content__notifications').innerText === ''); await u.waitUntil( () => view.querySelector('.chat-content__notifications').innerText === '');
expect(msg_el.querySelector('.chat-msg__text').textContent).toBe('hello world'); expect(msg_el.querySelector('.chat-msg__text').textContent).toBe('hello world');
done();
})); }));
it("is ignored if it's a composing carbon message sent by this user from a different client", it("is ignored if it's a composing carbon message sent by this user from a different client",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitUntilDiscoConfirmed(_converse, 'montague.lit', [], ['vcard-temp']); await mock.waitUntilDiscoConfirmed(_converse, 'montague.lit', [], ['vcard-temp']);
await u.waitUntil(() => _converse.xmppstatus.vcard.get('fullname')); await u.waitUntil(() => _converse.xmppstatus.vcard.get('fullname'));
@ -560,14 +542,13 @@ describe("Chatboxes", function () {
expect(view.model.messages.length).toEqual(0); expect(view.model.messages.length).toEqual(0);
const el = view.querySelector('.chat-content__notifications'); const el = view.querySelector('.chat-content__notifications');
expect(el.textContent).toBe(''); expect(el.textContent).toBe('');
done();
})); }));
}); });
describe("A paused notification", function () { describe("A paused notification", function () {
it("is sent if the user has stopped typing since 30 seconds", it("is sent if the user has stopped typing since 30 seconds",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@ -624,10 +605,9 @@ describe("Chatboxes", function () {
keyCode: 1 keyCode: 1
}); });
expect(view.model.get('chat_state')).toBe('composing'); expect(view.model.get('chat_state')).toBe('composing');
done();
})); }));
it("will be shown if received", mock.initConverse([], {}, async function (done, _converse) { it("will be shown if received", mock.initConverse([], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse); await mock.openControlBox(_converse);
const rosterview = document.querySelector('converse-roster'); const rosterview = document.querySelector('converse-roster');
@ -649,11 +629,10 @@ describe("Chatboxes", function () {
const csn = mock.cur_names[1] + ' has stopped typing'; const csn = mock.cur_names[1] + ' has stopped typing';
await u.waitUntil( () => view.querySelector('.chat-content__notifications').innerText === csn); await u.waitUntil( () => view.querySelector('.chat-content__notifications').innerText === csn);
expect(view.model.messages.length).toEqual(0); expect(view.model.messages.length).toEqual(0);
done();
})); }));
it("will not be shown if it's a paused carbon message that this user sent from a different client", it("will not be shown if it's a paused carbon message that this user sent from a different client",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitUntilDiscoConfirmed(_converse, 'montague.lit', [], ['vcard-temp']); await mock.waitUntilDiscoConfirmed(_converse, 'montague.lit', [], ['vcard-temp']);
await u.waitUntil(() => _converse.xmppstatus.vcard.get('fullname')); await u.waitUntil(() => _converse.xmppstatus.vcard.get('fullname'));
@ -681,15 +660,13 @@ describe("Chatboxes", function () {
expect(view.model.messages.length).toEqual(0); expect(view.model.messages.length).toEqual(0);
const el = view.querySelector('.chat-content__notifications'); const el = view.querySelector('.chat-content__notifications');
expect(el.textContent).toBe(''); expect(el.textContent).toBe('');
done();
done();
})); }));
}); });
describe("An inactive notification", function () { describe("An inactive notification", function () {
it("is sent if the user has stopped typing since 2 minutes", it("is sent if the user has stopped typing since 2 minutes",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
const sent_stanzas = _converse.connection.sent_stanzas; const sent_stanzas = _converse.connection.sent_stanzas;
// Make the timeouts shorter so that we can test // Make the timeouts shorter so that we can test
@ -750,11 +727,10 @@ describe("Chatboxes", function () {
`<no-permanent-store xmlns="urn:xmpp:hints"/>`+ `<no-permanent-store xmlns="urn:xmpp:hints"/>`+
`</message>`); `</message>`);
done();
})); }));
it("is sent when the user a minimizes a chat box", it("is sent when the user a minimizes a chat box",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse); await mock.openControlBox(_converse);
@ -769,11 +745,10 @@ describe("Chatboxes", function () {
var stanza = _converse.connection.send.calls.argsFor(0)[0]; var stanza = _converse.connection.send.calls.argsFor(0)[0];
expect(stanza.getAttribute('to')).toBe(contact_jid); expect(stanza.getAttribute('to')).toBe(contact_jid);
expect(stanza.childNodes[0].tagName).toBe('inactive'); expect(stanza.childNodes[0].tagName).toBe('inactive');
done();
})); }));
it("is sent if the user closes a chat box", it("is sent if the user closes a chat box",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@ -792,11 +767,10 @@ describe("Chatboxes", function () {
expect(stanza.childNodes[0].tagName).toBe('inactive'); expect(stanza.childNodes[0].tagName).toBe('inactive');
expect(stanza.childNodes[1].tagName).toBe('no-store'); expect(stanza.childNodes[1].tagName).toBe('no-store');
expect(stanza.childNodes[2].tagName).toBe('no-permanent-store'); expect(stanza.childNodes[2].tagName).toBe('no-permanent-store');
done();
})); }));
it("will clear any other chat status notifications", it("will clear any other chat status notifications",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse); await mock.openControlBox(_converse);
@ -829,13 +803,12 @@ describe("Chatboxes", function () {
_converse.connection._dataRecv(mock.createRequest(msg)); _converse.connection._dataRecv(mock.createRequest(msg));
await u.waitUntil(() => !view.querySelector('.chat-content__notifications').textContent); await u.waitUntil(() => !view.querySelector('.chat-content__notifications').textContent);
done();
})); }));
}); });
describe("A gone notification", function () { describe("A gone notification", function () {
it("will be shown if received", mock.initConverse([], {}, async function (done, _converse) { it("will be shown if received", mock.initConverse([], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current', 3); await mock.waitForRoster(_converse, 'current', 3);
await mock.openControlBox(_converse); await mock.openControlBox(_converse);
const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit'; const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@ -852,13 +825,12 @@ describe("Chatboxes", function () {
const view = _converse.chatboxviews.get(sender_jid); const view = _converse.chatboxviews.get(sender_jid);
const csntext = await u.waitUntil(() => view.querySelector('.chat-content__notifications').textContent); const csntext = await u.waitUntil(() => view.querySelector('.chat-content__notifications').textContent);
expect(csntext).toEqual(mock.cur_names[1] + ' has gone away'); expect(csntext).toEqual(mock.cur_names[1] + ' has gone away');
done();
})); }));
}); });
describe("On receiving a message correction", function () { describe("On receiving a message correction", function () {
it("will be removed", mock.initConverse([], {}, async function (done, _converse) { it("will be removed", mock.initConverse([], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse); await mock.openControlBox(_converse);
@ -910,7 +882,6 @@ describe("Chatboxes", function () {
await _converse.handleMessageStanza(edited); await _converse.handleMessageStanza(edited);
await u.waitUntil(() => !view.querySelector('.chat-content__notifications').textContent); await u.waitUntil(() => !view.querySelector('.chat-content__notifications').textContent);
done();
})); }));
}); });
}); });
@ -919,7 +890,7 @@ describe("Chatboxes", function () {
describe("Special Messages", function () { describe("Special Messages", function () {
it("'/clear' can be used to clear messages in a conversation", it("'/clear' can be used to clear messages in a conversation",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse); await mock.openControlBox(_converse);
@ -948,155 +919,14 @@ describe("Chatboxes", function () {
expect(window.confirm).toHaveBeenCalledWith('Are you sure you want to clear the messages from this conversation?'); expect(window.confirm).toHaveBeenCalledWith('Are you sure you want to clear the messages from this conversation?');
await u.waitUntil(() => view.model.messages.length === 0); await u.waitUntil(() => view.model.messages.length === 0);
await u.waitUntil(() => !view.querySelectorAll('.chat-msg__body').length); await u.waitUntil(() => !view.querySelectorAll('.chat-msg__body').length);
done();
})); }));
}); });
describe("A ChatBox's Unread Message Count", function () {
it("is incremented when the message is received and ChatBoxView is scrolled up",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 1);
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit',
msg = mock.createChatMessage(_converse, sender_jid, 'This message will be unread');
const view = await mock.openChatBoxFor(_converse, sender_jid)
const sent_stanzas = [];
spyOn(_converse.connection, 'send').and.callFake(s => sent_stanzas.push(s?.nodeTree ?? s));
view.model.ui.set('scrolled', true);
await _converse.handleMessageStanza(msg);
await u.waitUntil(() => view.model.messages.length);
expect(view.model.get('num_unread')).toBe(1);
const msgid = view.model.messages.last().get('id');
expect(view.model.get('first_unread_id')).toBe(msgid);
await u.waitUntil(() => sent_stanzas.length);
expect(sent_stanzas[0].querySelector('received')).toBeDefined();
done();
}));
it("is not incremented when the message is received and ChatBoxView is scrolled down",
mock.initConverse(['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 = mock.createChatMessage(_converse, sender_jid, 'This message will be read');
await mock.openChatBoxFor(_converse, sender_jid);
const sent_stanzas = [];
spyOn(_converse.connection, 'send').and.callFake(s => sent_stanzas.push(s?.nodeTree ?? s));
const chatbox = _converse.chatboxes.get(sender_jid);
await _converse.handleMessageStanza(msg);
expect(chatbox.get('num_unread')).toBe(0);
await u.waitUntil(() => sent_stanzas.filter(s => s.nodeName === 'message').length === 2);
expect(sent_stanzas[1].querySelector('displayed')).toBeDefined();
done();
}));
it("is incremented when message is received, chatbox is scrolled down and the window is not focused",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
await mock.waitForRoster(_converse, 'current');
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
const msgFactory = function () {
return mock.createChatMessage(_converse, sender_jid, 'This message will be unread');
};
await mock.openChatBoxFor(_converse, sender_jid);
const sent_stanzas = [];
spyOn(_converse.connection, 'send').and.callFake(s => sent_stanzas.push(s?.nodeTree ?? s));
const chatbox = _converse.chatboxes.get(sender_jid);
_converse.windowState = 'hidden';
const msg = msgFactory();
_converse.handleMessageStanza(msg);
await u.waitUntil(() => chatbox.messages.length);
expect(chatbox.get('num_unread')).toBe(1);
const msgid = chatbox.messages.last().get('id');
expect(chatbox.get('first_unread_id')).toBe(msgid);
await u.waitUntil(() => sent_stanzas.filter(s => s.nodeName === 'message').length);
expect(sent_stanzas[0].querySelector('received')).toBeDefined();
done();
}));
it("is incremented when message is received, chatbox is scrolled up and the window is not focused",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 1);
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
const msgFactory = () => mock.createChatMessage(_converse, sender_jid, 'This message will be unread');
await mock.openChatBoxFor(_converse, sender_jid);
const sent_stanzas = [];
spyOn(_converse.connection, 'send').and.callFake(s => sent_stanzas.push(s?.nodeTree ?? s));
const chatbox = _converse.chatboxes.get(sender_jid);
chatbox.ui.set('scrolled', true);
_converse.windowState = 'hidden';
const msg = msgFactory();
_converse.handleMessageStanza(msg);
await u.waitUntil(() => chatbox.messages.length);
expect(chatbox.get('num_unread')).toBe(1);
const msgid = chatbox.messages.last().get('id');
expect(chatbox.get('first_unread_id')).toBe(msgid);
await u.waitUntil(() => sent_stanzas.filter(s => s.nodeName === 'message').length === 1);
expect(sent_stanzas[0].querySelector('received')).toBeDefined();
done();
}));
it("is cleared when ChatBoxView was scrolled down and the window become focused",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 1);
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
const msgFactory = () => mock.createChatMessage(_converse, sender_jid, 'This message will be unread');
await mock.openChatBoxFor(_converse, sender_jid);
const sent_stanzas = [];
spyOn(_converse.connection, 'send').and.callFake(s => sent_stanzas.push(s?.nodeTree ?? s));
const chatbox = _converse.chatboxes.get(sender_jid);
_converse.windowState = 'hidden';
const msg = msgFactory();
_converse.handleMessageStanza(msg);
await u.waitUntil(() => chatbox.messages.length);
expect(chatbox.get('num_unread')).toBe(1);
const msgid = chatbox.messages.last().get('id');
expect(chatbox.get('first_unread_id')).toBe(msgid);
await u.waitUntil(() => sent_stanzas.filter(s => s.nodeName === 'message').length === 1);
expect(sent_stanzas[0].querySelector('received')).toBeDefined();
_converse.saveWindowState({'type': 'focus'});
await u.waitUntil(() => sent_stanzas.filter(s => s.nodeName === 'message').length === 2);
expect(sent_stanzas[1].querySelector('displayed')).toBeDefined();
expect(chatbox.get('num_unread')).toBe(0);
done();
}));
it("is not cleared when ChatBoxView was scrolled up and the windows become focused",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 1);
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
const msgFactory = () => mock.createChatMessage(_converse, sender_jid, 'This message will be unread');
await mock.openChatBoxFor(_converse, sender_jid);
const sent_stanzas = [];
spyOn(_converse.connection, 'send').and.callFake(s => sent_stanzas.push(s?.nodeTree ?? s));
const chatbox = _converse.chatboxes.get(sender_jid);
chatbox.ui.set('scrolled', true);
_converse.windowState = 'hidden';
const msg = msgFactory();
_converse.handleMessageStanza(msg);
await u.waitUntil(() => chatbox.messages.length);
expect(chatbox.get('num_unread')).toBe(1);
const msgid = chatbox.messages.last().get('id');
expect(chatbox.get('first_unread_id')).toBe(msgid);
await u.waitUntil(() => sent_stanzas.filter(s => s.nodeName === 'message').length === 1);
expect(sent_stanzas[0].querySelector('received')).toBeDefined();
_converse.saveWindowState({'type': 'focus'});
await u.waitUntil(() => chatbox.get('num_unread') === 1);
expect(chatbox.get('first_unread_id')).toBe(msgid);
expect(sent_stanzas[0].querySelector('received')).toBeDefined();
done();
}));
});
describe("A RosterView's Unread Message Count", function () { describe("A RosterView's Unread Message Count", function () {
it("is updated when message is received and chatbox is scrolled up", it("is updated when message is received and chatbox is scrolled up",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1); await mock.waitForRoster(_converse, 'current', 1);
let msg, indicator_el; let msg, indicator_el;
@ -1117,11 +947,10 @@ describe("Chatboxes", function () {
await u.waitUntil(() => chatbox.messages.length > 1); await u.waitUntil(() => chatbox.messages.length > 1);
indicator_el = sizzle(selector, rosterview).pop(); indicator_el = sizzle(selector, rosterview).pop();
expect(indicator_el.textContent).toBe('2'); expect(indicator_el.textContent).toBe('2');
done();
})); }));
it("is updated when message is received and chatbox is minimized", it("is updated when message is received and chatbox is minimized",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1); await mock.waitForRoster(_converse, 'current', 1);
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@ -1145,11 +974,10 @@ describe("Chatboxes", function () {
await u.waitUntil(() => chatbox.messages.length === 2); await u.waitUntil(() => chatbox.messages.length === 2);
indicator_el = sizzle(selector, rosterview).pop(); indicator_el = sizzle(selector, rosterview).pop();
expect(indicator_el.textContent).toBe('2'); expect(indicator_el.textContent).toBe('2');
done();
})); }));
it("is cleared when chatbox is maximzied after receiving messages in minimized mode", it("is cleared when chatbox is maximzied after receiving messages in minimized mode",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1); await mock.waitForRoster(_converse, 'current', 1);
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@ -1170,11 +998,10 @@ describe("Chatboxes", function () {
expect(select_msgs_indicator().textContent).toBe('2'); expect(select_msgs_indicator().textContent).toBe('2');
_converse.minimize.maximize(view.model); _converse.minimize.maximize(view.model);
u.waitUntil(() => typeof select_msgs_indicator() === 'undefined'); u.waitUntil(() => typeof select_msgs_indicator() === 'undefined');
done();
})); }));
it("is cleared when unread messages are viewed which were received in scrolled-up chatbox", it("is cleared when unread messages are viewed which were received in scrolled-up chatbox",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
await mock.openControlBox(_converse); await mock.openControlBox(_converse);
await mock.waitForRoster(_converse, 'current', 1); await mock.waitForRoster(_converse, 'current', 1);
@ -1194,11 +1021,10 @@ describe("Chatboxes", function () {
const chat_new_msgs_indicator = await u.waitUntil(() => view.querySelector('.new-msgs-indicator')); const chat_new_msgs_indicator = await u.waitUntil(() => view.querySelector('.new-msgs-indicator'));
chat_new_msgs_indicator.click(); chat_new_msgs_indicator.click();
await u.waitUntil(() => select_msgs_indicator() === undefined); await u.waitUntil(() => select_msgs_indicator() === undefined);
done();
})); }));
it("is not cleared after user clicks on roster view when chatbox is already opened and scrolled up", it("is not cleared after user clicks on roster view when chatbox is already opened and scrolled up",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1); await mock.waitForRoster(_converse, 'current', 1);
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@ -1217,7 +1043,6 @@ describe("Chatboxes", function () {
expect(select_msgs_indicator().textContent).toBe('1'); expect(select_msgs_indicator().textContent).toBe('1');
await mock.openChatBoxFor(_converse, sender_jid); await mock.openChatBoxFor(_converse, sender_jid);
expect(select_msgs_indicator().textContent).toBe('1'); expect(select_msgs_indicator().textContent).toBe('1');
done();
})); }));
}); });
}); });

View File

@ -5,7 +5,7 @@ const { Promise, $msg, Strophe, sizzle, u } = converse.env;
describe("A Chat Message", function () { describe("A Chat Message", function () {
it("can be sent as a correction by using the up arrow", it("can be sent as a correction by using the up arrow",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1); await mock.waitForRoster(_converse, 'current', 1);
await mock.openControlBox(_converse); await mock.openControlBox(_converse);
@ -161,12 +161,11 @@ describe("A Chat Message", function () {
expect(view.model.messages.at(0).get('correcting')).toBeFalsy(); expect(view.model.messages.at(0).get('correcting')).toBeFalsy();
expect(view.model.messages.at(1).get('correcting')).toBeFalsy(); expect(view.model.messages.at(1).get('correcting')).toBeFalsy();
expect(view.model.messages.at(2).get('correcting')).toBeFalsy(); expect(view.model.messages.at(2).get('correcting')).toBeFalsy();
done();
})); }));
it("can be sent as a correction by clicking the pencil icon", it("can be sent as a correction by clicking the pencil icon",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1); await mock.waitForRoster(_converse, 'current', 1);
await mock.openControlBox(_converse); await mock.openControlBox(_converse);
@ -285,14 +284,13 @@ describe("A Chat Message", function () {
['You have an unsent message which will be lost if you continue. Are you sure?']); ['You have an unsent message which will be lost if you continue. Are you sure?']);
expect(window.confirm.calls.argsFor(1)).toEqual( expect(window.confirm.calls.argsFor(1)).toEqual(
['You have an unsent message which will be lost if you continue. Are you sure?']); ['You have an unsent message which will be lost if you continue. Are you sure?']);
done();
})); }));
describe("when received from someone else", function () { describe("when received from someone else", function () {
it("can be replaced with a correction", it("can be replaced with a correction",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1); await mock.waitForRoster(_converse, 'current', 1);
await mock.openControlBox(_converse); await mock.openControlBox(_converse);
@ -346,7 +344,6 @@ describe("A Chat Message", function () {
expect(older_msgs.length).toBe(2); expect(older_msgs.length).toBe(2);
expect(older_msgs[0].textContent.includes('But soft, what light through yonder airlock breaks?')).toBe(true); expect(older_msgs[0].textContent.includes('But soft, what light through yonder airlock breaks?')).toBe(true);
expect(view.model.messages.models.length).toBe(1); expect(view.model.messages.models.length).toBe(1);
done();
})); }));
}); });
}); });

View File

@ -11,7 +11,7 @@ describe("Emojis", function () {
afterEach(() => (jasmine.DEFAULT_TIMEOUT_INTERVAL = original_timeout)); afterEach(() => (jasmine.DEFAULT_TIMEOUT_INTERVAL = original_timeout));
it("can be opened by clicking a button in the chat toolbar", it("can be opened by clicking a button in the chat toolbar",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit'; const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
@ -25,14 +25,13 @@ describe("Emojis", function () {
item.click() item.click()
expect(view.querySelector('textarea.chat-textarea').value).toBe(':smiley: '); expect(view.querySelector('textarea.chat-textarea').value).toBe(':smiley: ');
toolbar.querySelector('.toggle-emojis').click(); // Close the panel again toolbar.querySelector('.toggle-emojis').click(); // Close the panel again
done();
})); }));
}); });
describe("A Chat Message", function () { describe("A Chat Message", function () {
it("will display larger if it's only emojis", it("will display larger if it's only emojis",
mock.initConverse(['chatBoxesFetched'], {'use_system_emojis': true}, async function (done, _converse) { mock.initConverse(['chatBoxesFetched'], {'use_system_emojis': true}, async function (_converse) {
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit'; const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@ -114,13 +113,12 @@ describe("Emojis", function () {
message = view.querySelector('.message:last-child .chat-msg__text'); message = view.querySelector('.message:last-child .chat-msg__text');
expect(u.hasClass('chat-msg__text--larger', message)).toBe(true); expect(u.hasClass('chat-msg__text--larger', message)).toBe(true);
done()
})); }));
it("can render emojis as images", it("can render emojis as images",
mock.initConverse( mock.initConverse(
['chatBoxesFetched'], {'use_system_emojis': false}, ['chatBoxesFetched'], {'use_system_emojis': false},
async function (done, _converse) { async function (_converse) {
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
const contact_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit'; const contact_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@ -163,8 +161,7 @@ describe("Emojis", function () {
const sent_stanzas = _converse.connection.sent_stanzas; const sent_stanzas = _converse.connection.sent_stanzas;
const sent_stanza = sent_stanzas.filter(s => s.nodeName === 'message').pop(); const sent_stanza = sent_stanzas.filter(s => s.nodeName === 'message').pop();
expect(sent_stanza.querySelector('body').innerHTML).toBe('💩 😇'); expect(sent_stanza.querySelector('body').innerHTML).toBe('💩 😇');
done() }));
}));
it("can show custom emojis", it("can show custom emojis",
mock.initConverse( mock.initConverse(
@ -181,7 +178,7 @@ describe("Emojis", function () {
"flags": ":flag_ac:", "flags": ":flag_ac:",
"custom": ':xmpp:' "custom": ':xmpp:'
} }, } },
async function (done, _converse) { async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1); await mock.waitForRoster(_converse, 'current', 1);
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@ -208,7 +205,6 @@ describe("Emojis", function () {
const body = view.querySelector('converse-chat-message-body'); const body = view.querySelector('converse-chat-message-body');
await u.waitUntil(() => body.innerHTML.replace(/<!-.*?->/g, '').trim() === await u.waitUntil(() => body.innerHTML.replace(/<!-.*?->/g, '').trim() ===
'Running tests for <img class="emoji" draggable="false" title=":converse:" alt=":converse:" src="/dist/images/custom_emojis/converse.png">'); 'Running tests for <img class="emoji" draggable="false" title=":converse:" alt=":converse:" src="/dist/images/custom_emojis/converse.png">');
done();
})); }));
}); });
}); });

View File

@ -8,7 +8,7 @@ describe("XEP-0363: HTTP File Upload", function () {
describe("Discovering support", function () { describe("Discovering support", function () {
it("is done automatically", mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { it("is done automatically", mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
const IQ_stanzas = _converse.connection.IQ_stanzas; const IQ_stanzas = _converse.connection.IQ_stanzas;
await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, [], []); await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, [], []);
let selector = 'iq[to="montague.lit"] query[xmlns="http://jabber.org/protocol/disco#info"]'; let selector = 'iq[to="montague.lit"] query[xmlns="http://jabber.org/protocol/disco#info"]';
@ -133,7 +133,6 @@ describe("XEP-0363: HTTP File Upload", function () {
expect(features.length).toBe(1); expect(features.length).toBe(1);
expect(features[0].get('jid')).toBe('upload.montague.lit'); expect(features[0].get('jid')).toBe('upload.montague.lit');
expect(features[0].dataforms.where({'FORM_TYPE': {value: "urn:xmpp:http:upload:0", type: "hidden"}}).length).toBe(1); expect(features[0].dataforms.where({'FORM_TYPE': {value: "urn:xmpp:http:upload:0", type: "hidden"}}).length).toBe(1);
done();
})); }));
}); });
@ -141,7 +140,7 @@ describe("XEP-0363: HTTP File Upload", function () {
describe("A file upload toolbar button", function () { describe("A file upload toolbar button", function () {
it("does not appear in private chats", it("does not appear in private chats",
mock.initConverse([], {}, async function (done, _converse) { mock.initConverse([], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current', 3); await mock.waitForRoster(_converse, 'current', 3);
mock.openControlBox(_converse); mock.openControlBox(_converse);
@ -155,7 +154,6 @@ describe("XEP-0363: HTTP File Upload", function () {
await mock.waitUntilDiscoConfirmed(_converse, _converse.domain, [], [], [], 'items'); await mock.waitUntilDiscoConfirmed(_converse, _converse.domain, [], [], [], 'items');
const view = _converse.chatboxviews.get(contact_jid); const view = _converse.chatboxviews.get(contact_jid);
expect(view.querySelector('.chat-toolbar .fileupload')).toBe(null); expect(view.querySelector('.chat-toolbar .fileupload')).toBe(null);
done();
})); }));
}); });
}); });
@ -164,7 +162,7 @@ describe("XEP-0363: HTTP File Upload", function () {
describe("A file upload toolbar button", function () { describe("A file upload toolbar button", function () {
it("appears in private chats", mock.initConverse(async (done, _converse) => { it("appears in private chats", mock.initConverse(async (_converse) => {
await mock.waitUntilDiscoConfirmed( await mock.waitUntilDiscoConfirmed(
_converse, _converse.domain, _converse, _converse.domain,
[{'category': 'server', 'type':'IM'}], [{'category': 'server', 'type':'IM'}],
@ -178,12 +176,11 @@ describe("XEP-0363: HTTP File Upload", function () {
const view = _converse.chatboxviews.get(contact_jid); const view = _converse.chatboxviews.get(contact_jid);
const el = await u.waitUntil(() => view.querySelector('.chat-toolbar .fileupload')); const el = await u.waitUntil(() => view.querySelector('.chat-toolbar .fileupload'));
expect(el).not.toEqual(null); expect(el).not.toEqual(null);
done();
})); }));
describe("when clicked and a file chosen", function () { describe("when clicked and a file chosen", function () {
it("is uploaded and sent out", mock.initConverse(['chatBoxesFetched'], {} ,async (done, _converse) => { it("is uploaded and sent out", mock.initConverse(['chatBoxesFetched'], {} ,async (_converse) => {
const base_url = 'https://conversejs.org'; const base_url = 'https://conversejs.org';
await mock.waitUntilDiscoConfirmed( await mock.waitUntilDiscoConfirmed(
_converse, _converse.domain, _converse, _converse.domain,
@ -280,13 +277,12 @@ describe("XEP-0363: HTTP File Upload", function () {
expect(view.querySelector('.chat-msg .chat-msg__media').innerHTML.replace(/<!-.*?->/g, '').trim()).toEqual( expect(view.querySelector('.chat-msg .chat-msg__media').innerHTML.replace(/<!-.*?->/g, '').trim()).toEqual(
`<a target="_blank" rel="noopener" href="${base_url}/logo/conversejs-filled.svg">`+ `<a target="_blank" rel="noopener" href="${base_url}/logo/conversejs-filled.svg">`+
`Download image file "conversejs-filled.svg"</a>`); `Download file "conversejs-filled.svg"</a>`);
XMLHttpRequest.prototype.send = send_backup; XMLHttpRequest.prototype.send = send_backup;
done();
})); }));
it("shows an error message if the file is too large", it("shows an error message if the file is too large",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
const IQ_stanzas = _converse.connection.IQ_stanzas; const IQ_stanzas = _converse.connection.IQ_stanzas;
const IQ_ids = _converse.connection.IQ_ids; const IQ_ids = _converse.connection.IQ_ids;
@ -398,14 +394,13 @@ describe("XEP-0363: HTTP File Upload", function () {
expect(messages.length).toBe(1); expect(messages.length).toBe(1);
expect(messages[0].textContent.trim()).toBe( expect(messages[0].textContent.trim()).toBe(
'The size of your file, my-juliet.jpg, exceeds the maximum allowed by your server, which is 5 MB.'); 'The size of your file, my-juliet.jpg, exceeds the maximum allowed by your server, which is 5 MB.');
done();
})); }));
}); });
}); });
describe("While a file is being uploaded", function () { describe("While a file is being uploaded", function () {
it("shows a progress bar", mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { it("shows a progress bar", mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitUntilDiscoConfirmed( await mock.waitUntilDiscoConfirmed(
_converse, _converse.domain, _converse, _converse.domain,
[{'category': 'server', 'type':'IM'}], [{'category': 'server', 'type':'IM'}],
@ -457,6 +452,7 @@ describe("XEP-0363: HTTP File Upload", function () {
</slot> </slot>
</iq>`); </iq>`);
const promise = u.getOpenPromise();
spyOn(XMLHttpRequest.prototype, 'send').and.callFake(async () => { spyOn(XMLHttpRequest.prototype, 'send').and.callFake(async () => {
const message = view.model.messages.at(0); const message = view.model.messages.at(0);
const el = await u.waitUntil(() => view.querySelector('.chat-content progress')); const el = await u.waitUntil(() => view.querySelector('.chat-content progress'));
@ -466,9 +462,10 @@ describe("XEP-0363: HTTP File Upload", function () {
message.set('progress', 1); message.set('progress', 1);
await u.waitUntil(() => view.querySelector('.chat-content progress').getAttribute('value') === '1'); await u.waitUntil(() => view.querySelector('.chat-content progress').getAttribute('value') === '1');
expect(view.querySelector('.chat-content .chat-msg__text').textContent).toBe('Uploading file: my-juliet.jpg, 22.91 KB'); expect(view.querySelector('.chat-content .chat-msg__text').textContent).toBe('Uploading file: my-juliet.jpg, 22.91 KB');
done(); promise.resolve();
}); });
_converse.connection._dataRecv(mock.createRequest(stanza)); _converse.connection._dataRecv(mock.createRequest(stanza));
return promise;
})); }));
}); });
}); });

View File

@ -8,7 +8,7 @@ const u = converse.env.utils;
describe("A XEP-0333 Chat Marker", function () { describe("A XEP-0333 Chat Marker", function () {
it("is sent when a markable message is received from a roster contact", it("is sent when a markable message is received from a roster contact",
mock.initConverse([], {}, async function (done, _converse) { mock.initConverse([], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1); await mock.waitForRoster(_converse, 'current', 1);
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@ -33,11 +33,10 @@ describe("A XEP-0333 Chat Marker", function () {
`to="${contact_jid}" type="chat" xmlns="jabber:client">`+ `to="${contact_jid}" type="chat" xmlns="jabber:client">`+
`<received id="${msgid}" xmlns="urn:xmpp:chat-markers:0"/>`+ `<received id="${msgid}" xmlns="urn:xmpp:chat-markers:0"/>`+
`</message>`); `</message>`);
done();
})); }));
it("is not sent when a markable message is received from someone not on the roster", 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 (done, _converse) { mock.initConverse([], {'allow_non_roster_messaging': true}, async function (_converse) {
await mock.waitForRoster(_converse, 'current', 0); await mock.waitForRoster(_converse, 'current', 0);
const contact_jid = 'someone@montague.lit'; const contact_jid = 'someone@montague.lit';
@ -66,11 +65,10 @@ describe("A XEP-0333 Chat Marker", function () {
`<no-permanent-store xmlns="urn:xmpp:hints"/>`+ `<no-permanent-store xmlns="urn:xmpp:hints"/>`+
`</message>` `</message>`
); );
done();
})); }));
it("is ignored if it's a carbon copy of one that I sent from a different client", it("is ignored if it's a carbon copy of one that I sent from a different client",
mock.initConverse([], {}, async function (done, _converse) { mock.initConverse([], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1); await mock.waitForRoster(_converse, 'current', 1);
await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, [], [Strophe.NS.SID]); await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, [], [Strophe.NS.SID]);
@ -112,6 +110,5 @@ describe("A XEP-0333 Chat Marker", function () {
await u.waitUntil(() => _converse.api.trigger.calls.count(), 500); await u.waitUntil(() => _converse.api.trigger.calls.count(), 500);
expect(view.querySelectorAll('.chat-msg').length).toBe(1); expect(view.querySelectorAll('.chat-msg').length).toBe(1);
expect(view.model.messages.length).toBe(1); expect(view.model.messages.length).toBe(1);
done();
})); }));
}); });

View File

@ -2,62 +2,9 @@
const { u, sizzle, $msg } = converse.env; const { u, sizzle, $msg } = converse.env;
describe("A Groupchat Message", function () {
it("supports the /me command", mock.initConverse([], {}, async function (done, _converse) {
await mock.waitUntilDiscoConfirmed(_converse, 'montague.lit', [], ['vcard-temp']);
await u.waitUntil(() => _converse.xmppstatus.vcard.get('fullname'));
await mock.waitForRoster(_converse, 'current');
await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
const view = _converse.chatboxviews.get('lounge@montague.lit');
if (!view.querySelectorAll('.chat-area').length) {
view.renderChatArea();
}
let message = '/me is tired';
const nick = mock.chatroom_names[0];
let msg = $msg({
'from': 'lounge@montague.lit/'+nick,
'id': u.getUniqueId(),
'to': 'romeo@montague.lit',
'type': 'groupchat'
}).c('body').t(message).tree();
await view.model.handleMessageStanza(msg);
await u.waitUntil(() => sizzle('.chat-msg:last .chat-msg__text', view).pop());
await u.waitUntil(() => view.querySelector('.chat-msg__text').textContent.trim() === 'is tired');
expect(view.querySelector('.chat-msg__author').textContent.includes('**Dyon van de Wege')).toBeTruthy();
message = '/me is as well';
msg = $msg({
from: 'lounge@montague.lit/Romeo Montague',
id: u.getUniqueId(),
to: 'romeo@montague.lit',
type: 'groupchat'
}).c('body').t(message).tree();
await view.model.handleMessageStanza(msg);
await u.waitUntil(() => view.querySelectorAll('.chat-msg').length === 2);
await u.waitUntil(() => Array.from(view.querySelectorAll('.chat-msg__text')).pop().textContent.trim() === 'is as well');
expect(sizzle('.chat-msg__author:last', view).pop().textContent.includes('**Romeo Montague')).toBeTruthy();
// Check rendering of a mention inside a me message
const msg_text = "/me mentions romeo";
msg = $msg({
from: 'lounge@montague.lit/gibson',
id: u.getUniqueId(),
to: 'romeo@montague.lit',
type: 'groupchat'
}).c('body').t(msg_text).up()
.c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'13', 'end':'19', 'type':'mention', 'uri':'xmpp:romeo@montague.lit'}).nodeTree;
await view.model.handleMessageStanza(msg);
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 3);
await u.waitUntil(() => sizzle('.chat-msg__text:last', view).pop().innerHTML.replace(/<!-.*?->/g, '') ===
'mentions <span class="mention mention--self badge badge-info">romeo</span>');
done();
}));
});
describe("A Message", function () { describe("A Message", function () {
it("supports the /me command", mock.initConverse([], {}, async function (done, _converse) { it("supports the /me command", mock.initConverse([], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
await mock.waitUntilDiscoConfirmed(_converse, 'montague.lit', [], ['vcard-temp']); await mock.waitUntilDiscoConfirmed(_converse, 'montague.lit', [], ['vcard-temp']);
await u.waitUntil(() => _converse.xmppstatus.vcard.get('fullname')); await u.waitUntil(() => _converse.xmppstatus.vcard.get('fullname'));
@ -105,6 +52,5 @@ describe("A Message", function () {
expect(sizzle('.chat-msg__text:last', view).pop().textContent).toBe('wrote a 3rd person message'); expect(sizzle('.chat-msg__text:last', view).pop().textContent).toBe('wrote a 3rd person message');
expect(u.isVisible(sizzle('.chat-msg__author:last', view).pop())).toBeTruthy(); expect(u.isVisible(sizzle('.chat-msg__author:last', view).pop())).toBeTruthy();
done();
})); }));
}); });

View File

@ -6,7 +6,7 @@ describe("A Chat Message", function () {
it("will render audio files from their URLs", it("will render audio files from their URLs",
mock.initConverse(['chatBoxesFetched'], {}, mock.initConverse(['chatBoxesFetched'], {},
async function (done, _converse) { async function (_converse) {
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
const base_url = 'https://conversejs.org'; const base_url = 'https://conversejs.org';
const message = base_url+"/logo/audio.mp3"; const message = base_url+"/logo/audio.mp3";
@ -20,6 +20,5 @@ describe("A Chat Message", function () {
expect(msg.innerHTML.replace(/<!-.*?->/g, '').replace(/(\r\n|\n|\r)/gm, "").trim()).toEqual( expect(msg.innerHTML.replace(/<!-.*?->/g, '').replace(/(\r\n|\n|\r)/gm, "").trim()).toEqual(
`<audio controls="" src="${message}"></audio>`+ `<audio controls="" src="${message}"></audio>`+
`<a target="_blank" rel="noopener" href="${message}">${message}</a>`); `<a target="_blank" rel="noopener" href="${message}">${message}</a>`);
done();
})); }));
}); });

View File

@ -4,7 +4,7 @@ const { sizzle, u } = converse.env;
describe("A Chat Message", function () { describe("A Chat Message", function () {
it("will render images from their URLs", mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { it("will render images from their URLs", mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
const base_url = 'https://conversejs.org'; const base_url = 'https://conversejs.org';
let message = base_url+"/logo/conversejs-filled.svg"; let message = base_url+"/logo/conversejs-filled.svg";
@ -49,11 +49,10 @@ describe("A Chat Message", function () {
// Check that the Imgur URL gets a .png attached to make it render // Check that the Imgur URL gets a .png attached to make it render
await u.waitUntil(() => Array.from(view.querySelectorAll('.chat-content .chat-image')).pop().src.endsWith('png'), 1000); await u.waitUntil(() => Array.from(view.querySelectorAll('.chat-content .chat-image')).pop().src.endsWith('png'), 1000);
done();
})); }));
it("will not render images if show_images_inline is false", it("will not render images if show_images_inline is false",
mock.initConverse(['chatBoxesFetched'], {'show_images_inline': false}, async function (done, _converse) { mock.initConverse(['chatBoxesFetched'], {'show_images_inline': false}, async function (_converse) {
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
const base_url = 'https://conversejs.org'; const base_url = 'https://conversejs.org';
const message = base_url+"/logo/conversejs-filled.svg"; const message = base_url+"/logo/conversejs-filled.svg";
@ -65,13 +64,12 @@ describe("A Chat Message", function () {
const sel = '.chat-content .chat-msg:last .chat-msg__text'; const sel = '.chat-content .chat-msg:last .chat-msg__text';
await u.waitUntil(() => sizzle(sel).pop().innerHTML.replace(/<!-.*?->/g, '').trim() === message); await u.waitUntil(() => sizzle(sel).pop().innerHTML.replace(/<!-.*?->/g, '').trim() === message);
expect(true).toBe(true); expect(true).toBe(true);
done();
})); }));
it("will render images from approved URLs only", it("will render images from approved URLs only",
mock.initConverse( mock.initConverse(
['chatBoxesFetched'], {'show_images_inline': ['conversejs.org']}, ['chatBoxesFetched'], {'show_images_inline': ['conversejs.org']},
async function (done, _converse) { async function (_converse) {
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
const base_url = 'https://conversejs.org'; const base_url = 'https://conversejs.org';
@ -88,13 +86,12 @@ describe("A Chat Message", function () {
await u.waitUntil(() => view.querySelectorAll('.chat-content .chat-msg').length === 2, 1000); await u.waitUntil(() => view.querySelectorAll('.chat-content .chat-msg').length === 2, 1000);
await u.waitUntil(() => view.querySelectorAll('.chat-content .chat-image').length === 1, 1000) await u.waitUntil(() => view.querySelectorAll('.chat-content .chat-image').length === 1, 1000)
expect(view.querySelectorAll('.chat-content .chat-image').length).toBe(1); expect(view.querySelectorAll('.chat-content .chat-image').length).toBe(1);
done();
})); }));
it("will fall back to rendering images as URLs", it("will fall back to rendering images as URLs",
mock.initConverse( mock.initConverse(
['chatBoxesFetched'], {}, ['chatBoxesFetched'], {},
async function (done, _converse) { async function (_converse) {
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
const base_url = 'https://conversejs.org'; const base_url = 'https://conversejs.org';
@ -109,7 +106,6 @@ describe("A Chat Message", function () {
const msg = sizzle('.chat-content .chat-msg:last .chat-msg__text').pop(); const msg = sizzle('.chat-content .chat-msg:last .chat-msg__text').pop();
await u.waitUntil(() => msg.innerHTML.replace(/<!-.*?->/g, '').trim() == await u.waitUntil(() => msg.innerHTML.replace(/<!-.*?->/g, '').trim() ==
`<a target="_blank" rel="noopener" href="https://conversejs.org/logo/non-existing.svg">https://conversejs.org/logo/non-existing.svg</a>`, 1000); `<a target="_blank" rel="noopener" href="https://conversejs.org/logo/non-existing.svg">https://conversejs.org/logo/non-existing.svg</a>`, 1000);
done();
})); }));
it("will fall back to rendering URLs that match image_urls_regex as URLs", it("will fall back to rendering URLs that match image_urls_regex as URLs",
@ -118,7 +114,7 @@ describe("A Chat Message", function () {
'show_images_inline': ['twimg.com'], 'show_images_inline': ['twimg.com'],
'image_urls_regex': /^https?:\/\/(www.)?(pbs\.twimg\.com\/)/i 'image_urls_regex': /^https?:\/\/(www.)?(pbs\.twimg\.com\/)/i
}, },
async function (done, _converse) { async function (_converse) {
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
const message = "https://pbs.twimg.com/media/string?format=jpg&name=small"; const message = "https://pbs.twimg.com/media/string?format=jpg&name=small";
@ -132,13 +128,12 @@ describe("A Chat Message", function () {
const msg = view.querySelector('.chat-content .chat-msg .chat-msg__text'); const msg = view.querySelector('.chat-content .chat-msg .chat-msg__text');
await u.waitUntil(() => msg.innerHTML.replace(/<!-.*?->/g, '').trim() == await u.waitUntil(() => msg.innerHTML.replace(/<!-.*?->/g, '').trim() ==
`<a target="_blank" rel="noopener" href="https://pbs.twimg.com/media/string?format=jpg&amp;name=small">https://pbs.twimg.com/media/string?format=jpg&amp;name=small</a>`, 1000); `<a target="_blank" rel="noopener" href="https://pbs.twimg.com/media/string?format=jpg&amp;name=small">https://pbs.twimg.com/media/string?format=jpg&amp;name=small</a>`, 1000);
done();
})); }));
it("will respect a changed setting when re-rendered", it("will respect a changed setting when re-rendered",
mock.initConverse( mock.initConverse(
['chatBoxesFetched'], {'show_images_inline': true}, ['chatBoxesFetched'], {'show_images_inline': true},
async function (done, _converse) { async function (_converse) {
const { api } = _converse; const { api } = _converse;
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
@ -152,6 +147,32 @@ describe("A Chat Message", function () {
view.querySelector('converse-chat-message').requestUpdate(); view.querySelector('converse-chat-message').requestUpdate();
await u.waitUntil(() => view.querySelector('converse-chat-message-body .chat-image') === null); await u.waitUntil(() => view.querySelector('converse-chat-message-body .chat-image') === null);
expect(true).toBe(true); expect(true).toBe(true);
done(); }));
it("will allow the user to toggle visibility of rendered images",
mock.initConverse(['chatBoxesFetched'], {'show_images_inline': true}, async function (_converse) {
await mock.waitForRoster(_converse, 'current');
// let message = "https://i.imgur.com/Py9ifJE.mp4";
const base_url = 'https://conversejs.org';
const message = base_url+"/logo/conversejs-filled.svg";
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);
await mock.sendMessage(view, message);
const sel = '.chat-content .chat-msg:last .chat-msg__text';
await u.waitUntil(() => sizzle(sel).pop().innerHTML.replace(/<!-.*?->/g, '').trim() === message);
const actions_el = view.querySelector('converse-message-actions');
await u.waitUntil(() => actions_el.textContent.includes('Hide media'));
await u.waitUntil(() => view.querySelector('converse-chat-message-body img'));
actions_el.querySelector('.chat-msg__action-hide-previews').click();
await u.waitUntil(() => actions_el.textContent.includes('Show media'));
await u.waitUntil(() => !view.querySelector('converse-chat-message-body img'));
expect(view.querySelector('converse-chat-message-body').innerHTML.replace(/<!-.*?->/g, '').trim())
.toBe(`<a target="_blank" rel="noopener" href="${message}">${message}</a>`)
})); }));
}); });

View File

@ -2,9 +2,9 @@
const { Strophe, sizzle, u } = converse.env; const { Strophe, sizzle, u } = converse.env;
describe("A Chat Message", function () { describe("A chat message containing video URLs", function () {
it("will render videos from their URLs", mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { it("will render videos from their URLs", mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
// let message = "https://i.imgur.com/Py9ifJE.mp4"; // let message = "https://i.imgur.com/Py9ifJE.mp4";
const base_url = 'https://conversejs.org'; const base_url = 'https://conversejs.org';
@ -27,11 +27,10 @@ describe("A Chat Message", function () {
expect(msg.innerHTML.replace(/<!-.*?->/g, '').trim()).toEqual( expect(msg.innerHTML.replace(/<!-.*?->/g, '').trim()).toEqual(
`<video controls="" preload="metadata" src="${Strophe.xmlescape(message)}"></video>`+ `<video controls="" preload="metadata" src="${Strophe.xmlescape(message)}"></video>`+
`<a target="_blank" rel="noopener" href="${Strophe.xmlescape(message)}">${Strophe.xmlescape(message)}</a>`); `<a target="_blank" rel="noopener" href="${Strophe.xmlescape(message)}">${Strophe.xmlescape(message)}</a>`);
done();
})); }));
it("will not render videos if embed_videos is false", it("will not render videos if embed_videos is false",
mock.initConverse(['chatBoxesFetched'], {'embed_videos': false}, async function (done, _converse) { mock.initConverse(['chatBoxesFetched'], {'embed_videos': false}, async function (_converse) {
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
// let message = "https://i.imgur.com/Py9ifJE.mp4"; // let message = "https://i.imgur.com/Py9ifJE.mp4";
const base_url = 'https://conversejs.org'; const base_url = 'https://conversejs.org';
@ -44,13 +43,12 @@ describe("A Chat Message", function () {
const sel = '.chat-content .chat-msg:last .chat-msg__text'; const sel = '.chat-content .chat-msg:last .chat-msg__text';
await u.waitUntil(() => sizzle(sel).pop().innerHTML.replace(/<!-.*?->/g, '').trim() === message); await u.waitUntil(() => sizzle(sel).pop().innerHTML.replace(/<!-.*?->/g, '').trim() === message);
expect(true).toBe(true); expect(true).toBe(true);
done();
})); }));
it("will render videos from approved URLs only", it("will render videos from approved URLs only",
mock.initConverse( mock.initConverse(
['chatBoxesFetched'], {'embed_videos': ['conversejs.org']}, ['chatBoxesFetched'], {'embed_videos': ['conversejs.org']},
async function (done, _converse) { async function (_converse) {
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
let message = "https://i.imgur.com/Py9ifJE.mp4"; let message = "https://i.imgur.com/Py9ifJE.mp4";
@ -69,6 +67,32 @@ describe("A Chat Message", function () {
expect(msg.innerHTML.replace(/<!-.*?->/g, '').trim()).toEqual( expect(msg.innerHTML.replace(/<!-.*?->/g, '').trim()).toEqual(
`<video controls="" preload="metadata" src="${message}"></video>`+ `<video controls="" preload="metadata" src="${message}"></video>`+
`<a target="_blank" rel="noopener" href="${message}">${message}</a>`); `<a target="_blank" rel="noopener" href="${message}">${message}</a>`);
done(); }));
it("will allow the user to toggle visibility of rendered videos",
mock.initConverse(['chatBoxesFetched'], {'embed_videos': true}, async function (_converse) {
await mock.waitForRoster(_converse, 'current');
// let message = "https://i.imgur.com/Py9ifJE.mp4";
const base_url = 'https://conversejs.org';
const message = base_url+"/logo/conversejs-filled.mp4";
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);
await mock.sendMessage(view, message);
const sel = '.chat-content .chat-msg:last .chat-msg__text';
await u.waitUntil(() => sizzle(sel).pop().innerHTML.replace(/<!-.*?->/g, '').trim() === message);
const actions_el = view.querySelector('converse-message-actions');
await u.waitUntil(() => actions_el.textContent.includes('Hide media'));
await u.waitUntil(() => view.querySelector('converse-chat-message-body video'));
actions_el.querySelector('.chat-msg__action-hide-previews').click();
await u.waitUntil(() => actions_el.textContent.includes('Show media'));
await u.waitUntil(() => !view.querySelector('converse-chat-message-body video'));
expect(view.querySelector('converse-chat-message-body').innerHTML.replace(/<!-.*?->/g, '').trim())
.toBe(`<a target="_blank" rel="noopener" href="${message}">${message}</a>`)
})); }));
}); });

View File

@ -7,15 +7,14 @@ describe("A Chat Message", function () {
it("will be demarcated if it's the first newly received message", it("will be demarcated if it's the first newly received message",
mock.initConverse(['chatBoxesFetched'], {}, mock.initConverse(['chatBoxesFetched'], {},
async function (done, _converse) { async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1); await mock.waitForRoster(_converse, 'current', 1);
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await mock.openChatBoxFor(_converse, contact_jid); await mock.openChatBoxFor(_converse, contact_jid);
const view = _converse.chatboxviews.get(contact_jid); const view = _converse.chatboxviews.get(contact_jid);
await _converse.handleMessageStanza(mock.createChatMessage(_converse, contact_jid, 'This message will be read')); await _converse.handleMessageStanza(mock.createChatMessage(_converse, contact_jid, 'This message will be read'));
const msg_el = await u.waitUntil(() => view.querySelector('converse-chat-message')); await u.waitUntil(() => view.querySelector('converse-chat-message .chat-msg__text')?.textContent === 'This message will be read');
expect(msg_el.querySelector('.chat-msg__text').textContent).toBe('This message will be read');
expect(view.model.get('num_unread')).toBe(0); expect(view.model.get('num_unread')).toBe(0);
_converse.windowState = 'hidden'; _converse.windowState = 'hidden';
@ -26,16 +25,16 @@ describe("A Chat Message", function () {
expect(view.model.get('first_unread_id')).toBe(view.model.messages.last().get('id')); expect(view.model.get('first_unread_id')).toBe(view.model.messages.last().get('id'));
await u.waitUntil(() => view.querySelectorAll('converse-chat-message').length === 2); await u.waitUntil(() => view.querySelectorAll('converse-chat-message').length === 2);
await u.waitUntil(() => view.querySelector('converse-chat-message:last-child .chat-msg__text')?.textContent === 'This message will be new');
const last_msg_el = view.querySelector('converse-chat-message:last-child'); const last_msg_el = view.querySelector('converse-chat-message:last-child');
expect(last_msg_el.firstElementChild?.textContent).toBe('New messages'); expect(last_msg_el.firstElementChild?.textContent).toBe('New messages');
done();
})); }));
it("is rejected if it's an unencapsulated forwarded message", it("is rejected if it's an unencapsulated forwarded message",
mock.initConverse( mock.initConverse(
['chatBoxesFetched'], {}, ['chatBoxesFetched'], {},
async function (done, _converse) { async function (_converse) {
await mock.waitForRoster(_converse, 'current', 2); await mock.waitForRoster(_converse, 'current', 2);
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@ -74,11 +73,10 @@ describe("A Chat Message", function () {
'</message>'); '</message>');
models = await _converse.api.chats.get(); models = await _converse.api.chats.get();
expect(models.length).toBe(1); expect(models.length).toBe(1);
done();
})); }));
it("can be received out of order, and will still be displayed in the right order", it("can be received out of order, and will still be displayed in the right order",
mock.initConverse([], {}, async function (done, _converse) { mock.initConverse([], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse); await mock.openControlBox(_converse);
@ -238,11 +236,10 @@ describe("A Chat Message", function () {
expect(day.getAttribute('data-isodate')).toEqual(dayjs().startOf('day').toISOString()); expect(day.getAttribute('data-isodate')).toEqual(dayjs().startOf('day').toISOString());
expect(day.nextElementSibling.querySelector('.chat-msg__text').textContent).toBe('latest message'); expect(day.nextElementSibling.querySelector('.chat-msg__text').textContent).toBe('latest message');
expect(u.hasClass('chat-msg--followup', el)).toBe(false); expect(u.hasClass('chat-msg--followup', el)).toBe(false);
done();
})); }));
it("is ignored if it's a malformed headline message", it("is ignored if it's a malformed headline message",
mock.initConverse([], {}, async function (done, _converse) { mock.initConverse([], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse); await mock.openControlBox(_converse);
@ -250,7 +247,7 @@ describe("A Chat Message", function () {
// Ideally we wouldn't have to filter out headline // Ideally we wouldn't have to filter out headline
// messages, but Prosody gives them the wrong 'type' :( // messages, but Prosody gives them the wrong 'type' :(
spyOn(converse.env.log, 'info'); spyOn(converse.env.log, 'info');
sinon.spy(_converse.api.chatboxes, 'get'); spyOn(_converse.api.chatboxes, 'get');
const msg = $msg({ const msg = $msg({
from: 'montague.lit', from: 'montague.lit',
to: _converse.bare_jid, to: _converse.bare_jid,
@ -261,15 +258,12 @@ describe("A Chat Message", function () {
expect(converse.env.log.info).toHaveBeenCalledWith( expect(converse.env.log.info).toHaveBeenCalledWith(
"handleMessageStanza: Ignoring incoming server message from JID: montague.lit" "handleMessageStanza: Ignoring incoming server message from JID: montague.lit"
); );
expect(_converse.api.chatboxes.get.called).toBeFalsy(); expect(_converse.api.chatboxes.get).not.toHaveBeenCalled();
// Remove sinon spies
_converse.api.chatboxes.get.restore();
done();
})); }));
it("can be a carbon message, as defined in XEP-0280", it("can be a carbon message, as defined in XEP-0280",
mock.initConverse([], {}, async function (done, _converse) { mock.initConverse([], {}, async function (_converse) {
const include_nick = false; const include_nick = false;
await mock.waitForRoster(_converse, 'current', 2, include_nick); await mock.waitForRoster(_converse, 'current', 2, include_nick);
@ -314,11 +308,10 @@ describe("A Chat Message", function () {
expect(view.querySelector('.chat-msg__time').textContent.match(/^[0-9][0-9]:[0-9][0-9]/)).toBeTruthy(); expect(view.querySelector('.chat-msg__time').textContent.match(/^[0-9][0-9]:[0-9][0-9]/)).toBeTruthy();
await u.waitUntil(() => chatbox.vcard.get('fullname') === 'Juliet Capulet') await u.waitUntil(() => chatbox.vcard.get('fullname') === 'Juliet Capulet')
expect(view.querySelector('span.chat-msg__author').textContent.trim()).toBe('Juliet Capulet'); expect(view.querySelector('span.chat-msg__author').textContent.trim()).toBe('Juliet Capulet');
done();
})); }));
it("can be a carbon message that this user sent from a different client, as defined in XEP-0280", it("can be a carbon message that this user sent from a different client, as defined in XEP-0280",
mock.initConverse([], {}, async function (done, _converse) { mock.initConverse([], {}, async function (_converse) {
await mock.waitUntilDiscoConfirmed(_converse, 'montague.lit', [], ['vcard-temp']); await mock.waitUntilDiscoConfirmed(_converse, 'montague.lit', [], ['vcard-temp']);
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
@ -359,11 +352,10 @@ describe("A Chat Message", function () {
// Now check that the message appears inside the chatbox in the DOM // Now check that the message appears inside the chatbox in the DOM
const msg_el = await u.waitUntil(() => view.querySelector('.chat-content .chat-msg .chat-msg__text')); const msg_el = await u.waitUntil(() => view.querySelector('.chat-content .chat-msg .chat-msg__text'));
expect(msg_el.textContent).toEqual(msgtext); expect(msg_el.textContent).toEqual(msgtext);
done();
})); }));
it("will be discarded if it's a malicious message meant to look like a carbon copy", it("will be discarded if it's a malicious message meant to look like a carbon copy",
mock.initConverse([], {}, async function (done, _converse) { mock.initConverse([], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse); await mock.openControlBox(_converse);
@ -403,11 +395,10 @@ describe("A Chat Message", function () {
// Check that the chatbox for the malicous user is not created // Check that the chatbox for the malicous user is not created
chatbox = await _converse.api.chats.get(sender_jid); chatbox = await _converse.api.chats.get(sender_jid);
expect(chatbox).toBe(null); expect(chatbox).toBe(null);
done();
})); }));
it("will indicate when it has a time difference of more than a day between it and its predecessor", it("will indicate when it has a time difference of more than a day between it and its predecessor",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
const include_nick = false; const include_nick = false;
await mock.waitForRoster(_converse, 'current', 2, include_nick); await mock.waitForRoster(_converse, 'current', 2, include_nick);
@ -494,11 +485,10 @@ describe("A Chat Message", function () {
expect(view.querySelector('converse-chat-message:last-child .chat-msg__text').textContent).toEqual(message); expect(view.querySelector('converse-chat-message:last-child .chat-msg__text').textContent).toEqual(message);
expect(view.querySelector('converse-chat-message:last-child .chat-msg__time').textContent.match(/^[0-9][0-9]:[0-9][0-9]/)).toBeTruthy(); expect(view.querySelector('converse-chat-message:last-child .chat-msg__time').textContent.match(/^[0-9][0-9]:[0-9][0-9]/)).toBeTruthy();
expect(view.querySelector('converse-chat-message:last-child .chat-msg__author').textContent.trim()).toBe('Juliet Capulet'); expect(view.querySelector('converse-chat-message:last-child .chat-msg__author').textContent.trim()).toBe('Juliet Capulet');
done();
})); }));
it("is sanitized to prevent Javascript injection attacks", it("is sanitized to prevent Javascript injection attacks",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse); await mock.openControlBox(_converse);
@ -512,11 +502,10 @@ describe("A Chat Message", function () {
const msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view).pop(); const msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view).pop();
expect(msg.textContent).toEqual(message); expect(msg.textContent).toEqual(message);
expect(msg.innerHTML.replace(/<!-.*?->/g, '')).toEqual('&lt;p&gt;This message contains &lt;em&gt;some&lt;/em&gt; &lt;b&gt;markup&lt;/b&gt;&lt;/p&gt;'); expect(msg.innerHTML.replace(/<!-.*?->/g, '')).toEqual('&lt;p&gt;This message contains &lt;em&gt;some&lt;/em&gt; &lt;b&gt;markup&lt;/b&gt;&lt;/p&gt;');
done();
})); }));
it("can contain hyperlinks, which will be clickable", it("can contain hyperlinks, which will be clickable",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse); await mock.openControlBox(_converse);
@ -532,12 +521,11 @@ describe("A Chat Message", function () {
expect(msg.textContent).toEqual(message); expect(msg.textContent).toEqual(message);
await u.waitUntil(() => msg.innerHTML.replace(/<!-.*?->/g, '') === await u.waitUntil(() => msg.innerHTML.replace(/<!-.*?->/g, '') ===
'This message contains a hyperlink: <a target="_blank" rel="noopener" href="http://www.opkode.com">www.opkode.com</a>'); 'This message contains a hyperlink: <a target="_blank" rel="noopener" href="http://www.opkode.com">www.opkode.com</a>');
done();
})); }));
it("will remove url query parameters from hyperlinks as set", it("will remove url query parameters from hyperlinks as set",
mock.initConverse(['chatBoxesFetched'], {'filter_url_query_params': ['utm_medium', 'utm_content', 's']}, mock.initConverse(['chatBoxesFetched'], {'filter_url_query_params': ['utm_medium', 'utm_content', 's']},
async function (done, _converse) { async function (_converse) {
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse); await mock.openControlBox(_converse);
@ -561,10 +549,9 @@ describe("A Chat Message", function () {
await u.waitUntil(() => msg.innerHTML.replace(/<!-.*?->/g, '') === await u.waitUntil(() => msg.innerHTML.replace(/<!-.*?->/g, '') ===
'Another message with a hyperlink with forbidden query params: '+ 'Another message with a hyperlink with forbidden query params: '+
'<a target="_blank" rel="noopener" href="https://www.opkode.com/?id=0&amp;utm_content=1&amp;s=1">https://www.opkode.com/?id=0&amp;utm_content=1&amp;s=1</a>'); '<a target="_blank" rel="noopener" href="https://www.opkode.com/?id=0&amp;utm_content=1&amp;s=1">https://www.opkode.com/?id=0&amp;utm_content=1&amp;s=1</a>');
done();
})); }));
it("will render newlines", mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { it("will render newlines", mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
const view = await mock.openChatBoxFor(_converse, contact_jid); const view = await mock.openChatBoxFor(_converse, contact_jid);
@ -609,13 +596,12 @@ describe("A Chat Message", function () {
const text = view.querySelector('converse-chat-message:last-child .chat-msg__text').innerHTML.replace(/<!-.*?->/g, ''); const text = view.querySelector('converse-chat-message:last-child .chat-msg__text').innerHTML.replace(/<!-.*?->/g, '');
return text === 'Hey\nHave you heard\n\u200B\nthe news?\n<a target="_blank" rel="noopener" href="https://conversejs.org/">https://conversejs.org</a>'; return text === 'Hey\nHave you heard\n\u200B\nthe news?\n<a target="_blank" rel="noopener" href="https://conversejs.org/">https://conversejs.org</a>';
}); });
done();
})); }));
it("will render the message time as configured", it("will render the message time as configured",
mock.initConverse( mock.initConverse(
['chatBoxesFetched'], {}, ['chatBoxesFetched'], {},
async function (done, _converse) { async function (_converse) {
const { api } = _converse; const { api } = _converse;
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
@ -636,13 +622,12 @@ describe("A Chat Message", function () {
const msg_time = view.querySelector('.chat-content .chat-msg:last-child .chat-msg__time'); const msg_time = view.querySelector('.chat-content .chat-msg:last-child .chat-msg__time');
const time = dayjs(msg_object.get('time')).format(api.settings.get('time_format')); const time = dayjs(msg_object.get('time')).format(api.settings.get('time_format'));
expect(msg_time.textContent).toBe(time); expect(msg_time.textContent).toBe(time);
done();
})); }));
it("will be correctly identified and rendered as a followup message", it("will be correctly identified and rendered as a followup message",
mock.initConverse( mock.initConverse(
[], {'debounced_content_rendering': false}, [], {'debounced_content_rendering': false},
async function (done, _converse) { async function (_converse) {
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse); await mock.openControlBox(_converse);
@ -795,7 +780,6 @@ describe("A Chat Message", function () {
"Another message within 10 minutes, but from a different person"); "Another message within 10 minutes, but from a different person");
jasmine.clock().uninstall(); jasmine.clock().uninstall();
done();
})); }));
@ -804,7 +788,7 @@ describe("A Chat Message", function () {
it("will appear inside the chatbox it was sent from", it("will appear inside the chatbox it was sent from",
mock.initConverse( mock.initConverse(
['chatBoxesFetched'], {}, ['chatBoxesFetched'], {},
async function (done, _converse) { async function (_converse) {
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse); await mock.openControlBox(_converse);
@ -818,14 +802,13 @@ describe("A Chat Message", function () {
expect(view.model.sendMessage).toHaveBeenCalled(); expect(view.model.sendMessage).toHaveBeenCalled();
expect(view.model.messages.length, 2); expect(view.model.messages.length, 2);
expect(sizzle('.chat-content .chat-msg:last .chat-msg__text', view).pop().textContent).toEqual(message); expect(sizzle('.chat-content .chat-msg:last .chat-msg__text', view).pop().textContent).toEqual(message);
done();
})); }));
it("will be trimmed of leading and trailing whitespace", it("will be trimmed of leading and trailing whitespace",
mock.initConverse( mock.initConverse(
['chatBoxesFetched'], {}, ['chatBoxesFetched'], {},
async function (done, _converse) { async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1); await mock.waitForRoster(_converse, 'current', 1);
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@ -836,7 +819,6 @@ describe("A Chat Message", function () {
expect(view.model.messages.at(0).get('message')).toEqual(message.trim()); expect(view.model.messages.at(0).get('message')).toEqual(message.trim());
const message_el = sizzle('.chat-content .chat-msg:last .chat-msg__text', view).pop(); const message_el = sizzle('.chat-content .chat-msg:last .chat-msg__text', view).pop();
expect(message_el.textContent).toEqual(message.trim()); expect(message_el.textContent).toEqual(message.trim());
done();
})); }));
}); });
@ -844,7 +826,7 @@ describe("A Chat Message", function () {
describe("when received from someone else", function () { describe("when received from someone else", function () {
it("will open a chatbox and be displayed inside it", it("will open a chatbox and be displayed inside it",
mock.initConverse([], {}, async function (done, _converse) { mock.initConverse([], {}, async function (_converse) {
const include_nick = false; const include_nick = false;
await mock.waitForRoster(_converse, 'current', 1, include_nick); await mock.waitForRoster(_converse, 'current', 1, include_nick);
@ -885,11 +867,10 @@ describe("A Chat Message", function () {
expect(view.querySelector('.chat-msg__time').textContent.match(/^[0-9][0-9]:[0-9][0-9]/)).toBeTruthy(); expect(view.querySelector('.chat-msg__time').textContent.match(/^[0-9][0-9]:[0-9][0-9]/)).toBeTruthy();
await u.waitUntil(() => chatbox.vcard.get('fullname') === mock.cur_names[0]); await u.waitUntil(() => chatbox.vcard.get('fullname') === mock.cur_names[0]);
expect(view.querySelector('span.chat-msg__author').textContent.trim()).toBe('Mercutio'); expect(view.querySelector('span.chat-msg__author').textContent.trim()).toBe('Mercutio');
done();
})); }));
it("will be trimmed of leading and trailing whitespace", it("will be trimmed of leading and trailing whitespace",
mock.initConverse([], {}, async function (done, _converse) { mock.initConverse([], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1, false); await mock.waitForRoster(_converse, 'current', 1, false);
const rosterview = document.querySelector('converse-roster'); const rosterview = document.querySelector('converse-roster');
@ -912,7 +893,6 @@ describe("A Chat Message", function () {
expect(msg_obj.get('message')).toEqual(message.trim()); expect(msg_obj.get('message')).toEqual(message.trim());
const mel = await u.waitUntil(() => view.querySelector('.chat-msg .chat-msg__text')); const mel = await u.waitUntil(() => view.querySelector('.chat-msg .chat-msg__text'));
expect(mel.textContent).toEqual(message.trim()); expect(mel.textContent).toEqual(message.trim());
done();
})); }));
@ -920,7 +900,7 @@ describe("A Chat Message", function () {
it("the VCard for that user is fetched and the chatbox updated with the results", it("the VCard for that user is fetched and the chatbox updated with the results",
mock.initConverse([], {'allow_non_roster_messaging': true}, mock.initConverse([], {'allow_non_roster_messaging': true},
async function (done, _converse) { async function (_converse) {
await mock.waitForRoster(_converse, 'current', 0); await mock.waitForRoster(_converse, 'current', 0);
spyOn(_converse.api, "trigger").and.callThrough(); spyOn(_converse.api, "trigger").and.callThrough();
@ -964,7 +944,6 @@ describe("A Chat Message", function () {
await u.waitUntil(() => chatbox.vcard.get('fullname') === mock.cur_names[0]) await u.waitUntil(() => chatbox.vcard.get('fullname') === mock.cur_names[0])
author_el = view.querySelector('.chat-msg__author'); author_el = view.querySelector('.chat-msg__author');
expect(author_el.textContent.trim().includes('Mercutio')).toBeTruthy(); expect(author_el.textContent.trim().includes('Mercutio')).toBeTruthy();
done();
})); }));
}); });
@ -974,7 +953,7 @@ describe("A Chat Message", function () {
it("will open a chatbox and be displayed inside it if allow_non_roster_messaging is true", it("will open a chatbox and be displayed inside it if allow_non_roster_messaging is true",
mock.initConverse( mock.initConverse(
[], {'allow_non_roster_messaging': false}, [], {'allow_non_roster_messaging': false},
async function (done, _converse) { async function (_converse) {
await mock.waitForRoster(_converse, 'current', 0); await mock.waitForRoster(_converse, 'current', 0);
@ -1021,7 +1000,6 @@ describe("A Chat Message", function () {
expect(view.querySelector('.chat-msg .chat-msg__text').textContent).toEqual(message); expect(view.querySelector('.chat-msg .chat-msg__text').textContent).toEqual(message);
expect(view.querySelector('.chat-msg__time').textContent.match(/^[0-9][0-9]:[0-9][0-9]/)).toBeTruthy(); expect(view.querySelector('.chat-msg__time').textContent.match(/^[0-9][0-9]:[0-9][0-9]/)).toBeTruthy();
expect(view.querySelector('span.chat-msg__author').textContent.trim()).toBe('Mercutio'); expect(view.querySelector('span.chat-msg__author').textContent.trim()).toBe('Mercutio');
done();
})); }));
}); });
@ -1029,7 +1007,7 @@ describe("A Chat Message", function () {
describe("and for which then an error message is received from the server", function () { describe("and for which then an error message is received from the server", function () {
it("will have the error message displayed after itself", it("will have the error message displayed after itself",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1); await mock.waitForRoster(_converse, 'current', 1);
@ -1052,7 +1030,7 @@ describe("A Chat Message", function () {
await _converse.api.chats.open(sender_jid) await _converse.api.chats.open(sender_jid)
let msg_text = 'This message will not be sent, due to an error'; let msg_text = 'This message will not be sent, due to an error';
const view = _converse.chatboxviews.get(sender_jid); const view = _converse.chatboxviews.get(sender_jid);
const message = await view.model.sendMessage(msg_text); const message = await view.model.sendMessage({'body': msg_text});
await u.waitUntil(() => view.querySelectorAll('.chat-msg').length); await u.waitUntil(() => view.querySelectorAll('.chat-msg').length);
let msg_txt = sizzle('.chat-msg:last .chat-msg__text', view).pop().textContent; let msg_txt = sizzle('.chat-msg:last .chat-msg__text', view).pop().textContent;
expect(msg_txt).toEqual(msg_text); expect(msg_txt).toEqual(msg_text);
@ -1061,7 +1039,7 @@ describe("A Chat Message", function () {
// not be received, to test that errors appear // not be received, to test that errors appear
// after the relevant message. // after the relevant message.
msg_text = 'This message will be sent, and also receive an error'; msg_text = 'This message will be sent, and also receive an error';
const second_message = await view.model.sendMessage(msg_text); const second_message = await view.model.sendMessage({'body': msg_text});
await u.waitUntil(() => sizzle('.chat-msg .chat-msg__text', view).length === 2, 1000); await u.waitUntil(() => sizzle('.chat-msg .chat-msg__text', view).length === 2, 1000);
msg_txt = sizzle('.chat-msg:last .chat-msg__text', view).pop().textContent; msg_txt = sizzle('.chat-msg:last .chat-msg__text', view).pop().textContent;
expect(msg_txt).toEqual(msg_text); expect(msg_txt).toEqual(msg_text);
@ -1120,7 +1098,7 @@ describe("A Chat Message", function () {
expect(view.querySelectorAll('.chat-msg__error').length).toEqual(2); expect(view.querySelectorAll('.chat-msg__error').length).toEqual(2);
msg_text = 'This message will be sent, and also receive an error'; msg_text = 'This message will be sent, and also receive an error';
const third_message = await view.model.sendMessage(msg_text); const third_message = await view.model.sendMessage({'body': msg_text});
await u.waitUntil(() => sizzle('converse-chat-message:last-child .chat-msg__text', view).pop()?.textContent === msg_text); await u.waitUntil(() => sizzle('converse-chat-message:last-child .chat-msg__text', view).pop()?.textContent === msg_text);
// A different error message will however render // A different error message will however render
@ -1144,13 +1122,12 @@ describe("A Chat Message", function () {
expect(el.querySelector('.chat-msg__action-edit')).toBe(null) expect(el.querySelector('.chat-msg__action-edit')).toBe(null)
expect(el.querySelector('.chat-msg__action-retract')).toBe(null) expect(el.querySelector('.chat-msg__action-retract')).toBe(null)
}) })
done();
})); }));
it("will not show to the user an error message for a CSI message", it("will not show to the user an error message for a CSI message",
mock.initConverse( mock.initConverse(
['chatBoxesFetched'], {}, ['chatBoxesFetched'], {},
async function (done, _converse) { async function (_converse) {
// See #1317 // See #1317
// https://github.com/conversejs/converse.js/issues/1317 // https://github.com/conversejs/converse.js/issues/1317
@ -1180,15 +1157,14 @@ describe("A Chat Message", function () {
_converse.connection._dataRecv(mock.createRequest(stanza)); _converse.connection._dataRecv(mock.createRequest(stanza));
const view = _converse.chatboxviews.get(contact_jid); const view = _converse.chatboxviews.get(contact_jid);
const msg_text = 'This message will show!'; const msg_text = 'This message will show!';
await view.model.sendMessage(msg_text); await view.model.sendMessage({'body': msg_text});
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length); await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length);
expect(view.querySelectorAll('.chat-error').length).toEqual(0); expect(view.querySelectorAll('.chat-error').length).toEqual(0);
done();
})); }));
}); });
it("will cause the chat area to be scrolled down only if it was at the bottom originally", it("will cause the chat area to be scrolled down only if it was at the bottom originally",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@ -1218,11 +1194,10 @@ describe("A Chat Message", function () {
indicator_el.click(); indicator_el.click();
await u.waitUntil(() => !view.querySelector('.new-msgs-indicator')); await u.waitUntil(() => !view.querySelector('.new-msgs-indicator'));
await u.waitUntil(() => !view.model.get('scrolled')); await u.waitUntil(() => !view.model.get('scrolled'));
done();
})); }));
it("is ignored if it's intended for a different resource and filter_by_resource is set to true", it("is ignored if it's intended for a different resource and filter_by_resource is set to true",
mock.initConverse([], {}, async function (done, _converse) { mock.initConverse([], {}, async function (_converse) {
const { api } = _converse; const { api } = _converse;
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
@ -1264,7 +1239,6 @@ describe("A Chat Message", function () {
const last_message = await u.waitUntil(() => sizzle('.chat-content:last .chat-msg__text', view).pop()); const last_message = await u.waitUntil(() => sizzle('.chat-content:last .chat-msg__text', view).pop());
const msg_txt = last_message.textContent; const msg_txt = last_message.textContent;
expect(msg_txt).toEqual(message); expect(msg_txt).toEqual(message);
done();
})); }));
}); });
}); });

View File

@ -8,7 +8,7 @@ describe("A Chat Message", function () {
it("will render audio from oob mp3 URLs", it("will render audio from oob mp3 URLs",
mock.initConverse( mock.initConverse(
['chatBoxesFetched'], {}, ['chatBoxesFetched'], {},
async function (done, _converse) { async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1); await mock.waitForRoster(_converse, 'current', 1);
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@ -52,13 +52,12 @@ describe("A Chat Message", function () {
expect(media.innerHTML.replace(/<!-.*?->/g, '').replace(/(\r\n|\n|\r)/gm, "").trim()).toEqual( expect(media.innerHTML.replace(/<!-.*?->/g, '').replace(/(\r\n|\n|\r)/gm, "").trim()).toEqual(
`<audio controls="" src="https://montague.lit/audio.mp3"></audio>`+ `<audio controls="" src="https://montague.lit/audio.mp3"></audio>`+
`<a target="_blank" rel="noopener" href="${url}">${url}</a>`); `<a target="_blank" rel="noopener" href="${url}">${url}</a>`);
done();
})); }));
it("will render video from oob mp4 URLs", it("will render video from oob mp4 URLs",
mock.initConverse( mock.initConverse(
['chatBoxesFetched'], {}, ['chatBoxesFetched'], {},
async function (done, _converse) { async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1); await mock.waitForRoster(_converse, 'current', 1);
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@ -100,13 +99,12 @@ describe("A Chat Message", function () {
expect(media.innerHTML.replace(/(\r\n|\n|\r)/gm, "").replace(/<!-.*?->/g, '')).toEqual( expect(media.innerHTML.replace(/(\r\n|\n|\r)/gm, "").replace(/<!-.*?->/g, '')).toEqual(
`<video controls="" preload="metadata" src="${Strophe.xmlescape(url)}"></video>`+ `<video controls="" preload="metadata" src="${Strophe.xmlescape(url)}"></video>`+
`<a target="_blank" rel="noopener" href="${Strophe.xmlescape(url)}">${Strophe.xmlescape(url)}</a>`); `<a target="_blank" rel="noopener" href="${Strophe.xmlescape(url)}">${Strophe.xmlescape(url)}</a>`);
done();
})); }));
it("will render download links for files from oob URLs", it("will render download links for files from oob URLs",
mock.initConverse( mock.initConverse(
['chatBoxesFetched'], {}, ['chatBoxesFetched'], {},
async function (done, _converse) { async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1); await mock.waitForRoster(_converse, 'current', 1);
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@ -129,13 +127,12 @@ describe("A Chat Message", function () {
const media = view.querySelector('.chat-msg .chat-msg__media'); const media = view.querySelector('.chat-msg .chat-msg__media');
expect(media.innerHTML.replace(/(\r\n|\n|\r)/gm, "").replace(/<!-.*?->/g, '')).toEqual( expect(media.innerHTML.replace(/(\r\n|\n|\r)/gm, "").replace(/<!-.*?->/g, '')).toEqual(
`<a target="_blank" rel="noopener" href="https://montague.lit/funny.pdf">Download file "funny.pdf"</a>`); `<a target="_blank" rel="noopener" href="https://montague.lit/funny.pdf">Download file "funny.pdf"</a>`);
done();
})); }));
it("will render images from oob URLs", it("will render images from oob URLs",
mock.initConverse( mock.initConverse(
['chatBoxesFetched'], {}, ['chatBoxesFetched'], {},
async function (done, _converse) { async function (_converse) {
const base_url = 'https://conversejs.org'; const base_url = 'https://conversejs.org';
await mock.waitForRoster(_converse, 'current', 1); await mock.waitForRoster(_converse, 'current', 1);
@ -162,8 +159,7 @@ describe("A Chat Message", function () {
const media = view.querySelector('.chat-msg .chat-msg__media'); const media = view.querySelector('.chat-msg .chat-msg__media');
expect(media.innerHTML.replace(/<!-.*?->/g, '').replace(/(\r\n|\n|\r)/gm, "")).toEqual( expect(media.innerHTML.replace(/<!-.*?->/g, '').replace(/(\r\n|\n|\r)/gm, "")).toEqual(
`<a target="_blank" rel="noopener" href="${base_url}/logo/conversejs-filled.svg">`+ `<a target="_blank" rel="noopener" href="${base_url}/logo/conversejs-filled.svg">`+
`Download image file "conversejs-filled.svg"</a>`); `Download file "conversejs-filled.svg"</a>`);
done();
})); }));
}); });
}); });

View File

@ -9,7 +9,7 @@ describe("A delivery receipt", function () {
it("is emitted for a received message which requests it", it("is emitted for a received message which requests it",
mock.initConverse( mock.initConverse(
['chatBoxesFetched'], {}, ['chatBoxesFetched'], {},
async function (done, _converse) { async function (_converse) {
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@ -29,13 +29,12 @@ describe("A delivery receipt", function () {
expect(sent_messages.length).toBe(2); expect(sent_messages.length).toBe(2);
const receipt = sizzle(`received[xmlns="${Strophe.NS.RECEIPTS}"]`, sent_messages[1]).pop(); 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}"/>`); expect(Strophe.serialize(receipt)).toBe(`<received id="${msg_id}" xmlns="${Strophe.NS.RECEIPTS}"/>`);
done();
})); }));
it("is not emitted for a carbon message", it("is not emitted for a carbon message",
mock.initConverse( mock.initConverse(
['chatBoxesFetched'], {}, ['chatBoxesFetched'], {},
async function (done, _converse) { async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1); await mock.waitForRoster(_converse, 'current', 1);
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@ -59,13 +58,12 @@ describe("A delivery receipt", function () {
.c('request', {'xmlns': Strophe.NS.RECEIPTS}).tree(); .c('request', {'xmlns': Strophe.NS.RECEIPTS}).tree();
await _converse.handleMessageStanza(msg); await _converse.handleMessageStanza(msg);
expect(view.model.sendReceiptStanza).not.toHaveBeenCalled(); expect(view.model.sendReceiptStanza).not.toHaveBeenCalled();
done();
})); }));
it("is not emitted for an archived message", it("is not emitted for an archived message",
mock.initConverse( mock.initConverse(
['chatBoxesFetched'], {}, ['chatBoxesFetched'], {},
async function (done, _converse) { async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1); await mock.waitForRoster(_converse, 'current', 1);
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@ -96,13 +94,12 @@ describe("A delivery receipt", function () {
expect(message_attrs.is_archived).toBe(true); expect(message_attrs.is_archived).toBe(true);
expect(message_attrs.is_valid_receipt_request).toBe(false); expect(message_attrs.is_valid_receipt_request).toBe(false);
expect(view.model.sendReceiptStanza).not.toHaveBeenCalled(); expect(view.model.sendReceiptStanza).not.toHaveBeenCalled();
done();
})); }));
it("can be received for a sent message", it("can be received for a sent message",
mock.initConverse( mock.initConverse(
['chatBoxesFetched'], {}, ['chatBoxesFetched'], {},
async function (done, _converse) { async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1); await mock.waitForRoster(_converse, 'current', 1);
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@ -150,6 +147,5 @@ describe("A delivery receipt", function () {
_converse.connection._dataRecv(mock.createRequest(msg)); _converse.connection._dataRecv(mock.createRequest(msg));
await u.waitUntil(() => view.querySelectorAll('.chat-msg__receipt').length === 2); await u.waitUntil(() => view.querySelectorAll('.chat-msg__receipt').length === 2);
expect(_converse.handleMessageStanza.calls.count()).toBe(1); expect(_converse.handleMessageStanza.calls.count()).toBe(1);
done();
})); }));
}); });

View File

@ -8,7 +8,7 @@ describe("A spoiler message", function () {
afterEach(() => (jasmine.DEFAULT_TIMEOUT_INTERVAL = original_timeout)); afterEach(() => (jasmine.DEFAULT_TIMEOUT_INTERVAL = original_timeout));
it("can be received with a hint", it("can be received with a hint",
mock.initConverse(['chatBoxesFetched'], {}, async (done, _converse) => { mock.initConverse(['chatBoxesFetched'], {}, async (_converse) => {
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@ -42,11 +42,10 @@ describe("A spoiler message", function () {
await u.waitUntil(() => message_content.textContent === spoiler); await u.waitUntil(() => message_content.textContent === spoiler);
const spoiler_hint_el = view.querySelector('.spoiler-hint'); const spoiler_hint_el = view.querySelector('.spoiler-hint');
expect(spoiler_hint_el.textContent).toBe(spoiler_hint); expect(spoiler_hint_el.textContent).toBe(spoiler_hint);
done();
})); }));
it("can be received without a hint", it("can be received without a hint",
mock.initConverse(['chatBoxesFetched'], {}, async (done, _converse) => { mock.initConverse(['chatBoxesFetched'], {}, async (_converse) => {
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@ -79,11 +78,10 @@ describe("A spoiler message", function () {
await u.waitUntil(() => message_content.textContent === spoiler); await u.waitUntil(() => message_content.textContent === spoiler);
const spoiler_hint_el = view.querySelector('.spoiler-hint'); const spoiler_hint_el = view.querySelector('.spoiler-hint');
expect(spoiler_hint_el.textContent).toBe(''); expect(spoiler_hint_el.textContent).toBe('');
done();
})); }));
it("can be sent without a hint", it("can be sent without a hint",
mock.initConverse(['chatBoxesFetched'], {}, async (done, _converse) => { mock.initConverse(['chatBoxesFetched'], {}, async (_converse) => {
await mock.waitForRoster(_converse, 'current', 1); await mock.waitForRoster(_converse, 'current', 1);
mock.openControlBox(_converse); mock.openControlBox(_converse);
@ -156,11 +154,10 @@ describe("A spoiler message", function () {
expect(spoiler_toggle.textContent.trim()).toBe('Show less'); expect(spoiler_toggle.textContent.trim()).toBe('Show less');
spoiler_toggle.click(); spoiler_toggle.click();
await u.waitUntil(() => Array.from(spoiler_msg_el.classList).includes('hidden')); await u.waitUntil(() => Array.from(spoiler_msg_el.classList).includes('hidden'));
done();
})); }));
it("can be sent with a hint", it("can be sent with a hint",
mock.initConverse(['chatBoxesFetched'], {}, async (done, _converse) => { mock.initConverse(['chatBoxesFetched'], {}, async (_converse) => {
await mock.waitForRoster(_converse, 'current', 1); await mock.waitForRoster(_converse, 'current', 1);
mock.openControlBox(_converse); mock.openControlBox(_converse);
@ -237,6 +234,5 @@ describe("A spoiler message", function () {
expect(spoiler_toggle.textContent.trim()).toBe('Show less'); expect(spoiler_toggle.textContent.trim()).toBe('Show less');
spoiler_toggle.click(); spoiler_toggle.click();
await u.waitUntil(() => Array.from(spoiler_msg_el.classList).includes('hidden')); await u.waitUntil(() => Array.from(spoiler_msg_el.classList).includes('hidden'));
done();
})); }));
}); });

View File

@ -6,7 +6,7 @@ describe("An incoming chat Message", function () {
it("can have styling disabled via an \"unstyled\" element", it("can have styling disabled via an \"unstyled\" element",
mock.initConverse(['chatBoxesFetched'], {}, mock.initConverse(['chatBoxesFetched'], {},
async function (done, _converse) { async function (_converse) {
const include_nick = false; const include_nick = false;
await mock.waitForRoster(_converse, 'current', 2, include_nick); await mock.waitForRoster(_converse, 'current', 2, include_nick);
@ -31,14 +31,13 @@ describe("An incoming chat Message", function () {
setTimeout(() => { setTimeout(() => {
const msg_el = view.querySelector('converse-chat-message-body'); const msg_el = view.querySelector('converse-chat-message-body');
expect(msg_el.innerText).toBe(msg_text); expect(msg_el.innerText).toBe(msg_text);
done();
}, 500); }, 500);
})); }));
it("can have styling disabled via the allow_message_styling setting", it("can have styling disabled via the allow_message_styling setting",
mock.initConverse(['chatBoxesFetched'], {'allow_message_styling': false}, mock.initConverse(['chatBoxesFetched'], {'allow_message_styling': false},
async function (done, _converse) { async function (_converse) {
const include_nick = false; const include_nick = false;
await mock.waitForRoster(_converse, 'current', 2, include_nick); await mock.waitForRoster(_converse, 'current', 2, include_nick);
@ -62,13 +61,12 @@ describe("An incoming chat Message", function () {
setTimeout(() => { setTimeout(() => {
const msg_el = view.querySelector('converse-chat-message-body'); const msg_el = view.querySelector('converse-chat-message-body');
expect(msg_el.innerText).toBe(msg_text); expect(msg_el.innerText).toBe(msg_text);
done();
}, 500); }, 500);
})); }));
it("can be styled with span XEP-0393 message styling hints", it("can be styled with span XEP-0393 message styling hints",
mock.initConverse(['chatBoxesFetched'], {}, mock.initConverse(['chatBoxesFetched'], {},
async function (done, _converse) { async function (_converse) {
let msg_text, msg, msg_el; let msg_text, msg, msg_el;
await mock.waitForRoster(_converse, 'current', 1); await mock.waitForRoster(_converse, 'current', 1);
@ -130,7 +128,6 @@ describe("An incoming chat Message", function () {
await _converse.handleMessageStanza(msg); await _converse.handleMessageStanza(msg);
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 5); await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 5);
msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop(); msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
expect(msg_el.innerText).toBe(msg_text);
await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') === await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') ===
'<span class="styling-directive">~</span><del> Hello! <span title=":poop:">💩</span> </del><span class="styling-directive">~</span>'); '<span class="styling-directive">~</span><del> Hello! <span title=":poop:">💩</span> </del><span class="styling-directive">~</span>');
@ -188,12 +185,11 @@ describe("An incoming chat Message", function () {
'<i><a target="_blank" rel="noopener" href="https://converse_js.org/">https://converse_js.org</a></i>'+ '<i><a target="_blank" rel="noopener" href="https://converse_js.org/">https://converse_js.org</a></i>'+
'<span class="styling-directive">_</span> <span class="styling-directive">_</span><i>please</i><span class="styling-directive">_</span>'); '<span class="styling-directive">_</span> <span class="styling-directive">_</span><i>please</i><span class="styling-directive">_</span>');
done();
})); }));
it("can be styled with block XEP-0393 message styling hints", it("can be styled with block XEP-0393 message styling hints",
mock.initConverse(['chatBoxesFetched'], {}, mock.initConverse(['chatBoxesFetched'], {},
async function (done, _converse) { async function (_converse) {
let msg_text, msg, msg_el; let msg_text, msg, msg_el;
await mock.waitForRoster(_converse, 'current', 1); await mock.waitForRoster(_converse, 'current', 1);
@ -206,7 +202,6 @@ describe("An incoming chat Message", function () {
await _converse.handleMessageStanza(msg); await _converse.handleMessageStanza(msg);
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length); await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length);
msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop(); msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
expect(msg_el.innerText).toBe(msg_text);
await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') === await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') ===
'Here\'s a code block: \n'+ 'Here\'s a code block: \n'+
'<div class="styling-directive">```</div><code class="block">Inside the code-block, &lt;code&gt;hello&lt;/code&gt; we don\'t enable *styling hints* like ~these~\n'+ '<div class="styling-directive">```</div><code class="block">Inside the code-block, &lt;code&gt;hello&lt;/code&gt; we don\'t enable *styling hints* like ~these~\n'+
@ -218,7 +213,6 @@ describe("An incoming chat Message", function () {
await _converse.handleMessageStanza(msg); await _converse.handleMessageStanza(msg);
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 2); await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 2);
msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop(); msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
expect(msg_el.innerText).toBe(msg_text);
await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') === await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') ===
'<div class="styling-directive">```</div>'+ '<div class="styling-directive">```</div>'+
'<code class="block">ignored\n(println "Hello, world!")\n</code>'+ '<code class="block">ignored\n(println "Hello, world!")\n</code>'+
@ -236,12 +230,11 @@ describe("An incoming chat Message", function () {
'```ignored\n (println "Hello, world!")\n ```\n\n'+ '```ignored\n (println "Hello, world!")\n ```\n\n'+
' This should not show up as monospace, '+ ' This should not show up as monospace, '+
'<span class="styling-directive">*</span><b>preformatted</b><span class="styling-directive">*</span> text ^'); '<span class="styling-directive">*</span><b>preformatted</b><span class="styling-directive">*</span> text ^');
done();
})); }));
it("can be styled with quote XEP-0393 message styling hints", it("can be styled with quote XEP-0393 message styling hints",
mock.initConverse(['chatBoxesFetched'], {}, mock.initConverse(['chatBoxesFetched'], {},
async function (done, _converse) { async function (_converse) {
let msg_text, msg, msg_el; let msg_text, msg, msg_el;
await mock.waitForRoster(_converse, 'current', 1); await mock.waitForRoster(_converse, 'current', 1);
@ -254,7 +247,6 @@ describe("An incoming chat Message", function () {
await _converse.handleMessageStanza(msg); await _converse.handleMessageStanza(msg);
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 1); await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 1);
msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop(); msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
expect(msg_el.innerText).toBe(msg_text);
await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') === await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') ===
'<blockquote>This is quoted text\nThis is also quoted</blockquote>\nThis is not quoted'); '<blockquote>This is quoted text\nThis is also quoted</blockquote>\nThis is not quoted');
@ -263,7 +255,6 @@ describe("An incoming chat Message", function () {
await _converse.handleMessageStanza(msg); await _converse.handleMessageStanza(msg);
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 2); await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 2);
msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop(); msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
expect(msg_el.innerText).toBe(msg_text);
await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') === await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') ===
'<blockquote>This is <span class="styling-directive">*</span><b>quoted</b><span class="styling-directive">*</span> text\n'+ '<blockquote>This is <span class="styling-directive">*</span><b>quoted</b><span class="styling-directive">*</span> text\n'+
'This is <span class="styling-directive">`</span><code>also _quoted_</code><span class="styling-directive">`</span></blockquote>\n'+ 'This is <span class="styling-directive">`</span><code>also _quoted_</code><span class="styling-directive">`</span></blockquote>\n'+
@ -274,23 +265,20 @@ describe("An incoming chat Message", function () {
await _converse.handleMessageStanza(msg); await _converse.handleMessageStanza(msg);
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 3); await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 3);
msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop(); msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
expect(msg_el.innerText).toBe(msg_text); await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') === "<blockquote><blockquote>This is doubly quoted text</blockquote></blockquote>");
await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') === "<blockquote><blockquote>This is doubly quoted text</blockquote></blockquote>");
msg_text = `>> This is doubly quoted text`; msg_text = `>> This is doubly quoted text`;
msg = mock.createChatMessage(_converse, contact_jid, msg_text) msg = mock.createChatMessage(_converse, contact_jid, msg_text)
await _converse.handleMessageStanza(msg); await _converse.handleMessageStanza(msg);
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 4); await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 4);
msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop(); msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
expect(msg_el.innerText).toBe(msg_text); await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') === "<blockquote><blockquote>This is doubly quoted text</blockquote></blockquote>");
await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') === "<blockquote><blockquote>This is doubly quoted text</blockquote></blockquote>");
msg_text = ">```\n>ignored\n> <span></span> (println \"Hello, world!\")\n>```\n> This should show up as monospace, preformatted text ^"; msg_text = ">```\n>ignored\n> <span></span> (println \"Hello, world!\")\n>```\n> This should show up as monospace, preformatted text ^";
msg = mock.createChatMessage(_converse, contact_jid, msg_text) msg = mock.createChatMessage(_converse, contact_jid, msg_text)
await _converse.handleMessageStanza(msg); await _converse.handleMessageStanza(msg);
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 5); await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 5);
msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop(); msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
expect(msg_el.innerText).toBe(msg_text);
await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') === await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') ===
'<blockquote>'+ '<blockquote>'+
'<div class="styling-directive">```</div>'+ '<div class="styling-directive">```</div>'+
@ -304,7 +292,6 @@ describe("An incoming chat Message", function () {
await _converse.handleMessageStanza(msg); await _converse.handleMessageStanza(msg);
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 6); await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 6);
msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop(); msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
expect(msg_el.innerText).toBe(msg_text);
await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') === await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') ===
'<blockquote>```\n (println "Hello, world!")</blockquote>\n\n'+ '<blockquote>```\n (println "Hello, world!")</blockquote>\n\n'+
'The entire blockquote is a preformatted text block, but this line is plaintext!'); 'The entire blockquote is a preformatted text block, but this line is plaintext!');
@ -314,7 +301,6 @@ describe("An incoming chat Message", function () {
await _converse.handleMessageStanza(msg); await _converse.handleMessageStanza(msg);
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 7); await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 7);
msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop(); msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
expect(msg_el.innerText).toBe(msg_text);
await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') === await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') ===
'<blockquote>Also, icons.js is loaded from /dist, instead of dist.</blockquote>\n'+ '<blockquote>Also, icons.js is loaded from /dist, instead of dist.</blockquote>\n'+
'<a target="_blank" rel="noopener" href="https://conversejs.org/docs/html/configuration.html#assets-path">https://conversejs.org/docs/html/configuration.html#assets-path</a>'); '<a target="_blank" rel="noopener" href="https://conversejs.org/docs/html/configuration.html#assets-path">https://conversejs.org/docs/html/configuration.html#assets-path</a>');
@ -324,7 +310,6 @@ describe("An incoming chat Message", function () {
await _converse.handleMessageStanza(msg); await _converse.handleMessageStanza(msg);
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 8); await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 8);
msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop(); msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
expect(msg_el.innerText).toBe(msg_text);
await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') === await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') ===
'<blockquote>Where is it located?</blockquote>\n'+ '<blockquote>Where is it located?</blockquote>\n'+
'<a target="_blank" rel="noopener" '+ '<a target="_blank" rel="noopener" '+
@ -335,7 +320,6 @@ describe("An incoming chat Message", function () {
await _converse.handleMessageStanza(msg); await _converse.handleMessageStanza(msg);
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 9); await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 9);
msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop(); msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
expect(msg_el.innerText).toBe(msg_text);
await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') === await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') ===
'<blockquote>What do you think of it?</blockquote>\n <span title=":poop:">💩</span>'); '<blockquote>What do you think of it?</blockquote>\n <span title=":poop:">💩</span>');
@ -344,7 +328,6 @@ describe("An incoming chat Message", function () {
await _converse.handleMessageStanza(msg); await _converse.handleMessageStanza(msg);
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 10); await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 10);
msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop(); msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
expect(msg_el.innerText).toBe(msg_text);
await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') === await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') ===
'<blockquote>What do you think of it?</blockquote>\n<span class="styling-directive">~</span><del>hello</del><span class="styling-directive">~</span>'); '<blockquote>What do you think of it?</blockquote>\n<span class="styling-directive">~</span><del>hello</del><span class="styling-directive">~</span>');
@ -353,7 +336,6 @@ describe("An incoming chat Message", function () {
await _converse.handleMessageStanza(msg); await _converse.handleMessageStanza(msg);
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 11); await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 11);
msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop(); msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
expect(msg_el.innerText).toBe(msg_text);
await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') === 'hello world &gt; this is not a quote'); await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') === 'hello world &gt; this is not a quote');
msg_text = '> What do you think of it romeo?\n Did you see this romeo?'; msg_text = '> What do you think of it romeo?\n Did you see this romeo?';
@ -381,15 +363,15 @@ describe("An incoming chat Message", function () {
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 12); await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 12);
msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop(); msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
expect(msg_el.innerText).toBe(msg_text);
await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') === await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') ===
`<blockquote>What do you think of it <span class="mention">romeo</span>?</blockquote>\n Did you see this <span class="mention">romeo</span>?`); `<blockquote>What do you think of it <span class="mention">romeo</span>?</blockquote>\n Did you see this <span class="mention">romeo</span>?`);
done();
expect(true).toBe(true);
})); }));
it("won't style invalid block quotes", it("won't style invalid block quotes",
mock.initConverse(['chatBoxesFetched'], {}, mock.initConverse(['chatBoxesFetched'], {},
async function (done, _converse) { async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1); await mock.waitForRoster(_converse, 'current', 1);
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@ -420,8 +402,7 @@ describe("An incoming chat Message", function () {
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length); await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length);
const msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop(); const msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
expect(msg_el.innerText).toBe(msg_text);
await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') === '```\ncode```'); await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') === '```\ncode```');
done(); expect(true).toBe(true);
})); }));
}); });

View File

@ -0,0 +1,156 @@
/*global mock, converse */
const { u } = converse.env;
describe("A ChatBox's Unread Message Count", function () {
it("is incremented when the message is received and ChatBoxView is scrolled up",
mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1);
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit',
msg = mock.createChatMessage(_converse, sender_jid, 'This message will be unread');
const view = await mock.openChatBoxFor(_converse, sender_jid)
const sent_stanzas = [];
spyOn(_converse.connection, 'send').and.callFake(s => sent_stanzas.push(s?.nodeTree ?? s));
view.model.ui.set('scrolled', true);
await _converse.handleMessageStanza(msg);
await u.waitUntil(() => view.model.messages.length);
expect(view.model.get('num_unread')).toBe(1);
const msgid = view.model.messages.last().get('id');
expect(view.model.get('first_unread_id')).toBe(msgid);
await u.waitUntil(() => sent_stanzas.length);
expect(sent_stanzas[0].querySelector('received')).toBeDefined();
}));
it("is not incremented when the message is received and ChatBoxView is scrolled down",
mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1);
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
const msg = mock.createChatMessage(_converse, sender_jid, 'This message will be read');
await mock.openChatBoxFor(_converse, sender_jid);
const sent_stanzas = [];
spyOn(_converse.connection, 'send').and.callFake(s => sent_stanzas.push(s?.nodeTree ?? s));
const chatbox = _converse.chatboxes.get(sender_jid);
await _converse.handleMessageStanza(msg);
expect(chatbox.get('num_unread')).toBe(0);
await u.waitUntil(() => sent_stanzas.filter(s => s.nodeName === 'message').length === 2);
expect(sent_stanzas[1].querySelector('displayed')).toBeDefined();
}));
it("is incremented when message is received, chatbox is scrolled down and the window is not focused",
mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current');
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
const msgFactory = function () {
return mock.createChatMessage(_converse, sender_jid, 'This message will be unread');
};
await mock.openChatBoxFor(_converse, sender_jid);
const sent_stanzas = [];
spyOn(_converse.connection, 'send').and.callFake(s => sent_stanzas.push(s?.nodeTree ?? s));
const chatbox = _converse.chatboxes.get(sender_jid);
_converse.windowState = 'hidden';
const msg = msgFactory();
_converse.handleMessageStanza(msg);
await u.waitUntil(() => chatbox.messages.length);
expect(chatbox.get('num_unread')).toBe(1);
const msgid = chatbox.messages.last().get('id');
expect(chatbox.get('first_unread_id')).toBe(msgid);
await u.waitUntil(() => sent_stanzas.filter(s => s.nodeName === 'message').length);
expect(sent_stanzas[0].querySelector('received')).toBeDefined();
}));
it("is incremented when message is received, chatbox is scrolled up and the window is not focused",
mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1);
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
const msgFactory = () => mock.createChatMessage(_converse, sender_jid, 'This message will be unread');
await mock.openChatBoxFor(_converse, sender_jid);
const sent_stanzas = [];
spyOn(_converse.connection, 'send').and.callFake(s => sent_stanzas.push(s?.nodeTree ?? s));
const chatbox = _converse.chatboxes.get(sender_jid);
chatbox.ui.set('scrolled', true);
_converse.windowState = 'hidden';
const msg = msgFactory();
_converse.handleMessageStanza(msg);
await u.waitUntil(() => chatbox.messages.length);
expect(chatbox.get('num_unread')).toBe(1);
const msgid = chatbox.messages.last().get('id');
expect(chatbox.get('first_unread_id')).toBe(msgid);
await u.waitUntil(() => sent_stanzas.filter(s => s.nodeName === 'message').length === 1);
expect(sent_stanzas[0].querySelector('received')).toBeDefined();
}));
it("is cleared when the chat was scrolled down and the window become focused",
mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1);
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
const msgFactory = () => mock.createChatMessage(_converse, sender_jid, 'This message will be unread');
await mock.openChatBoxFor(_converse, sender_jid);
const sent_stanzas = [];
spyOn(_converse.connection, 'send').and.callFake(s => sent_stanzas.push(s?.nodeTree ?? s));
const chatbox = _converse.chatboxes.get(sender_jid);
_converse.windowState = 'hidden';
const msg = msgFactory();
_converse.handleMessageStanza(msg);
await u.waitUntil(() => chatbox.messages.length);
expect(chatbox.get('num_unread')).toBe(1);
const msgid = chatbox.messages.last().get('id');
expect(chatbox.get('first_unread_id')).toBe(msgid);
await u.waitUntil(() => sent_stanzas.filter(s => s.nodeName === 'message').length === 1);
expect(sent_stanzas[0].querySelector('received')).toBeDefined();
_converse.saveWindowState({'type': 'focus'});
await u.waitUntil(() => sent_stanzas.filter(s => s.nodeName === 'message').length === 2);
expect(sent_stanzas[1].querySelector('displayed')).toBeDefined();
expect(chatbox.get('num_unread')).toBe(0);
}));
it("is cleared when the chat was hidden in fullscreen mode and then becomes visible",
mock.initConverse(['chatBoxesFetched'], {'view_mode': 'fullscreen'},
async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1);
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await mock.openChatBoxFor(_converse, sender_jid);
const chatbox = _converse.chatboxes.get(sender_jid);
chatbox.save({'hidden': true});
_converse.handleMessageStanza(mock.createChatMessage(_converse, sender_jid, 'This message will be unread'));
await u.waitUntil(() => chatbox.messages.length);
expect(chatbox.get('num_unread')).toBe(1);
chatbox.save({'hidden': false});
await u.waitUntil(() => chatbox.get('num_unread') === 0);
chatbox.close();
}));
it("is not cleared when ChatBoxView was scrolled up and the windows become focused",
mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1);
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
const msgFactory = () => mock.createChatMessage(_converse, sender_jid, 'This message will be unread');
await mock.openChatBoxFor(_converse, sender_jid);
const sent_stanzas = [];
spyOn(_converse.connection, 'send').and.callFake(s => sent_stanzas.push(s?.nodeTree ?? s));
const chatbox = _converse.chatboxes.get(sender_jid);
chatbox.ui.set('scrolled', true);
_converse.windowState = 'hidden';
const msg = msgFactory();
_converse.handleMessageStanza(msg);
await u.waitUntil(() => chatbox.messages.length);
expect(chatbox.get('num_unread')).toBe(1);
const msgid = chatbox.messages.last().get('id');
expect(chatbox.get('first_unread_id')).toBe(msgid);
await u.waitUntil(() => sent_stanzas.filter(s => s.nodeName === 'message').length === 1);
expect(sent_stanzas[0].querySelector('received')).toBeDefined();
_converse.saveWindowState({'type': 'focus'});
await u.waitUntil(() => chatbox.get('num_unread') === 1);
expect(chatbox.get('first_unread_id')).toBe(msgid);
expect(sent_stanzas[0].querySelector('received')).toBeDefined();
}));
});

View File

@ -6,7 +6,7 @@ const u = converse.env.utils;
describe("XSS", function () { describe("XSS", function () {
describe("A Chat Message", function () { describe("A Chat Message", function () {
it("will escape IMG payload XSS attempts", mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { it("will escape IMG payload XSS attempts", mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
spyOn(window, 'alert').and.callThrough(); spyOn(window, 'alert').and.callThrough();
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse); await mock.openControlBox(_converse);
@ -59,10 +59,9 @@ describe("XSS", function () {
expect(msg.innerHTML.replace(/<!-.*?->/g, '')).toEqual("&gt;&lt;img src=x onerror=alert(String.fromCharCode(88,83,83));&gt;"); expect(msg.innerHTML.replace(/<!-.*?->/g, '')).toEqual("&gt;&lt;img src=x onerror=alert(String.fromCharCode(88,83,83));&gt;");
expect(window.alert).not.toHaveBeenCalled(); expect(window.alert).not.toHaveBeenCalled();
done();
})); }));
it("will escape SVG payload XSS attempts", mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { it("will escape SVG payload XSS attempts", mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
spyOn(window, 'alert').and.callThrough(); spyOn(window, 'alert').and.callThrough();
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse); await mock.openControlBox(_converse);
@ -114,11 +113,10 @@ describe("XSS", function () {
expect(msg.innerHTML.replace(/<!-.*?->/g, '')).toEqual('"&gt;&lt;svg/onload=alert(/XSS/)'); expect(msg.innerHTML.replace(/<!-.*?->/g, '')).toEqual('"&gt;&lt;svg/onload=alert(/XSS/)');
expect(window.alert).not.toHaveBeenCalled(); expect(window.alert).not.toHaveBeenCalled();
done();
})); }));
it("will have properly escaped URLs", it("will have properly escaped URLs",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse); await mock.openControlBox(_converse);
@ -170,11 +168,10 @@ describe("XSS", function () {
expect(msg.textContent).toEqual(message); expect(msg.textContent).toEqual(message);
await u.waitUntil(() => msg.innerHTML.replace(/<!-.*?->/g, '') === await u.waitUntil(() => msg.innerHTML.replace(/<!-.*?->/g, '') ===
`<a target="_blank" rel="noopener" href="https://www.google.com/maps/place/Kochstraat+6,+2041+CE+Zandvoort/@52.3775999,4.548971,3a,15y,170.85h,88.39t/data=%213m6%211e1%213m4%211sQ7SdHo_bPLPlLlU8GSGWaQ%212e0%217i13312%218i6656%214m5%213m4%211s0x47c5ec1e56f845ad:0x1de0bc4a5771fb08%218m2%213d52.3773668%214d4.5489388%215m1%211e2">https://www.google.com/maps/place/Kochstraat+6,+2041+CE+Zandvoort/@52.3775999,4.548971,3a,15y,170.85h,88.39t/data=!3m6!1e1!3m4!1sQ7SdHo_bPLPlLlU8GSGWaQ!2e0!7i13312!8i6656!4m5!3m4!1s0x47c5ec1e56f845ad:0x1de0bc4a5771fb08!8m2!3d52.3773668!4d4.5489388!5m1!1e2</a>`); `<a target="_blank" rel="noopener" href="https://www.google.com/maps/place/Kochstraat+6,+2041+CE+Zandvoort/@52.3775999,4.548971,3a,15y,170.85h,88.39t/data=%213m6%211e1%213m4%211sQ7SdHo_bPLPlLlU8GSGWaQ%212e0%217i13312%218i6656%214m5%213m4%211s0x47c5ec1e56f845ad:0x1de0bc4a5771fb08%218m2%213d52.3773668%214d4.5489388%215m1%211e2">https://www.google.com/maps/place/Kochstraat+6,+2041+CE+Zandvoort/@52.3775999,4.548971,3a,15y,170.85h,88.39t/data=!3m6!1e1!3m4!1sQ7SdHo_bPLPlLlU8GSGWaQ!2e0!7i13312!8i6656!4m5!3m4!1s0x47c5ec1e56f845ad:0x1de0bc4a5771fb08!8m2!3d52.3773668!4d4.5489388!5m1!1e2</a>`);
done();
})); }));
it("will avoid malformed and unsafe urls urls from rendering as anchors", it("will avoid malformed and unsafe urls urls from rendering as anchors",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse); await mock.openControlBox(_converse);
@ -250,7 +247,6 @@ describe("XSS", function () {
await mock.sendMessage(view, good_urls[5].entered); await mock.sendMessage(view, good_urls[5].entered);
await checkParsedURL(good_urls[5]); await checkParsedURL(good_urls[5]);
done();
})); }));
}); });
}); });

View File

@ -1,5 +1,4 @@
/** /**
* @module converse-controlbox
* @copyright 2020, the Converse.js contributors * @copyright 2020, the Converse.js contributors
* @license Mozilla Public License (MPLv2) * @license Mozilla Public License (MPLv2)
*/ */
@ -7,45 +6,16 @@ import "shared/components/brand-heading";
import "../chatview/index.js"; import "../chatview/index.js";
import './loginpanel.js'; import './loginpanel.js';
import './navback.js'; import './navback.js';
import ControlBoxMixin from './model.js'; import ControlBox from './model.js';
import ControlBoxToggle from './toggle.js'; import ControlBoxToggle from './toggle.js';
import ControlBoxView from './controlbox.js'; import ControlBoxView from './controlbox.js';
import controlbox_api from './api.js'; import controlbox_api from './api.js';
import log from '@converse/headless/log'; import log from '@converse/headless/log';
import { _converse, api, converse } from '@converse/headless/core'; import { _converse, api, converse } from '@converse/headless/core';
import { addControlBox } from './utils.js'; import { addControlBox, clearSession, disconnect, onChatBoxesFetched } from './utils.js';
import './styles/_controlbox.scss'; import './styles/_controlbox.scss';
const u = converse.env.utils;
function disconnect () {
/* Upon disconnection, set connected to `false`, so that if
* we reconnect, "onConnected" will be called,
* to fetch the roster again and to send out a presence stanza.
*/
const view = _converse.chatboxviews.get('controlbox');
view.model.set({ 'connected': false });
return view;
}
function clearSession () {
const chatboxviews = _converse?.chatboxviews;
const view = chatboxviews && chatboxviews.get('controlbox');
if (view) {
u.safeSave(view.model, { 'connected': false });
if (view?.controlbox_pane) {
view.controlbox_pane.remove();
delete view.controlbox_pane;
}
}
}
function onChatBoxesFetched () {
const controlbox = _converse.chatboxes.get('controlbox') || addControlBox();
controlbox.save({ 'connected': true });
}
converse.plugins.add('converse-controlbox', { converse.plugins.add('converse-controlbox', {
/* Plugin dependencies are other plugins which might be /* Plugin dependencies are other plugins which might be
@ -73,9 +43,8 @@ converse.plugins.add('converse-controlbox', {
ChatBoxes: { ChatBoxes: {
model (attrs, options) { model (attrs, options) {
const { _converse } = this.__super__;
if (attrs && attrs.id == 'controlbox') { if (attrs && attrs.id == 'controlbox') {
return new _converse.ControlBox(attrs, options); return new ControlBox(attrs, options);
} else { } else {
return this.__super__.model.apply(this, arguments); return this.__super__.model.apply(this, arguments);
} }
@ -97,7 +66,7 @@ converse.plugins.add('converse-controlbox', {
Object.assign(api, controlbox_api); Object.assign(api, controlbox_api);
_converse.ControlBoxView = ControlBoxView; _converse.ControlBoxView = ControlBoxView;
_converse.ControlBox = _converse.ChatBox.extend(ControlBoxMixin); _converse.ControlBox = ControlBox;
_converse.ControlBoxToggle = ControlBoxToggle; _converse.ControlBoxToggle = ControlBoxToggle;
/******************** Event Handlers ********************/ /******************** Event Handlers ********************/

View File

@ -1,10 +1,9 @@
import { _converse, api, converse } from '@converse/headless/core'; import { _converse, api, converse } from '@converse/headless/core';
import { Model } from '@converse/skeletor/src/model.js';
const { dayjs } = converse.env; const { dayjs } = converse.env;
/** /**
* Mixin which turns a ChatBox model into a ControlBox model.
*
* The ControlBox is the section of the chat that contains the open groupchats, * The ControlBox is the section of the chat that contains the open groupchats,
* bookmarks and roster. * bookmarks and roster.
* *
@ -12,7 +11,8 @@ const { dayjs } = converse.env;
* `view_mode` it's a left-aligned sidebar. * `view_mode` it's a left-aligned sidebar.
* @mixin * @mixin
*/ */
const ControlBoxMixin = { const ControlBox = Model.extend({
defaults () { defaults () {
return { return {
'bookmarked': false, 'bookmarked': false,
@ -20,20 +20,12 @@ const ControlBoxMixin = {
'chat_state': undefined, 'chat_state': undefined,
'closed': !api.settings.get('show_controlbox_by_default'), 'closed': !api.settings.get('show_controlbox_by_default'),
'num_unread': 0, 'num_unread': 0,
'time_opened': this.get('time_opened') || new Date().getTime(), 'time_opened': dayjs(0).valueOf(),
'type': _converse.CONTROLBOX_TYPE, 'type': _converse.CONTROLBOX_TYPE,
'url': '' 'url': ''
}; };
}, },
initialize () {
if (this.get('id') === 'controlbox') {
this.set({ 'time_opened': dayjs(0).valueOf() });
} else {
_converse.ChatBox.prototype.initialize.apply(this, arguments);
}
},
validate (attrs) { validate (attrs) {
if (attrs.type === _converse.CONTROLBOX_TYPE) { if (attrs.type === _converse.CONTROLBOX_TYPE) {
if (api.settings.get('view_mode') === 'embedded' && api.settings.get('singleton')) { if (api.settings.get('view_mode') === 'embedded' && api.settings.get('singleton')) {
@ -55,7 +47,6 @@ const ControlBoxMixin = {
onReconnection () { onReconnection () {
this.save('connected', true); this.save('connected', true);
} }
});
}; export default ControlBox;
export default ControlBoxMixin;

View File

@ -9,19 +9,18 @@ const sizzle = converse.env.sizzle;
describe("The Controlbox", function () { describe("The Controlbox", function () {
it("can be opened by clicking a DOM element with class 'toggle-controlbox'", it("can be opened by clicking a DOM element with class 'toggle-controlbox'",
mock.initConverse([], {}, function (done, _converse) { mock.initConverse([], {}, function (_converse) {
spyOn(_converse.api, "trigger").and.callThrough(); spyOn(_converse.api, "trigger").and.callThrough();
document.querySelector('.toggle-controlbox').click(); document.querySelector('.toggle-controlbox').click();
expect(_converse.api.trigger).toHaveBeenCalledWith('controlBoxOpened', jasmine.any(Object)); expect(_converse.api.trigger).toHaveBeenCalledWith('controlBoxOpened', jasmine.any(Object));
const el = document.querySelector("#controlbox"); const el = document.querySelector("#controlbox");
expect(u.isVisible(el)).toBe(true); expect(u.isVisible(el)).toBe(true);
done();
})); }));
it("can be closed by clicking a DOM element with class 'close-chatbox-button'", it("can be closed by clicking a DOM element with class 'close-chatbox-button'",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
await mock.openControlBox(_converse); await mock.openControlBox(_converse);
const view = _converse.chatboxviews.get('controlbox'); const view = _converse.chatboxviews.get('controlbox');
@ -32,14 +31,13 @@ describe("The Controlbox", function () {
view.querySelector('.close-chatbox-button').click(); view.querySelector('.close-chatbox-button').click();
expect(view.close).toHaveBeenCalled(); expect(view.close).toHaveBeenCalled();
expect(_converse.api.trigger).toHaveBeenCalledWith('controlBoxClosed', jasmine.any(Object)); expect(_converse.api.trigger).toHaveBeenCalledWith('controlBoxClosed', jasmine.any(Object));
done();
})); }));
describe("The \"Contacts\" section", function () { describe("The \"Contacts\" section", function () {
it("can be used to add contact and it checks for case-sensivity", it("can be used to add contact and it checks for case-sensivity",
mock.initConverse([], {}, async function (done, _converse) { mock.initConverse([], {}, async function (_converse) {
spyOn(_converse.api, "trigger").and.callThrough(); spyOn(_converse.api, "trigger").and.callThrough();
await mock.waitForRoster(_converse, 'all', 0); await mock.waitForRoster(_converse, 'all', 0);
@ -61,11 +59,10 @@ describe("The Controlbox", function () {
await u.waitUntil(() => Array.from(rosterview.querySelectorAll('.roster-group li')).filter(u.isVisible).length, 700); await u.waitUntil(() => Array.from(rosterview.querySelectorAll('.roster-group li')).filter(u.isVisible).length, 700);
// Checking that only one entry is created because both JID is same (Case sensitive check) // Checking that only one entry is created because both JID is same (Case sensitive check)
expect(Array.from(rosterview.querySelectorAll('li')).filter(u.isVisible).length).toBe(1); expect(Array.from(rosterview.querySelectorAll('li')).filter(u.isVisible).length).toBe(1);
done();
})); }));
it("shows the number of unread mentions received", it("shows the number of unread mentions received",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'all'); await mock.waitForRoster(_converse, 'all');
await mock.openControlBox(_converse); await mock.openControlBox(_converse);
@ -108,23 +105,21 @@ describe("The Controlbox", function () {
chatview.model.set({'minimized': false}); chatview.model.set({'minimized': false});
expect(el.querySelector('.restore-chat .message-count')).toBe(null); expect(el.querySelector('.restore-chat .message-count')).toBe(null);
await u.waitUntil(() => rosterview.querySelector('.msgs-indicator') === null); await u.waitUntil(() => rosterview.querySelector('.msgs-indicator') === null);
done();
})); }));
}); });
describe("The Status Widget", function () { describe("The Status Widget", function () {
it("shows the user's chat status, which is online by default", it("shows the user's chat status, which is online by default",
mock.initConverse([], {}, async function (done, _converse) { mock.initConverse([], {}, async function (_converse) {
mock.openControlBox(_converse); mock.openControlBox(_converse);
const view = await u.waitUntil(() => document.querySelector('converse-user-profile')); const view = await u.waitUntil(() => document.querySelector('converse-user-profile'));
expect(u.hasClass('online', view.querySelector('.xmpp-status span:first-child'))).toBe(true); expect(u.hasClass('online', view.querySelector('.xmpp-status span:first-child'))).toBe(true);
expect(view.querySelector('.xmpp-status span.online').textContent.trim()).toBe('I am online'); expect(view.querySelector('.xmpp-status span.online').textContent.trim()).toBe('I am online');
done();
})); }));
it("can be used to set the current user's chat status", it("can be used to set the current user's chat status",
mock.initConverse([], {}, async function (done, _converse) { mock.initConverse([], {}, async function (_converse) {
await mock.openControlBox(_converse); await mock.openControlBox(_converse);
var cbview = _converse.chatboxviews.get('controlbox'); var cbview = _converse.chatboxviews.get('controlbox');
@ -146,11 +141,10 @@ describe("The Controlbox", function () {
expect(u.hasClass('online', first_child)).toBe(false); expect(u.hasClass('online', first_child)).toBe(false);
expect(u.hasClass('dnd', first_child)).toBe(true); expect(u.hasClass('dnd', first_child)).toBe(true);
expect(view.querySelector('.xmpp-status span:first-child').textContent.trim()).toBe('I am busy'); expect(view.querySelector('.xmpp-status span:first-child').textContent.trim()).toBe('I am busy');
done();
})); }));
it("can be used to set a custom status message", it("can be used to set a custom status message",
mock.initConverse([], {}, async function (done, _converse) { mock.initConverse([], {}, async function (_converse) {
await mock.openControlBox(_converse); await mock.openControlBox(_converse);
const cbview = _converse.chatboxviews.get('controlbox'); const cbview = _converse.chatboxviews.get('controlbox');
@ -174,7 +168,6 @@ describe("The Controlbox", function () {
const first_child = view.querySelector('.xmpp-status span:first-child'); const first_child = view.querySelector('.xmpp-status span:first-child');
expect(u.hasClass('online', first_child)).toBe(true); expect(u.hasClass('online', first_child)).toBe(true);
expect(view.querySelector('.xmpp-status span:first-child').textContent.trim()).toBe(msg); expect(view.querySelector('.xmpp-status span:first-child').textContent.trim()).toBe(msg);
done();
})); }));
}); });
}); });
@ -182,7 +175,7 @@ describe("The Controlbox", function () {
describe("The 'Add Contact' widget", function () { describe("The 'Add Contact' widget", function () {
it("opens up an add modal when you click on it", it("opens up an add modal when you click on it",
mock.initConverse([], {}, async function (done, _converse) { mock.initConverse([], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'all'); await mock.waitForRoster(_converse, 'all');
await mock.openControlBox(_converse); await mock.openControlBox(_converse);
@ -210,11 +203,10 @@ describe("The 'Add Contact' widget", function () {
`<iq id="${sent_stanza.getAttribute('id')}" type="set" xmlns="jabber:client">`+ `<iq id="${sent_stanza.getAttribute('id')}" type="set" xmlns="jabber:client">`+
`<query xmlns="jabber:iq:roster"><item jid="someone@montague.lit" name="Someone"/></query>`+ `<query xmlns="jabber:iq:roster"><item jid="someone@montague.lit" name="Someone"/></query>`+
`</iq>`); `</iq>`);
done();
})); }));
it("can be configured to not provide search suggestions", it("can be configured to not provide search suggestions",
mock.initConverse([], {'autocomplete_add_contact': false}, async function (done, _converse) { mock.initConverse([], {'autocomplete_add_contact': false}, async function (_converse) {
await mock.waitForRoster(_converse, 'all', 0); await mock.waitForRoster(_converse, 'all', 0);
await mock.openControlBox(_converse); await mock.openControlBox(_converse);
@ -239,13 +231,12 @@ describe("The 'Add Contact' widget", function () {
`<query xmlns="jabber:iq:roster"><item jid="someone@montague.lit"/></query>`+ `<query xmlns="jabber:iq:roster"><item jid="someone@montague.lit"/></query>`+
`</iq>` `</iq>`
); );
done();
})); }));
it("integrates with xhr_user_search_url to search for contacts", it("integrates with xhr_user_search_url to search for contacts",
mock.initConverse([], { 'xhr_user_search_url': 'http://example.org/?' }, mock.initConverse([], { 'xhr_user_search_url': 'http://example.org/?' },
async function (done, _converse) { async function (_converse) {
await mock.waitForRoster(_converse, 'all', 0); await mock.waitForRoster(_converse, 'all', 0);
@ -296,14 +287,13 @@ describe("The 'Add Contact' widget", function () {
`<query xmlns="jabber:iq:roster"><item jid="marty@mcfly.net" name="Marty McFly"/></query>`+ `<query xmlns="jabber:iq:roster"><item jid="marty@mcfly.net" name="Marty McFly"/></query>`+
`</iq>`); `</iq>`);
window.XMLHttpRequest = XMLHttpRequestBackup; window.XMLHttpRequest = XMLHttpRequestBackup;
done();
})); }));
it("can be configured to not provide search suggestions for XHR search results", it("can be configured to not provide search suggestions for XHR search results",
mock.initConverse([], mock.initConverse([],
{ 'autocomplete_add_contact': false, { 'autocomplete_add_contact': false,
'xhr_user_search_url': 'http://example.org/?' }, 'xhr_user_search_url': 'http://example.org/?' },
async function (done, _converse) { async function (_converse) {
await mock.waitForRoster(_converse, 'all'); await mock.waitForRoster(_converse, 'all');
await mock.openControlBox(_converse); await mock.openControlBox(_converse);
@ -371,6 +361,5 @@ describe("The 'Add Contact' widget", function () {
`<query xmlns="jabber:iq:roster"><item jid="marty@mcfly.net" name="Marty McFly"/></query>`+ `<query xmlns="jabber:iq:roster"><item jid="marty@mcfly.net" name="Marty McFly"/></query>`+
`</iq>`); `</iq>`);
window.XMLHttpRequest = XMLHttpRequestBackup; window.XMLHttpRequest = XMLHttpRequestBackup;
done();
})); }));
}); });

View File

@ -9,7 +9,7 @@ describe("The Login Form", function () {
['chatBoxesInitialized'], ['chatBoxesInitialized'],
{ auto_login: false, { auto_login: false,
allow_registration: false }, allow_registration: false },
async function (done, _converse) { async function (_converse) {
const cbview = await u.waitUntil(() => _converse.chatboxviews.get('controlbox')); const cbview = await u.waitUntil(() => _converse.chatboxviews.get('controlbox'));
mock.toggleControlBox(); mock.toggleControlBox();
@ -34,7 +34,6 @@ describe("The Login Form", function () {
cbview.querySelector('input[type="submit"]').click(); cbview.querySelector('input[type="submit"]').click();
expect(_converse.config.get('trusted')).toBe(false); expect(_converse.config.get('trusted')).toBe(false);
expect(_converse.getDefaultStore()).toBe('session'); expect(_converse.getDefaultStore()).toBe('session');
done();
})); }));
it("checkbox can be set to false by default", it("checkbox can be set to false by default",
@ -43,7 +42,7 @@ describe("The Login Form", function () {
{ auto_login: false, { auto_login: false,
allow_user_trust_override: 'off', allow_user_trust_override: 'off',
allow_registration: false }, allow_registration: false },
async function (done, _converse) { async function (_converse) {
await u.waitUntil(() => _converse.chatboxviews.get('controlbox')) await u.waitUntil(() => _converse.chatboxviews.get('controlbox'))
const cbview = _converse.chatboxviews.get('controlbox'); const cbview = _converse.chatboxviews.get('controlbox');
@ -67,6 +66,5 @@ describe("The Login Form", function () {
cbview.querySelector('input[type="submit"]').click(); cbview.querySelector('input[type="submit"]').click();
expect(_converse.config.get('trusted')).toBe(true); expect(_converse.config.get('trusted')).toBe(true);
expect(_converse.getDefaultStore()).toBe('persistent'); expect(_converse.getDefaultStore()).toBe('persistent');
done();
})); }));
}); });

View File

@ -19,3 +19,30 @@ export function navigateToControlBox (jid) {
const model = _converse.chatboxes.get(jid); const model = _converse.chatboxes.get(jid);
u.safeSave(model, {'hidden': true}); u.safeSave(model, {'hidden': true});
} }
export function disconnect () {
/* Upon disconnection, set connected to `false`, so that if
* we reconnect, "onConnected" will be called,
* to fetch the roster again and to send out a presence stanza.
*/
const view = _converse.chatboxviews.get('controlbox');
view.model.set({ 'connected': false });
return view;
}
export function clearSession () {
const chatboxviews = _converse?.chatboxviews;
const view = chatboxviews && chatboxviews.get('controlbox');
if (view) {
u.safeSave(view.model, { 'connected': false });
if (view?.controlbox_pane) {
view.controlbox_pane.remove();
delete view.controlbox_pane;
}
}
}
export function onChatBoxesFetched () {
const controlbox = _converse.chatboxes.get('controlbox') || addControlBox();
controlbox.save({ 'connected': true });
}

View File

@ -12,7 +12,7 @@ export default (o) => {
<div class="chatbox-title__text" title="${o.jid}">${ o.display_name }</div> <div class="chatbox-title__text" title="${o.jid}">${ o.display_name }</div>
</div> </div>
<div class="chatbox-title__buttons row no-gutters"> <div class="chatbox-title__buttons row no-gutters">
${ o.dropdown_btns.length ? html`<converse-dropdown .items=${o.dropdown_btns}></converse-dropdown>` : '' } ${ o.dropdown_btns.length ? html`<converse-dropdown class="dropleft" .items=${o.dropdown_btns}></converse-dropdown>` : '' }
${ o.standalone_btns.length ? tpl_standalone_btns(o) : '' } ${ o.standalone_btns.length ? tpl_standalone_btns(o) : '' }
</div> </div>
</div> </div>

View File

@ -3,7 +3,7 @@
describe("A headlines box", function () { describe("A headlines box", function () {
it("will not open nor display non-headline messages", it("will not open nor display non-headline messages",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current', 0); await mock.waitForRoster(_converse, 'current', 0);
const { $msg } = converse.env; const { $msg } = converse.env;
@ -27,11 +27,10 @@ describe("A headlines box", function () {
.c('body').t('SORRY FOR THIS ADVERT'); .c('body').t('SORRY FOR THIS ADVERT');
_converse.connection._dataRecv(mock.createRequest(stanza)); _converse.connection._dataRecv(mock.createRequest(stanza));
expect(_converse.api.headlines.get().length === 0); expect(_converse.api.headlines.get().length === 0);
done();
})); }));
it("will open and display headline messages", mock.initConverse( it("will open and display headline messages", mock.initConverse(
[], {}, async function (done, _converse) { [], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current', 0); await mock.waitForRoster(_converse, 'current', 0);
const { u, $msg} = converse.env; const { u, $msg} = converse.env;
@ -64,11 +63,10 @@ describe("A headlines box", function () {
const view = _converse.chatboxviews.get('notify.example.com'); const view = _converse.chatboxviews.get('notify.example.com');
expect(view.model.get('show_avatar')).toBeFalsy(); expect(view.model.get('show_avatar')).toBeFalsy();
expect(view.querySelector('img.avatar')).toBe(null); expect(view.querySelector('img.avatar')).toBe(null);
done();
})); }));
it("will show headline messages in the controlbox", mock.initConverse( it("will show headline messages in the controlbox", mock.initConverse(
[], {}, async function (done, _converse) { [], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current', 0); await mock.waitForRoster(_converse, 'current', 0);
const { u, $msg} = converse.env; const { u, $msg} = converse.env;
@ -101,11 +99,10 @@ describe("A headlines box", function () {
await u.waitUntil(() => view.querySelectorAll(".open-headline").length); await u.waitUntil(() => view.querySelectorAll(".open-headline").length);
expect(view.querySelectorAll('.open-headline').length).toBe(1); expect(view.querySelectorAll('.open-headline').length).toBe(1);
expect(view.querySelector('.open-headline').text).toBe('notify.example.com'); expect(view.querySelector('.open-headline').text).toBe('notify.example.com');
done();
})); }));
it("will remove headline messages from the controlbox if closed", mock.initConverse( it("will remove headline messages from the controlbox if closed", mock.initConverse(
[], {}, async function (done, _converse) { [], {}, async function (_converse) {
const { u, $msg} = converse.env; const { u, $msg} = converse.env;
await mock.waitForRoster(_converse, 'current', 0); await mock.waitForRoster(_converse, 'current', 0);
@ -143,12 +140,11 @@ describe("A headlines box", function () {
close_el.click(); close_el.click();
await u.waitUntil(() => cbview.querySelectorAll(".open-headline").length === 0); await u.waitUntil(() => cbview.querySelectorAll(".open-headline").length === 0);
expect(cbview.querySelectorAll('.open-headline').length).toBe(0); expect(cbview.querySelectorAll('.open-headline').length).toBe(0);
done();
})); }));
it("will not show a headline messages from a full JID if allow_non_roster_messaging is false", it("will not show a headline messages from a full JID if allow_non_roster_messaging is false",
mock.initConverse( mock.initConverse(
['chatBoxesFetched'], {}, async function (done, _converse) { ['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current', 0); await mock.waitForRoster(_converse, 'current', 0);
const { $msg } = converse.env; const { $msg } = converse.env;
@ -163,6 +159,5 @@ describe("A headlines box", function () {
.c('body').t('Здравствуйте друзья'); .c('body').t('Здравствуйте друзья');
_converse.connection._dataRecv(mock.createRequest(stanza)); _converse.connection._dataRecv(mock.createRequest(stanza));
expect(_.without('controlbox', _converse.chatboxviews.keys()).length).toBe(0); expect(_.without('controlbox', _converse.chatboxviews.keys()).length).toBe(0);
done();
})); }));
}); });

View File

@ -18,7 +18,6 @@ class HeadlinesView extends BaseChatView {
this.listenTo(_converse, 'windowStateChanged', this.onWindowStateChanged); this.listenTo(_converse, 'windowStateChanged', this.onWindowStateChanged);
this.listenTo(this.model, 'change:hidden', () => this.afterShown()); this.listenTo(this.model, 'change:hidden', () => this.afterShown());
this.listenTo(this.model, 'destroy', this.remove); this.listenTo(this.model, 'destroy', this.remove);
this.listenTo(this.model, 'show', this.show);
this.listenTo(this.model.messages, 'add', this.requestUpdate); this.listenTo(this.model.messages, 'add', this.requestUpdate);
this.listenTo(this.model.messages, 'remove', this.requestUpdate); this.listenTo(this.model.messages, 'remove', this.requestUpdate);
this.listenTo(this.model.messages, 'reset', this.requestUpdate); this.listenTo(this.model.messages, 'reset', this.requestUpdate);
@ -44,7 +43,6 @@ class HeadlinesView extends BaseChatView {
_converse.router.navigate(''); _converse.router.navigate('');
} }
await this.model.close(ev); await this.model.close(ev);
api.trigger('chatBoxClosed', this);
return this; return this;
} }
@ -54,8 +52,8 @@ class HeadlinesView extends BaseChatView {
return []; return [];
} }
afterShown () { // eslint-disable-line class-methods-use-this afterShown () {
return; this.model.clearUnreadMsgCounter();
} }
} }

View File

@ -19,7 +19,7 @@ describe("Message Archive Management", function () {
describe("The XEP-0313 Archive", function () { describe("The XEP-0313 Archive", function () {
it("is queried when the user scrolls up", it("is queried when the user scrolls up",
mock.initConverse(['discoInitialized'], {'archived_messages_page_size': 2}, async function (done, _converse) { mock.initConverse(['discoInitialized'], {'archived_messages_page_size': 2}, async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1); await mock.waitForRoster(_converse, 'current', 1);
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@ -78,7 +78,6 @@ describe("Message Archive Management", function () {
`<set xmlns="http://jabber.org/protocol/rsm"><before>${view.model.messages.at(0).get('stanza_id romeo@montague.lit')}</before><max>2</max></set></query>`+ `<set xmlns="http://jabber.org/protocol/rsm"><before>${view.model.messages.at(0).get('stanza_id romeo@montague.lit')}</before><max>2</max></set></query>`+
`</iq>` `</iq>`
); );
done();
})); }));
it("is queried when the user enters a new MUC", it("is queried when the user enters a new MUC",
@ -86,7 +85,7 @@ describe("Message Archive Management", function () {
{ {
'archived_messages_page_size': 2, 'archived_messages_page_size': 2,
'muc_clear_messages_on_leave': false, 'muc_clear_messages_on_leave': false,
}, async function (done, _converse) { }, async function (_converse) {
const sent_IQs = _converse.connection.IQ_stanzas; const sent_IQs = _converse.connection.IQ_stanzas;
const muc_jid = 'orchard@chat.shakespeare.lit'; const muc_jid = 'orchard@chat.shakespeare.lit';
@ -269,7 +268,6 @@ describe("Message Archive Management", function () {
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length); await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length);
await u.waitUntil(() => Array.from(view.querySelectorAll('.chat-msg__text')) await u.waitUntil(() => Array.from(view.querySelectorAll('.chat-msg__text'))
.map(e => e.textContent).join(' ') === "2nd Message 3rd Message 4th Message 5th Message 6th Message", 1000); .map(e => e.textContent).join(' ') === "2nd Message 3rd Message 4th Message 5th Message 6th Message", 1000);
done();
})); }));
it("queries for messages since the most recent cached message in a newly entered MUC", it("queries for messages since the most recent cached message in a newly entered MUC",
@ -278,7 +276,7 @@ describe("Message Archive Management", function () {
'archived_messages_page_size': 2, 'archived_messages_page_size': 2,
'muc_nickname_from_jid': false, 'muc_nickname_from_jid': false,
'muc_clear_messages_on_leave': false, 'muc_clear_messages_on_leave': false,
}, async function (done, _converse) { }, async function (_converse) {
const { api } = _converse; const { api } = _converse;
const sent_IQs = _converse.connection.IQ_stanzas; const sent_IQs = _converse.connection.IQ_stanzas;
@ -322,7 +320,6 @@ describe("Message Archive Management", function () {
`<set xmlns="http://jabber.org/protocol/rsm"><max>2</max></set>`+ `<set xmlns="http://jabber.org/protocol/rsm"><max>2</max></set>`+
`</query>`+ `</query>`+
`</iq>`); `</iq>`);
return done();
})); }));
}); });
@ -332,7 +329,7 @@ describe("Message Archive Management", function () {
it("is discarded if it doesn't come from the right sender", it("is discarded if it doesn't come from the right sender",
mock.initConverse( mock.initConverse(
['discoInitialized'], {}, ['discoInitialized'], {},
async function (done, _converse) { async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1); await mock.waitForRoster(_converse, 'current', 1);
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@ -382,13 +379,12 @@ describe("Message Archive Management", function () {
.filter(el => el.textContent === "Thrice the brinded cat hath mew'd.").length, 1000); .filter(el => el.textContent === "Thrice the brinded cat hath mew'd.").length, 1000);
expect(view.model.messages.length).toBe(1); expect(view.model.messages.length).toBe(1);
expect(view.model.messages.at(0).get('message')).toBe("Thrice the brinded cat hath mew'd."); expect(view.model.messages.at(0).get('message')).toBe("Thrice the brinded cat hath mew'd.");
done();
})); }));
it("is not discarded if it comes from the right sender", it("is not discarded if it comes from the right sender",
mock.initConverse( mock.initConverse(
['discoInitialized'], {}, ['discoInitialized'], {},
async function (done, _converse) { async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1); await mock.waitForRoster(_converse, 'current', 1);
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@ -437,13 +433,12 @@ describe("Message Archive Management", function () {
expect(view.model.messages.length).toBe(2); expect(view.model.messages.length).toBe(2);
expect(view.model.messages.at(0).get('message')).toBe("Meet me at the dance"); expect(view.model.messages.at(0).get('message')).toBe("Meet me at the dance");
expect(view.model.messages.at(1).get('message')).toBe("Thrice the brinded cat hath mew'd."); expect(view.model.messages.at(1).get('message')).toBe("Thrice the brinded cat hath mew'd.");
done();
})); }));
it("updates the is_archived value of an already cached version", it("updates the is_archived value of an already cached version",
mock.initConverse( mock.initConverse(
['discoInitialized'], {}, ['discoInitialized'], {},
async function (done, _converse) { async function (_converse) {
await mock.openAndEnterChatRoom(_converse, 'trek-radio@conference.lightwitch.org', 'romeo'); await mock.openAndEnterChatRoom(_converse, 'trek-radio@conference.lightwitch.org', 'romeo');
@ -485,13 +480,12 @@ describe("Message Archive Management", function () {
expect(view.model.messages.length).toBe(1); expect(view.model.messages.length).toBe(1);
expect(view.model.messages.at(0).get('is_archived')).toBe(true); expect(view.model.messages.at(0).get('is_archived')).toBe(true);
expect(view.model.messages.at(0).get('stanza_id trek-radio@conference.lightwitch.org')).toBe('45fbbf2a-1059-479d-9283-c8effaf05621'); expect(view.model.messages.at(0).get('stanza_id trek-radio@conference.lightwitch.org')).toBe('45fbbf2a-1059-479d-9283-c8effaf05621');
done();
})); }));
it("isn't shown as duplicate by comparing its stanza id or archive id", it("isn't shown as duplicate by comparing its stanza id or archive id",
mock.initConverse( mock.initConverse(
['discoInitialized'], {}, ['discoInitialized'], {},
async function (done, _converse) { async function (_converse) {
await mock.openAndEnterChatRoom(_converse, 'trek-radio@conference.lightwitch.org', 'jcbrand'); await mock.openAndEnterChatRoom(_converse, 'trek-radio@conference.lightwitch.org', 'jcbrand');
const view = _converse.chatboxviews.get('trek-radio@conference.lightwitch.org'); const view = _converse.chatboxviews.get('trek-radio@conference.lightwitch.org');
@ -524,13 +518,12 @@ describe("Message Archive Management", function () {
const result = await view.model.getDuplicateMessage.calls.all()[0].returnValue const result = await view.model.getDuplicateMessage.calls.all()[0].returnValue
expect(result instanceof _converse.Message).toBe(true); expect(result instanceof _converse.Message).toBe(true);
expect(view.querySelectorAll('.chat-msg').length).toBe(1); expect(view.querySelectorAll('.chat-msg').length).toBe(1);
done();
})); }));
it("isn't shown as duplicate by comparing only the archive id", it("isn't shown as duplicate by comparing only the archive id",
mock.initConverse( mock.initConverse(
['discoInitialized'], {}, ['discoInitialized'], {},
async function (done, _converse) { async function (_converse) {
await mock.openAndEnterChatRoom(_converse, 'discuss@conference.conversejs.org', 'romeo'); await mock.openAndEnterChatRoom(_converse, 'discuss@conference.conversejs.org', 'romeo');
const view = _converse.chatboxviews.get('discuss@conference.conversejs.org'); const view = _converse.chatboxviews.get('discuss@conference.conversejs.org');
@ -574,7 +567,6 @@ describe("Message Archive Management", function () {
const result = await view.model.getDuplicateMessage.calls.all()[0].returnValue const result = await view.model.getDuplicateMessage.calls.all()[0].returnValue
expect(result instanceof _converse.Message).toBe(true); expect(result instanceof _converse.Message).toBe(true);
expect(view.querySelectorAll('.chat-msg').length).toBe(1); expect(view.querySelectorAll('.chat-msg').length).toBe(1);
done();
})) }))
}); });
}); });
@ -582,7 +574,7 @@ describe("Message Archive Management", function () {
describe("The archive.query API", function () { describe("The archive.query API", function () {
it("can be used to query for all archived messages", it("can be used to query for all archived messages",
mock.initConverse(['discoInitialized'], {}, async function (done, _converse) { mock.initConverse(['discoInitialized'], {}, async function (_converse) {
const sendIQ = _converse.connection.sendIQ; const sendIQ = _converse.connection.sendIQ;
await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]); await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]);
@ -596,11 +588,10 @@ describe("Message Archive Management", function () {
const queryid = sent_stanza.querySelector('query').getAttribute('queryid'); const queryid = sent_stanza.querySelector('query').getAttribute('queryid');
expect(Strophe.serialize(sent_stanza)).toBe( expect(Strophe.serialize(sent_stanza)).toBe(
`<iq id="${IQ_id}" type="set" xmlns="jabber:client"><query queryid="${queryid}" xmlns="urn:xmpp:mam:2"/></iq>`); `<iq id="${IQ_id}" type="set" xmlns="jabber:client"><query queryid="${queryid}" xmlns="urn:xmpp:mam:2"/></iq>`);
done();
})); }));
it("can be used to query for all messages to/from a particular JID", it("can be used to query for all messages to/from a particular JID",
mock.initConverse([], {}, async function (done, _converse) { mock.initConverse([], {}, async function (_converse) {
await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]); await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]);
let sent_stanza, IQ_id; let sent_stanza, IQ_id;
@ -625,11 +616,10 @@ describe("Message Archive Management", function () {
`</x>`+ `</x>`+
`</query>`+ `</query>`+
`</iq>`); `</iq>`);
done();
})); }));
it("can be used to query for archived messages from a chat room", it("can be used to query for archived messages from a chat room",
mock.initConverse(['statusInitialized'], {}, async function (done, _converse) { mock.initConverse(['statusInitialized'], {}, async function (_converse) {
const room_jid = 'coven@chat.shakespeare.lit'; const room_jid = 'coven@chat.shakespeare.lit';
_converse.api.archive.query({'with': room_jid, 'groupchat': true}); _converse.api.archive.query({'with': room_jid, 'groupchat': true});
@ -650,11 +640,10 @@ describe("Message Archive Management", function () {
`</x>`+ `</x>`+
`</query>`+ `</query>`+
`</iq>`); `</iq>`);
done();
})); }));
it("checks whether returned MAM messages from a MUC room are from the right JID", it("checks whether returned MAM messages from a MUC room are from the right JID",
mock.initConverse(['statusInitialized'], {}, async function (done, _converse) { mock.initConverse(['statusInitialized'], {}, async function (_converse) {
const room_jid = 'coven@chat.shakespeare.lit'; const room_jid = 'coven@chat.shakespeare.lit';
const promise = _converse.api.archive.query({'with': room_jid, 'groupchat': true, 'max':'10'}); const promise = _converse.api.archive.query({'with': room_jid, 'groupchat': true, 'max':'10'});
@ -719,11 +708,10 @@ describe("Message Archive Management", function () {
const result = await promise; const result = await promise;
expect(result.messages.length).toBe(0); expect(result.messages.length).toBe(0);
done();
})); }));
it("can be used to query for all messages in a certain timespan", it("can be used to query for all messages in a certain timespan",
mock.initConverse([], {}, async function (done, _converse) { mock.initConverse([], {}, async function (_converse) {
await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]); await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]);
let sent_stanza, IQ_id; let sent_stanza, IQ_id;
@ -757,11 +745,10 @@ describe("Message Archive Management", function () {
`</query>`+ `</query>`+
`</iq>` `</iq>`
); );
done();
})); }));
it("throws a TypeError if an invalid date is provided", it("throws a TypeError if an invalid date is provided",
mock.initConverse([], {}, async function (done, _converse) { mock.initConverse([], {}, async function (_converse) {
await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]); await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]);
try { try {
@ -769,11 +756,10 @@ describe("Message Archive Management", function () {
} catch (e) { } catch (e) {
expect(() => {throw e}).toThrow(new TypeError('archive.query: invalid date provided for: start')); expect(() => {throw e}).toThrow(new TypeError('archive.query: invalid date provided for: start'));
} }
done();
})); }));
it("can be used to query for all messages after a certain time", it("can be used to query for all messages after a certain time",
mock.initConverse([], {}, async function (done, _converse) { mock.initConverse([], {}, async function (_converse) {
await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]); await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]);
let sent_stanza, IQ_id; let sent_stanza, IQ_id;
@ -803,11 +789,10 @@ describe("Message Archive Management", function () {
`</query>`+ `</query>`+
`</iq>` `</iq>`
); );
done();
})); }));
it("can be used to query for a limited set of results", it("can be used to query for a limited set of results",
mock.initConverse([], {}, async function (done, _converse) { mock.initConverse([], {}, async function (_converse) {
await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]); await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]);
let sent_stanza, IQ_id; let sent_stanza, IQ_id;
@ -837,11 +822,10 @@ describe("Message Archive Management", function () {
`</query>`+ `</query>`+
`</iq>` `</iq>`
); );
done();
})); }));
it("can be used to page through results", it("can be used to page through results",
mock.initConverse([], {}, async function (done, _converse) { mock.initConverse([], {}, async function (_converse) {
await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]); await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]);
let sent_stanza, IQ_id; let sent_stanza, IQ_id;
@ -875,11 +859,10 @@ describe("Message Archive Management", function () {
`</set>`+ `</set>`+
`</query>`+ `</query>`+
`</iq>`); `</iq>`);
done();
})); }));
it("accepts \"before\" with an empty string as value to reverse the order", it("accepts \"before\" with an empty string as value to reverse the order",
mock.initConverse([], {}, async function (done, _converse) { mock.initConverse([], {}, async function (_converse) {
await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]); await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]);
let sent_stanza, IQ_id; let sent_stanza, IQ_id;
@ -905,11 +888,10 @@ describe("Message Archive Management", function () {
`</set>`+ `</set>`+
`</query>`+ `</query>`+
`</iq>`); `</iq>`);
done();
})); }));
it("returns an object which includes the messages and a _converse.RSM object", it("returns an object which includes the messages and a _converse.RSM object",
mock.initConverse([], {}, async function (done, _converse) { mock.initConverse([], {}, async function (_converse) {
await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]); await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]);
let sent_stanza, IQ_id; let sent_stanza, IQ_id;
@ -988,14 +970,13 @@ describe("Message Archive Management", function () {
expect(result.rsm.result.count).toBe(16); expect(result.rsm.result.count).toBe(16);
expect(result.rsm.result.first).toBe('23452-4534-1'); expect(result.rsm.result.first).toBe('23452-4534-1');
expect(result.rsm.result.last).toBe('09af3-cc343-b409f'); expect(result.rsm.result.last).toBe('09af3-cc343-b409f');
done()
})); }));
}); });
describe("The default preference", function () { describe("The default preference", function () {
it("is set once server support for MAM has been confirmed", it("is set once server support for MAM has been confirmed",
mock.initConverse([], {}, async function (done, _converse) { mock.initConverse([], {}, async function (_converse) {
const { api } = _converse; const { api } = _converse;
@ -1065,7 +1046,6 @@ describe("Message Archive Management", function () {
await u.waitUntil(() => feature.save.calls.count()); await u.waitUntil(() => feature.save.calls.count());
expect(feature.save).toHaveBeenCalled(); expect(feature.save).toHaveBeenCalled();
expect(feature.get('preferences')['default']).toBe('never'); // eslint-disable-line dot-notation expect(feature.get('preferences')['default']).toBe('never'); // eslint-disable-line dot-notation
done();
})); }));
}); });
}); });
@ -1074,7 +1054,7 @@ describe("Chatboxes", function () {
describe("A Chatbox", function () { describe("A Chatbox", function () {
it("will fetch archived messages once it's opened", it("will fetch archived messages once it's opened",
mock.initConverse(['discoInitialized'], {}, async function (done, _converse) { mock.initConverse(['discoInitialized'], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1); await mock.waitForRoster(_converse, 'current', 1);
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@ -1130,11 +1110,10 @@ describe("Chatboxes", function () {
.c('last').t('09af3-cc343-b409f').up() .c('last').t('09af3-cc343-b409f').up()
.c('count').t('16'); .c('count').t('16');
_converse.connection._dataRecv(mock.createRequest(stanza)); _converse.connection._dataRecv(mock.createRequest(stanza));
done();
})); }));
it("will show an error message if the MAM query times out", it("will show an error message if the MAM query times out",
mock.initConverse(['discoInitialized'], {}, async function (done, _converse) { mock.initConverse(['discoInitialized'], {}, async function (_converse) {
const sendIQ = _converse.connection.sendIQ; const sendIQ = _converse.connection.sendIQ;
@ -1231,7 +1210,6 @@ describe("Chatboxes", function () {
await u.waitUntil(() => view.model.messages.length === 2, 500); await u.waitUntil(() => view.model.messages.length === 2, 500);
err_message = view.querySelector('.message.chat-error'); err_message = view.querySelector('.message.chat-error');
expect(err_message).toBe(null); expect(err_message).toBe(null);
done();
})); }));
}); });
}); });

View File

@ -14,7 +14,7 @@ describe("Message Archive Management", function () {
'persistent_store': 'localStorage', 'persistent_store': 'localStorage',
'mam_request_all_pages': false 'mam_request_all_pages': false
}, },
async function (done, _converse) { async function (_converse) {
const sent_IQs = _converse.connection.IQ_stanzas; const sent_IQs = _converse.connection.IQ_stanzas;
const muc_jid = 'orchard@chat.shakespeare.lit'; const muc_jid = 'orchard@chat.shakespeare.lit';
@ -152,12 +152,11 @@ describe("Message Archive Management", function () {
_converse.connection._dataRecv(mock.createRequest(result)); _converse.connection._dataRecv(mock.createRequest(result));
await u.waitUntil(() => view.model.messages.length === 4); await u.waitUntil(() => view.model.messages.length === 4);
await u.waitUntil(() => view.querySelector('converse-mam-placeholder') === null); await u.waitUntil(() => view.querySelector('converse-mam-placeholder') === null);
done();
})); }));
it("is not created when there isn't a gap because the cached history is empty", it("is not created when there isn't a gap because the cached history is empty",
mock.initConverse(['discoInitialized'], {'archived_messages_page_size': 2}, mock.initConverse(['discoInitialized'], {'archived_messages_page_size': 2},
async function (done, _converse) { async function (_converse) {
const sent_IQs = _converse.connection.IQ_stanzas; const sent_IQs = _converse.connection.IQ_stanzas;
const muc_jid = 'orchard@chat.shakespeare.lit'; const muc_jid = 'orchard@chat.shakespeare.lit';
@ -213,7 +212,6 @@ describe("Message Archive Management", function () {
_converse.connection._dataRecv(mock.createRequest(result)); _converse.connection._dataRecv(mock.createRequest(result));
await u.waitUntil(() => view.model.messages.length === 2); await u.waitUntil(() => view.model.messages.length === 2);
expect(true).toBe(true); expect(true).toBe(true);
done();
})); }));
}); });
}); });

View File

@ -8,11 +8,8 @@ const sizzle = converse.env.sizzle;
describe("A chat message", function () { describe("A chat message", function () {
it("received for a minimized chat box will increment a counter on its header", it("received for a minimized chat box will increment a counter on its header",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { mock.initConverse(['chatBoxesFetched'], {'view_mode': 'overlayed'}, async function (_converse) {
if (_converse.view_mode === 'fullscreen') {
return done();
}
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
const contact_name = mock.cur_names[0]; const contact_name = mock.cur_names[0];
const contact_jid = contact_name.replace(/ /g,'.').toLowerCase() + '@montague.lit'; const contact_jid = contact_name.replace(/ /g,'.').toLowerCase() + '@montague.lit';
@ -65,7 +62,6 @@ describe("A chat message", function () {
expect(count.textContent).toBe('2'); expect(count.textContent).toBe('2');
document.querySelector("converse-minimized-chat a.restore-chat").click(); document.querySelector("converse-minimized-chat a.restore-chat").click();
expect(_converse.chatboxes.filter('minimized').length).toBe(0); expect(_converse.chatboxes.filter('minimized').length).toBe(0);
done();
})); }));
}); });
@ -73,7 +69,7 @@ describe("A chat message", function () {
describe("A Groupchat", function () { describe("A Groupchat", function () {
it("can be minimized by clicking a DOM element with class 'toggle-chatbox-button'", it("can be minimized by clicking a DOM element with class 'toggle-chatbox-button'",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
await mock.openChatRoom(_converse, 'lounge', 'montague.lit', 'romeo'); await mock.openChatRoom(_converse, 'lounge', 'montague.lit', 'romeo');
const view = _converse.chatboxviews.get('lounge@montague.lit'); const view = _converse.chatboxviews.get('lounge@montague.lit');
@ -89,7 +85,6 @@ describe("A Groupchat", function () {
expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxMaximized', jasmine.any(Object)); expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxMaximized', jasmine.any(Object));
expect(view.model.get('minimized')).toBeFalsy(); expect(view.model.get('minimized')).toBeFalsy();
expect(_converse.api.trigger.calls.count(), 3); expect(_converse.api.trigger.calls.count(), 3);
done();
})); }));
}); });
@ -97,7 +92,7 @@ describe("A Groupchat", function () {
describe("A Chatbox", function () { describe("A Chatbox", function () {
it("can be minimized by clicking a DOM element with class 'toggle-chatbox-button'", it("can be minimized by clicking a DOM element with class 'toggle-chatbox-button'",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse); await mock.openControlBox(_converse);
@ -121,11 +116,10 @@ describe("A Chatbox", function () {
minimized_chats.querySelector("a.restore-chat").click(); minimized_chats.querySelector("a.restore-chat").click();
expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxMaximized', jasmine.any(Object)); expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxMaximized', jasmine.any(Object));
expect(chatview.model.get('minimized')).toBeFalsy(); expect(chatview.model.get('minimized')).toBeFalsy();
done();
})); }));
it("can be opened in minimized mode initially", mock.initConverse([], {}, async function (done, _converse) { it("can be opened in minimized mode initially", mock.initConverse([], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
const minimized_chats = document.querySelector("converse-minimized-chats") const minimized_chats = document.querySelector("converse-minimized-chats")
@ -136,11 +130,10 @@ describe("A Chatbox", function () {
expect(u.isVisible(minimized_chats.firstElementChild)).toBe(true); expect(u.isVisible(minimized_chats.firstElementChild)).toBe(true);
expect(minimized_chats.firstElementChild.querySelectorAll('converse-minimized-chat').length).toBe(1); expect(minimized_chats.firstElementChild.querySelectorAll('converse-minimized-chat').length).toBe(1);
expect(_converse.chatboxes.filter('minimized').length).toBe(1); expect(_converse.chatboxes.filter('minimized').length).toBe(1);
done();
})); }));
it("can be trimmed to conserve space", mock.initConverse([], {}, async function (done, _converse) { it("can be trimmed to conserve space", mock.initConverse([], {}, async function (_converse) {
spyOn(_converse.minimize, 'trimChats'); spyOn(_converse.minimize, 'trimChats');
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse); await mock.openControlBox(_converse);
@ -173,7 +166,6 @@ describe("A Chatbox", function () {
const minimized_chats = document.querySelector("converse-minimized-chats") const minimized_chats = document.querySelector("converse-minimized-chats")
minimized_chats.querySelector("a.restore-chat").click(); minimized_chats.querySelector("a.restore-chat").click();
expect(_converse.minimize.trimChats.calls.count()).toBe(17); expect(_converse.minimize.trimChats.calls.count()).toBe(17);
done();
})); }));
}); });
@ -181,7 +173,7 @@ describe("A Chatbox", function () {
describe("A Minimized ChatBoxView's Unread Message Count", function () { describe("A Minimized ChatBoxView's Unread Message Count", function () {
it("is displayed when scrolled up chatbox is minimized after receiving unread messages", it("is displayed when scrolled up chatbox is minimized after receiving unread messages",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1); await mock.waitForRoster(_converse, 'current', 1);
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@ -199,11 +191,10 @@ describe("A Minimized ChatBoxView's Unread Message Count", function () {
const unread_count = selectUnreadMsgCount(); const unread_count = selectUnreadMsgCount();
expect(u.isVisible(unread_count)).toBeTruthy(); expect(u.isVisible(unread_count)).toBeTruthy();
expect(unread_count.innerHTML.replace(/<!-.*?->/g, '')).toBe('1'); expect(unread_count.innerHTML.replace(/<!-.*?->/g, '')).toBe('1');
done();
})); }));
it("is incremented when message is received and windows is not focused", it("is incremented when message is received and windows is not focused",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1); await mock.waitForRoster(_converse, 'current', 1);
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@ -217,11 +208,10 @@ describe("A Minimized ChatBoxView's Unread Message Count", function () {
const unread_count = selectUnreadMsgCount(); const unread_count = selectUnreadMsgCount();
expect(u.isVisible(unread_count)).toBeTruthy(); expect(u.isVisible(unread_count)).toBeTruthy();
expect(unread_count.innerHTML.replace(/<!-.*?->/g, '')).toBe('1'); expect(unread_count.innerHTML.replace(/<!-.*?->/g, '')).toBe('1');
done();
})); }));
it("will render Openstreetmap-URL from geo-URI", it("will render Openstreetmap-URL from geo-URI",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1); await mock.waitForRoster(_converse, 'current', 1);
const message = "geo:37.786971,-122.399677"; const message = "geo:37.786971,-122.399677";
@ -236,7 +226,6 @@ describe("A Minimized ChatBoxView's Unread Message Count", function () {
await u.waitUntil(() => msg.innerHTML.replace(/\<!-.*?-\>/g, '') === await u.waitUntil(() => msg.innerHTML.replace(/\<!-.*?-\>/g, '') ===
'<a target="_blank" rel="noopener" href="https://www.openstreetmap.org/?mlat=37.786971&amp;'+ '<a target="_blank" rel="noopener" href="https://www.openstreetmap.org/?mlat=37.786971&amp;'+
'mlon=-122.399677#map=18/37.786971/-122.399677">https://www.openstreetmap.org/?mlat=37.786971&amp;mlon=-122.399677#map=18/37.786971/-122.399677</a>'); 'mlon=-122.399677#map=18/37.786971/-122.399677">https://www.openstreetmap.org/?mlat=37.786971&amp;mlon=-122.399677#map=18/37.786971/-122.399677</a>');
done();
})); }));
}); });
@ -244,7 +233,7 @@ describe("A Minimized ChatBoxView's Unread Message Count", function () {
describe("The Minimized Chats Widget", function () { describe("The Minimized Chats Widget", function () {
it("shows chats that have been minimized", it("shows chats that have been minimized",
mock.initConverse([], {}, async function (done, _converse) { mock.initConverse([], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse); await mock.openControlBox(_converse);
@ -271,11 +260,10 @@ describe("The Minimized Chats Widget", function () {
expect(u.isVisible(minimized_chats)).toBe(true); expect(u.isVisible(minimized_chats)).toBe(true);
expect(_converse.chatboxes.filter('minimized').length).toBe(2); expect(_converse.chatboxes.filter('minimized').length).toBe(2);
expect(_converse.chatboxes.filter('minimized').map(c => c.get('jid')).includes(contact_jid)).toBeTruthy(); expect(_converse.chatboxes.filter('minimized').map(c => c.get('jid')).includes(contact_jid)).toBeTruthy();
done();
})); }));
it("can be toggled to hide or show minimized chats", it("can be toggled to hide or show minimized chats",
mock.initConverse([], {}, async function (done, _converse) { mock.initConverse([], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current'); await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse); await mock.openControlBox(_converse);
@ -297,11 +285,10 @@ describe("The Minimized Chats Widget", function () {
minimized_chats.querySelector('#toggle-minimized-chats').click(); minimized_chats.querySelector('#toggle-minimized-chats').click();
await u.waitUntil(() => u.isVisible(minimized_chats.querySelector('.minimized-chats-flyout'))); await u.waitUntil(() => u.isVisible(minimized_chats.querySelector('.minimized-chats-flyout')));
expect(minimized_chats.minchats.get('collapsed')).toBeTruthy(); expect(minimized_chats.minchats.get('collapsed')).toBeTruthy();
done();
})); }));
it("shows the number messages received to minimized chats", it("shows the number messages received to minimized chats",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current', 4); await mock.waitForRoster(_converse, 'current', 4);
await mock.openControlBox(_converse); await mock.openControlBox(_converse);
@ -312,11 +299,13 @@ describe("The Minimized Chats Widget", function () {
const unread_el = minimized_chats.querySelector('.unread-message-count'); const unread_el = minimized_chats.querySelector('.unread-message-count');
expect(u.isVisible(unread_el)).toBe(false); expect(u.isVisible(unread_el)).toBe(false);
const promises = [];
let i, contact_jid; let i, contact_jid;
for (i=0; i<3; i++) { for (i=0; i<3; i++) {
contact_jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@montague.lit'; contact_jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@montague.lit';
mock.openChatBoxFor(_converse, contact_jid); promises.push(mock.openChatBoxFor(_converse, contact_jid));
} }
await Promise.all(promises);
await u.waitUntil(() => _converse.chatboxes.length == 4); await u.waitUntil(() => _converse.chatboxes.length == 4);
const chatview = _converse.chatboxviews.get(contact_jid); const chatview = _converse.chatboxviews.get(contact_jid);
@ -372,11 +361,10 @@ describe("The Minimized Chats Widget", function () {
id: u.getUniqueId() id: u.getUniqueId()
}).c('inactive', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree()); }).c('inactive', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree());
expect(minimized_chats.querySelector('.unread-message-count').textContent).toBe((i).toString()); expect(minimized_chats.querySelector('.unread-message-count').textContent).toBe((i).toString());
done();
})); }));
it("shows the number messages received to minimized groupchats", it("shows the number messages received to minimized groupchats",
mock.initConverse([], {}, async function (done, _converse) { mock.initConverse([], {}, async function (_converse) {
const muc_jid = 'kitchen@conference.shakespeare.lit'; const muc_jid = 'kitchen@conference.shakespeare.lit';
await mock.openAndEnterChatRoom(_converse, 'kitchen@conference.shakespeare.lit', 'fires'); await mock.openAndEnterChatRoom(_converse, 'kitchen@conference.shakespeare.lit', 'fires');
@ -395,6 +383,5 @@ describe("The Minimized Chats Widget", function () {
const minimized_chats = document.querySelector("converse-minimized-chats") const minimized_chats = document.querySelector("converse-minimized-chats")
expect(u.isVisible(minimized_chats.querySelector('.unread-message-count'))).toBeTruthy(); expect(u.isVisible(minimized_chats.querySelector('.unread-message-count'))).toBeTruthy();
expect(minimized_chats.querySelector('.unread-message-count').textContent).toBe('1'); expect(minimized_chats.querySelector('.unread-message-count').textContent).toBe('1');
done();
})); }));
}); });

View File

@ -1,8 +1,6 @@
import 'shared/autocomplete/index.js'; import 'shared/autocomplete/index.js';
import BottomPanel from 'plugins/chatview/bottom-panel.js'; import BottomPanel from 'plugins/chatview/bottom-panel.js';
import debounce from 'lodash-es/debounce';
import tpl_muc_bottom_panel from './templates/muc-bottom-panel.js'; import tpl_muc_bottom_panel from './templates/muc-bottom-panel.js';
import { __ } from 'i18n';
import { _converse, api, converse } from "@converse/headless/core"; import { _converse, api, converse } from "@converse/headless/core";
import { render } from 'lit'; import { render } from 'lit';
@ -16,17 +14,14 @@ export default class MUCBottomPanel extends BottomPanel {
'click .send-button': 'sendButtonClicked', 'click .send-button': 'sendButtonClicked',
} }
async connectedCallback () { async initialize () {
// this.model gets set in the super method and we also wait there for this.model.initialized await super.initialize();
await super.connectedCallback();
this.debouncedRender = debounce(this.render, 100);
this.listenTo(this.model, 'change:hidden_occupants', this.debouncedRender); this.listenTo(this.model, 'change:hidden_occupants', this.debouncedRender);
this.listenTo(this.model, 'change:num_unread_general', this.debouncedRender) this.listenTo(this.model, 'change:num_unread_general', this.debouncedRender)
this.listenTo(this.model.features, 'change:moderated', this.debouncedRender); this.listenTo(this.model.features, 'change:moderated', this.debouncedRender);
this.listenTo(this.model.occupants, 'add', this.renderIfOwnOccupant) this.listenTo(this.model.occupants, 'add', this.renderIfOwnOccupant)
this.listenTo(this.model.occupants, 'change:role', this.renderIfOwnOccupant); this.listenTo(this.model.occupants, 'change:role', this.renderIfOwnOccupant);
this.listenTo(this.model.session, 'change:connection_status', this.debouncedRender); this.listenTo(this.model.session, 'change:connection_status', this.debouncedRender);
this.render();
} }
render () { render () {
@ -35,6 +30,7 @@ export default class MUCBottomPanel extends BottomPanel {
render(tpl_muc_bottom_panel({ render(tpl_muc_bottom_panel({
can_edit, entered, can_edit, entered,
'model': this.model, 'model': this.model,
'is_groupchat': true,
'viewUnreadMessages': ev => this.viewUnreadMessages(ev) 'viewUnreadMessages': ev => this.viewUnreadMessages(ev)
}), this); }), this);
} }
@ -47,14 +43,6 @@ export default class MUCBottomPanel extends BottomPanel {
this.querySelector('converse-message-form')?.onFormSubmitted(ev); this.querySelector('converse-message-form')?.onFormSubmitted(ev);
} }
getToolbarOptions () {
return Object.assign(super.getToolbarOptions(), {
'is_groupchat': true,
'label_hide_occupants': __('Hide the list of participants'),
'show_occupants_toggle': api.settings.get('visible_toolbar_buttons').toggle_occupants
});
}
hideOccupants (ev) { hideOccupants (ev) {
ev?.preventDefault?.(); ev?.preventDefault?.();
ev?.stopPropagation?.(); ev?.stopPropagation?.();

View File

@ -19,7 +19,6 @@ export default class MUCView extends BaseChatView {
this.listenTo(_converse, 'windowStateChanged', this.onWindowStateChanged); this.listenTo(_converse, 'windowStateChanged', this.onWindowStateChanged);
this.listenTo(this.model, 'change:composing_spoiler', this.requestUpdateMessageForm); this.listenTo(this.model, 'change:composing_spoiler', this.requestUpdateMessageForm);
this.listenTo(this.model, 'show', this.show);
this.listenTo(this.model.session, 'change:connection_status', this.onConnectionStatusChanged); this.listenTo(this.model.session, 'change:connection_status', this.onConnectionStatusChanged);
this.listenTo(this.model.session, 'change:view', this.requestUpdate); this.listenTo(this.model.session, 'change:view', this.requestUpdate);

View File

@ -23,10 +23,9 @@ const tpl_can_edit = (o) => {
.model=${o.model} .model=${o.model}
?composing_spoiler="${o.model.get('composing_spoiler')}" ?composing_spoiler="${o.model.get('composing_spoiler')}"
?hidden_occupants="${o.model.get('hidden_occupants')}" ?hidden_occupants="${o.model.get('hidden_occupants')}"
?is_groupchat="${o.model.get('is_groupchat')}" ?is_groupchat="${o.is_groupchat}"
?show_call_button="${show_call_button}" ?show_call_button="${show_call_button}"
?show_emoji_button="${show_emoji_button}" ?show_emoji_button="${show_emoji_button}"
?show_occupants_toggle="${o.model.get('show_occupants_toggle')}"
?show_send_button="${show_send_button}" ?show_send_button="${show_send_button}"
?show_spoiler_button="${show_spoiler_button}" ?show_spoiler_button="${show_spoiler_button}"
?show_toolbar="${show_toolbar}" ?show_toolbar="${show_toolbar}"

View File

@ -3,7 +3,7 @@ import { __ } from 'i18n';
export default (o) => { export default (o) => {
const i18n_desc = __('Description:'); const i18n_desc = __('Description:');
const i18n_jid = __('Groupchat Address (JID):'); const i18n_jid = __('Groupchat XMPP Address:');
const i18n_occ = __('Participants:'); const i18n_occ = __('Participants:');
const i18n_features = __('Features:'); const i18n_features = __('Features:');
const i18n_requires_auth = __('Requires authentication'); const i18n_requires_auth = __('Requires authentication');

View File

@ -20,7 +20,7 @@ export default (o) => {
</div> </div>
<div class="chatbox-title__buttons row no-gutters"> <div class="chatbox-title__buttons row no-gutters">
${ o.standalone_btns.length ? tpl_standalone_btns(o) : '' } ${ o.standalone_btns.length ? tpl_standalone_btns(o) : '' }
${ o.dropdown_btns.length ? html`<converse-dropdown .items=${o.dropdown_btns}></converse-dropdown>` : '' } ${ o.dropdown_btns.length ? html`<converse-dropdown class="dropleft" .items=${o.dropdown_btns}></converse-dropdown>` : '' }
</div> </div>
</div> </div>
${ show_subject ? html`<p class="chat-head__desc" title="${i18n_hide_topic}"> ${ show_subject ? html`<p class="chat-head__desc" title="${i18n_hide_topic}">

View File

@ -9,7 +9,7 @@ describe("The nickname autocomplete feature", function () {
it("shows all autocompletion options when the user presses @", it("shows all autocompletion options when the user presses @",
mock.initConverse(['chatBoxesFetched'], {}, mock.initConverse(['chatBoxesFetched'], {},
async function (done, _converse) { async function (_converse) {
await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'tom'); await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'tom');
const view = _converse.chatboxviews.get('lounge@montague.lit'); const view = _converse.chatboxviews.get('lounge@montague.lit');
@ -58,12 +58,11 @@ describe("The nickname autocomplete feature", function () {
expect(view.querySelector('.suggestion-box__results li:nth-child(2)').textContent).toBe('harry'); expect(view.querySelector('.suggestion-box__results li:nth-child(2)').textContent).toBe('harry');
expect(view.querySelector('.suggestion-box__results li:nth-child(3)').textContent).toBe('jane'); expect(view.querySelector('.suggestion-box__results li:nth-child(3)').textContent).toBe('jane');
expect(view.querySelector('.suggestion-box__results li:nth-child(4)').textContent).toBe('tom'); expect(view.querySelector('.suggestion-box__results li:nth-child(4)').textContent).toBe('tom');
done();
})); }));
it("shows all autocompletion options when the user presses @ right after a new line", it("shows all autocompletion options when the user presses @ right after a new line",
mock.initConverse(['chatBoxesFetched'], {}, mock.initConverse(['chatBoxesFetched'], {},
async function (done, _converse) { async function (_converse) {
await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'tom'); await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'tom');
const view = _converse.chatboxviews.get('lounge@montague.lit'); const view = _converse.chatboxviews.get('lounge@montague.lit');
@ -113,13 +112,12 @@ describe("The nickname autocomplete feature", function () {
expect(view.querySelector('.suggestion-box__results li:nth-child(2)').textContent).toBe('harry'); expect(view.querySelector('.suggestion-box__results li:nth-child(2)').textContent).toBe('harry');
expect(view.querySelector('.suggestion-box__results li:nth-child(3)').textContent).toBe('jane'); expect(view.querySelector('.suggestion-box__results li:nth-child(3)').textContent).toBe('jane');
expect(view.querySelector('.suggestion-box__results li:nth-child(4)').textContent).toBe('tom'); expect(view.querySelector('.suggestion-box__results li:nth-child(4)').textContent).toBe('tom');
done();
})); }));
it("shows all autocompletion options when the user presses @ right after an allowed character", it("shows all autocompletion options when the user presses @ right after an allowed character",
mock.initConverse( mock.initConverse(
['chatBoxesFetched'], {'opening_mention_characters':['(']}, ['chatBoxesFetched'], {'opening_mention_characters':['(']},
async function (done, _converse) { async function (_converse) {
await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'tom'); await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'tom');
const view = _converse.chatboxviews.get('lounge@montague.lit'); const view = _converse.chatboxviews.get('lounge@montague.lit');
@ -169,11 +167,10 @@ describe("The nickname autocomplete feature", function () {
expect(view.querySelector('.suggestion-box__results li:nth-child(2)').textContent).toBe('harry'); expect(view.querySelector('.suggestion-box__results li:nth-child(2)').textContent).toBe('harry');
expect(view.querySelector('.suggestion-box__results li:nth-child(3)').textContent).toBe('jane'); expect(view.querySelector('.suggestion-box__results li:nth-child(3)').textContent).toBe('jane');
expect(view.querySelector('.suggestion-box__results li:nth-child(4)').textContent).toBe('tom'); expect(view.querySelector('.suggestion-box__results li:nth-child(4)').textContent).toBe('tom');
done();
})); }));
it("should order by query index position and length", mock.initConverse( it("should order by query index position and length", mock.initConverse(
['chatBoxesFetched'], {}, async function (done, _converse) { ['chatBoxesFetched'], {}, async function (_converse) {
await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'tom'); await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'tom');
const view = _converse.chatboxviews.get('lounge@montague.lit'); const view = _converse.chatboxviews.get('lounge@montague.lit');
@ -217,11 +214,10 @@ describe("The nickname autocomplete feature", function () {
await u.waitUntil(() => view.querySelectorAll('.suggestion-box__results li').length === 2); await u.waitUntil(() => view.querySelectorAll('.suggestion-box__results li').length === 2);
expect(view.querySelector('.suggestion-box__results li:first-child').textContent).toBe('john'); expect(view.querySelector('.suggestion-box__results li:first-child').textContent).toBe('john');
expect(view.querySelector('.suggestion-box__results li:nth-child(2)').textContent).toBe('jones'); expect(view.querySelector('.suggestion-box__results li:nth-child(2)').textContent).toBe('jones');
done();
})); }));
it("autocompletes when the user presses tab", it("autocompletes when the user presses tab",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
const view = _converse.chatboxviews.get('lounge@montague.lit'); const view = _converse.chatboxviews.get('lounge@montague.lit');
@ -328,11 +324,10 @@ describe("The nickname autocomplete feature", function () {
message_form.onKeyDown(tab_event); message_form.onKeyDown(tab_event);
message_form.onKeyUp(tab_event); message_form.onKeyUp(tab_event);
await u.waitUntil(() => textarea.value === 'hello @z3r0 '); await u.waitUntil(() => textarea.value === 'hello @z3r0 ');
done();
})); }));
it("autocompletes when the user presses backspace", it("autocompletes when the user presses backspace",
mock.initConverse([], {}, async function (done, _converse) { mock.initConverse([], {}, async function (_converse) {
await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
const view = _converse.chatboxviews.get('lounge@montague.lit'); const view = _converse.chatboxviews.get('lounge@montague.lit');
@ -368,6 +363,5 @@ describe("The nickname autocomplete feature", function () {
await u.waitUntil(() => view.querySelector('.suggestion-box__results').hidden === false); await u.waitUntil(() => view.querySelector('.suggestion-box__results').hidden === false);
expect(view.querySelectorAll('.suggestion-box__results li').length).toBe(1); expect(view.querySelectorAll('.suggestion-box__results li').length).toBe(1);
expect(view.querySelector('.suggestion-box__results li').textContent).toBe('some1'); expect(view.querySelector('.suggestion-box__results li').textContent).toBe('some1');
done();
})); }));
}); });

View File

@ -6,7 +6,7 @@ const u = converse.env.utils;
describe("The <converse-muc> component", function () { describe("The <converse-muc> component", function () {
it("can be rendered as a standalone component", it("can be rendered as a standalone component",
mock.initConverse([], {'auto_insert': false}, async function (done, _converse) { mock.initConverse([], {'auto_insert': false}, async function (_converse) {
const { api } = _converse; const { api } = _converse;
const muc_jid = 'lounge@montague.lit'; const muc_jid = 'lounge@montague.lit';
@ -33,11 +33,10 @@ describe("The <converse-muc> component", function () {
await u.waitUntil(() => muc_el.querySelector('converse-muc-bottom-panel')); await u.waitUntil(() => muc_el.querySelector('converse-muc-bottom-panel'));
body.removeChild(span_el); body.removeChild(span_el);
expect(true).toBe(true); expect(true).toBe(true);
done();
})); }));
it("will update correctly when the jid property changes", it("will update correctly when the jid property changes",
mock.initConverse([], {'auto_insert': false}, async function (done, _converse) { mock.initConverse([], {'auto_insert': false}, async function (_converse) {
const { api } = _converse; const { api } = _converse;
const muc_jid = 'lounge@montague.lit'; const muc_jid = 'lounge@montague.lit';
@ -54,7 +53,7 @@ describe("The <converse-muc> component", function () {
await mock.returnMemberLists(_converse, muc_jid, [], all_affiliations); await mock.returnMemberLists(_converse, muc_jid, [], all_affiliations);
await model.messages.fetched; await model.messages.fetched;
model.sendMessage('hello from the lounge!'); model.sendMessage({'body': 'hello from the lounge!'});
const span_el = document.createElement('span'); const span_el = document.createElement('span');
span_el.classList.add('conversejs'); span_el.classList.add('conversejs');
@ -84,11 +83,10 @@ describe("The <converse-muc> component", function () {
await mock.returnMemberLists(_converse, muc2_jid, [], all_affiliations); await mock.returnMemberLists(_converse, muc2_jid, [], all_affiliations);
await model.messages.fetched; await model.messages.fetched;
model2.sendMessage('hello from the bar!'); model2.sendMessage({'body': 'hello from the bar!'});
muc_el.setAttribute('jid', muc2_jid); muc_el.setAttribute('jid', muc2_jid);
await u.waitUntil(() => muc_el.querySelector('converse-chat-message-body').textContent.trim() === 'hello from the bar!'); await u.waitUntil(() => muc_el.querySelector('converse-chat-message-body').textContent.trim() === 'hello from the bar!');
body.removeChild(span_el); body.removeChild(span_el);
done();
})); }));
}); });

View File

@ -5,7 +5,7 @@ const { $msg, $pres, Strophe, u } = converse.env;
describe("A Groupchat Message", function () { describe("A Groupchat Message", function () {
it("can be replaced with a correction", it("can be replaced with a correction",
mock.initConverse([], {}, async function (done, _converse) { mock.initConverse([], {}, async function (_converse) {
const muc_jid = 'lounge@montague.lit'; const muc_jid = 'lounge@montague.lit';
await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
@ -66,11 +66,10 @@ describe("A Groupchat Message", function () {
expect(older_msgs.length).toBe(2); expect(older_msgs.length).toBe(2);
expect(older_msgs[0].textContent.includes('But soft, what light through yonder airlock breaks?')).toBe(true); expect(older_msgs[0].textContent.includes('But soft, what light through yonder airlock breaks?')).toBe(true);
expect(older_msgs[1].textContent.includes('But soft, what light through yonder chimney breaks?')).toBe(true); expect(older_msgs[1].textContent.includes('But soft, what light through yonder chimney breaks?')).toBe(true);
done();
})); }));
it("keeps the same position in history after a correction", it("keeps the same position in history after a correction",
mock.initConverse([], {}, async function (done, _converse) { mock.initConverse([], {}, async function (_converse) {
const muc_jid = 'lounge@montague.lit'; const muc_jid = 'lounge@montague.lit';
await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
@ -158,11 +157,10 @@ describe("A Groupchat Message", function () {
expect(older_msgs.length).toBe(2); expect(older_msgs.length).toBe(2);
expect(older_msgs[0].textContent.includes('But soft, what light through yonder airlock breaks?')).toBe(true); expect(older_msgs[0].textContent.includes('But soft, what light through yonder airlock breaks?')).toBe(true);
expect(older_msgs[1].textContent.includes('But soft, what light through yonder chimney breaks?')).toBe(true); expect(older_msgs[1].textContent.includes('But soft, what light through yonder chimney breaks?')).toBe(true);
done();
})); }));
it("can be sent as a correction by using the up arrow", it("can be sent as a correction by using the up arrow",
mock.initConverse([], {}, async function (done, _converse) { mock.initConverse([], {}, async function (_converse) {
const muc_jid = 'lounge@montague.lit'; const muc_jid = 'lounge@montague.lit';
await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
@ -262,6 +260,5 @@ describe("A Groupchat Message", function () {
expect(view.model.messages.at(0).get('correcting')).toBe(false); expect(view.model.messages.at(0).get('correcting')).toBe(false);
expect(view.querySelectorAll('.chat-msg').length).toBe(2); expect(view.querySelectorAll('.chat-msg').length).toBe(2);
await u.waitUntil(() => !u.hasClass('correcting', view.querySelector('.chat-msg')), 500); await u.waitUntil(() => !u.hasClass('correcting', view.querySelector('.chat-msg')), 500);
done();
})); }));
}); });

View File

@ -6,13 +6,13 @@ const u = converse.env.utils;
describe("Emojis", function () { describe("Emojis", function () {
describe("The emoji picker", function () { describe("The emoji picker", function () {
it("is opened to autocomplete emojis in the textarea", it("is opened to autocomplete emojis in the textarea",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current', 0); await mock.waitForRoster(_converse, 'current', 0);
const muc_jid = 'lounge@montague.lit'; const muc_jid = 'lounge@montague.lit';
await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
const view = _converse.chatboxviews.get(muc_jid); const view = _converse.chatboxviews.get(muc_jid);
await u.waitUntil(() => view.querySelector('converse-emoji-dropdown')); await u.waitUntil(() => view.querySelector('converse-emoji-picker'));
const textarea = view.querySelector('textarea.chat-textarea'); const textarea = view.querySelector('textarea.chat-textarea');
textarea.value = ':gri'; textarea.value = ':gri';
@ -70,17 +70,16 @@ describe("Emojis", function () {
await u.waitUntil(() => input.value === ':use'); await u.waitUntil(() => input.value === ':use');
visible_emojis = sizzle('.insert-emoji:not(.hidden)', picker); visible_emojis = sizzle('.insert-emoji:not(.hidden)', picker);
expect(visible_emojis.length).toBe(0); expect(visible_emojis.length).toBe(0);
done();
})); }));
it("is focused to autocomplete emojis in the textarea", it("is focused to autocomplete emojis in the textarea",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
const muc_jid = 'lounge@montague.lit'; const muc_jid = 'lounge@montague.lit';
await mock.waitForRoster(_converse, 'current', 0); await mock.waitForRoster(_converse, 'current', 0);
await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
const view = _converse.chatboxviews.get(muc_jid); const view = _converse.chatboxviews.get(muc_jid);
await u.waitUntil(() => view.querySelector('converse-emoji-dropdown')); await u.waitUntil(() => view.querySelector('converse-emoji-picker'));
const textarea = view.querySelector('textarea.chat-textarea'); const textarea = view.querySelector('textarea.chat-textarea');
textarea.value = ':'; textarea.value = ':';
// Press tab // Press tab
@ -119,18 +118,17 @@ describe("Emojis", function () {
emoji = sizzle('.emojis-lists__container--search .insert-emoji:not(.hidden) a', view).pop(); emoji = sizzle('.emojis-lists__container--search .insert-emoji:not(.hidden) a', view).pop();
emoji.click(); emoji.click();
await u.waitUntil(() => textarea.value === ':grinning: :grimacing: '); await u.waitUntil(() => textarea.value === ':grinning: :grimacing: ');
done();
})); }));
it("properly inserts emojis into the chat textarea", it("properly inserts emojis into the chat textarea",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
const muc_jid = 'lounge@montague.lit'; const muc_jid = 'lounge@montague.lit';
await mock.waitForRoster(_converse, 'current', 0); await mock.waitForRoster(_converse, 'current', 0);
await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
const view = _converse.chatboxviews.get(muc_jid); const view = _converse.chatboxviews.get(muc_jid);
await u.waitUntil(() => view.querySelector('converse-emoji-dropdown')); await u.waitUntil(() => view.querySelector('converse-emoji-picker'));
const textarea = view.querySelector('textarea.chat-textarea'); const textarea = view.querySelector('textarea.chat-textarea');
textarea.value = ':gri'; textarea.value = ':gri';
@ -164,12 +162,11 @@ describe("Emojis", function () {
const emoji = sizzle('.emojis-lists__container--search .insert-emoji:not(.hidden) a', view).pop(); const emoji = sizzle('.emojis-lists__container--search .insert-emoji:not(.hidden) a', view).pop();
emoji.click(); emoji.click();
expect(textarea.value).toBe(':100: '); expect(textarea.value).toBe(':100: ');
done();
})); }));
it("allows you to search for particular emojis", it("allows you to search for particular emojis",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
const muc_jid = 'lounge@montague.lit'; const muc_jid = 'lounge@montague.lit';
await mock.waitForRoster(_converse, 'current', 0); await mock.waitForRoster(_converse, 'current', 0);
@ -222,7 +219,6 @@ describe("Emojis", function () {
input.dispatchEvent(new KeyboardEvent('keydown', enter_event)); input.dispatchEvent(new KeyboardEvent('keydown', enter_event));
await u.waitUntil(() => input.value === ''); await u.waitUntil(() => input.value === '');
expect(view.querySelector('textarea.chat-textarea').value).toBe(':smiley: '); expect(view.querySelector('textarea.chat-textarea').value).toBe(':smiley: ');
done();
})); }));
}); });
}); });

View File

@ -5,7 +5,7 @@ const u = converse.env.utils;
describe("A XEP-0317 MUC Hat", function () { describe("A XEP-0317 MUC Hat", function () {
it("can be included in a presence stanza", it("can be included in a presence stanza",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) { mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
const muc_jid = 'lounge@montague.lit'; const muc_jid = 'lounge@montague.lit';
await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
const view = _converse.chatboxviews.get(muc_jid); const view = _converse.chatboxviews.get(muc_jid);
@ -70,6 +70,5 @@ describe("A XEP-0317 MUC Hat", function () {
`))); `)));
await u.waitUntil(() => view.model.getOccupant("Terry").get('hats').length === 0); await u.waitUntil(() => view.model.getOccupant("Terry").get('hats').length === 0);
await u.waitUntil(() => view.querySelectorAll('.chat-msg .badge').length === 0); await u.waitUntil(() => view.querySelectorAll('.chat-msg .badge').length === 0);
done();
})); }));
}) })

View File

@ -8,7 +8,7 @@ describe("XEP-0363: HTTP File Upload", function () {
describe("When not supported", function () { describe("When not supported", function () {
describe("A file upload toolbar button", function () { describe("A file upload toolbar button", function () {
it("does not appear in MUC chats", mock.initConverse([], {}, async (done, _converse) => { it("does not appear in MUC chats", mock.initConverse([], {}, async (_converse) => {
await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
mock.waitUntilDiscoConfirmed( mock.waitUntilDiscoConfirmed(
_converse, _converse.domain, _converse, _converse.domain,
@ -19,7 +19,6 @@ describe("XEP-0363: HTTP File Upload", function () {
const view = _converse.chatboxviews.get('lounge@montague.lit'); const view = _converse.chatboxviews.get('lounge@montague.lit');
await u.waitUntil(() => view.querySelector('.chat-toolbar .fileupload') === null); await u.waitUntil(() => view.querySelector('.chat-toolbar .fileupload') === null);
expect(1).toBe(1); expect(1).toBe(1);
done();
})); }));
}); });
@ -29,7 +28,7 @@ describe("XEP-0363: HTTP File Upload", function () {
describe("A file upload toolbar button", function () { describe("A file upload toolbar button", function () {
it("appears in MUC chats", mock.initConverse(['chatBoxesFetched'], {}, async (done, _converse) => { it("appears in MUC chats", mock.initConverse(['chatBoxesFetched'], {}, async (_converse) => {
await mock.waitUntilDiscoConfirmed( await mock.waitUntilDiscoConfirmed(
_converse, _converse.domain, _converse, _converse.domain,
[{'category': 'server', 'type':'IM'}], [{'category': 'server', 'type':'IM'}],
@ -41,12 +40,11 @@ describe("XEP-0363: HTTP File Upload", function () {
await u.waitUntil(() => _converse.chatboxviews.get('lounge@montague.lit').querySelector('.fileupload')); await u.waitUntil(() => _converse.chatboxviews.get('lounge@montague.lit').querySelector('.fileupload'));
const view = _converse.chatboxviews.get('lounge@montague.lit'); const view = _converse.chatboxviews.get('lounge@montague.lit');
expect(view.querySelector('.chat-toolbar .fileupload')).not.toBe(null); expect(view.querySelector('.chat-toolbar .fileupload')).not.toBe(null);
done();
})); }));
describe("when clicked and a file chosen", function () { describe("when clicked and a file chosen", function () {
it("is uploaded and sent out from a groupchat", mock.initConverse(['chatBoxesFetched'], {} ,async (done, _converse) => { it("is uploaded and sent out from a groupchat", mock.initConverse(['chatBoxesFetched'], {} ,async (_converse) => {
const base_url = 'https://conversejs.org'; const base_url = 'https://conversejs.org';
await mock.waitUntilDiscoConfirmed( await mock.waitUntilDiscoConfirmed(
_converse, _converse.domain, _converse, _converse.domain,
@ -145,10 +143,9 @@ describe("XEP-0363: HTTP File Upload", function () {
expect(view.querySelector('.chat-msg .chat-msg__media').innerHTML.replace(/<!-.*?->/g, '').trim()).toEqual( expect(view.querySelector('.chat-msg .chat-msg__media').innerHTML.replace(/<!-.*?->/g, '').trim()).toEqual(
`<a target="_blank" rel="noopener" href="${base_url}/logo/conversejs-filled.svg">`+ `<a target="_blank" rel="noopener" href="${base_url}/logo/conversejs-filled.svg">`+
`Download image file "conversejs-filled.svg"</a>`); `Download file "conversejs-filled.svg"</a>`);
XMLHttpRequest.prototype.send = send_backup; XMLHttpRequest.prototype.send = send_backup;
done();
})); }));

Some files were not shown because too many files have changed in this diff Show More