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)
- #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
- #2348: `auto_join_room` not showing the room in `fullscreen` `view_mode`.
- #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
$(GETTEXT) 2>&1 > /dev/null; exit $$?;
rm dist/converse-no-dependencies.js
rm dist/tmp.css
.PHONY: 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-0425](https://xmpp.org/extensions/xep-0425.html) Message Moderation
- [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

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.
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']``
@ -1513,7 +1513,7 @@ muc_show_ogp_unfurls
Supports showing extra metadata (picture and description) for URLs contained in
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.
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/tests/converse.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/chatview/tests/chatbox.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/receipts.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/controlbox/tests/controlbox.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/http-file-upload.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/modtools.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/retractions.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/xss.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/register/tests/register.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/protocol.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: {

64169
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -15,7 +15,7 @@ mock.initConverse = function (promise_names=[], settings=null, func) {
settings = null;
}
return async done => {
return async () => {
if (_converse && _converse.api.connection.connected()) {
await _converse.api.user.logout();
}
@ -28,11 +28,10 @@ mock.initConverse = function (promise_names=[], settings=null, func) {
await initConverse(settings);
await Promise.all((promise_names || []).map(_converse.api.waitUntil));
try {
await func(done, _converse);
await func(_converse);
} catch(e) {
console.error(e);
fail(e);
await done();
}
}
};
@ -673,3 +672,68 @@ const initConverse = async (settings) => {
window.converse_disable_effects = true;
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 Strophe = converse.env.Strophe;
@ -18,7 +18,7 @@ describe("XEP-0357 Push Notifications", function () {
'jid': 'push-5@client.example',
'node': 'yxs32uqsflafdk3iuqo'
}]
}, async function (done, _converse) {
}, async function (_converse) {
const IQ_stanzas = _converse.connection.IQ_stanzas;
expect(_converse.session.get('push_enabled')).toBeFalsy();
@ -33,7 +33,7 @@ describe("XEP-0357 Push Notifications", function () {
[{'category': 'account', 'type':'registered'}],
['urn:xmpp:push:0'], [], 'info');
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(
`<iq id="${stanza.getAttribute('id')}" type="set" xmlns="jabber:client">`+
@ -46,7 +46,6 @@ describe("XEP-0357 Push Notifications", function () {
'id': stanza.getAttribute('id')
})));
await u.waitUntil(() => _converse.session.get('push_enabled'));
done();
}));
it("can be enabled for a MUC domain",
@ -57,7 +56,7 @@ describe("XEP-0357 Push Notifications", function () {
'jid': 'push-5@client.example',
'node': 'yxs32uqsflafdk3iuqo'
}]
}, async function (done, _converse) {
}, async function (_converse) {
const IQ_stanzas = _converse.connection.IQ_stanzas;
await mock.waitUntilDiscoConfirmed(
@ -80,16 +79,14 @@ describe("XEP-0357 Push Notifications", function () {
await u.waitUntil(() => _converse.session.get('push_enabled'));
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');
await mock.waitUntilDiscoConfirmed(
_converse, 'chat.shakespeare.lit',
[{'category': 'account', 'type':'registered'}],
['urn:xmpp:push:0'], [], 'info');
iq = await u.waitUntil(() => _.filter(
IQ_stanzas,
iq => sizzle(`iq[type="set"][to="chat.shakespeare.lit"] enable[xmlns="${Strophe.NS.PUSH}"]`, iq).length
iq = await u.waitUntil(() => IQ_stanzas.filter(iq => sizzle(`iq[type="set"][to="chat.shakespeare.lit"] enable[xmlns="${Strophe.NS.PUSH}"]`, iq).length
).pop());
expect(Strophe.serialize(iq)).toEqual(
@ -102,8 +99,7 @@ describe("XEP-0357 Push Notifications", function () {
'type': 'result',
'id': iq.getAttribute('id')
})));
await u.waitUntil(() => _.includes(_converse.session.get('push_enabled'), 'chat.shakespeare.lit'));
done();
await u.waitUntil(() => _converse.session.get('push_enabled').includes('chat.shakespeare.lit'));
}));
it("can be disabled",
@ -114,7 +110,7 @@ describe("XEP-0357 Push Notifications", function () {
'node': 'yxs32uqsflafdk3iuqo',
'disable': true
}]
}, async function (done, _converse) {
}, async function (_converse) {
const IQ_stanzas = _converse.connection.IQ_stanzas;
expect(_converse.session.get('push_enabled')).toBeFalsy();
@ -136,7 +132,6 @@ describe("XEP-0357 Push Notifications", function () {
'id': stanza.getAttribute('id')
})));
await u.waitUntil(() => _converse.session.get('push_enabled'))
done();
}));
@ -147,7 +142,7 @@ describe("XEP-0357 Push Notifications", function () {
'node': 'yxs32uqsflafdk3iuqo',
'secret': 'eruio234vzxc2kla-91'
}]
}, async function (done, _converse) {
}, async function (_converse) {
const IQ_stanzas = _converse.connection.IQ_stanzas;
expect(_converse.session.get('push_enabled')).toBeFalsy();
@ -179,6 +174,5 @@ describe("XEP-0357 Push Notifications", function () {
'id': stanza.getAttribute('id')
})));
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 () {
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);
_converse.api.trigger('rosterContactsFetched');
@ -30,11 +30,10 @@ describe("The User Details Modal", function () {
show_modal_button.click();
remove_contact_button = modal.el.querySelector('button.remove-contact');
expect(remove_contact_button === null).toBeTruthy();
done();
}));
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);
_converse.api.trigger('rosterContactsFetched');
@ -71,6 +70,5 @@ describe("The User Details Modal", function () {
remove_contact_button = modal.el.querySelector('button.remove-contact');
expect(u.isVisible(remove_contact_button)).toBeTruthy();
done();
}));
});

View File

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

View File

@ -1,77 +1,178 @@
{
"name": "@converse/headless",
"version": "6.0.0",
"version": "8.0.0",
"lockfileVersion": 1,
"requires": true,
"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": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz",
"integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==",
"dev": true
"integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q=="
},
"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": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz",
"integrity": "sha512-LpCHtPQ3sFx67z+uh2HnSyWSLLu5Jxo21795uRDuar/EOuYWXib5EmPaGIBuSnRqH2IODiKA2k5re/K9OnN/Yg==",
"dev": true
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/filesize/-/filesize-6.3.0.tgz",
"integrity": "sha512-ytx0ruGpDHKWVoiui6+BY/QMNngtDQ/pJaFwfBpQif0J63+E8DLdFyqS3NkKQn7vIruUEpoGD9JUJSg7Kp+I0g=="
},
"immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=",
"dev": true
"integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps="
},
"lie": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
"integrity": "sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=",
"dev": true,
"requires": {
"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": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/localforage/-/localforage-1.7.3.tgz",
"integrity": "sha512-1TulyYfc4udS7ECSBT2vwJksWbkwwTX8BzeUIiq8Y07Riy7bDAAnxDaPU/tWyOVmQAcWJIEIFP9lPfBGqVoPgQ==",
"dev": true,
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/localforage/-/localforage-1.9.0.tgz",
"integrity": "sha512-rR1oyNrKulpe+VM9cYmcFn6tsHuokyVHFaCM3+osEmxaHTbEk8oQu6eGDfS6DQLWi/N67XRmB8ECG37OES368g==",
"requires": {
"lie": "3.1.1"
}
},
"lodash": {
"version": "4.17.20",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==",
"dev": true
"localforage-driver-commons": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/localforage-driver-commons/-/localforage-driver-commons-1.0.3.tgz",
"integrity": "sha512-K9PiNNXcyX98lQVyCADjv+QKxFD71y0DtVUhqMjwCkFY/d/g7GdJLPN9U92M7RUvfkL8mzPhC+mWEKo9tur5oQ=="
},
"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": {
"version": "4.17.15",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.15.tgz",
"integrity": "sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ==",
"dev": true
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
},
"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": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/pluggable.js/-/pluggable.js-2.0.1.tgz",
"integrity": "sha512-SBt6v6Tbp20Jf8hU0cpcc/+HBHGMY8/Q+yA6Ih0tBQE8tfdZ6U4PRG0iNvUUjLx/hVyOP53n0UfGBymlfaaXCg==",
"dev": true,
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/pluggable.js/-/pluggable.js-3.0.1.tgz",
"integrity": "sha512-DQC51A6aKLk6anvyvQfukNcVzGHOI5B04DerHioqLSF7ptI+Nla2hHzG4PGxq8tKqOGwQHnXnj9qxcFM3VViEQ==",
"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": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz",
"integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==",
"dev": true
"integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug=="
},
"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": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz",
"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
}
}

View File

@ -37,9 +37,9 @@
"gitHead": "9641dcdc820e029b05930479c242d2b707bbe8e2",
"devDependencies": {},
"dependencies": {
"@converse/skeletor": "conversejs/skeletor#f354bc530493a17d031f6f9c524cc34e073908e3",
"dayjs": "1.10.4",
"filesize": "^6.1.0",
"@converse/skeletor": "0.0.5",
"dayjs": "1.10.5",
"filesize": "^6.3.0",
"localforage": "^1.9.0",
"localforage-driver-memory": "^1.0.5",
"localforage-setitems": "^1.4.0",
@ -48,6 +48,7 @@
"pluggable.js": "3.0.1",
"sizzle": "^2.3.5",
"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));
it("includes a entity capabilities node",
mock.initConverse([], {}, async (done, _converse) => {
mock.initConverse([], {}, async (_converse) => {
await mock.waitForRoster(_converse, 'current', 0);
_converse.api.disco.own.identities.clear();
@ -27,10 +27,9 @@ describe("A sent presence stanza", function () {
`<priority>0</priority>`+
`<c hash="sha-1" node="https://conversejs.org" ver="QgayPKawpkPSDYmwT/WM94uAlu0=" xmlns="http://jabber.org/protocol/caps"/>`+
`</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;
let pres = await _converse.xmppstatus.constructPresence('online', null, 'Hello world');
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"/>`+
`</presence>`
);
done();
}));
});

View File

@ -224,15 +224,23 @@ const MessageMixin = {
uploadFile () {
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
xhr.onreadystatechange = async () => {
if (xhr.readyState === XMLHttpRequest.DONE) {
log.info('Status: ' + xhr.status);
if (xhr.status === 200 || xhr.status === 201) {
this.save({
let attrs = {
'upload': _converse.SUCCESS,
'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 {
xhr.onerror();
}

View File

@ -9,6 +9,7 @@ import { _converse, api, converse } from "../../core.js";
import { getOpenPromise } from '@converse/openpromise';
import { initStorage } from '@converse/headless/shared/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 { sendMarker } from '@converse/headless/shared/actions';
@ -56,8 +57,8 @@ const ChatBox = ModelWithContact.extend({
}
this.set({'box_id': `box-${jid}`});
this.initNotifications();
this.initMessages();
this.initUI();
this.initMessages();
if (this.get('type') === _converse.PRIVATE_CHAT_TYPE) {
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) {
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 () {
if (!this.ui.get('scrolled') &&
if (
api.settings.get('prune_messages_above') &&
api.settings.get('pruning_behavior') === 'unscrolled'
api.settings.get('pruning_behavior') === 'unscrolled' &&
!this.ui.get('scrolled')
) {
pruneHistory(this);
}
@ -841,11 +850,12 @@ const ChatBox = ModelWithContact.extend({
return stanza;
},
getOutgoingMessageAttributes (text, spoiler_hint) {
const is_spoiler = this.get('composing_spoiler');
getOutgoingMessageAttributes (attrs) {
const is_spoiler = !!this.get('composing_spoiler');
const origin_id = u.getUniqueId();
const text = attrs?.body;
const body = text ? u.httpToGeoUri(u.shortnamesToUnicode(text), _converse) : undefined;
return {
return Object.assign({}, attrs, {
'from': _converse.bare_jid,
'fullname': _converse.xmppstatus.get('fullname'),
'id': origin_id,
@ -855,13 +865,12 @@ const ChatBox = ModelWithContact.extend({
'msgid': origin_id,
'nickname': this.get('nickname'),
'sender': 'me',
'spoiler_hint': is_spoiler ? spoiler_hint : undefined,
'time': (new Date()).toISOString(),
'type': this.get('message_type'),
body,
is_spoiler,
origin_id
}
}, getMediaURLs(text));
},
/**
@ -910,15 +919,14 @@ const ChatBox = ModelWithContact.extend({
* @private
* @method _converse.ChatBox#sendMessage
* @memberOf _converse.ChatBox
* @param { String } text - The chat message text
* @param { String } spoiler_hint - An optional hint, if the message being sent is a spoiler
* @param { Object } [attrs] - A map of attributes to be saved on the message
* @returns { _converse.Message }
* @example
* const chat = api.chats.get('buddy1@example.com');
* chat.sendMessage('hello world');
* const chat = api.chats.get('buddy1@example.org');
* chat.sendMessage({'body': 'hello world'});
*/
async sendMessage (text, spoiler_hint) {
const attrs = this.getOutgoingMessageAttributes(text, spoiler_hint);
async sendMessage (attrs) {
attrs = this.getOutgoingMessageAttributes(attrs);
let message = this.messages.findWhere('correcting')
if (message) {
const older_versions = message.get('older_versions') || {};
@ -1001,6 +1009,13 @@ const ChatBox = ModelWithContact.extend({
return;
}
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) {
return this.createMessage({
'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,
getEncryptionAttributes,
getErrorAttributes,
getMediaURLs,
getOutOfBandAttributes,
getReceiptId,
getReferences,
@ -215,5 +216,10 @@ export async function parseMessage (stanza, _converse) {
* *Hook* which allows plugins to add additional parsing
* @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(
['rosterInitialized', 'chatBoxesInitialized'], {},
async (done, _converse) => {
async (_converse) => {
const u = converse.env.utils;
@ -28,17 +28,16 @@ describe("The \"chats\" API", function() {
expect(chat.get('box_id')).toBe(`box-${jid}`);
// Test for multiple JIDs
mock.openChatBoxFor(_converse, jid2);
await mock.openChatBoxFor(_converse, jid2);
await u.waitUntil(() => _converse.chatboxes.length == 3);
const list = await _converse.api.chats.get([jid, jid2]);
expect(Array.isArray(list)).toBeTruthy();
expect(list[0].get('box_id')).toBe(`box-${jid}`);
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(
['chatBoxesInitialized'], {}, async (done, _converse) => {
['chatBoxesInitialized'], {}, async (_converse) => {
await mock.openControlBox(_converse);
await mock.waitForRoster(_converse, 'current', 2);
@ -62,6 +61,5 @@ describe("The \"chats\" API", function() {
expect(Array.isArray(list)).toBeTruthy();
expect(list[0].get('box_id')).toBe(`box-${jid}`);
expect(list[1].get('box_id')).toBe(`box-${jid2}`);
done();
}));
});

View File

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

View File

@ -7,7 +7,7 @@ describe("Service Discovery", function () {
it("stores the features it receives",
mock.initConverse(
['discoInitialized'], {},
async function (done, _converse) {
async function (_converse) {
const { u, $iq } = converse.env;
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).identities.where({'category': 'conference'}).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",
mock.initConverse(
['discoInitialized'], {},
function (done, _converse) {
function (_converse) {
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});
expect(_converse.api.trigger.called).toBe(true);
expect(_converse.api.trigger.args[0][0]).toBe('serviceDiscovered');
expect(_converse.api.trigger.args[0][1].get('var')).toBe(Strophe.NS.MAM);
done();
expect(_converse.api.trigger).toHaveBeenCalled();
const last_call = _converse.api.trigger.calls.all().pop();
expect(last_call.args[0]).toBe('serviceDiscovered');
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.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.
initStreamFeatures();
Array.from(_converse.connection.features.childNodes).forEach(feature => {
@ -123,3 +123,12 @@ export function populateStreamFeatures () {
});
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 () {
this.set({'box_id': `box-${this.get('jid')}`});
this.initUI();
this.initMessages();
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 { getOpenPromise } from '@converse/openpromise';
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 { sendMarker } from '@converse/headless/shared/actions';
@ -438,10 +438,14 @@ const ChatRoomMixin = {
};
if (attrs.msgid === message.get('retraction_id')) {
// The error message refers to a retraction
new_attrs.retracted = undefined;
new_attrs.retraction_id = undefined;
new_attrs.retracted_id = undefined;
if (!attrs.error) {
if (attrs.error_condition === 'forbidden') {
new_attrs.error = __("You're not allowed to retract your message.");
} else if (attrs.error_condition === 'not-acceptable') {
new_attrs.error = __(
"Your retraction was not delivered because you're not present in the groupchat."
@ -665,8 +669,7 @@ const ChatRoomMixin = {
* @method _converse.ChatRoom#sendTimedMessage
* @param { _converse.Message|XMLElement } message
* @returns { Promise<XMLElement>|Promise<_converse.TimeoutError> } Returns a promise
* which resolves with the reflected message stanza or rejects
* with an error stanza or with a {@link _converse.TimeoutError}.
* which resolves with the reflected message stanza or with an error stanza or {@link _converse.TimeoutError}.
*/
sendTimedMessage (el) {
if (typeof el.tree === 'function') {
@ -681,23 +684,15 @@ const ChatRoomMixin = {
const promise = getOpenPromise();
const timeoutHandler = _converse.connection.addTimedHandler(_converse.STANZA_TIMEOUT, () => {
_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;
});
const handler = _converse.connection.addHandler(
stanza => {
timeoutHandler && _converse.connection.deleteTimedHandler(timeoutHandler);
if (stanza.getAttribute('type') === 'groupchat') {
promise.resolve(stanza);
} else {
promise.reject(stanza);
}
},
null,
'message',
['error', 'groupchat'],
id
);
promise.resolve(stanza);
}, null, 'message', ['error', 'groupchat'], id);
api.send(el);
return promise;
},
@ -735,17 +730,20 @@ const ChatRoomMixin = {
'retraction_id': stanza.nodeTree.getAttribute('id'),
'editable': false
});
try {
await this.sendTimedMessage(stanza);
} catch (e) {
const result = await this.sendTimedMessage(stanza);
if (u.isErrorStanza(result)) {
log.error(result);
} else if (result instanceof _converse.TimeoutError) {
log.error(result);
message.save({
editable,
'error_type': 'timeout',
'error': __('A timeout happened while while trying to retract your message.'),
'retracted': undefined,
'retracted_id': undefined
'retracted_id': undefined,
'retraction_id': undefined
});
throw e;
}
},
@ -961,12 +959,15 @@ const ChatRoomMixin = {
return [updated_message, updated_references];
},
getOutgoingMessageAttributes (original_message, spoiler_hint) {
getOutgoingMessageAttributes (attrs) {
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 body = text ? u.httpToGeoUri(u.shortnamesToUnicode(text), _converse) : undefined;
return {
return Object.assign({}, attrs, {
body,
is_spoiler,
origin_id,
@ -979,9 +980,8 @@ const ChatRoomMixin = {
'message': body,
'nick': this.get('nick'),
'sender': 'me',
'spoiler_hint': is_spoiler ? spoiler_hint : undefined,
'type': 'groupchat'
};
}, getMediaURLs(text));
},
/**

View File

@ -6,6 +6,7 @@ import {
getCorrectionAttributes,
getEncryptionAttributes,
getErrorAttributes,
getMediaURLs,
getOpenGraphMetadata,
getOutOfBandAttributes,
getReceiptId,
@ -184,9 +185,10 @@ export async function parseMUCMessage (stanza, chatbox, _converse) {
getOpenGraphMetadata(stanza),
getRetractionAttributes(stanza, original_stanza),
getModerationAttributes(stanza),
getEncryptionAttributes(stanza, _converse)
getEncryptionAttributes(stanza, _converse),
);
await api.emojis.initialize();
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.
attrs['id'] = attrs['origin_id'] || attrs[`stanza_id ${attrs.from_muc || attrs.from}`] || u.getUniqueId();
/**
* *Hook* which allows plugins to add additional parsing
* @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 () {
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 user_jid = 'annoyingguy@montague.lit';
const muc_jid = 'lounge@montague.lit';
@ -36,7 +36,6 @@ describe('The MUC Affiliations API', function () {
`</query>` +
`</iq>`);
done();
})
);
});

View File

@ -8,26 +8,26 @@ describe("A Groupchat Message", function () {
mock.initConverse(
['chatBoxesFetched'],
{'prune_messages_above': 3},
async function (done, _converse) {
async function (_converse) {
const muc_jid = 'lounge@montague.lit';
const model = await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
expect(model.ui.get('scrolled')).toBeFalsy();
model.sendMessage('1st message');
model.sendMessage('2nd message');
model.sendMessage('3rd message');
model.sendMessage({'body': '1st message'});
model.sendMessage({'body': '2nd message'});
model.sendMessage({'body': '3rd message'});
await u.waitUntil(() => model.messages.length === 3);
// Make sure pruneHistory fires
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 === 3, 550);
model.ui.set('scrolled', true);
model.sendMessage('5th message');
model.sendMessage('6th message');
model.sendMessage({'body': '5th message'});
model.sendMessage({'body': '6th message'});
await u.waitUntil(() => model.messages.length === 5);
// 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));
await u.waitUntil(() => model.messages.length === 4);
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",
mock.initConverse(['chatBoxesFetched'], {'auto_register_muc_nickname': true},
async function (done, _converse) {
async function (_converse) {
const muc_jid = 'coven@chat.shakespeare.lit';
const room = await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
@ -47,12 +47,11 @@ describe("Chatrooms", function () {
`</x>`+
`</query>`+
`</iq>`);
done();
}));
it("allows you to automatically deregister your nickname when closing a room",
mock.initConverse(['chatBoxesFetched'], {'auto_register_muc_nickname': 'unregister'},
async function (done, _converse) {
async function (_converse) {
const muc_jid = 'coven@chat.shakespeare.lit';
const room = await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
@ -96,7 +95,6 @@ describe("Chatrooms", function () {
}).c('query', {'xmlns': 'jabber:iq:register'});
_converse.connection._dataRecv(mock.createRequest(result));
done();
}));
});
});

View File

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

View File

@ -1,6 +1,7 @@
import { Model } from '@converse/skeletor/src/model.js';
import { _converse, api, converse } from "@converse/headless/core";
import { getOpenPromise } from '@converse/openpromise';
import { rejectPresenceSubscription } from './utils.js';
const { Strophe, $iq, $pres } = converse.env;
@ -11,6 +12,7 @@ const { Strophe, $iq, $pres } = converse.env;
const RosterContact = Model.extend({
defaults: {
'chat_state': undefined,
'groups': [],
'image': _converse.DEFAULT_IMAGE,
'image_type': _converse.DEFAULT_IMAGE_TYPE,
'num_unread': 0,
@ -24,7 +26,6 @@ const RosterContact = Model.extend({
const bare_jid = Strophe.getBareJidFromJid(jid).toLowerCase();
attributes.jid = bare_jid;
this.set(Object.assign({
'groups': [],
'id': bare_jid,
'jid': bare_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
*/
unauthorize (message) {
_converse.rejectPresenceSubscription(this.get('jid'), message);
rejectPresenceSubscription(this.get('jid'), message);
return this;
},

View File

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

View File

@ -2,206 +2,56 @@
* @copyright The Converse.js contributors
* @license Mozilla Public License (MPLv2)
*/
import "@converse/headless/plugins/status";
import '@converse/headless/plugins/status';
import RosterContact from './contact.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 { Presence, Presences } from './presence.js';
import { __ } from 'i18n';
import { _converse, api, converse } from "@converse/headless/core";
import { clearPresences, initRoster, updateUnreadCounter } from './utils.js';
import { initStorage } from '@converse/headless/shared/utils.js';
const { $pres } = converse.env;
import { _converse, api, converse } from '@converse/headless/core';
import {
onChatBoxesInitialized,
onClearSession,
onPresencesInitialized,
onRosterContactsFetched,
onStatusInitialized,
unregisterPresenceHandler,
} from './utils.js';
converse.plugins.add('converse-roster', {
dependencies: ['converse-status'],
initialize () {
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
api.settings.extend({
'allow_contact_requests': true,
'auto_subscribe': false,
'synchronize_availability': true,
'synchronize_availability': true
});
api.promises.add([
'cachedRoster',
'roster',
'rosterContactsFetched',
'rosterInitialized',
]);
api.promises.add(['cachedRoster', 'roster', 'rosterContactsFetched', 'rosterInitialized']);
// API methods only available to plugins
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_REQUESTING_CONTACTS = __('Contact requests');
_converse.HEADER_UNGROUPED = __('Ungrouped');
_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.Presences = Presences;
_converse.RosterContact = RosterContact;
_converse.RosterContacts = RosterContacts;
_converse.unregisterPresenceHandler = function () {
if (_converse.presence_ref !== undefined) {
_converse.connection.deleteHandler(_converse.presence_ref);
delete _converse.presence_ref;
}
};
/******************** 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('beforeTearDown', () => unregisterPresenceHandler());
api.listen.on('chatBoxesInitialized', onChatBoxesInitialized);
api.listen.on('clearSession', onClearSession);
api.listen.on('presencesInitialized', onPresencesInitialized);
api.listen.on('statusInitialized', onStatusInitialized);
api.listen.on('streamResumptionFailed', () => _converse.session.set('roster_cached', false));
api.listen.on('clearSession', async () => {
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);
});
api.waitUntil('rosterContactsFetched').then(onRosterContactsFetched);
}
});

View File

@ -5,7 +5,7 @@
describe("A received presence stanza", function () {
it("has its priority taken into account",
mock.initConverse([], {}, async (done, _converse) => {
mock.initConverse([], {}, async (_converse) => {
const u = converse.env.utils;
mock.openControlBox(_converse);
@ -177,6 +177,5 @@ describe("A received presence stanza", function () {
_converse.connection._dataRecv(mock.createRequest(stanza));
expect(_converse.roster.get(contact_jid).presence.get('show')).toBe('offline');
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 { _converse, api, converse } from "@converse/headless/core";
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
// roster and the roster groups.
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')});
if (contact !== undefined) {
contact.save({'num_unread': chatbox.get('num_unread')});
/**
* Fetch all the roster groups, and then the roster contacts.
* Emit an event after fetching is done in each case.
* @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();
}
/**
* 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) {
const status1 = contact1.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,
'smacks_max_unacked_stanzas': 2
},
async function (done, _converse) {
async function (_converse) {
await _converse.api.user.login('romeo@montague.lit/orchard', 'secret');
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(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,
'smacks_max_unacked_stanzas': 2
},
async function (done, _converse) {
async function (_converse) {
await _converse.api.user.login('romeo@montague.lit/orchard', 'secret');
const sent_stanzas = _converse.connection.sent_stanzas;
@ -173,7 +172,6 @@ describe("XEP-0198 Stream Management", function () {
// Check that the roster gets fetched
await mock.waitForRoster(_converse, 'current', 1);
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,
'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";
sessionStorage.setItem(
@ -266,6 +264,5 @@ describe("XEP-0198 Stream Management", function () {
await u.waitUntil(() => muc.messages.length);
expect(muc.messages.at(0).get('message')).toBe('First message')
delete _converse.no_connection_on_bind;
done();
}));
});

View File

@ -5,7 +5,7 @@ const u = converse.env.utils;
describe("The XMPPStatus model", function () {
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;
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('priority').length).toBe(1);
expect(stanza.querySelector('priority').textContent).toBe('0');
done();
}));
});

View File

@ -1,9 +1,19 @@
import URI from 'urijs';
import dayjs from 'dayjs';
import log from '@converse/headless/log';
import sizzle from 'sizzle';
import { Strophe } from 'strophe.js/src/strophe';
import { _converse, api } from '@converse/headless/core';
import { decodeHTMLEntities } from '@converse/headless/shared/utils';
import { rejectMessage } from '@converse/headless/shared/actions';
import {
isAudioDomainAllowed,
isAudioURL,
isImageDomainAllowed,
isImageURL,
isVideoDomainAllowed,
isVideoURL
} from '@converse/headless/utils/url.js';
const { NS } = Strophe;
@ -48,13 +58,29 @@ export function getStanzaIDs (stanza, original_stanza) {
}
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 attrs = { 'is_encrypted': !!encrypted };
if (!eme_tag) {
attrs.is_encrypted = !!encrypted;
}
if (!encrypted || api.settings.get('clear_cache_on_logout')) {
return attrs;
}
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 key = device_id && sizzle(`key[rid="${device_id}"]`, encrypted).pop();
@ -150,6 +176,34 @@ export function getOpenGraphMetadata (stanza) {
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) {
const spoiler = sizzle(`spoiler[xmlns="${Strophe.NS.SPOILER}"]`, stanza).pop();
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) {
const name = store || getDefaultStore();
const s = _converse.storage[name];
if (typeof s === 'undefined') {
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) {
const store = type || getDefaultStore();
model.browserStorage = _converse.createStore(id, store);
if (store === 'persistent' && api.settings.get('persistent_store') === 'IndexedDB') {
if (storeUsesIndexedDB(store)) {
const flush = () => model.browserStorage.flush();
window.addEventListener(_converse.unloadevent, flush);
model.on('destroy', () => window.removeEventListener(_converse.unloadevent, flush));

View File

@ -6,7 +6,7 @@ describe("Converse", 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 connection = _converse.connection;
_converse.api.settings.set('bosh_service_url', undefined);
@ -17,7 +17,6 @@ describe("Converse", function() {
_converse.api.settings.set('bosh_service_url', url);
_converse.connection = connection;
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 () {
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();
let sent_stanza;
@ -47,14 +46,13 @@ describe("Converse", function() {
_converse.onUserActivity();
expect(_converse.sendCSI).toHaveBeenCalledWith('active');
expect(Strophe.serialize(sent_stanza)).toBe('<active xmlns="urn:xmpp:csi:0"/>');
done();
}));
});
describe("Automatic status change", function () {
it("happens when the client is idle for long enough",
mock.initConverse(['initialized'], {}, async (done, _converse) => {
mock.initConverse(['initialized'], {}, async (_converse) => {
let i = 0;
// Usually initialized by registerIntervalHandler
_converse.idle_seconds = 0;
@ -120,7 +118,6 @@ describe("Converse", function() {
_converse.onUserActivity();
expect(await _converse.api.user.status.get()).toBe('dnd');
expect(_converse.auto_changed_status).toBe(false);
done();
}));
});
@ -129,15 +126,14 @@ describe("Converse", function() {
describe("The \"status\" API", function () {
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');
expect(await _converse.api.user.status.get()).toBe('online');
_converse.xmppstatus.set('status', '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');
expect(await _converse.xmppstatus.get('status')).toBe('away');
await _converse.api.user.status.set('dnd');
@ -149,39 +145,35 @@ describe("Converse", function() {
const promise = _converse.api.user.status.set('invalid')
promise.catch(e => {
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");
expect(_converse.xmppstatus.get('status')).toBe('away');
expect(_converse.xmppstatus.get('status_message')).toBe("I'm in a meeting");
done();
}));
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);
expect(await _converse.api.user.status.message.get()).toBe(undefined);
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");
done();
}));
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);
await _converse.api.user.status.message.set("I'm in a meeting");
expect(_converse.xmppstatus.get('status_message')).toBe("I'm in a meeting");
done();
}));
});
});
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');
const old_connection = _converse.connection;
_converse.connection._proto.rid = '1234';
@ -190,10 +182,9 @@ describe("Converse", function() {
expect(_converse.api.tokens.get('rid')).toBe(null);
// Restore the 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');
const old_connection = _converse.connection;
_converse.connection._proto.sid = '1234';
@ -202,14 +193,13 @@ describe("Converse", function() {
expect(_converse.api.tokens.get('sid')).toBe(null);
// Restore the connection
_converse.connection = old_connection;
done();
}));
});
describe("The \"contacts\" API", function () {
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');
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
list = await _converse.api.contacts.get();
expect(list.length).toBe(mock.cur_names.length);
done();
}));
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);
try {
@ -251,13 +240,12 @@ describe("Converse", function() {
spyOn(_converse.roster, 'addAndSubscribe');
await _converse.api.contacts.add("newcontact@example.org");
expect(_converse.roster.addAndSubscribe).toHaveBeenCalled();
done();
}));
});
describe("The \"settings\" API", function() {
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(_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");
_converse.api.settings.set("non_existing", true);
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",
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:');
@ -283,7 +270,6 @@ describe("Converse", function() {
expect(_converse.api.settings.get('emoji_categories')?.travel).toBe(':rocket:');
expect(_converse.api.settings.get('emoji_categories')?.food).toBe(undefined);
done();
}));
it("only overrides the passed in properties",
@ -292,7 +278,7 @@ describe("Converse", function() {
'root': document.createElement('div').attachShadow({ 'mode': 'open' }),
'emoji_categories': { 'travel': ':rocket:' },
},
(done, _converse) => {
(_converse) => {
expect(_converse.api.settings.get('emoji_categories')?.travel).toBe(':rocket:');
// 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').food).toBe(undefined);
done();
}
)
);
@ -311,7 +296,7 @@ describe("Converse", 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"]);
// Cheating a little bit. We clear the plugins to test more easily.
const _old_plugins = _converse.pluggable.plugins;
@ -321,17 +306,15 @@ describe("Converse", function() {
converse.plugins.add('plugin2', {});
expect(Object.keys(_converse.pluggable.plugins)).toEqual(['plugin1', 'plugin2']);
_converse.pluggable.plugins = _old_plugins;
done();
}));
describe("The \"plugins.add\" method", function() {
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', {});
const error = new TypeError('Error: plugin with name "myplugin" has already been registered!');
expect(() => converse.plugins.add('myplugin', {})).toThrow(error);
done();
}));
});
});

View File

@ -2,7 +2,7 @@
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 () {};
spyOn(this, 'callback');
_converse.on('connected', this.callback);
@ -12,10 +12,9 @@ describe("The _converse Event Emitter", function() {
expect(this.callback.calls.count(), 2);
_converse.api.trigger('connected');
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 () {};
spyOn(this, 'callback');
_converse.once('connected', this.callback);
@ -25,10 +24,9 @@ describe("The _converse Event Emitter", function() {
expect(this.callback.calls.count(), 1);
_converse.api.trigger('connected');
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.anotherCallback = function () {};
this.neverCalled = function () {};
@ -56,6 +54,5 @@ describe("The _converse Event Emitter", function() {
expect(this.callback.calls.count(), 1);
expect(this.anotherCallback.calls.count(), 3);
expect(this.neverCalled).not.toHaveBeenCalled();
done();
}));
});

View File

@ -3,10 +3,9 @@
describe("The persistent store", function() {
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().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;
};
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) {
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"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-03-17 12:35+0100\n"
"PO-Revision-Date: 2021-01-20 09:32+0000\n"
"Last-Translator: Juanro49 <juanrobertogarciasanchez@gmail.com>\n"
"PO-Revision-Date: 2021-05-23 22:31+0000\n"
"Last-Translator: juliojulian <majuga@publicar.uy>\n"
"Language-Team: Spanish <https://hosted.weblate.org/projects/conversejs/"
"translations/es/>\n"
"Language: es\n"
@ -16,7 +16,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\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"
"lang: 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:58125
#, fuzzy
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:58129

View File

@ -7,8 +7,8 @@ msgstr ""
"Project-Id-Version: Converse.js 0.4\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-03-17 12:35+0100\n"
"PO-Revision-Date: 2021-01-26 23:32+0000\n"
"Last-Translator: Vincent Finance <linuxmario@linuxmario.net>\n"
"PO-Revision-Date: 2021-05-20 18:33+0000\n"
"Last-Translator: lilim <lionel@les-miquelots.net>\n"
"Language-Team: French <https://hosted.weblate.org/projects/conversejs/"
"translations/fr/>\n"
"Language: fr\n"
@ -16,7 +16,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\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"
"lang: fr\n"
"Language-Code: fr\n"
@ -762,7 +762,6 @@ msgstr ""
#: dist/converse-no-dependencies.js:76031
#: dist/converse-no-dependencies.js:76122
#, fuzzy
msgid ""
"Be aware that other XMPP/Jabber clients (and servers) may not yet support "
"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:76129
#: dist/converse-no-dependencies.js:76166
#, fuzzy
msgid "Are you sure you want to retract this 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."
#: dist/converse-no-dependencies.js:76190
#, fuzzy
msgid "You are about to retract this message."
msgstr "Vous êtes sur le point de retirer ce message."
#: dist/converse-no-dependencies.js:76190
#, fuzzy
msgid ""
"You may optionally include a message, explaining the reason for the "
"retraction."
@ -811,7 +807,6 @@ msgstr ""
"du retrait."
#: dist/converse-no-dependencies.js:76197
#, fuzzy
msgid "Message Retraction"
msgstr "Versions du message"
@ -820,9 +815,8 @@ msgid "Optional reason"
msgstr "Raison facultative"
#: dist/converse-no-dependencies.js:76208
#, fuzzy
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
msgid "Cancel Editing"
@ -838,19 +832,19 @@ msgstr "Retirer"
#: dist/converse-no-dependencies.js:76316
msgid "Show URL previews"
msgstr ""
msgstr "Montrer les prévisualisations d'URL"
#: dist/converse-no-dependencies.js:76316
msgid "Hide URL previews"
msgstr ""
msgstr "Cacher les prévisualisations d'URL"
#: dist/converse-no-dependencies.js:76318
msgid "Show URL preview"
msgstr ""
msgstr "Montrer la prévisualisation de l'URL"
#: dist/converse-no-dependencies.js:76318
msgid "Hide URL preview"
msgstr ""
msgstr "Cacher la prévisualisation de l'URL"
#. harmony default export
#: dist/converse-no-dependencies.js:76429
@ -1248,7 +1242,7 @@ msgstr ""
#: dist/converse-no-dependencies.js:94078
msgid "Loading configuration form"
msgstr ""
msgstr "Chargement du formulaire de configuration"
#: dist/converse-no-dependencies.js:94373
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"
#: dist/converse-no-dependencies.js:96345
#, fuzzy
msgid ""
"The conversation has moved to a new address. Click the link below to enter."
msgstr ""
@ -1518,9 +1511,9 @@ msgid "This groupchat no longer exists"
msgstr "Ce salon nexiste plus"
#: dist/converse-no-dependencies.js:96355
#, fuzzy, javascript-format
#, javascript-format
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
#, javascript-format

View File

@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: Converse.js 4.0.4\n"
"Report-Msgid-Bugs-To: \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"
"Language-Team: Galician <https://hosted.weblate.org/projects/conversejs/"
"translations/gl/>\n"
@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\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
#: dist/converse-no-dependencies.js:42989
@ -778,7 +778,7 @@ msgstr "Razón (optativo)"
#: dist/converse-no-dependencies.js:76184
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
msgid "Cancel Editing"
@ -1461,7 +1461,7 @@ msgstr "Esta conversa en grupo xa non existe"
#: dist/converse-no-dependencies.js:96194
#, javascript-format
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
#, javascript-format

View File

@ -9,8 +9,8 @@ msgstr ""
"Project-Id-Version: Converse.js 0.9.6\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-03-17 12:35+0100\n"
"PO-Revision-Date: 2020-11-18 18:41+0100\n"
"Last-Translator: Michal Biesiada <blade-14@o2.pl>\n"
"PO-Revision-Date: 2021-05-20 18:33+0000\n"
"Last-Translator: G <emgrzegorz@interia.pl>\n"
"Language-Team: Polish <https://hosted.weblate.org/projects/conversejs/"
"translations/pl/>\n"
"Language: pl\n"
@ -19,7 +19,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
"|| n%100>=20) ? 1 : 2;\n"
"X-Generator: Weblate 4.4-dev\n"
"X-Generator: Weblate 4.7-dev\n"
#. Strophe
#: 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."
#: dist/converse-no-dependencies.js:43246
#, fuzzy
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
#, javascript-format
@ -69,26 +68,22 @@ msgstr "%1$s odszedł od klawiatury"
#: dist/converse-no-dependencies.js:51675
#: dist/converse-no-dependencies.js:58125
#, fuzzy
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:58129
#, fuzzy
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
#, fuzzy
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:58138
#, fuzzy
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
#, javascript-format
@ -122,9 +117,8 @@ msgstr ""
"serwer, który wynosi %2$s."
#: dist/converse-no-dependencies.js:53208
#, fuzzy
msgid "Undecryptable OMEMO message"
msgstr "Nieszyfrowalny komunikat OMEMO"
msgstr "Nierozszyfrowalna wiadomość OMEMO"
#: dist/converse-no-dependencies.js:53272
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"
#: dist/converse-no-dependencies.js:59253
#, fuzzy
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:94373
#, fuzzy
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:59301
@ -340,19 +332,19 @@ msgid "%1$s have stopped typing"
msgstr "%1$s przestał pisać"
#: dist/converse-no-dependencies.js:60543
#, fuzzy, javascript-format
#, javascript-format
msgid "%1$s have gone away"
msgstr "%1$s odszedł od klawiatury"
msgstr "%1$s oddalił się"
#: dist/converse-no-dependencies.js:60545
#, fuzzy, javascript-format
#, javascript-format
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
#, fuzzy, javascript-format
#, javascript-format
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
#, fuzzy, javascript-format
@ -375,9 +367,9 @@ msgid "%1$s have been muted"
msgstr "%1$s został wyciszony"
#: dist/converse-no-dependencies.js:60859
#, fuzzy, javascript-format
#, javascript-format
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
#, javascript-format
@ -390,9 +382,9 @@ msgid "%1$s's nickname has changed"
msgstr "%1$s zmienił pseudonim"
#: dist/converse-no-dependencies.js:60863
#, fuzzy, javascript-format
#, javascript-format
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
#, javascript-format
@ -539,10 +531,8 @@ msgid "You have been banned from this groupchat"
msgstr "Zostałeś zablokowany na tym czacie grupowym"
#: dist/converse-no-dependencies.js:62490
#, fuzzy
msgid "You have exited this groupchat due to a technical problem"
msgstr ""
"Zostałeś usunięty z tego czatu grupowego z powodu zmiany przynależności"
msgstr "Wyszedłeś z tego czatu grupowego z powodu problemu technicznego"
#: dist/converse-no-dependencies.js:62491
msgid "You have been kicked from this groupchat"
@ -616,9 +606,8 @@ msgstr ""
"aby poprosić o nie ponownie."
#: dist/converse-no-dependencies.js:65180
#, fuzzy
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
#, fuzzy
@ -742,9 +731,9 @@ msgid "Download audio file \"%1$s\""
msgstr "Pobierz plik audio \"%1$s\""
#: dist/converse-no-dependencies.js:74156
#, fuzzy, javascript-format
#, javascript-format
msgid "Download image file \"%1$s\""
msgstr "Pobierz zdjęcie \"%1$s\""
msgstr "Pobierz obraz pliku \"%1$s\""
#: dist/converse-no-dependencies.js:74164
#, javascript-format
@ -771,7 +760,6 @@ msgstr ""
#: dist/converse-no-dependencies.js:76032
#: dist/converse-no-dependencies.js:76129
#: dist/converse-no-dependencies.js:76166
#, fuzzy
msgid "Are you sure you want to retract this message?"
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."
#: dist/converse-no-dependencies.js:76190
#, fuzzy
msgid "You are about to retract this message."
msgstr "Masz zamiar wycofać tę wiadomość."
#: dist/converse-no-dependencies.js:76190
#, fuzzy
msgid ""
"You may optionally include a message, explaining the reason for the "
"retraction."
msgstr "Opcjonalnie możesz dołączyć wiadomość wyjaśniającą powód zwinięcia."
#: dist/converse-no-dependencies.js:76197
#, fuzzy
msgid "Message Retraction"
msgstr "Wersja wiadomości"
msgstr "Wycofanie wiadomości"
#: dist/converse-no-dependencies.js:76197
msgid "Optional reason"
msgstr "Opcjonalny powód"
#: dist/converse-no-dependencies.js:76208
#, fuzzy
msgid "Sorry, you're not allowed to retract this message"
msgstr "Przepraszam, nie wolno ci wycofać tej wiadomości."
@ -824,32 +808,32 @@ msgstr "Zmiana ustawień"
#: dist/converse-no-dependencies.js:76270
msgid "Edit"
msgstr ""
msgstr "Edycja"
#: dist/converse-no-dependencies.js:76299
msgid "Retract"
msgstr ""
msgstr "Wycofaj"
#: dist/converse-no-dependencies.js:76316
msgid "Show URL previews"
msgstr ""
msgstr "Pokaż podgląd URL"
#: dist/converse-no-dependencies.js:76316
msgid "Hide URL previews"
msgstr ""
msgstr "Ukryj podgląd URL"
#: dist/converse-no-dependencies.js:76318
msgid "Show URL preview"
msgstr ""
msgstr "Pokaż podgląd URL"
#: dist/converse-no-dependencies.js:76318
msgid "Hide URL preview"
msgstr ""
msgstr "Ukryj podgląd URL"
#. harmony default export
#: dist/converse-no-dependencies.js:76429
msgid "Image: "
msgstr ""
msgstr "Obraz: "
#. harmony default export
#: dist/converse-no-dependencies.js:77991
@ -869,9 +853,8 @@ msgid "OMEMO Fingerprints"
msgstr "Odciski palców OMEMO"
#: dist/converse-no-dependencies.js:78232
#, fuzzy
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
msgid "Remove as contact"
@ -942,7 +925,7 @@ msgstr "Jesteś pewien, że chcesz usunąć ten kontakt?"
#: dist/converse-no-dependencies.js:79208
msgid "Retry"
msgstr ""
msgstr "Ponów"
#: dist/converse-no-dependencies.js:79215
msgid "Uploading file:"
@ -963,9 +946,8 @@ msgid "Show more"
msgstr "Pokaż więcej"
#: dist/converse-no-dependencies.js:79359
#, fuzzy
msgid "Show less"
msgstr "Pokaż użytkowników"
msgstr "Pokaż mniej"
#: dist/converse-no-dependencies.js:80828
msgid "Search results"
@ -992,14 +974,12 @@ msgid "Message characters remaining"
msgstr "Pozostałe znaki wiadomości"
#: dist/converse-no-dependencies.js:82222
#, fuzzy
msgid "Hide participants"
msgstr "Uczestnicy"
msgstr "Ukryj uczestników"
#: dist/converse-no-dependencies.js:82224
#, fuzzy
msgid "Show participants"
msgstr "Uczestnicy"
msgstr "Pokaż uczestników"
#: dist/converse-no-dependencies.js:82247
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:97658
msgid "Details"
msgstr ""
msgstr "Detale"
#: dist/converse-no-dependencies.js:82963
#, fuzzy
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
#, fuzzy
msgid "Close and end this conversation"
msgstr "Zakończ szyfrowaną rozmowę"
msgstr "Zamknij i zakoń tę rozmowę"
#: dist/converse-no-dependencies.js:83478
msgid "Hidden message"
@ -1051,6 +1029,8 @@ msgstr "Masz nieprzeczytane wiadomości"
#: dist/converse-no-dependencies.js:83639
msgid "Sorry, the connection has been lost, and your message could not be sent"
msgstr ""
"Przepraszam, ale połączenie zostało utracone i twoja wiadomość nie mogła "
"zostać wysłana"
#. eslint-disable-line class-methods-use-this
#: dist/converse-no-dependencies.js:84684
@ -1091,9 +1071,8 @@ msgid "This is a trusted device"
msgstr "To jest zaufane urządzenie"
#: dist/converse-no-dependencies.js:87541
#, fuzzy
msgid "Password"
msgstr "Hasło:"
msgstr "Hasło"
#: dist/converse-no-dependencies.js:87547
msgid "Create an account"
@ -1139,18 +1118,16 @@ msgid "Toggle chat"
msgstr "Przełącz czat"
#: dist/converse-no-dependencies.js:89998
#, fuzzy
msgid "Close these announcements"
msgstr "Ogłoszenia"
msgstr "Zamknij ogłoszenia"
#: dist/converse-no-dependencies.js:90595
msgid "Announcements"
msgstr "Ogłoszenia"
#: dist/converse-no-dependencies.js:90599
#, fuzzy
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
#: 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:91071
#, fuzzy
msgid "Minimize"
msgstr "Zminimalizowane"
msgstr "Zminimalizuj"
#: dist/converse-no-dependencies.js:91054
#, fuzzy
msgid "Minimize this chat"
msgstr "Zminimalizuj to okno czatu"
msgstr "Zminimalizuj ten czat"
#: dist/converse-no-dependencies.js:91072
#, fuzzy
msgid "Minimize this groupchat"
msgstr "Zminimalizuj to okno czatu"
msgstr "Zminimalizuj ten czat grupowy"
#. harmony default export
#: dist/converse-no-dependencies.js:91421
@ -1181,17 +1155,17 @@ msgstr "Zminimalizowane"
#. harmony default export
#: dist/converse-no-dependencies.js:92962
msgid "Hide"
msgstr ""
msgstr "Ukryj"
#: dist/converse-no-dependencies.js:92964
msgid "Execute"
msgstr ""
msgstr "Uruchom"
#: dist/converse-no-dependencies.js:93293
msgid ""
"Couldn't find a participant with that nickname. They might have left the "
"groupchat."
msgstr ""
msgstr "Nie znaleziono uczestnika o tym nicku. Być moze opuścij czat grupowy."
#. e.g. Your nickname is "coolguy69"
#: dist/converse-no-dependencies.js:93422
@ -1206,7 +1180,7 @@ msgstr "Błąd: nieprawidłowa liczba argumentów"
#. harmony default export
#: dist/converse-no-dependencies.js:93540
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
msgid ""
@ -1215,18 +1189,16 @@ msgid ""
msgstr ""
#: dist/converse-no-dependencies.js:93544
#, fuzzy
msgid "Commands found"
msgstr "Pokoje na %1$s"
msgstr "Znaleziono polecenia"
#: dist/converse-no-dependencies.js:93546
msgid "List available commands"
msgstr ""
msgstr "Lista dostępnych poleceń"
#: dist/converse-no-dependencies.js:93550
#, fuzzy
msgid "No commands found"
msgstr "Pokoje na %1$s"
msgstr "Nie znaleziono polecenia"
#: dist/converse-no-dependencies.js:93838
#, fuzzy
@ -1235,20 +1207,19 @@ msgstr "Wystąpił błąd w czasie próby usunięcia urządzenia."
#: dist/converse-no-dependencies.js:93849
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
#, fuzzy
msgid ""
"Sorry, an error occurred while trying to execute the command. See the "
"developer console for details"
msgstr ""
"Ups, podczas wykonywania polecenia wystąpił błąd. Szczegółowe informacje "
"można znaleźć w konsoli deweloperskiej przeglądarki."
"Przepraszam, podczas wykonywania polecenia wystąpił błąd. Szczegółowe "
"informacje można znaleźć na konsoli dewelopera."
#: dist/converse-no-dependencies.js:94078
msgid "Loading configuration form"
msgstr ""
msgstr "Wczytywanie formularza konfiguracyjnego"
#: dist/converse-no-dependencies.js:94373
#, fuzzy
@ -1349,7 +1320,7 @@ msgstr "Nie znaleziono użytkowników z tą rolą."
#: dist/converse-no-dependencies.js:94965
msgid "Type here to filter the search results"
msgstr ""
msgstr "Wpisz tutaj aby odfiltrować rezultaty przeszukiwań"
#: dist/converse-no-dependencies.js:94969
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"
#: dist/converse-no-dependencies.js:95315
#, fuzzy
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
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ę"
#: dist/converse-no-dependencies.js:96345
#, fuzzy
msgid ""
"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
#: dist/converse-no-dependencies.js:96353
@ -1517,7 +1486,7 @@ msgid "This groupchat no longer exists"
msgstr "Ten czat grupowy już nie istnieje"
#: dist/converse-no-dependencies.js:96355
#, fuzzy, javascript-format
#, javascript-format
msgid "The following reason was given: \"%1$s\""
msgstr "Podany powód to: \"%1$s\"."
@ -1538,19 +1507,16 @@ msgid "Invite"
msgstr "Zaproś"
#: dist/converse-no-dependencies.js:96916
#, fuzzy
msgid "Invite someone to this groupchat"
msgstr "Usuń ten czat grupowy"
msgstr "Zaproś kogoś na ten czat grupowy"
#: dist/converse-no-dependencies.js:96918
#, fuzzy
msgid "user@example.org"
msgstr "np. użytkownik@przykładowa-domena.pl"
msgstr "użytkownik@example.org"
#: dist/converse-no-dependencies.js:96924
#, fuzzy
msgid "Optional reason for the invitation"
msgstr "Opcjonalny powód"
msgstr "Opcjonalny powód zaproszenia"
#: dist/converse-no-dependencies.js:97156
msgid "Topic"
@ -1701,42 +1667,36 @@ msgstr "Informacje o czacie grupowym dla %1$s"
#. harmony default export
#: dist/converse-no-dependencies.js:97327
#, fuzzy
msgid "Hide the groupchat topic"
msgstr "Wejdź do pokoju"
msgstr "Ukryj temat czatu grupowego"
#: dist/converse-no-dependencies.js:97329
#, fuzzy
msgid "This groupchat is bookmarked"
msgstr "Ten człowiek jest moderatorem"
msgstr "Ten czat grupowy został zapamiętany"
#: dist/converse-no-dependencies.js:97659
#, fuzzy
msgid "Show more information about this groupchat"
msgstr "Pokaż więcej informacji na temat tego czatu grupowego"
#: dist/converse-no-dependencies.js:97670
#, fuzzy
msgid "Configure"
msgstr "Potwierdź"
msgstr "Konfiguracja"
#: dist/converse-no-dependencies.js:97671
msgid "Configure this groupchat"
msgstr "Skonfiguruj ten pokój"
#: dist/converse-no-dependencies.js:97684
#, fuzzy
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
#, fuzzy
msgid "Show topic"
msgstr "Pokaż pokoje"
msgstr "Pokaż temat"
#: dist/converse-no-dependencies.js:97698
msgid "Hide topic"
msgstr ""
msgstr "Ukryj temat"
#: dist/converse-no-dependencies.js:97699
msgid "Show the topic message in the heading"
@ -1757,23 +1717,20 @@ msgid "Moderate this groupchat"
msgstr "Opuść ten pokój"
#: dist/converse-no-dependencies.js:97729
#, fuzzy
msgid "Destroy"
msgstr "Zniszcz pokój"
msgstr "Zniszcz"
#: dist/converse-no-dependencies.js:97743
msgid "Leave"
msgstr ""
msgstr "Opuść"
#: dist/converse-no-dependencies.js:97744
#, fuzzy
msgid "Leave and close this groupchat"
msgstr "Opuść ten pokój"
msgstr "Opuść i zamknij ten czat grupowy"
#: dist/converse-no-dependencies.js:97753
#, fuzzy
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
msgid "This user is a moderator."
@ -1818,14 +1775,12 @@ msgid "Participants"
msgstr "Uczestnicy"
#: dist/converse-no-dependencies.js:99003
#, fuzzy
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
#, fuzzy
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
#: dist/converse-no-dependencies.js:99008
@ -1834,9 +1789,8 @@ msgid "name@example.org"
msgstr "np. użytkownik@przykładowa-domena.pl"
#: dist/converse-no-dependencies.js:99012
#, fuzzy
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
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"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-03-17 12:35+0100\n"
"PO-Revision-Date: 2020-12-19 10:29+0000\n"
"Last-Translator: LL Magical <lolayami2004@gmail.com>\n"
"PO-Revision-Date: 2021-05-04 18:32+0000\n"
"Last-Translator: bashl <esqueleto777@disroot.org>\n"
"Language-Team: Portuguese (Brazil) <https://hosted.weblate.org/projects/"
"conversejs/translations/pt_BR/>\n"
"Language: pt_BR\n"
@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\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"
"lang: pt_BR\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."
#: dist/converse-no-dependencies.js:76190
#, fuzzy
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
#, fuzzy
@ -811,19 +810,19 @@ msgstr "Retirar"
#: dist/converse-no-dependencies.js:76316
msgid "Show URL previews"
msgstr ""
msgstr "Mostrar pré-visualizações da URL"
#: dist/converse-no-dependencies.js:76316
msgid "Hide URL previews"
msgstr ""
msgstr "Ocultar pré-visualizações da URL"
#: dist/converse-no-dependencies.js:76318
msgid "Show URL preview"
msgstr ""
msgstr "Mostrar pré-visualização da URL"
#: dist/converse-no-dependencies.js:76318
msgid "Hide URL preview"
msgstr ""
msgstr "Ocultar pré-visualização da URL"
#. harmony default export
#: dist/converse-no-dependencies.js:76429
@ -1215,7 +1214,7 @@ msgstr ""
#: dist/converse-no-dependencies.js:94078
msgid "Loading configuration form"
msgstr ""
msgstr "Carregando configuração de formulário"
#: dist/converse-no-dependencies.js:94373
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"
#: dist/converse-no-dependencies.js:96345
#, fuzzy
msgid ""
"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
#: 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:97683
msgid "Invite"
msgstr "convite"
msgstr "Convite"
#: dist/converse-no-dependencies.js:96916
msgid "Invite someone to this groupchat"

View File

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

View File

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

View File

@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: Converse.js 6.0.0\n"
"Report-Msgid-Bugs-To: \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"
"Language-Team: Swedish <https://hosted.weblate.org/projects/conversejs/"
"translations/sv/>\n"
@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\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
#: 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."
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:52863
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
#, javascript-format
msgid "%1$s has gone offline"
msgstr "%1$s kopplade ifrån"
msgstr "%1$s har kopplat ner"
#: dist/converse-no-dependencies.js:84726
#, javascript-format
@ -1055,7 +1040,7 @@ msgstr "%1$s är upptagen"
#: dist/converse-no-dependencies.js:84728
#, javascript-format
msgid "%1$s is online"
msgstr "%1$s är uppkopplad"
msgstr "%1$s är online"
#: dist/converse-no-dependencies.js:87546
msgid ""

View File

@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: Converse.js 3.3.2\n"
"Report-Msgid-Bugs-To: \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"
"Language-Team: Turkish <https://hosted.weblate.org/projects/conversejs/"
"translations/tr/>\n"
@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.4-dev\n"
"X-Generator: Weblate 4.6-dev\n"
#. Strophe
#: dist/converse-no-dependencies.js:42989
@ -727,7 +727,6 @@ msgstr ""
#: dist/converse-no-dependencies.js:76031
#: dist/converse-no-dependencies.js:76122
#, fuzzy
msgid ""
"Be aware that other XMPP/Jabber clients (and servers) may not yet support "
"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:76129
#: dist/converse-no-dependencies.js:76166
#, fuzzy
msgid "Are you sure you want to retract this message?"
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."
#: dist/converse-no-dependencies.js:76190
#, fuzzy
msgid "You are about to retract this message."
msgstr "Bu mesajı geri çekmek üzeresiniz."
#: dist/converse-no-dependencies.js:76190
#, fuzzy
msgid ""
"You may optionally include a message, explaining the reason for the "
"retraction."
@ -774,18 +770,16 @@ msgstr ""
"ekleyebilirsiniz."
#: dist/converse-no-dependencies.js:76197
#, fuzzy
msgid "Message Retraction"
msgstr "Mesaj versiyonları"
msgstr "Mesaj Geri Çekme"
#: dist/converse-no-dependencies.js:76197
msgid "Optional reason"
msgstr "İsteğe bağlı neden"
#: dist/converse-no-dependencies.js:76208
#, fuzzy
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
msgid "Cancel Editing"
@ -801,19 +795,19 @@ msgstr "Geri çek"
#: dist/converse-no-dependencies.js:76316
msgid "Show URL previews"
msgstr ""
msgstr "URL ön izlemelerini göster"
#: dist/converse-no-dependencies.js:76316
msgid "Hide URL previews"
msgstr ""
msgstr "URL ön izlemelerini gizle"
#: dist/converse-no-dependencies.js:76318
msgid "Show URL preview"
msgstr ""
msgstr "URL ön izlemesini göster"
#: dist/converse-no-dependencies.js:76318
msgid "Hide URL preview"
msgstr ""
msgstr "URL ön izlemesini gizle"
#. harmony default export
#: 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
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
msgid "Hidden message"
@ -1204,7 +1198,7 @@ msgstr ""
#: dist/converse-no-dependencies.js:94078
msgid "Loading configuration form"
msgstr ""
msgstr "Yapılandırma formu yükleniyor"
#: dist/converse-no-dependencies.js:94373
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"
#: dist/converse-no-dependencies.js:96345
#, fuzzy
msgid ""
"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
#: 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"
#: dist/converse-no-dependencies.js:96355
#, fuzzy, javascript-format
#, javascript-format
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
#, javascript-format

View File

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

View File

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

View File

@ -6,7 +6,7 @@ const { Strophe, u, sizzle, $iq } = converse.env;
describe("A chat room", function () {
it("can be bookmarked", mock.initConverse(
['chatBoxesFetched'], {}, async function (done, _converse) {
['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current', 0);
await mock.waitUntilDiscoConfirmed(
@ -14,9 +14,8 @@ describe("A chat room", function () {
[{'category': 'pubsub', 'type': 'pep'}],
['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 muc_jid = 'theplay@conference.shakespeare.lit';
await mock.openChatRoom(_converse, 'theplay', 'conference.shakespeare.lit', 'JC');
@ -37,6 +36,7 @@ describe("A chat room", function () {
cancel_button.click();
await u.waitUntil(() => view.model.session.get('view') === null);
expect(u.hasClass('on-button', toggle), false);
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);
// We ignore this IQ stanza... (unless it's an error stanza), so
// nothing to test for here.
done();
}));
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;
await mock.waitForRoster(_converse, 'current', 0);
@ -172,13 +171,12 @@ describe("A chat room", function () {
'nick': ' Othello'
});
expect(_converse.chatboxviews.get(jid) === undefined).toBe(true);
done();
}));
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;
await mock.waitForRoster(_converse, 'current', 0);
await mock.waitUntilBookmarksReturned(_converse);
@ -195,10 +193,9 @@ describe("A chat room", function () {
const room = await room_creation_promise;
await u.waitUntil(() => room.getAndPersistNickname.calls.count());
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;
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);
view.model.set('bookmarked', false);
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;
await mock.waitForRoster(_converse, 'current', 0);
@ -282,14 +278,13 @@ describe("A chat room", function () {
`</pubsub>`+
`</iq>`
);
done();
}));
});
describe("and when autojoin is set", function () {
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.waitUntilBookmarksReturned(_converse);
@ -310,7 +305,6 @@ describe("A chat room", function () {
'nick': ''
});
expect(_converse.api.rooms.create).toHaveBeenCalled();
done();
}));
});
});
@ -318,7 +312,7 @@ describe("A chat room", function () {
describe("Bookmarks", function () {
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;
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.chatboxviews.get('theplay@conference.shakespeare.lit')).not.toBeUndefined();
expect(Object.keys(_converse.chatboxviews.getAll()).length).toBe(2);
done();
}));
it("can be retrieved from the XMPP server", mock.initConverse(
['chatBoxesFetched'], {},
async function (done, _converse) {
async function (_converse) {
const { Strophe, sizzle, u, $iq } = converse.env;
await mock.waitForRoster(_converse, 'current', 0);
@ -483,13 +476,12 @@ describe("Bookmarks", function () {
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': 'another@conference.shakespeare.lit'}).get('autojoin')).toBe(false);
done();
}));
describe("The bookmarks list", function () {
it("shows a list of bookmarks", mock.initConverse(
[], {}, async function (done, _converse) {
[], {}, async function (_converse) {
await mock.waitUntilDiscoConfirmed(
_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[2].textContent).toBe("noname@conference.shakespeare.lit");
expect(els[3].textContent).toBe("The Play's the Thing");
done();
}));
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;
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');
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);
done();
}));
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.openControlBox(_converse);
@ -665,7 +655,6 @@ describe("Bookmarks", function () {
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(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 () {
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.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);
view.close();
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'
};
constructor () {
super();
this.debouncedRender = debounce(this.render, 100);
}
async connectedCallback () {
super.connectedCallback();
this.debouncedRender = debounce(this.render, 100);
this.model = _converse.chatboxes.get(this.getAttribute('jid'));
await this.initialize();
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;
this.listenTo(this.model, 'change:num_unread', this.debouncedRender)
this.listenTo(this.model, 'emoji-picker-autocomplete', this.autocompleteInPicker);
this.addEventListener('focusin', ev => this.emitFocused(ev));
this.addEventListener('focusout', ev => this.emitBlurred(ev));
this.render();
}
render () {
@ -52,10 +61,6 @@ export default class ChatBottomPanel extends ElementView {
_converse.chatboxviews.get(this.getAttribute('jid'))?.emitBlurred(ev);
}
getToolbarOptions () { // eslint-disable-line class-methods-use-this
return {};
}
onDrop (evt) {
if (evt.dataTransfer.files.length == 0) {
// 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 () {
this.model.setChatState(_converse.ACTIVE);
this.model.clearUnreadMsgCounter();
this.maybeFocus();
}
}

View File

@ -195,7 +195,7 @@ export default class MessageForm extends ElementView {
this.querySelector('converse-emoji-dropdown')?.hideMenu();
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) {
hint_el.value = '';
textarea.value = '';

View File

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

View File

@ -35,7 +35,7 @@ export default (o) => {
const display_name = o.model.getDisplayName();
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)
.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 Strophe = converse.env.Strophe;
@ -13,7 +13,7 @@ describe("Chatboxes", 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.openControlBox(_converse);
@ -40,11 +40,10 @@ describe("Chatboxes", function () {
await u.waitUntil(() => view.querySelectorAll('.chat-msg').length);
const msg_txt_sel = 'converse-chat-message:last-child .chat-msg__body';
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.openControlBox(_converse);
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@ -67,12 +66,11 @@ describe("Chatboxes", function () {
});
expect(window.confirm).toHaveBeenCalled();
await u.waitUntil(() => sizzle('converse-chat-message', view).length === 0);
done();
}));
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.openControlBox(_converse);
@ -98,12 +96,11 @@ describe("Chatboxes", function () {
// Check that new chat boxes are created to the left of the
// controlbox (but to the right of all existing chat boxes)
expect(document.querySelectorAll("#conversejs .chatbox").length).toBe(3);
done();
}));
it("opens when a new message is received", mock.initConverse(
[], {'allow_non_roster_messaging': true},
async function (done, _converse) {
async function (_converse) {
await mock.waitForRoster(_converse, 'current', 0);
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@ -120,10 +117,9 @@ describe("Chatboxes", function () {
await u.waitUntil(() => message_promise);
expect(_converse.chatboxviews.keys().length).toBe(2);
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);
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
const stanza = u.toStanza(`
@ -136,11 +132,10 @@ describe("Chatboxes", function () {
_converse.connection._dataRecv(mock.createRequest(stanza));
await u.waitUntil(() => message_promise);
expect(_converse.chatboxviews.keys().length).toBe(1);
done();
}));
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.openControlBox(_converse);
@ -159,11 +154,10 @@ describe("Chatboxes", function () {
await u.waitUntil(() => view.focus.calls.count(), 1000);
expect(view.focus).toHaveBeenCalled();
expect(_converse.chatboxes.length).toEqual(2);
done();
}));
it("can be saved to, and retrieved from, browserStorage",
mock.initConverse([], {}, async function (done, _converse) {
mock.initConverse([], {}, async function (_converse) {
spyOn(_converse.minimize, 'trimChats');
await mock.waitForRoster(_converse, 'current');
@ -188,15 +182,14 @@ describe("Chatboxes", function () {
const attrs = ['id', 'box_id', 'visible'];
let new_attrs, old_attrs;
for (let i=0; i<attrs.length; i++) {
new_attrs = _.map(_.map(newchatboxes.models, 'attributes'), attrs[i]);
old_attrs = _.map(_.map(_converse.chatboxes.models, 'attributes'), attrs[i]);
expect(_.isEqual(new_attrs, old_attrs)).toEqual(true);
new_attrs = newchatboxes.models.map(m => m.attributes[i]);
old_attrs = _converse.chatboxes.models.map(m => m.attributes[i]);
expect(new_attrs).toEqual(old_attrs);
}
done();
}));
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.openControlBox(_converse);
@ -211,11 +204,10 @@ describe("Chatboxes", function () {
expect(chatview.model.close).toHaveBeenCalled();
await new Promise(resolve => _converse.api.listen.once('chatBoxClosed', resolve));
expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxClosed', jasmine.any(Object));
done();
}));
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.openControlBox(_converse);
@ -248,13 +240,12 @@ describe("Chatboxes", function () {
await new Promise(resolve => _converse.api.listen.on('chatBoxesFetched', resolve));
expect(newchatboxes.length).toEqual(1);
expect(newchatboxes.models[0].id).toBe("controlbox");
done();
}));
describe("A chat toolbar", function () {
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.openControlBox(_converse);
@ -288,12 +279,11 @@ describe("Chatboxes", function () {
textarea.value = 'hello world';
message_form.onKeyUp(ev);
await u.waitUntil(() => counter.textContent === '189');
done();
}));
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.openControlBox(_converse);
@ -302,12 +292,11 @@ describe("Chatboxes", function () {
const view = _converse.chatboxviews.get(contact_jid);
const counter = view.querySelector('.chat-toolbar .message-limit');
expect(counter).toBe(null);
done();
}));
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.openControlBox(_converse);
@ -333,13 +322,12 @@ describe("Chatboxes", function () {
call_button = toolbar.querySelector('.toggle-call');
call_button.click();
expect(_converse.api.trigger).toHaveBeenCalledWith('callButtonClicked', jasmine.any(Object));
done();
}));
});
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.openControlBox(_converse);
@ -357,13 +345,12 @@ describe("Chatboxes", function () {
await u.waitUntil(() => _converse.api.trigger.calls.count());
expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object));
expect(_converse.chatboxviews.keys().length).toBe(1);
done();
}));
describe("An active notification", function () {
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');
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[1].tagName).toBe('no-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(
['chatBoxesFetched'], {}, async function (done, _converse) {
['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1);
await mock.openControlBox(_converse);
@ -409,14 +395,13 @@ describe("Chatboxes", function () {
`<no-permanent-store xmlns="urn:xmpp:hints"/>`+
`</message>`
);
done();
}));
});
describe("A composing notification", function () {
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.openControlBox(_converse);
@ -452,12 +437,11 @@ describe("Chatboxes", function () {
});
expect(view.model.get('chat_state')).toBe('composing');
expect(_converse.api.trigger.calls.count(), 1);
done();
}));
it("is NOT sent out if send_chat_state_notifications doesn't allow it",
mock.initConverse(['chatBoxesFetched'], {'send_chat_state_notifications': []},
async function (done, _converse) {
async function (_converse) {
await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse);
@ -477,10 +461,9 @@ describe("Chatboxes", function () {
});
expect(view.model.get('chat_state')).toBe('composing');
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.openControlBox(_converse);
@ -525,11 +508,10 @@ describe("Chatboxes", function () {
const msg_el = await u.waitUntil(() => view.querySelector('.chat-msg'));
await u.waitUntil( () => view.querySelector('.chat-content__notifications').innerText === '');
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",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitUntilDiscoConfirmed(_converse, 'montague.lit', [], ['vcard-temp']);
await u.waitUntil(() => _converse.xmppstatus.vcard.get('fullname'));
@ -560,14 +542,13 @@ describe("Chatboxes", function () {
expect(view.model.messages.length).toEqual(0);
const el = view.querySelector('.chat-content__notifications');
expect(el.textContent).toBe('');
done();
}));
});
describe("A paused notification", function () {
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');
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@ -624,10 +605,9 @@ describe("Chatboxes", function () {
keyCode: 1
});
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.openControlBox(_converse);
const rosterview = document.querySelector('converse-roster');
@ -649,11 +629,10 @@ describe("Chatboxes", function () {
const csn = mock.cur_names[1] + ' has stopped typing';
await u.waitUntil( () => view.querySelector('.chat-content__notifications').innerText === csn);
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",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitUntilDiscoConfirmed(_converse, 'montague.lit', [], ['vcard-temp']);
await u.waitUntil(() => _converse.xmppstatus.vcard.get('fullname'));
@ -681,15 +660,13 @@ describe("Chatboxes", function () {
expect(view.model.messages.length).toEqual(0);
const el = view.querySelector('.chat-content__notifications');
expect(el.textContent).toBe('');
done();
done();
}));
});
describe("An inactive notification", function () {
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;
// Make the timeouts shorter so that we can test
@ -750,11 +727,10 @@ describe("Chatboxes", function () {
`<no-permanent-store xmlns="urn:xmpp:hints"/>`+
`</message>`);
done();
}));
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.openControlBox(_converse);
@ -769,11 +745,10 @@ describe("Chatboxes", function () {
var stanza = _converse.connection.send.calls.argsFor(0)[0];
expect(stanza.getAttribute('to')).toBe(contact_jid);
expect(stanza.childNodes[0].tagName).toBe('inactive');
done();
}));
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');
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[1].tagName).toBe('no-store');
expect(stanza.childNodes[2].tagName).toBe('no-permanent-store');
done();
}));
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.openControlBox(_converse);
@ -829,13 +803,12 @@ describe("Chatboxes", function () {
_converse.connection._dataRecv(mock.createRequest(msg));
await u.waitUntil(() => !view.querySelector('.chat-content__notifications').textContent);
done();
}));
});
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.openControlBox(_converse);
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 csntext = await u.waitUntil(() => view.querySelector('.chat-content__notifications').textContent);
expect(csntext).toEqual(mock.cur_names[1] + ' has gone away');
done();
}));
});
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.openControlBox(_converse);
@ -910,7 +882,6 @@ describe("Chatboxes", function () {
await _converse.handleMessageStanza(edited);
await u.waitUntil(() => !view.querySelector('.chat-content__notifications').textContent);
done();
}));
});
});
@ -919,7 +890,7 @@ describe("Chatboxes", function () {
describe("Special Messages", function () {
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.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?');
await u.waitUntil(() => view.model.messages.length === 0);
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 () {
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);
let msg, indicator_el;
@ -1117,11 +947,10 @@ describe("Chatboxes", function () {
await u.waitUntil(() => chatbox.messages.length > 1);
indicator_el = sizzle(selector, rosterview).pop();
expect(indicator_el.textContent).toBe('2');
done();
}));
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);
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);
indicator_el = sizzle(selector, rosterview).pop();
expect(indicator_el.textContent).toBe('2');
done();
}));
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);
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');
_converse.minimize.maximize(view.model);
u.waitUntil(() => typeof select_msgs_indicator() === 'undefined');
done();
}));
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.waitForRoster(_converse, 'current', 1);
@ -1194,11 +1021,10 @@ describe("Chatboxes", function () {
const chat_new_msgs_indicator = await u.waitUntil(() => view.querySelector('.new-msgs-indicator'));
chat_new_msgs_indicator.click();
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",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1);
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');
await mock.openChatBoxFor(_converse, sender_jid);
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 () {
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.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(1).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",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1);
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?']);
expect(window.confirm.calls.argsFor(1)).toEqual(
['You have an unsent message which will be lost if you continue. Are you sure?']);
done();
}));
describe("when received from someone else", function () {
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.openControlBox(_converse);
@ -346,7 +344,6 @@ describe("A Chat Message", function () {
expect(older_msgs.length).toBe(2);
expect(older_msgs[0].textContent.includes('But soft, what light through yonder airlock breaks?')).toBe(true);
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));
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';
await mock.waitForRoster(_converse, 'current');
@ -25,14 +25,13 @@ describe("Emojis", function () {
item.click()
expect(view.querySelector('textarea.chat-textarea').value).toBe(':smiley: ');
toolbar.querySelector('.toggle-emojis').click(); // Close the panel again
done();
}));
});
describe("A Chat Message", function () {
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');
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');
expect(u.hasClass('chat-msg__text--larger', message)).toBe(true);
done()
}));
it("can render emojis as images",
mock.initConverse(
['chatBoxesFetched'], {'use_system_emojis': false},
async function (done, _converse) {
async function (_converse) {
await mock.waitForRoster(_converse, 'current');
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_stanza = sent_stanzas.filter(s => s.nodeName === 'message').pop();
expect(sent_stanza.querySelector('body').innerHTML).toBe('💩 😇');
done()
}));
}));
it("can show custom emojis",
mock.initConverse(
@ -181,7 +178,7 @@ describe("Emojis", function () {
"flags": ":flag_ac:",
"custom": ':xmpp:'
} },
async function (done, _converse) {
async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1);
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');
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">');
done();
}));
});
});

View File

@ -8,7 +8,7 @@ describe("XEP-0363: HTTP File Upload", 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;
await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, [], []);
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[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);
done();
}));
});
@ -141,7 +140,7 @@ describe("XEP-0363: HTTP File Upload", function () {
describe("A file upload toolbar button", function () {
it("does not appear in private chats",
mock.initConverse([], {}, async function (done, _converse) {
mock.initConverse([], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current', 3);
mock.openControlBox(_converse);
@ -155,7 +154,6 @@ describe("XEP-0363: HTTP File Upload", function () {
await mock.waitUntilDiscoConfirmed(_converse, _converse.domain, [], [], [], 'items');
const view = _converse.chatboxviews.get(contact_jid);
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 () {
it("appears in private chats", mock.initConverse(async (done, _converse) => {
it("appears in private chats", mock.initConverse(async (_converse) => {
await mock.waitUntilDiscoConfirmed(
_converse, _converse.domain,
[{'category': 'server', 'type':'IM'}],
@ -178,12 +176,11 @@ describe("XEP-0363: HTTP File Upload", function () {
const view = _converse.chatboxviews.get(contact_jid);
const el = await u.waitUntil(() => view.querySelector('.chat-toolbar .fileupload'));
expect(el).not.toEqual(null);
done();
}));
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';
await mock.waitUntilDiscoConfirmed(
_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(
`<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;
done();
}));
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_ids = _converse.connection.IQ_ids;
@ -398,14 +394,13 @@ describe("XEP-0363: HTTP File Upload", function () {
expect(messages.length).toBe(1);
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.');
done();
}));
});
});
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(
_converse, _converse.domain,
[{'category': 'server', 'type':'IM'}],
@ -457,6 +452,7 @@ describe("XEP-0363: HTTP File Upload", function () {
</slot>
</iq>`);
const promise = u.getOpenPromise();
spyOn(XMLHttpRequest.prototype, 'send').and.callFake(async () => {
const message = view.model.messages.at(0);
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);
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');
done();
promise.resolve();
});
_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 () {
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);
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">`+
`<received id="${msgid}" xmlns="urn:xmpp:chat-markers:0"/>`+
`</message>`);
done();
}));
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);
const contact_jid = 'someone@montague.lit';
@ -66,11 +65,10 @@ describe("A XEP-0333 Chat Marker", function () {
`<no-permanent-store xmlns="urn:xmpp:hints"/>`+
`</message>`
);
done();
}));
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.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);
expect(view.querySelectorAll('.chat-msg').length).toBe(1);
expect(view.model.messages.length).toBe(1);
done();
}));
});

View File

@ -2,62 +2,9 @@
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 () {
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.waitUntilDiscoConfirmed(_converse, 'montague.lit', [], ['vcard-temp']);
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(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",
mock.initConverse(['chatBoxesFetched'], {},
async function (done, _converse) {
async function (_converse) {
await mock.waitForRoster(_converse, 'current');
const base_url = 'https://conversejs.org';
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(
`<audio controls="" src="${message}"></audio>`+
`<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 () {
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');
const base_url = 'https://conversejs.org';
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
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",
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');
const base_url = 'https://conversejs.org';
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';
await u.waitUntil(() => sizzle(sel).pop().innerHTML.replace(/<!-.*?->/g, '').trim() === message);
expect(true).toBe(true);
done();
}));
it("will render images from approved URLs only",
mock.initConverse(
['chatBoxesFetched'], {'show_images_inline': ['conversejs.org']},
async function (done, _converse) {
async function (_converse) {
await mock.waitForRoster(_converse, 'current');
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-image').length === 1, 1000)
expect(view.querySelectorAll('.chat-content .chat-image').length).toBe(1);
done();
}));
it("will fall back to rendering images as URLs",
mock.initConverse(
['chatBoxesFetched'], {},
async function (done, _converse) {
async function (_converse) {
await mock.waitForRoster(_converse, 'current');
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();
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);
done();
}));
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'],
'image_urls_regex': /^https?:\/\/(www.)?(pbs\.twimg\.com\/)/i
},
async function (done, _converse) {
async function (_converse) {
await mock.waitForRoster(_converse, 'current');
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');
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);
done();
}));
it("will respect a changed setting when re-rendered",
mock.initConverse(
['chatBoxesFetched'], {'show_images_inline': true},
async function (done, _converse) {
async function (_converse) {
const { api } = _converse;
await mock.waitForRoster(_converse, 'current');
@ -152,6 +147,32 @@ describe("A Chat Message", function () {
view.querySelector('converse-chat-message').requestUpdate();
await u.waitUntil(() => view.querySelector('converse-chat-message-body .chat-image') === null);
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;
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');
// let message = "https://i.imgur.com/Py9ifJE.mp4";
const base_url = 'https://conversejs.org';
@ -27,11 +27,10 @@ describe("A Chat Message", function () {
expect(msg.innerHTML.replace(/<!-.*?->/g, '').trim()).toEqual(
`<video controls="" preload="metadata" src="${Strophe.xmlescape(message)}"></video>`+
`<a target="_blank" rel="noopener" href="${Strophe.xmlescape(message)}">${Strophe.xmlescape(message)}</a>`);
done();
}));
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');
// let message = "https://i.imgur.com/Py9ifJE.mp4";
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';
await u.waitUntil(() => sizzle(sel).pop().innerHTML.replace(/<!-.*?->/g, '').trim() === message);
expect(true).toBe(true);
done();
}));
it("will render videos from approved URLs only",
mock.initConverse(
['chatBoxesFetched'], {'embed_videos': ['conversejs.org']},
async function (done, _converse) {
async function (_converse) {
await mock.waitForRoster(_converse, 'current');
let message = "https://i.imgur.com/Py9ifJE.mp4";
@ -69,6 +67,32 @@ describe("A Chat Message", function () {
expect(msg.innerHTML.replace(/<!-.*?->/g, '').trim()).toEqual(
`<video controls="" preload="metadata" src="${message}"></video>`+
`<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",
mock.initConverse(['chatBoxesFetched'], {},
async function (done, _converse) {
async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1);
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await mock.openChatBoxFor(_converse, contact_jid);
const view = _converse.chatboxviews.get(contact_jid);
await _converse.handleMessageStanza(mock.createChatMessage(_converse, contact_jid, 'This message will be read'));
const msg_el = await u.waitUntil(() => view.querySelector('converse-chat-message'));
expect(msg_el.querySelector('.chat-msg__text').textContent).toBe('This message will be read');
await u.waitUntil(() => view.querySelector('converse-chat-message .chat-msg__text')?.textContent === 'This message will be read');
expect(view.model.get('num_unread')).toBe(0);
_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'));
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');
expect(last_msg_el.firstElementChild?.textContent).toBe('New messages');
done();
}));
it("is rejected if it's an unencapsulated forwarded message",
mock.initConverse(
['chatBoxesFetched'], {},
async function (done, _converse) {
async function (_converse) {
await mock.waitForRoster(_converse, 'current', 2);
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@ -74,11 +73,10 @@ describe("A Chat Message", function () {
'</message>');
models = await _converse.api.chats.get();
expect(models.length).toBe(1);
done();
}));
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.openControlBox(_converse);
@ -238,11 +236,10 @@ describe("A Chat Message", function () {
expect(day.getAttribute('data-isodate')).toEqual(dayjs().startOf('day').toISOString());
expect(day.nextElementSibling.querySelector('.chat-msg__text').textContent).toBe('latest message');
expect(u.hasClass('chat-msg--followup', el)).toBe(false);
done();
}));
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.openControlBox(_converse);
@ -250,7 +247,7 @@ describe("A Chat Message", function () {
// Ideally we wouldn't have to filter out headline
// messages, but Prosody gives them the wrong 'type' :(
spyOn(converse.env.log, 'info');
sinon.spy(_converse.api.chatboxes, 'get');
spyOn(_converse.api.chatboxes, 'get');
const msg = $msg({
from: 'montague.lit',
to: _converse.bare_jid,
@ -261,15 +258,12 @@ describe("A Chat Message", function () {
expect(converse.env.log.info).toHaveBeenCalledWith(
"handleMessageStanza: Ignoring incoming server message from JID: montague.lit"
);
expect(_converse.api.chatboxes.get.called).toBeFalsy();
// Remove sinon spies
_converse.api.chatboxes.get.restore();
done();
expect(_converse.api.chatboxes.get).not.toHaveBeenCalled();
}));
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;
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();
await u.waitUntil(() => chatbox.vcard.get('fullname') === '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",
mock.initConverse([], {}, async function (done, _converse) {
mock.initConverse([], {}, async function (_converse) {
await mock.waitUntilDiscoConfirmed(_converse, 'montague.lit', [], ['vcard-temp']);
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
const msg_el = await u.waitUntil(() => view.querySelector('.chat-content .chat-msg .chat-msg__text'));
expect(msg_el.textContent).toEqual(msgtext);
done();
}));
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.openControlBox(_converse);
@ -403,11 +395,10 @@ describe("A Chat Message", function () {
// Check that the chatbox for the malicous user is not created
chatbox = await _converse.api.chats.get(sender_jid);
expect(chatbox).toBe(null);
done();
}));
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;
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__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');
done();
}));
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.openControlBox(_converse);
@ -512,11 +502,10 @@ describe("A Chat Message", function () {
const msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view).pop();
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;');
done();
}));
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.openControlBox(_converse);
@ -532,12 +521,11 @@ describe("A Chat Message", function () {
expect(msg.textContent).toEqual(message);
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>');
done();
}));
it("will remove url query parameters from hyperlinks as set",
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.openControlBox(_converse);
@ -561,10 +549,9 @@ describe("A Chat Message", function () {
await u.waitUntil(() => msg.innerHTML.replace(/<!-.*?->/g, '') ===
'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>');
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');
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
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, '');
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",
mock.initConverse(
['chatBoxesFetched'], {},
async function (done, _converse) {
async function (_converse) {
const { api } = _converse;
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 time = dayjs(msg_object.get('time')).format(api.settings.get('time_format'));
expect(msg_time.textContent).toBe(time);
done();
}));
it("will be correctly identified and rendered as a followup message",
mock.initConverse(
[], {'debounced_content_rendering': false},
async function (done, _converse) {
async function (_converse) {
await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse);
@ -795,7 +780,6 @@ describe("A Chat Message", function () {
"Another message within 10 minutes, but from a different person");
jasmine.clock().uninstall();
done();
}));
@ -804,7 +788,7 @@ describe("A Chat Message", function () {
it("will appear inside the chatbox it was sent from",
mock.initConverse(
['chatBoxesFetched'], {},
async function (done, _converse) {
async function (_converse) {
await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse);
@ -818,14 +802,13 @@ describe("A Chat Message", function () {
expect(view.model.sendMessage).toHaveBeenCalled();
expect(view.model.messages.length, 2);
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",
mock.initConverse(
['chatBoxesFetched'], {},
async function (done, _converse) {
async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1);
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());
const message_el = sizzle('.chat-content .chat-msg:last .chat-msg__text', view).pop();
expect(message_el.textContent).toEqual(message.trim());
done();
}));
});
@ -844,7 +826,7 @@ describe("A Chat Message", function () {
describe("when received from someone else", function () {
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;
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();
await u.waitUntil(() => chatbox.vcard.get('fullname') === mock.cur_names[0]);
expect(view.querySelector('span.chat-msg__author').textContent.trim()).toBe('Mercutio');
done();
}));
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);
const rosterview = document.querySelector('converse-roster');
@ -912,7 +893,6 @@ describe("A Chat Message", function () {
expect(msg_obj.get('message')).toEqual(message.trim());
const mel = await u.waitUntil(() => view.querySelector('.chat-msg .chat-msg__text'));
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",
mock.initConverse([], {'allow_non_roster_messaging': true},
async function (done, _converse) {
async function (_converse) {
await mock.waitForRoster(_converse, 'current', 0);
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])
author_el = view.querySelector('.chat-msg__author');
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",
mock.initConverse(
[], {'allow_non_roster_messaging': false},
async function (done, _converse) {
async function (_converse) {
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__time').textContent.match(/^[0-9][0-9]:[0-9][0-9]/)).toBeTruthy();
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 () {
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);
@ -1052,7 +1030,7 @@ describe("A Chat Message", function () {
await _converse.api.chats.open(sender_jid)
let msg_text = 'This message will not be sent, due to an error';
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);
let msg_txt = sizzle('.chat-msg:last .chat-msg__text', view).pop().textContent;
expect(msg_txt).toEqual(msg_text);
@ -1061,7 +1039,7 @@ describe("A Chat Message", function () {
// not be received, to test that errors appear
// after the relevant message.
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);
msg_txt = sizzle('.chat-msg:last .chat-msg__text', view).pop().textContent;
expect(msg_txt).toEqual(msg_text);
@ -1120,7 +1098,7 @@ describe("A Chat Message", function () {
expect(view.querySelectorAll('.chat-msg__error').length).toEqual(2);
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);
// 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-retract')).toBe(null)
})
done();
}));
it("will not show to the user an error message for a CSI message",
mock.initConverse(
['chatBoxesFetched'], {},
async function (done, _converse) {
async function (_converse) {
// See #1317
// https://github.com/conversejs/converse.js/issues/1317
@ -1180,15 +1157,14 @@ describe("A Chat Message", function () {
_converse.connection._dataRecv(mock.createRequest(stanza));
const view = _converse.chatboxviews.get(contact_jid);
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);
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",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current');
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@ -1218,11 +1194,10 @@ describe("A Chat Message", function () {
indicator_el.click();
await u.waitUntil(() => !view.querySelector('.new-msgs-indicator'));
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",
mock.initConverse([], {}, async function (done, _converse) {
mock.initConverse([], {}, async function (_converse) {
const { api } = _converse;
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 msg_txt = last_message.textContent;
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",
mock.initConverse(
['chatBoxesFetched'], {},
async function (done, _converse) {
async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1);
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(
`<audio controls="" src="https://montague.lit/audio.mp3"></audio>`+
`<a target="_blank" rel="noopener" href="${url}">${url}</a>`);
done();
}));
it("will render video from oob mp4 URLs",
mock.initConverse(
['chatBoxesFetched'], {},
async function (done, _converse) {
async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1);
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(
`<video controls="" preload="metadata" src="${Strophe.xmlescape(url)}"></video>`+
`<a target="_blank" rel="noopener" href="${Strophe.xmlescape(url)}">${Strophe.xmlescape(url)}</a>`);
done();
}));
it("will render download links for files from oob URLs",
mock.initConverse(
['chatBoxesFetched'], {},
async function (done, _converse) {
async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1);
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');
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>`);
done();
}));
it("will render images from oob URLs",
mock.initConverse(
['chatBoxesFetched'], {},
async function (done, _converse) {
async function (_converse) {
const base_url = 'https://conversejs.org';
await mock.waitForRoster(_converse, 'current', 1);
@ -162,8 +159,7 @@ describe("A Chat Message", function () {
const media = view.querySelector('.chat-msg .chat-msg__media');
expect(media.innerHTML.replace(/<!-.*?->/g, '').replace(/(\r\n|\n|\r)/gm, "")).toEqual(
`<a target="_blank" rel="noopener" href="${base_url}/logo/conversejs-filled.svg">`+
`Download image file "conversejs-filled.svg"</a>`);
done();
`Download file "conversejs-filled.svg"</a>`);
}));
});
});

View File

@ -9,7 +9,7 @@ describe("A delivery receipt", function () {
it("is emitted for a received message which requests it",
mock.initConverse(
['chatBoxesFetched'], {},
async function (done, _converse) {
async function (_converse) {
await mock.waitForRoster(_converse, 'current');
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);
const receipt = sizzle(`received[xmlns="${Strophe.NS.RECEIPTS}"]`, sent_messages[1]).pop();
expect(Strophe.serialize(receipt)).toBe(`<received id="${msg_id}" xmlns="${Strophe.NS.RECEIPTS}"/>`);
done();
}));
it("is not emitted for a carbon message",
mock.initConverse(
['chatBoxesFetched'], {},
async function (done, _converse) {
async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1);
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();
await _converse.handleMessageStanza(msg);
expect(view.model.sendReceiptStanza).not.toHaveBeenCalled();
done();
}));
it("is not emitted for an archived message",
mock.initConverse(
['chatBoxesFetched'], {},
async function (done, _converse) {
async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1);
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_valid_receipt_request).toBe(false);
expect(view.model.sendReceiptStanza).not.toHaveBeenCalled();
done();
}));
it("can be received for a sent message",
mock.initConverse(
['chatBoxesFetched'], {},
async function (done, _converse) {
async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1);
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));
await u.waitUntil(() => view.querySelectorAll('.chat-msg__receipt').length === 2);
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));
it("can be received with a hint",
mock.initConverse(['chatBoxesFetched'], {}, async (done, _converse) => {
mock.initConverse(['chatBoxesFetched'], {}, async (_converse) => {
await mock.waitForRoster(_converse, 'current');
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);
const spoiler_hint_el = view.querySelector('.spoiler-hint');
expect(spoiler_hint_el.textContent).toBe(spoiler_hint);
done();
}));
it("can be received without a hint",
mock.initConverse(['chatBoxesFetched'], {}, async (done, _converse) => {
mock.initConverse(['chatBoxesFetched'], {}, async (_converse) => {
await mock.waitForRoster(_converse, 'current');
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);
const spoiler_hint_el = view.querySelector('.spoiler-hint');
expect(spoiler_hint_el.textContent).toBe('');
done();
}));
it("can be sent without a hint",
mock.initConverse(['chatBoxesFetched'], {}, async (done, _converse) => {
mock.initConverse(['chatBoxesFetched'], {}, async (_converse) => {
await mock.waitForRoster(_converse, 'current', 1);
mock.openControlBox(_converse);
@ -156,11 +154,10 @@ describe("A spoiler message", function () {
expect(spoiler_toggle.textContent.trim()).toBe('Show less');
spoiler_toggle.click();
await u.waitUntil(() => Array.from(spoiler_msg_el.classList).includes('hidden'));
done();
}));
it("can be sent with a hint",
mock.initConverse(['chatBoxesFetched'], {}, async (done, _converse) => {
mock.initConverse(['chatBoxesFetched'], {}, async (_converse) => {
await mock.waitForRoster(_converse, 'current', 1);
mock.openControlBox(_converse);
@ -237,6 +234,5 @@ describe("A spoiler message", function () {
expect(spoiler_toggle.textContent.trim()).toBe('Show less');
spoiler_toggle.click();
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",
mock.initConverse(['chatBoxesFetched'], {},
async function (done, _converse) {
async function (_converse) {
const include_nick = false;
await mock.waitForRoster(_converse, 'current', 2, include_nick);
@ -31,14 +31,13 @@ describe("An incoming chat Message", function () {
setTimeout(() => {
const msg_el = view.querySelector('converse-chat-message-body');
expect(msg_el.innerText).toBe(msg_text);
done();
}, 500);
}));
it("can have styling disabled via the allow_message_styling setting",
mock.initConverse(['chatBoxesFetched'], {'allow_message_styling': false},
async function (done, _converse) {
async function (_converse) {
const include_nick = false;
await mock.waitForRoster(_converse, 'current', 2, include_nick);
@ -62,13 +61,12 @@ describe("An incoming chat Message", function () {
setTimeout(() => {
const msg_el = view.querySelector('converse-chat-message-body');
expect(msg_el.innerText).toBe(msg_text);
done();
}, 500);
}));
it("can be styled with span XEP-0393 message styling hints",
mock.initConverse(['chatBoxesFetched'], {},
async function (done, _converse) {
async function (_converse) {
let msg_text, msg, msg_el;
await mock.waitForRoster(_converse, 'current', 1);
@ -130,7 +128,6 @@ describe("An incoming chat Message", function () {
await _converse.handleMessageStanza(msg);
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 5);
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, '') ===
'<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>'+
'<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",
mock.initConverse(['chatBoxesFetched'], {},
async function (done, _converse) {
async function (_converse) {
let msg_text, msg, msg_el;
await mock.waitForRoster(_converse, 'current', 1);
@ -206,7 +202,6 @@ describe("An incoming chat Message", function () {
await _converse.handleMessageStanza(msg);
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length);
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, '') ===
'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'+
@ -218,7 +213,6 @@ describe("An incoming chat Message", function () {
await _converse.handleMessageStanza(msg);
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 2);
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, '') ===
'<div class="styling-directive">```</div>'+
'<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'+
' This should not show up as monospace, '+
'<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",
mock.initConverse(['chatBoxesFetched'], {},
async function (done, _converse) {
async function (_converse) {
let msg_text, msg, msg_el;
await mock.waitForRoster(_converse, 'current', 1);
@ -254,7 +247,6 @@ describe("An incoming chat Message", function () {
await _converse.handleMessageStanza(msg);
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 1);
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>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 u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 2);
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>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'+
@ -274,23 +265,20 @@ describe("An incoming chat Message", function () {
await _converse.handleMessageStanza(msg);
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 3);
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 = mock.createChatMessage(_converse, contact_jid, msg_text)
await _converse.handleMessageStanza(msg);
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 4);
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 = mock.createChatMessage(_converse, contact_jid, msg_text)
await _converse.handleMessageStanza(msg);
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 5);
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>'+
'<div class="styling-directive">```</div>'+
@ -304,7 +292,6 @@ describe("An incoming chat Message", function () {
await _converse.handleMessageStanza(msg);
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 6);
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>```\n (println "Hello, world!")</blockquote>\n\n'+
'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 u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 7);
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>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>');
@ -324,7 +310,6 @@ describe("An incoming chat Message", function () {
await _converse.handleMessageStanza(msg);
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 8);
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>Where is it located?</blockquote>\n'+
'<a target="_blank" rel="noopener" '+
@ -335,7 +320,6 @@ describe("An incoming chat Message", function () {
await _converse.handleMessageStanza(msg);
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 9);
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>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 u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 10);
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>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 u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 11);
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');
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);
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>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",
mock.initConverse(['chatBoxesFetched'], {},
async function (done, _converse) {
mock.initConverse(['chatBoxesFetched'], {},
async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1);
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);
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```');
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("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();
await mock.waitForRoster(_converse, 'current');
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(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();
await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse);
@ -114,11 +113,10 @@ describe("XSS", function () {
expect(msg.innerHTML.replace(/<!-.*?->/g, '')).toEqual('"&gt;&lt;svg/onload=alert(/XSS/)');
expect(window.alert).not.toHaveBeenCalled();
done();
}));
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.openControlBox(_converse);
@ -170,11 +168,10 @@ describe("XSS", function () {
expect(msg.textContent).toEqual(message);
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>`);
done();
}));
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.openControlBox(_converse);
@ -250,7 +247,6 @@ describe("XSS", function () {
await mock.sendMessage(view, good_urls[5].entered);
await checkParsedURL(good_urls[5]);
done();
}));
});
});

View File

@ -1,5 +1,4 @@
/**
* @module converse-controlbox
* @copyright 2020, the Converse.js contributors
* @license Mozilla Public License (MPLv2)
*/
@ -7,45 +6,16 @@ import "shared/components/brand-heading";
import "../chatview/index.js";
import './loginpanel.js';
import './navback.js';
import ControlBoxMixin from './model.js';
import ControlBox from './model.js';
import ControlBoxToggle from './toggle.js';
import ControlBoxView from './controlbox.js';
import controlbox_api from './api.js';
import log from '@converse/headless/log';
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';
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', {
/* Plugin dependencies are other plugins which might be
@ -73,9 +43,8 @@ converse.plugins.add('converse-controlbox', {
ChatBoxes: {
model (attrs, options) {
const { _converse } = this.__super__;
if (attrs && attrs.id == 'controlbox') {
return new _converse.ControlBox(attrs, options);
return new ControlBox(attrs, options);
} else {
return this.__super__.model.apply(this, arguments);
}
@ -97,7 +66,7 @@ converse.plugins.add('converse-controlbox', {
Object.assign(api, controlbox_api);
_converse.ControlBoxView = ControlBoxView;
_converse.ControlBox = _converse.ChatBox.extend(ControlBoxMixin);
_converse.ControlBox = ControlBox;
_converse.ControlBoxToggle = ControlBoxToggle;
/******************** Event Handlers ********************/

View File

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

View File

@ -9,19 +9,18 @@ const sizzle = converse.env.sizzle;
describe("The Controlbox", function () {
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();
document.querySelector('.toggle-controlbox').click();
expect(_converse.api.trigger).toHaveBeenCalledWith('controlBoxOpened', jasmine.any(Object));
const el = document.querySelector("#controlbox");
expect(u.isVisible(el)).toBe(true);
done();
}));
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);
const view = _converse.chatboxviews.get('controlbox');
@ -32,14 +31,13 @@ describe("The Controlbox", function () {
view.querySelector('.close-chatbox-button').click();
expect(view.close).toHaveBeenCalled();
expect(_converse.api.trigger).toHaveBeenCalledWith('controlBoxClosed', jasmine.any(Object));
done();
}));
describe("The \"Contacts\" section", function () {
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();
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);
// 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);
done();
}));
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.openControlBox(_converse);
@ -108,23 +105,21 @@ describe("The Controlbox", function () {
chatview.model.set({'minimized': false});
expect(el.querySelector('.restore-chat .message-count')).toBe(null);
await u.waitUntil(() => rosterview.querySelector('.msgs-indicator') === null);
done();
}));
});
describe("The Status Widget", function () {
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);
const view = await u.waitUntil(() => document.querySelector('converse-user-profile'));
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');
done();
}));
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);
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('dnd', first_child)).toBe(true);
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",
mock.initConverse([], {}, async function (done, _converse) {
mock.initConverse([], {}, async function (_converse) {
await mock.openControlBox(_converse);
const cbview = _converse.chatboxviews.get('controlbox');
@ -174,7 +168,6 @@ describe("The Controlbox", function () {
const first_child = view.querySelector('.xmpp-status span:first-child');
expect(u.hasClass('online', first_child)).toBe(true);
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 () {
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.openControlBox(_converse);
@ -210,11 +203,10 @@ describe("The 'Add Contact' widget", function () {
`<iq id="${sent_stanza.getAttribute('id')}" type="set" xmlns="jabber:client">`+
`<query xmlns="jabber:iq:roster"><item jid="someone@montague.lit" name="Someone"/></query>`+
`</iq>`);
done();
}));
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.openControlBox(_converse);
@ -239,13 +231,12 @@ describe("The 'Add Contact' widget", function () {
`<query xmlns="jabber:iq:roster"><item jid="someone@montague.lit"/></query>`+
`</iq>`
);
done();
}));
it("integrates with xhr_user_search_url to search for contacts",
mock.initConverse([], { 'xhr_user_search_url': 'http://example.org/?' },
async function (done, _converse) {
async function (_converse) {
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>`+
`</iq>`);
window.XMLHttpRequest = XMLHttpRequestBackup;
done();
}));
it("can be configured to not provide search suggestions for XHR search results",
mock.initConverse([],
{ 'autocomplete_add_contact': false,
'xhr_user_search_url': 'http://example.org/?' },
async function (done, _converse) {
async function (_converse) {
await mock.waitForRoster(_converse, 'all');
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>`+
`</iq>`);
window.XMLHttpRequest = XMLHttpRequestBackup;
done();
}));
});

View File

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

View File

@ -19,3 +19,30 @@ export function navigateToControlBox (jid) {
const model = _converse.chatboxes.get(jid);
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>
<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) : '' }
</div>
</div>

View File

@ -3,7 +3,7 @@
describe("A headlines box", function () {
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);
const { $msg } = converse.env;
@ -27,11 +27,10 @@ describe("A headlines box", function () {
.c('body').t('SORRY FOR THIS ADVERT');
_converse.connection._dataRecv(mock.createRequest(stanza));
expect(_converse.api.headlines.get().length === 0);
done();
}));
it("will open and display headline messages", mock.initConverse(
[], {}, async function (done, _converse) {
[], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current', 0);
const { u, $msg} = converse.env;
@ -64,11 +63,10 @@ describe("A headlines box", function () {
const view = _converse.chatboxviews.get('notify.example.com');
expect(view.model.get('show_avatar')).toBeFalsy();
expect(view.querySelector('img.avatar')).toBe(null);
done();
}));
it("will show headline messages in the controlbox", mock.initConverse(
[], {}, async function (done, _converse) {
[], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current', 0);
const { u, $msg} = converse.env;
@ -101,11 +99,10 @@ describe("A headlines box", function () {
await u.waitUntil(() => view.querySelectorAll(".open-headline").length);
expect(view.querySelectorAll('.open-headline').length).toBe(1);
expect(view.querySelector('.open-headline').text).toBe('notify.example.com');
done();
}));
it("will remove headline messages from the controlbox if closed", mock.initConverse(
[], {}, async function (done, _converse) {
[], {}, async function (_converse) {
const { u, $msg} = converse.env;
await mock.waitForRoster(_converse, 'current', 0);
@ -143,12 +140,11 @@ describe("A headlines box", function () {
close_el.click();
await u.waitUntil(() => cbview.querySelectorAll(".open-headline").length === 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",
mock.initConverse(
['chatBoxesFetched'], {}, async function (done, _converse) {
['chatBoxesFetched'], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current', 0);
const { $msg } = converse.env;
@ -163,6 +159,5 @@ describe("A headlines box", function () {
.c('body').t('Здравствуйте друзья');
_converse.connection._dataRecv(mock.createRequest(stanza));
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(this.model, 'change:hidden', () => this.afterShown());
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, 'remove', this.requestUpdate);
this.listenTo(this.model.messages, 'reset', this.requestUpdate);
@ -44,7 +43,6 @@ class HeadlinesView extends BaseChatView {
_converse.router.navigate('');
}
await this.model.close(ev);
api.trigger('chatBoxClosed', this);
return this;
}
@ -54,8 +52,8 @@ class HeadlinesView extends BaseChatView {
return [];
}
afterShown () { // eslint-disable-line class-methods-use-this
return;
afterShown () {
this.model.clearUnreadMsgCounter();
}
}

View File

@ -19,7 +19,7 @@ describe("Message Archive Management", function () {
describe("The XEP-0313 Archive", function () {
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);
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>`+
`</iq>`
);
done();
}));
it("is queried when the user enters a new MUC",
@ -86,7 +85,7 @@ describe("Message Archive Management", function () {
{
'archived_messages_page_size': 2,
'muc_clear_messages_on_leave': false,
}, async function (done, _converse) {
}, async function (_converse) {
const sent_IQs = _converse.connection.IQ_stanzas;
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(() => Array.from(view.querySelectorAll('.chat-msg__text'))
.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",
@ -278,7 +276,7 @@ describe("Message Archive Management", function () {
'archived_messages_page_size': 2,
'muc_nickname_from_jid': false,
'muc_clear_messages_on_leave': false,
}, async function (done, _converse) {
}, async function (_converse) {
const { api } = _converse;
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>`+
`</query>`+
`</iq>`);
return done();
}));
});
@ -332,7 +329,7 @@ describe("Message Archive Management", function () {
it("is discarded if it doesn't come from the right sender",
mock.initConverse(
['discoInitialized'], {},
async function (done, _converse) {
async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1);
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);
expect(view.model.messages.length).toBe(1);
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",
mock.initConverse(
['discoInitialized'], {},
async function (done, _converse) {
async function (_converse) {
await mock.waitForRoster(_converse, 'current', 1);
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.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.");
done();
}));
it("updates the is_archived value of an already cached version",
mock.initConverse(
['discoInitialized'], {},
async function (done, _converse) {
async function (_converse) {
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.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');
done();
}));
it("isn't shown as duplicate by comparing its stanza id or archive id",
mock.initConverse(
['discoInitialized'], {},
async function (done, _converse) {
async function (_converse) {
await mock.openAndEnterChatRoom(_converse, 'trek-radio@conference.lightwitch.org', 'jcbrand');
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
expect(result instanceof _converse.Message).toBe(true);
expect(view.querySelectorAll('.chat-msg').length).toBe(1);
done();
}));
it("isn't shown as duplicate by comparing only the archive id",
mock.initConverse(
['discoInitialized'], {},
async function (done, _converse) {
async function (_converse) {
await mock.openAndEnterChatRoom(_converse, 'discuss@conference.conversejs.org', 'romeo');
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
expect(result instanceof _converse.Message).toBe(true);
expect(view.querySelectorAll('.chat-msg').length).toBe(1);
done();
}))
});
});
@ -582,7 +574,7 @@ describe("Message Archive Management", function () {
describe("The archive.query API", function () {
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;
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');
expect(Strophe.serialize(sent_stanza)).toBe(
`<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",
mock.initConverse([], {}, async function (done, _converse) {
mock.initConverse([], {}, async function (_converse) {
await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]);
let sent_stanza, IQ_id;
@ -625,11 +616,10 @@ describe("Message Archive Management", function () {
`</x>`+
`</query>`+
`</iq>`);
done();
}));
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';
_converse.api.archive.query({'with': room_jid, 'groupchat': true});
@ -650,11 +640,10 @@ describe("Message Archive Management", function () {
`</x>`+
`</query>`+
`</iq>`);
done();
}));
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 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;
expect(result.messages.length).toBe(0);
done();
}));
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]);
let sent_stanza, IQ_id;
@ -757,11 +745,10 @@ describe("Message Archive Management", function () {
`</query>`+
`</iq>`
);
done();
}));
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]);
try {
@ -769,11 +756,10 @@ describe("Message Archive Management", function () {
} catch (e) {
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",
mock.initConverse([], {}, async function (done, _converse) {
mock.initConverse([], {}, async function (_converse) {
await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]);
let sent_stanza, IQ_id;
@ -803,11 +789,10 @@ describe("Message Archive Management", function () {
`</query>`+
`</iq>`
);
done();
}));
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]);
let sent_stanza, IQ_id;
@ -837,11 +822,10 @@ describe("Message Archive Management", function () {
`</query>`+
`</iq>`
);
done();
}));
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]);
let sent_stanza, IQ_id;
@ -875,11 +859,10 @@ describe("Message Archive Management", function () {
`</set>`+
`</query>`+
`</iq>`);
done();
}));
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]);
let sent_stanza, IQ_id;
@ -905,11 +888,10 @@ describe("Message Archive Management", function () {
`</set>`+
`</query>`+
`</iq>`);
done();
}));
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]);
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.first).toBe('23452-4534-1');
expect(result.rsm.result.last).toBe('09af3-cc343-b409f');
done()
}));
});
describe("The default preference", function () {
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;
@ -1065,7 +1046,6 @@ describe("Message Archive Management", function () {
await u.waitUntil(() => feature.save.calls.count());
expect(feature.save).toHaveBeenCalled();
expect(feature.get('preferences')['default']).toBe('never'); // eslint-disable-line dot-notation
done();
}));
});
});
@ -1074,7 +1054,7 @@ describe("Chatboxes", function () {
describe("A Chatbox", function () {
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);
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('count').t('16');
_converse.connection._dataRecv(mock.createRequest(stanza));
done();
}));
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;
@ -1231,7 +1210,6 @@ describe("Chatboxes", function () {
await u.waitUntil(() => view.model.messages.length === 2, 500);
err_message = view.querySelector('.message.chat-error');
expect(err_message).toBe(null);
done();
}));
});
});

View File

@ -14,7 +14,7 @@ describe("Message Archive Management", function () {
'persistent_store': 'localStorage',
'mam_request_all_pages': false
},
async function (done, _converse) {
async function (_converse) {
const sent_IQs = _converse.connection.IQ_stanzas;
const muc_jid = 'orchard@chat.shakespeare.lit';
@ -152,12 +152,11 @@ describe("Message Archive Management", function () {
_converse.connection._dataRecv(mock.createRequest(result));
await u.waitUntil(() => view.model.messages.length === 4);
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",
mock.initConverse(['discoInitialized'], {'archived_messages_page_size': 2},
async function (done, _converse) {
async function (_converse) {
const sent_IQs = _converse.connection.IQ_stanzas;
const muc_jid = 'orchard@chat.shakespeare.lit';
@ -213,7 +212,6 @@ describe("Message Archive Management", function () {
_converse.connection._dataRecv(mock.createRequest(result));
await u.waitUntil(() => view.model.messages.length === 2);
expect(true).toBe(true);
done();
}));
});
});

View File

@ -8,11 +8,8 @@ const sizzle = converse.env.sizzle;
describe("A chat message", function () {
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');
const contact_name = mock.cur_names[0];
const contact_jid = contact_name.replace(/ /g,'.').toLowerCase() + '@montague.lit';
@ -65,7 +62,6 @@ describe("A chat message", function () {
expect(count.textContent).toBe('2');
document.querySelector("converse-minimized-chat a.restore-chat").click();
expect(_converse.chatboxes.filter('minimized').length).toBe(0);
done();
}));
});
@ -73,7 +69,7 @@ describe("A chat message", function () {
describe("A Groupchat", function () {
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');
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(view.model.get('minimized')).toBeFalsy();
expect(_converse.api.trigger.calls.count(), 3);
done();
}));
});
@ -97,7 +92,7 @@ describe("A Groupchat", function () {
describe("A Chatbox", function () {
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.openControlBox(_converse);
@ -121,11 +116,10 @@ describe("A Chatbox", function () {
minimized_chats.querySelector("a.restore-chat").click();
expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxMaximized', jasmine.any(Object));
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');
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
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(minimized_chats.firstElementChild.querySelectorAll('converse-minimized-chat').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');
await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse);
@ -173,7 +166,6 @@ describe("A Chatbox", function () {
const minimized_chats = document.querySelector("converse-minimized-chats")
minimized_chats.querySelector("a.restore-chat").click();
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 () {
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);
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();
expect(u.isVisible(unread_count)).toBeTruthy();
expect(unread_count.innerHTML.replace(/<!-.*?->/g, '')).toBe('1');
done();
}));
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);
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();
expect(u.isVisible(unread_count)).toBeTruthy();
expect(unread_count.innerHTML.replace(/<!-.*?->/g, '')).toBe('1');
done();
}));
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);
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, '') ===
'<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>');
done();
}));
});
@ -244,7 +233,7 @@ describe("A Minimized ChatBoxView's Unread Message Count", function () {
describe("The Minimized Chats Widget", function () {
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.openControlBox(_converse);
@ -271,11 +260,10 @@ describe("The Minimized Chats Widget", function () {
expect(u.isVisible(minimized_chats)).toBe(true);
expect(_converse.chatboxes.filter('minimized').length).toBe(2);
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",
mock.initConverse([], {}, async function (done, _converse) {
mock.initConverse([], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse);
@ -297,11 +285,10 @@ describe("The Minimized Chats Widget", function () {
minimized_chats.querySelector('#toggle-minimized-chats').click();
await u.waitUntil(() => u.isVisible(minimized_chats.querySelector('.minimized-chats-flyout')));
expect(minimized_chats.minchats.get('collapsed')).toBeTruthy();
done();
}));
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.openControlBox(_converse);
@ -312,11 +299,13 @@ describe("The Minimized Chats Widget", function () {
const unread_el = minimized_chats.querySelector('.unread-message-count');
expect(u.isVisible(unread_el)).toBe(false);
const promises = [];
let i, contact_jid;
for (i=0; i<3; i++) {
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);
const chatview = _converse.chatboxviews.get(contact_jid);
@ -372,11 +361,10 @@ describe("The Minimized Chats Widget", function () {
id: u.getUniqueId()
}).c('inactive', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree());
expect(minimized_chats.querySelector('.unread-message-count').textContent).toBe((i).toString());
done();
}));
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';
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")
expect(u.isVisible(minimized_chats.querySelector('.unread-message-count'))).toBeTruthy();
expect(minimized_chats.querySelector('.unread-message-count').textContent).toBe('1');
done();
}));
});

View File

@ -1,8 +1,6 @@
import 'shared/autocomplete/index.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 { __ } from 'i18n';
import { _converse, api, converse } from "@converse/headless/core";
import { render } from 'lit';
@ -16,17 +14,14 @@ export default class MUCBottomPanel extends BottomPanel {
'click .send-button': 'sendButtonClicked',
}
async connectedCallback () {
// this.model gets set in the super method and we also wait there for this.model.initialized
await super.connectedCallback();
this.debouncedRender = debounce(this.render, 100);
async initialize () {
await super.initialize();
this.listenTo(this.model, 'change:hidden_occupants', 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.occupants, 'add', this.renderIfOwnOccupant)
this.listenTo(this.model.occupants, 'change:role', this.renderIfOwnOccupant);
this.listenTo(this.model.session, 'change:connection_status', this.debouncedRender);
this.render();
}
render () {
@ -35,6 +30,7 @@ export default class MUCBottomPanel extends BottomPanel {
render(tpl_muc_bottom_panel({
can_edit, entered,
'model': this.model,
'is_groupchat': true,
'viewUnreadMessages': ev => this.viewUnreadMessages(ev)
}), this);
}
@ -47,14 +43,6 @@ export default class MUCBottomPanel extends BottomPanel {
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) {
ev?.preventDefault?.();
ev?.stopPropagation?.();

View File

@ -19,7 +19,6 @@ export default class MUCView extends BaseChatView {
this.listenTo(_converse, 'windowStateChanged', this.onWindowStateChanged);
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:view', this.requestUpdate);

View File

@ -23,10 +23,9 @@ const tpl_can_edit = (o) => {
.model=${o.model}
?composing_spoiler="${o.model.get('composing_spoiler')}"
?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_emoji_button="${show_emoji_button}"
?show_occupants_toggle="${o.model.get('show_occupants_toggle')}"
?show_send_button="${show_send_button}"
?show_spoiler_button="${show_spoiler_button}"
?show_toolbar="${show_toolbar}"

View File

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

View File

@ -20,7 +20,7 @@ export default (o) => {
</div>
<div class="chatbox-title__buttons row no-gutters">
${ 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>
${ 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 @",
mock.initConverse(['chatBoxesFetched'], {},
async function (done, _converse) {
async function (_converse) {
await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'tom');
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(3)').textContent).toBe('jane');
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",
mock.initConverse(['chatBoxesFetched'], {},
async function (done, _converse) {
async function (_converse) {
await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'tom');
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(3)').textContent).toBe('jane');
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",
mock.initConverse(
['chatBoxesFetched'], {'opening_mention_characters':['(']},
async function (done, _converse) {
async function (_converse) {
await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'tom');
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(3)').textContent).toBe('jane');
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(
['chatBoxesFetched'], {}, async function (done, _converse) {
['chatBoxesFetched'], {}, async function (_converse) {
await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'tom');
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);
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');
done();
}));
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');
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.onKeyUp(tab_event);
await u.waitUntil(() => textarea.value === 'hello @z3r0 ');
done();
}));
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');
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);
expect(view.querySelectorAll('.suggestion-box__results li').length).toBe(1);
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 () {
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 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'));
body.removeChild(span_el);
expect(true).toBe(true);
done();
}));
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 muc_jid = 'lounge@montague.lit';
@ -54,7 +53,7 @@ describe("The <converse-muc> component", function () {
await mock.returnMemberLists(_converse, muc_jid, [], all_affiliations);
await model.messages.fetched;
model.sendMessage('hello from the lounge!');
model.sendMessage({'body': 'hello from the lounge!'});
const span_el = document.createElement('span');
span_el.classList.add('conversejs');
@ -84,11 +83,10 @@ describe("The <converse-muc> component", function () {
await mock.returnMemberLists(_converse, muc2_jid, [], all_affiliations);
await model.messages.fetched;
model2.sendMessage('hello from the bar!');
model2.sendMessage({'body': 'hello from the bar!'});
muc_el.setAttribute('jid', muc2_jid);
await u.waitUntil(() => muc_el.querySelector('converse-chat-message-body').textContent.trim() === 'hello from the bar!');
body.removeChild(span_el);
done();
}));
});

View File

@ -5,7 +5,7 @@ const { $msg, $pres, Strophe, u } = converse.env;
describe("A Groupchat Message", function () {
it("can be replaced with a correction",
mock.initConverse([], {}, async function (done, _converse) {
mock.initConverse([], {}, async function (_converse) {
const muc_jid = 'lounge@montague.lit';
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[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);
done();
}));
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';
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[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);
done();
}));
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';
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.querySelectorAll('.chat-msg').length).toBe(2);
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("The emoji picker", function () {
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);
const muc_jid = 'lounge@montague.lit';
await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
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');
textarea.value = ':gri';
@ -70,17 +70,16 @@ describe("Emojis", function () {
await u.waitUntil(() => input.value === ':use');
visible_emojis = sizzle('.insert-emoji:not(.hidden)', picker);
expect(visible_emojis.length).toBe(0);
done();
}));
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';
await mock.waitForRoster(_converse, 'current', 0);
await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
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');
textarea.value = ':';
// Press tab
@ -119,18 +118,17 @@ describe("Emojis", function () {
emoji = sizzle('.emojis-lists__container--search .insert-emoji:not(.hidden) a', view).pop();
emoji.click();
await u.waitUntil(() => textarea.value === ':grinning: :grimacing: ');
done();
}));
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';
await mock.waitForRoster(_converse, 'current', 0);
await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
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');
textarea.value = ':gri';
@ -164,12 +162,11 @@ describe("Emojis", function () {
const emoji = sizzle('.emojis-lists__container--search .insert-emoji:not(.hidden) a', view).pop();
emoji.click();
expect(textarea.value).toBe(':100: ');
done();
}));
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';
await mock.waitForRoster(_converse, 'current', 0);
@ -222,7 +219,6 @@ describe("Emojis", function () {
input.dispatchEvent(new KeyboardEvent('keydown', enter_event));
await u.waitUntil(() => input.value === '');
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 () {
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';
await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
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.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("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');
mock.waitUntilDiscoConfirmed(
_converse, _converse.domain,
@ -19,7 +19,6 @@ describe("XEP-0363: HTTP File Upload", function () {
const view = _converse.chatboxviews.get('lounge@montague.lit');
await u.waitUntil(() => view.querySelector('.chat-toolbar .fileupload') === null);
expect(1).toBe(1);
done();
}));
});
@ -29,7 +28,7 @@ describe("XEP-0363: HTTP File Upload", 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(
_converse, _converse.domain,
[{'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'));
const view = _converse.chatboxviews.get('lounge@montague.lit');
expect(view.querySelector('.chat-toolbar .fileupload')).not.toBe(null);
done();
}));
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';
await mock.waitUntilDiscoConfirmed(
_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(
`<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;
done();
}));

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