Render chat messages as web components
- Render chat content as a <converse-chat-content> component - Create new component for rendering the message body - Get rid of `showMessage` method
This commit is contained in:
parent
a497e8df3a
commit
7651d58470
@ -35,6 +35,7 @@
|
|||||||
"lodash/prefer-startswith": "off",
|
"lodash/prefer-startswith": "off",
|
||||||
"lodash/preferred-alias": "off",
|
"lodash/preferred-alias": "off",
|
||||||
"lodash/matches-prop-shorthand": "off",
|
"lodash/matches-prop-shorthand": "off",
|
||||||
|
"lodash/prop-shorthand": "off",
|
||||||
"accessor-pairs": "error",
|
"accessor-pairs": "error",
|
||||||
"array-bracket-spacing": "off",
|
"array-bracket-spacing": "off",
|
||||||
"array-callback-return": "error",
|
"array-callback-return": "error",
|
||||||
|
@ -13,6 +13,12 @@ module.exports = function(config) {
|
|||||||
"dist/converse.js",
|
"dist/converse.js",
|
||||||
"dist/converse.css",
|
"dist/converse.css",
|
||||||
{ pattern: "dist/webfonts/**/*.*", included: false },
|
{ pattern: "dist/webfonts/**/*.*", included: false },
|
||||||
|
{ pattern: "dist/\@fortawesome/fontawesome-free/sprites/solid.svg",
|
||||||
|
watched: false,
|
||||||
|
included: false,
|
||||||
|
served: true,
|
||||||
|
nocache: false
|
||||||
|
},
|
||||||
{ pattern: "node_modules/sinon/pkg/sinon.js", type: 'module' },
|
{ pattern: "node_modules/sinon/pkg/sinon.js", type: 'module' },
|
||||||
{ pattern: "spec/mock.js", type: 'module' },
|
{ pattern: "spec/mock.js", type: 'module' },
|
||||||
|
|
||||||
@ -50,9 +56,13 @@ module.exports = function(config) {
|
|||||||
{ pattern: "spec/hats.js", type: 'module' },
|
{ pattern: "spec/hats.js", type: 'module' },
|
||||||
{ pattern: "spec/http-file-upload.js", type: 'module' },
|
{ pattern: "spec/http-file-upload.js", type: 'module' },
|
||||||
{ pattern: "spec/emojis.js", type: 'module' },
|
{ pattern: "spec/emojis.js", type: 'module' },
|
||||||
{ pattern: "spec/xss.js", type: 'module' },
|
{ pattern: "spec/xss.js", type: 'module' }
|
||||||
|
|
||||||
],
|
],
|
||||||
|
|
||||||
|
proxies: {
|
||||||
|
"/dist/\@fortawesome/fontawesome-free/sprites/solid.svg": "/base/dist/\@fortawesome/fontawesome-free/sprites/solid.svg"
|
||||||
|
},
|
||||||
|
|
||||||
exclude: ['**/*.sw?'],
|
exclude: ['**/*.sw?'],
|
||||||
|
|
||||||
// preprocess matching files before serving them to the browser
|
// preprocess matching files before serving them to the browser
|
||||||
|
@ -219,13 +219,46 @@
|
|||||||
font-size: var(--message-font-size);
|
font-size: var(--message-font-size);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
line-height: 1.3em;
|
line-height: 1.3em;
|
||||||
overflow-y: auto;
|
overflow: hidden;
|
||||||
padding: 1em 0 0 0;
|
padding: 0;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
||||||
|
converse-chat-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
converse-chat-message {
|
||||||
|
.spinner {
|
||||||
|
width: 100%;
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-content__help {
|
||||||
|
converse-chat-help {
|
||||||
|
border-top: 1px solid var(--chat-head-color);
|
||||||
|
display: block;
|
||||||
|
padding: 0.5em 0;
|
||||||
|
}
|
||||||
|
.close-chat-help {
|
||||||
|
float: right;
|
||||||
|
padding-right: 1em;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--chat-content-background-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-content__messages {
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.chat-content__notifications {
|
.chat-content__notifications {
|
||||||
height: 1.7em;
|
height: 1.7em;
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
@ -235,7 +268,6 @@
|
|||||||
font-style: italic;
|
font-style: italic;
|
||||||
line-height: var(--line-height-small);
|
line-height: var(--line-height-small);
|
||||||
padding: 0 1em 0.3em;
|
padding: 0 1em 0.3em;
|
||||||
|
|
||||||
&:before {
|
&:before {
|
||||||
content: " ";
|
content: " ";
|
||||||
}
|
}
|
||||||
|
@ -97,6 +97,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.empty-history-feedback {
|
||||||
|
position: relative;
|
||||||
|
span {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
position: absolute;
|
||||||
|
margin-top: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.chatroom {
|
.chatroom {
|
||||||
width: var(--chatroom-width);
|
width: var(--chatroom-width);
|
||||||
@media screen and (max-height: $mobile-landscape-height){
|
@media screen and (max-height: $mobile-landscape-height){
|
||||||
@ -166,6 +176,16 @@
|
|||||||
.chat-content {
|
.chat-content {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
.chat-content__help {
|
||||||
|
converse-chat-help {
|
||||||
|
border-top: 1px solid var(--chatroom-head-bg-color);
|
||||||
|
}
|
||||||
|
.close-chat-help {
|
||||||
|
svg {
|
||||||
|
fill: 1px solid var(--chatroom-head-bg-color) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.occupants {
|
.occupants {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -330,18 +350,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-history-feedback {
|
|
||||||
position: relative;
|
|
||||||
height: 100%;
|
|
||||||
color: var(--text-color-lighten-15-percent);
|
|
||||||
span {
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
position: absolute;
|
|
||||||
margin-top: 50%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.muc-bottom-panel {
|
.muc-bottom-panel {
|
||||||
border-top: var(--message-input-border-top);
|
border-top: var(--message-input-border-top);
|
||||||
height: 3em;
|
height: 3em;
|
||||||
|
@ -340,7 +340,7 @@ body.converse-fullscreen {
|
|||||||
q {
|
q {
|
||||||
quotes: "“" "”" "‘" "’";
|
quotes: "“" "”" "‘" "’";
|
||||||
&.reason {
|
&.reason {
|
||||||
display: block;
|
display: inline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
q:before {
|
q:before {
|
||||||
|
@ -196,7 +196,7 @@
|
|||||||
a {
|
a {
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
display: inline-block;
|
display: inline;
|
||||||
&.chat-image__link {
|
&.chat-image__link {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
@ -222,7 +222,6 @@
|
|||||||
|
|
||||||
.chat-msg__error {
|
.chat-msg__error {
|
||||||
color: var(--error-color);
|
color: var(--error-color);
|
||||||
font-weight: bold;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-msg__media {
|
.chat-msg__media {
|
||||||
|
@ -5,9 +5,13 @@ const $msg = converse.env.$msg;
|
|||||||
const Strophe = converse.env.Strophe;
|
const Strophe = converse.env.Strophe;
|
||||||
const u = converse.env.utils;
|
const u = converse.env.utils;
|
||||||
const sizzle = converse.env.sizzle;
|
const sizzle = converse.env.sizzle;
|
||||||
|
const original_timeout = jasmine.DEFAULT_TIMEOUT_INTERVAL;
|
||||||
|
|
||||||
describe("Chatboxes", function () {
|
describe("Chatboxes", function () {
|
||||||
|
|
||||||
|
beforeEach(() => (jasmine.DEFAULT_TIMEOUT_INTERVAL = 7000));
|
||||||
|
afterEach(() => (jasmine.DEFAULT_TIMEOUT_INTERVAL = original_timeout));
|
||||||
|
|
||||||
describe("A Chatbox", function () {
|
describe("A Chatbox", function () {
|
||||||
|
|
||||||
it("has a /help command to show the available commands", mock.initConverse(['rosterGroupsFetched', 'chatBoxesFetched'], {}, async function (done, _converse) {
|
it("has a /help command to show the available commands", mock.initConverse(['rosterGroupsFetched', 'chatBoxesFetched'], {}, async function (done, _converse) {
|
||||||
@ -20,7 +24,8 @@ describe("Chatboxes", function () {
|
|||||||
const view = _converse.chatboxviews.get(contact_jid);
|
const view = _converse.chatboxviews.get(contact_jid);
|
||||||
mock.sendMessage(view, '/help');
|
mock.sendMessage(view, '/help');
|
||||||
|
|
||||||
const info_messages = Array.prototype.slice.call(view.el.querySelectorAll('.chat-info:not(.chat-date)'), 0);
|
await u.waitUntil(() => sizzle('.chat-info:not(.chat-date)', view.el).length);
|
||||||
|
const info_messages = await u.waitUntil(() => sizzle('.chat-info:not(.chat-date)', view.el));
|
||||||
expect(info_messages.length).toBe(4);
|
expect(info_messages.length).toBe(4);
|
||||||
expect(info_messages.pop().textContent).toBe('/help: Show this menu');
|
expect(info_messages.pop().textContent).toBe('/help: Show this menu');
|
||||||
expect(info_messages.pop().textContent).toBe('/me: Write in the third person');
|
expect(info_messages.pop().textContent).toBe('/me: Write in the third person');
|
||||||
@ -35,7 +40,8 @@ describe("Chatboxes", function () {
|
|||||||
}).c('body').t('hello world').tree();
|
}).c('body').t('hello world').tree();
|
||||||
await _converse.handleMessageStanza(msg);
|
await _converse.handleMessageStanza(msg);
|
||||||
await u.waitUntil(() => view.content.querySelectorAll('.chat-msg').length);
|
await u.waitUntil(() => view.content.querySelectorAll('.chat-msg').length);
|
||||||
expect(view.msgs_container.lastElementChild.textContent.trim().indexOf('hello world')).not.toBe(-1);
|
const msg_txt_sel = 'converse-chat-message:last-child .chat-msg__body';
|
||||||
|
await u.waitUntil(() => view.el.querySelector(msg_txt_sel).textContent.trim() === 'hello world');
|
||||||
done();
|
done();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -58,30 +64,36 @@ describe("Chatboxes", function () {
|
|||||||
|
|
||||||
await _converse.handleMessageStanza(msg);
|
await _converse.handleMessageStanza(msg);
|
||||||
const view = _converse.chatboxviews.get(sender_jid);
|
const view = _converse.chatboxviews.get(sender_jid);
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await u.waitUntil(() => view.el.querySelector('.chat-msg__text'));
|
||||||
expect(view.el.querySelectorAll('.chat-msg--action').length).toBe(1);
|
expect(view.el.querySelectorAll('.chat-msg--action').length).toBe(1);
|
||||||
expect(_.includes(view.el.querySelector('.chat-msg__author').textContent, '**Mercutio')).toBeTruthy();
|
expect(view.el.querySelector('.chat-msg__author').textContent.includes('**Mercutio')).toBeTruthy();
|
||||||
expect(view.el.querySelector('.chat-msg__text').textContent).toBe('is tired');
|
expect(view.el.querySelector('.chat-msg__text').textContent).toBe('is tired');
|
||||||
|
|
||||||
message = '/me is as well';
|
message = '/me is as well';
|
||||||
await mock.sendMessage(view, message);
|
await mock.sendMessage(view, message);
|
||||||
expect(view.el.querySelectorAll('.chat-msg--action').length).toBe(2);
|
expect(view.el.querySelectorAll('.chat-msg--action').length).toBe(2);
|
||||||
await u.waitUntil(() => sizzle('.chat-msg__author:last', view.el).pop().textContent.trim() === '**Romeo Montague');
|
await u.waitUntil(() => sizzle('.chat-msg__author:last', view.el).pop().textContent.trim() === '**Romeo Montague');
|
||||||
const last_el = sizzle('.chat-msg__text:last', view.el).pop();
|
const last_el = sizzle('.chat-msg__text:last', view.el).pop();
|
||||||
expect(last_el.textContent).toBe('is as well');
|
await u.waitUntil(() => last_el.textContent === 'is as well');
|
||||||
expect(u.hasClass('chat-msg--followup', last_el)).toBe(false);
|
expect(u.hasClass('chat-msg--followup', last_el)).toBe(false);
|
||||||
|
|
||||||
// Check that /me messages after a normal message don't
|
// Check that /me messages after a normal message don't
|
||||||
// get the 'chat-msg--followup' class.
|
// get the 'chat-msg--followup' class.
|
||||||
message = 'This a normal message';
|
message = 'This a normal message';
|
||||||
await mock.sendMessage(view, message);
|
await mock.sendMessage(view, message);
|
||||||
let message_el = view.el.querySelector('.message:last-child');
|
const msg_txt_sel = 'converse-chat-message:last-child .chat-msg__body';
|
||||||
expect(u.hasClass('chat-msg--followup', message_el)).toBeFalsy();
|
await u.waitUntil(() => view.el.querySelector(msg_txt_sel).textContent.trim() === message);
|
||||||
|
let el = view.el.querySelector('converse-chat-message:last-child .chat-msg__body');
|
||||||
|
expect(u.hasClass('chat-msg--followup', el)).toBeFalsy();
|
||||||
|
|
||||||
message = '/me wrote a 3rd person message';
|
message = '/me wrote a 3rd person message';
|
||||||
await mock.sendMessage(view, message);
|
await mock.sendMessage(view, message);
|
||||||
message_el = view.el.querySelector('.message:last-child');
|
await u.waitUntil(() => view.el.querySelector(msg_txt_sel).textContent.trim() === message.replace('/me ', ''));
|
||||||
|
el = view.el.querySelector('converse-chat-message:last-child .chat-msg__body');
|
||||||
expect(view.el.querySelectorAll('.chat-msg--action').length).toBe(3);
|
expect(view.el.querySelectorAll('.chat-msg--action').length).toBe(3);
|
||||||
|
|
||||||
expect(sizzle('.chat-msg__text:last', view.el).pop().textContent).toBe('wrote a 3rd person message');
|
expect(sizzle('.chat-msg__text:last', view.el).pop().textContent).toBe('wrote a 3rd person message');
|
||||||
expect(u.isVisible(sizzle('.chat-msg__author:last', view.el).pop())).toBeTruthy();
|
expect(u.isVisible(sizzle('.chat-msg__author:last', view.el).pop())).toBeTruthy();
|
||||||
expect(u.hasClass('chat-msg--followup', message_el)).toBeFalsy();
|
|
||||||
done();
|
done();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -451,7 +463,7 @@ describe("Chatboxes", function () {
|
|||||||
keyCode: 13 // Enter
|
keyCode: 13 // Enter
|
||||||
};
|
};
|
||||||
view.onKeyDown(ev);
|
view.onKeyDown(ev);
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
view.onKeyUp(ev);
|
view.onKeyUp(ev);
|
||||||
expect(counter.textContent).toBe('200');
|
expect(counter.textContent).toBe('200');
|
||||||
|
|
||||||
@ -1166,8 +1178,6 @@ describe("Chatboxes", function () {
|
|||||||
expect(document.title).toBe('Converse Tests');
|
expect(document.title).toBe('Converse Tests');
|
||||||
|
|
||||||
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
|
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
|
||||||
const view = await mock.openChatBoxFor(_converse, sender_jid)
|
|
||||||
|
|
||||||
const previous_state = _converse.windowState;
|
const previous_state = _converse.windowState;
|
||||||
const message = 'This message will increment the message counter';
|
const message = 'This message will increment the message counter';
|
||||||
const msg = $msg({
|
const msg = $msg({
|
||||||
@ -1184,7 +1194,6 @@ describe("Chatboxes", function () {
|
|||||||
spyOn(_converse, 'clearMsgCounter').and.callThrough();
|
spyOn(_converse, 'clearMsgCounter').and.callThrough();
|
||||||
|
|
||||||
await _converse.handleMessageStanza(msg);
|
await _converse.handleMessageStanza(msg);
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
|
||||||
expect(_converse.incrementMsgCounter).toHaveBeenCalled();
|
expect(_converse.incrementMsgCounter).toHaveBeenCalled();
|
||||||
expect(_converse.clearMsgCounter).not.toHaveBeenCalled();
|
expect(_converse.clearMsgCounter).not.toHaveBeenCalled();
|
||||||
expect(document.title).toBe('Messages (1) Converse Tests');
|
expect(document.title).toBe('Messages (1) Converse Tests');
|
||||||
@ -1604,9 +1613,8 @@ describe("Chatboxes", function () {
|
|||||||
|
|
||||||
await mock.waitForRoster(_converse, 'current', 1);
|
await mock.waitForRoster(_converse, 'current', 1);
|
||||||
|
|
||||||
const message = "geo:37.786971,-122.399677",
|
const message = "geo:37.786971,-122.399677";
|
||||||
contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
|
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
|
||||||
|
|
||||||
await mock.openChatBoxFor(_converse, contact_jid);
|
await mock.openChatBoxFor(_converse, contact_jid);
|
||||||
const view = _converse.chatboxviews.get(contact_jid);
|
const view = _converse.chatboxviews.get(contact_jid);
|
||||||
spyOn(view.model, 'sendMessage').and.callThrough();
|
spyOn(view.model, 'sendMessage').and.callThrough();
|
||||||
@ -1614,10 +1622,9 @@ describe("Chatboxes", function () {
|
|||||||
await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-msg').length, 1000);
|
await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-msg').length, 1000);
|
||||||
expect(view.model.sendMessage).toHaveBeenCalled();
|
expect(view.model.sendMessage).toHaveBeenCalled();
|
||||||
const msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
|
const msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
|
||||||
expect(msg.innerHTML).toEqual(
|
expect(msg.innerHTML.replace(/\<!----\>/g, '')).toEqual(
|
||||||
'<a target="_blank" rel="noopener" href="https://www.openstreetmap.org/?mlat=37.786971&'+
|
'<a target="_blank" rel="noopener" href="https://www.openstreetmap.org/?mlat=37.786971&'+
|
||||||
'mlon=-122.399677#map=18/37.786971/-122.399677">https://www.openstreetmap.org/?mlat=37.7869'+
|
'mlon=-122.399677#map=18/37.786971/-122.399677">https://www.openstreetmap.org/?mlat=37.786971&mlon=-122.399677#map=18/37.786971/-122.399677</a>');
|
||||||
'71&mlon=-122.399677#map=18/37.786971/-122.399677</a>');
|
|
||||||
done();
|
done();
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
@ -170,9 +170,8 @@ describe("Emojis", function () {
|
|||||||
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree());
|
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree());
|
||||||
await new Promise(resolve => _converse.on('chatBoxViewInitialized', resolve));
|
await new Promise(resolve => _converse.on('chatBoxViewInitialized', resolve));
|
||||||
const view = _converse.api.chatviews.get(sender_jid);
|
const view = _converse.api.chatviews.get(sender_jid);
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
let message = view.content.querySelector('.chat-msg__text');
|
await u.waitUntil(() => u.hasClass('chat-msg__text--larger', view.content.querySelector('.chat-msg__text')));
|
||||||
expect(u.hasClass('chat-msg__text--larger', message)).toBe(true);
|
|
||||||
|
|
||||||
_converse.handleMessageStanza($msg({
|
_converse.handleMessageStanza($msg({
|
||||||
'from': sender_jid,
|
'from': sender_jid,
|
||||||
@ -181,9 +180,10 @@ describe("Emojis", function () {
|
|||||||
'id': _converse.connection.getUniqueId()
|
'id': _converse.connection.getUniqueId()
|
||||||
}).c('body').t('😇 Hello world! 😇 😇').up()
|
}).c('body').t('😇 Hello world! 😇 😇').up()
|
||||||
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree());
|
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree());
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
message = view.content.querySelector('.message:last-child .chat-msg__text');
|
|
||||||
expect(u.hasClass('chat-msg__text--larger', message)).toBe(false);
|
let sel = '.message:last-child .chat-msg__text';
|
||||||
|
await u.waitUntil(() => u.hasClass('chat-msg__text--larger', view.content.querySelector(sel)));
|
||||||
|
|
||||||
// Test that a modified message that no longer contains only
|
// Test that a modified message that no longer contains only
|
||||||
// emojis now renders normally again.
|
// emojis now renders normally again.
|
||||||
@ -194,9 +194,11 @@ describe("Emojis", function () {
|
|||||||
preventDefault: function preventDefault () {},
|
preventDefault: function preventDefault () {},
|
||||||
keyCode: 13 // Enter
|
keyCode: 13 // Enter
|
||||||
});
|
});
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
expect(view.el.querySelectorAll('.chat-msg').length).toBe(3);
|
expect(view.el.querySelectorAll('.chat-msg').length).toBe(3);
|
||||||
expect(view.content.querySelector('.message:last-child .chat-msg__text').textContent).toBe('💩 😇');
|
const last_msg_sel = 'converse-chat-message:last-child .chat-msg__text';
|
||||||
|
await u.waitUntil(() => view.content.querySelector(last_msg_sel).textContent === '💩 😇');
|
||||||
|
|
||||||
expect(textarea.value).toBe('');
|
expect(textarea.value).toBe('');
|
||||||
view.onKeyDown({
|
view.onKeyDown({
|
||||||
target: textarea,
|
target: textarea,
|
||||||
@ -204,7 +206,8 @@ describe("Emojis", function () {
|
|||||||
});
|
});
|
||||||
expect(textarea.value).toBe('💩 😇');
|
expect(textarea.value).toBe('💩 😇');
|
||||||
expect(view.model.messages.at(2).get('correcting')).toBe(true);
|
expect(view.model.messages.at(2).get('correcting')).toBe(true);
|
||||||
await u.waitUntil(() => u.hasClass('correcting', view.el.querySelector('.chat-msg:last-child')), 500);
|
sel = 'converse-chat-message:last-child .chat-msg'
|
||||||
|
await u.waitUntil(() => u.hasClass('correcting', view.el.querySelector(sel)), 500);
|
||||||
textarea.value = textarea.value += 'This is no longer an emoji-only message';
|
textarea.value = textarea.value += 'This is no longer an emoji-only message';
|
||||||
view.onKeyDown({
|
view.onKeyDown({
|
||||||
target: textarea,
|
target: textarea,
|
||||||
@ -213,7 +216,7 @@ describe("Emojis", function () {
|
|||||||
});
|
});
|
||||||
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
expect(view.model.messages.models.length).toBe(3);
|
expect(view.model.messages.models.length).toBe(3);
|
||||||
message = view.content.querySelector('.message:last-child .chat-msg__text');
|
let message = view.content.querySelector(last_msg_sel);
|
||||||
expect(u.hasClass('chat-msg__text--larger', message)).toBe(false);
|
expect(u.hasClass('chat-msg__text--larger', message)).toBe(false);
|
||||||
|
|
||||||
textarea.value = ':smile: Hello world!';
|
textarea.value = ':smile: Hello world!';
|
||||||
@ -222,7 +225,7 @@ describe("Emojis", function () {
|
|||||||
preventDefault: function preventDefault () {},
|
preventDefault: function preventDefault () {},
|
||||||
keyCode: 13 // Enter
|
keyCode: 13 // Enter
|
||||||
});
|
});
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
|
|
||||||
textarea.value = ':smile: :smiley: :imp:';
|
textarea.value = ':smile: :smiley: :imp:';
|
||||||
view.onKeyDown({
|
view.onKeyDown({
|
||||||
@ -230,7 +233,7 @@ describe("Emojis", function () {
|
|||||||
preventDefault: function preventDefault () {},
|
preventDefault: function preventDefault () {},
|
||||||
keyCode: 13 // Enter
|
keyCode: 13 // Enter
|
||||||
});
|
});
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
|
|
||||||
message = view.content.querySelector('.message:last-child .chat-msg__text');
|
message = view.content.querySelector('.message:last-child .chat-msg__text');
|
||||||
expect(u.hasClass('chat-msg__text--larger', message)).toBe(true);
|
expect(u.hasClass('chat-msg__text--larger', message)).toBe(true);
|
||||||
|
@ -60,7 +60,7 @@ describe("A XEP-0317 MUC Hat", function () {
|
|||||||
await u.waitUntil(() => view.model.getOccupant("Terry").get('hats').length === 3);
|
await u.waitUntil(() => view.model.getOccupant("Terry").get('hats').length === 3);
|
||||||
hats = view.model.getOccupant("Terry").get('hats');
|
hats = view.model.getOccupant("Terry").get('hats');
|
||||||
expect(hats.map(h => h.title).join(' ')).toBe("Teacher's Assistant Dark Mage Mad hatter");
|
expect(hats.map(h => h.title).join(' ')).toBe("Teacher's Assistant Dark Mage Mad hatter");
|
||||||
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg .badge').length === 3);
|
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg .badge').length === 3, 1000);
|
||||||
badges = Array.from(view.el.querySelectorAll('.chat-msg .badge'));
|
badges = Array.from(view.el.querySelectorAll('.chat-msg .badge'));
|
||||||
expect(badges.map(b => b.textContent.trim()).join(' ' )).toBe("Teacher's Assistant Dark Mage Mad hatter");
|
expect(badges.map(b => b.textContent.trim()).join(' ' )).toBe("Teacher's Assistant Dark Mage Mad hatter");
|
||||||
|
|
||||||
|
@ -247,7 +247,7 @@ describe("XEP-0363: HTTP File Upload", function () {
|
|||||||
'name': "my-juliet.jpg"
|
'name': "my-juliet.jpg"
|
||||||
};
|
};
|
||||||
view.model.sendFiles([file]);
|
view.model.sendFiles([file]);
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
|
|
||||||
await u.waitUntil(() => _.filter(IQ_stanzas, iq => iq.querySelector('iq[to="upload.montague.tld"] request')).length);
|
await u.waitUntil(() => _.filter(IQ_stanzas, iq => iq.querySelector('iq[to="upload.montague.tld"] request')).length);
|
||||||
const iq = IQ_stanzas.pop();
|
const iq = IQ_stanzas.pop();
|
||||||
@ -352,7 +352,7 @@ describe("XEP-0363: HTTP File Upload", function () {
|
|||||||
'name': "my-juliet.jpg"
|
'name': "my-juliet.jpg"
|
||||||
};
|
};
|
||||||
view.model.sendFiles([file]);
|
view.model.sendFiles([file]);
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
|
|
||||||
await u.waitUntil(() => _.filter(IQ_stanzas, iq => iq.querySelector('iq[to="upload.montague.tld"] request')).length);
|
await u.waitUntil(() => _.filter(IQ_stanzas, iq => iq.querySelector('iq[to="upload.montague.tld"] request')).length);
|
||||||
const iq = IQ_stanzas.pop();
|
const iq = IQ_stanzas.pop();
|
||||||
@ -575,7 +575,7 @@ describe("XEP-0363: HTTP File Upload", function () {
|
|||||||
'name': "my-juliet.jpg"
|
'name': "my-juliet.jpg"
|
||||||
};
|
};
|
||||||
view.model.sendFiles([file]);
|
view.model.sendFiles([file]);
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
await u.waitUntil(() => _.filter(IQ_stanzas, iq => iq.querySelector('iq[to="upload.montague.tld"] request')).length)
|
await u.waitUntil(() => _.filter(IQ_stanzas, iq => iq.querySelector('iq[to="upload.montague.tld"] request')).length)
|
||||||
const iq = IQ_stanzas.pop();
|
const iq = IQ_stanzas.pop();
|
||||||
expect(Strophe.serialize(iq)).toBe(
|
expect(Strophe.serialize(iq)).toBe(
|
||||||
@ -606,18 +606,16 @@ describe("XEP-0363: HTTP File Upload", function () {
|
|||||||
<get url="${message}" />
|
<get url="${message}" />
|
||||||
</slot>
|
</slot>
|
||||||
</iq>`);
|
</iq>`);
|
||||||
spyOn(XMLHttpRequest.prototype, 'send').and.callFake(function () {
|
|
||||||
|
spyOn(XMLHttpRequest.prototype, 'send').and.callFake(async () => {
|
||||||
const message = view.model.messages.at(0);
|
const message = view.model.messages.at(0);
|
||||||
expect(view.el.querySelector('.chat-content progress').getAttribute('value')).toBe('0');
|
expect(view.el.querySelector('.chat-content progress').getAttribute('value')).toBe('0');
|
||||||
message.set('progress', 0.5);
|
message.set('progress', 0.5);
|
||||||
u.waitUntil(() => view.el.querySelector('.chat-content progress').getAttribute('value') === '0.5')
|
await u.waitUntil(() => view.el.querySelector('.chat-content progress').getAttribute('value') === '0.5');
|
||||||
.then(() => {
|
message.set('progress', 1);
|
||||||
message.set('progress', 1);
|
await u.waitUntil(() => view.el.querySelector('.chat-content progress').getAttribute('value') === '1');
|
||||||
u.waitUntil(() => view.el.querySelector('.chat-content progress').getAttribute('value') === '1')
|
expect(view.el.querySelector('.chat-content .chat-msg__text').textContent).toBe('Uploading file: my-juliet.jpg, 22.91 KB');
|
||||||
}).then(() => {
|
done();
|
||||||
expect(view.el.querySelector('.chat-content .chat-msg__text').textContent).toBe('Uploading file: my-juliet.jpg, 22.91 KB');
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
_converse.connection._dataRecv(mock.createRequest(stanza));
|
_converse.connection._dataRecv(mock.createRequest(stanza));
|
||||||
}));
|
}));
|
||||||
|
19
spec/mam.js
19
spec/mam.js
@ -7,11 +7,15 @@ const $msg = converse.env.$msg;
|
|||||||
const dayjs = converse.env.dayjs;
|
const dayjs = converse.env.dayjs;
|
||||||
const u = converse.env.utils;
|
const u = converse.env.utils;
|
||||||
const sizzle = converse.env.sizzle;
|
const sizzle = converse.env.sizzle;
|
||||||
|
const original_timeout = jasmine.DEFAULT_TIMEOUT_INTERVAL;
|
||||||
// See: https://xmpp.org/rfcs/rfc3921.html
|
// See: https://xmpp.org/rfcs/rfc3921.html
|
||||||
|
|
||||||
// Implements the protocol defined in https://xmpp.org/extensions/xep-0313.html#config
|
// Implements the protocol defined in https://xmpp.org/extensions/xep-0313.html#config
|
||||||
describe("Message Archive Management", function () {
|
describe("Message Archive Management", function () {
|
||||||
|
|
||||||
|
beforeEach(() => (jasmine.DEFAULT_TIMEOUT_INTERVAL = 7000));
|
||||||
|
afterEach(() => (jasmine.DEFAULT_TIMEOUT_INTERVAL = original_timeout));
|
||||||
|
|
||||||
describe("The XEP-0313 Archive", function () {
|
describe("The XEP-0313 Archive", function () {
|
||||||
|
|
||||||
it("is queried when the user enters a new MUC",
|
it("is queried when the user enters a new MUC",
|
||||||
@ -194,8 +198,12 @@ describe("Message Archive Management", function () {
|
|||||||
</iq>`);
|
</iq>`);
|
||||||
_converse.connection._dataRecv(mock.createRequest(result));
|
_converse.connection._dataRecv(mock.createRequest(result));
|
||||||
await u.waitUntil(() => view.model.messages.length === 5);
|
await u.waitUntil(() => view.model.messages.length === 5);
|
||||||
const msg_els = view.content.querySelectorAll('.chat-msg__text');
|
await u.waitUntil(() => view.content.querySelectorAll('.chat-msg__text').length);
|
||||||
expect(Array.from(msg_els).map(e => e.textContent).join(' ')).toBe("2nd Message 3rd Message 4th Message 5th Message 6th Message");
|
const msg_els = Array.from(view.content.querySelectorAll('.chat-msg__text'));
|
||||||
|
await u.waitUntil(
|
||||||
|
() => msg_els.map(e => e.textContent).join(' ') === "2nd Message 3rd Message 4th Message 5th Message 6th Message",
|
||||||
|
1000
|
||||||
|
);
|
||||||
done();
|
done();
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
@ -253,7 +261,7 @@ describe("Message Archive Management", function () {
|
|||||||
.c('count').t('16');
|
.c('count').t('16');
|
||||||
_converse.connection._dataRecv(mock.createRequest(iq_result));
|
_converse.connection._dataRecv(mock.createRequest(iq_result));
|
||||||
|
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
expect(view.model.messages.length).toBe(1);
|
expect(view.model.messages.length).toBe(1);
|
||||||
expect(view.model.messages.at(0).get('message')).toBe("Thrice the brinded cat hath mew'd.");
|
expect(view.model.messages.at(0).get('message')).toBe("Thrice the brinded cat hath mew'd.");
|
||||||
done();
|
done();
|
||||||
@ -1038,9 +1046,8 @@ describe("Chatboxes", function () {
|
|||||||
expect(view.model.messages.at(0).get('type')).toBe('error');
|
expect(view.model.messages.at(0).get('type')).toBe('error');
|
||||||
expect(view.model.messages.at(0).get('message')).toBe('Timeout while trying to fetch archived messages.');
|
expect(view.model.messages.at(0).get('message')).toBe('Timeout while trying to fetch archived messages.');
|
||||||
|
|
||||||
let err_message = view.el.querySelector('.message.chat-error');
|
let err_message = await u.waitUntil(() => view.el.querySelector('.message.chat-error'));
|
||||||
err_message.querySelector('.retry').click();
|
err_message.querySelector('.retry').click();
|
||||||
expect(err_message.querySelector('.spinner')).not.toBe(null);
|
|
||||||
|
|
||||||
while (_converse.connection.IQ_stanzas.length) {
|
while (_converse.connection.IQ_stanzas.length) {
|
||||||
_converse.connection.IQ_stanzas.pop();
|
_converse.connection.IQ_stanzas.pop();
|
||||||
@ -1058,6 +1065,8 @@ describe("Chatboxes", function () {
|
|||||||
`</query>`+
|
`</query>`+
|
||||||
`</iq>`);
|
`</iq>`);
|
||||||
|
|
||||||
|
await u.waitUntil(() => view.el.querySelector('converse-chat-message .spinner'), 1000);
|
||||||
|
|
||||||
const msg1 = $msg({'id':'aeb212', 'to': contact_jid})
|
const msg1 = $msg({'id':'aeb212', 'to': contact_jid})
|
||||||
.c('result', {'xmlns': 'urn:xmpp:mam:2', 'queryid': queryid, 'id':'28482-98726-73623'})
|
.c('result', {'xmlns': 'urn:xmpp:mam:2', 'queryid': queryid, 'id':'28482-98726-73623'})
|
||||||
.c('forwarded', {'xmlns':'urn:xmpp:forward:0'})
|
.c('forwarded', {'xmlns':'urn:xmpp:forward:0'})
|
||||||
|
247
spec/messages.js
247
spec/messages.js
@ -69,7 +69,7 @@ describe("A Chat Message", function () {
|
|||||||
preventDefault: function preventDefault () {},
|
preventDefault: function preventDefault () {},
|
||||||
keyCode: 13 // Enter
|
keyCode: 13 // Enter
|
||||||
});
|
});
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
|
|
||||||
expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
|
expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
|
||||||
expect(view.el.querySelector('.chat-msg__text').textContent)
|
expect(view.el.querySelector('.chat-msg__text').textContent)
|
||||||
@ -87,7 +87,6 @@ describe("A Chat Message", function () {
|
|||||||
expect(textarea.value).toBe('But soft, what light through yonder airlock breaks?');
|
expect(textarea.value).toBe('But soft, what light through yonder airlock breaks?');
|
||||||
expect(view.model.messages.at(0).get('correcting')).toBe(true);
|
expect(view.model.messages.at(0).get('correcting')).toBe(true);
|
||||||
expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
|
expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
|
||||||
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
|
||||||
await u.waitUntil(() => u.hasClass('correcting', view.el.querySelector('.chat-msg')));
|
await u.waitUntil(() => u.hasClass('correcting', view.el.querySelector('.chat-msg')));
|
||||||
|
|
||||||
spyOn(_converse.connection, 'send');
|
spyOn(_converse.connection, 'send');
|
||||||
@ -98,7 +97,6 @@ describe("A Chat Message", function () {
|
|||||||
keyCode: 13 // Enter
|
keyCode: 13 // Enter
|
||||||
});
|
});
|
||||||
expect(_converse.connection.send).toHaveBeenCalled();
|
expect(_converse.connection.send).toHaveBeenCalled();
|
||||||
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
|
||||||
|
|
||||||
const msg = _converse.connection.send.calls.all()[0].args[0];
|
const msg = _converse.connection.send.calls.all()[0].args[0];
|
||||||
expect(msg.toLocaleString())
|
expect(msg.toLocaleString())
|
||||||
@ -121,14 +119,13 @@ describe("A Chat Message", function () {
|
|||||||
expect(keys.length).toBe(1);
|
expect(keys.length).toBe(1);
|
||||||
expect(older_versions[keys[0]]).toBe('But soft, what light through yonder airlock breaks?');
|
expect(older_versions[keys[0]]).toBe('But soft, what light through yonder airlock breaks?');
|
||||||
|
|
||||||
|
await u.waitUntil(() => u.hasClass('correcting', view.el.querySelector('.chat-msg')) === false);
|
||||||
expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
|
expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
|
||||||
expect(u.hasClass('correcting', view.el.querySelector('.chat-msg'))).toBe(false);
|
|
||||||
|
|
||||||
// Test that clicking the pencil icon a second time cancels editing.
|
// Test that clicking the pencil icon a second time cancels editing.
|
||||||
action = view.el.querySelector('.chat-msg .chat-msg__action');
|
action = view.el.querySelector('.chat-msg .chat-msg__action');
|
||||||
action.style.opacity = 1;
|
action.style.opacity = 1;
|
||||||
action.click();
|
action.click();
|
||||||
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
|
||||||
|
|
||||||
expect(textarea.value).toBe('But soft, what light through yonder window breaks?');
|
expect(textarea.value).toBe('But soft, what light through yonder window breaks?');
|
||||||
expect(view.model.messages.at(0).get('correcting')).toBe(true);
|
expect(view.model.messages.at(0).get('correcting')).toBe(true);
|
||||||
@ -153,7 +150,7 @@ describe("A Chat Message", function () {
|
|||||||
}).c('body').t('Hello').up()
|
}).c('body').t('Hello').up()
|
||||||
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree()
|
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree()
|
||||||
);
|
);
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
expect(view.el.querySelectorAll('.chat-msg .chat-msg__action').length).toBe(2);
|
expect(view.el.querySelectorAll('.chat-msg .chat-msg__action').length).toBe(2);
|
||||||
|
|
||||||
// Test confirmation dialog
|
// Test confirmation dialog
|
||||||
@ -203,7 +200,7 @@ describe("A Chat Message", function () {
|
|||||||
preventDefault: function preventDefault () {},
|
preventDefault: function preventDefault () {},
|
||||||
keyCode: 13 // Enter
|
keyCode: 13 // Enter
|
||||||
});
|
});
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
|
expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
|
||||||
expect(view.el.querySelector('.chat-msg__text').textContent)
|
expect(view.el.querySelector('.chat-msg__text').textContent)
|
||||||
.toBe('But soft, what light through yonder airlock breaks?');
|
.toBe('But soft, what light through yonder airlock breaks?');
|
||||||
@ -279,7 +276,7 @@ describe("A Chat Message", function () {
|
|||||||
preventDefault: function preventDefault () {},
|
preventDefault: function preventDefault () {},
|
||||||
keyCode: 13 // Enter
|
keyCode: 13 // Enter
|
||||||
});
|
});
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
expect(view.el.querySelectorAll('.chat-msg').length).toBe(2);
|
expect(view.el.querySelectorAll('.chat-msg').length).toBe(2);
|
||||||
|
|
||||||
textarea.value = 'Arise, fair sun, and kill the envious moon';
|
textarea.value = 'Arise, fair sun, and kill the envious moon';
|
||||||
@ -288,7 +285,7 @@ describe("A Chat Message", function () {
|
|||||||
preventDefault: function preventDefault () {},
|
preventDefault: function preventDefault () {},
|
||||||
keyCode: 13 // Enter
|
keyCode: 13 // Enter
|
||||||
});
|
});
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
expect(view.el.querySelectorAll('.chat-msg').length).toBe(3);
|
expect(view.el.querySelectorAll('.chat-msg').length).toBe(3);
|
||||||
|
|
||||||
view.onKeyDown({
|
view.onKeyDown({
|
||||||
@ -372,7 +369,7 @@ describe("A Chat Message", function () {
|
|||||||
.c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2017-12-31T22:08:25Z'})
|
.c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2017-12-31T22:08:25Z'})
|
||||||
.tree();
|
.tree();
|
||||||
_converse.handleMessageStanza(msg);
|
_converse.handleMessageStanza(msg);
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
|
|
||||||
msg = $msg({
|
msg = $msg({
|
||||||
'xmlns': 'jabber:client',
|
'xmlns': 'jabber:client',
|
||||||
@ -384,7 +381,7 @@ describe("A Chat Message", function () {
|
|||||||
.c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-01T13:18:23Z'})
|
.c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-01T13:18:23Z'})
|
||||||
.tree();
|
.tree();
|
||||||
_converse.handleMessageStanza(msg);
|
_converse.handleMessageStanza(msg);
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
|
|
||||||
msg = $msg({
|
msg = $msg({
|
||||||
'xmlns': 'jabber:client',
|
'xmlns': 'jabber:client',
|
||||||
@ -396,7 +393,7 @@ describe("A Chat Message", function () {
|
|||||||
.c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-01T13:18:23Z'})
|
.c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-01T13:18:23Z'})
|
||||||
.tree();
|
.tree();
|
||||||
_converse.handleMessageStanza(msg);
|
_converse.handleMessageStanza(msg);
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
|
|
||||||
msg = $msg({
|
msg = $msg({
|
||||||
'xmlns': 'jabber:client',
|
'xmlns': 'jabber:client',
|
||||||
@ -408,7 +405,7 @@ describe("A Chat Message", function () {
|
|||||||
.c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-02T12:18:23Z'})
|
.c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-02T12:18:23Z'})
|
||||||
.tree();
|
.tree();
|
||||||
_converse.handleMessageStanza(msg);
|
_converse.handleMessageStanza(msg);
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
|
|
||||||
msg = $msg({
|
msg = $msg({
|
||||||
'xmlns': 'jabber:client',
|
'xmlns': 'jabber:client',
|
||||||
@ -420,7 +417,7 @@ describe("A Chat Message", function () {
|
|||||||
.c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-02T22:28:23Z'})
|
.c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-02T22:28:23Z'})
|
||||||
.tree();
|
.tree();
|
||||||
_converse.handleMessageStanza(msg);
|
_converse.handleMessageStanza(msg);
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
|
|
||||||
// Insert <composing> message, to also check that
|
// Insert <composing> message, to also check that
|
||||||
// text messages are inserted correctly with
|
// text messages are inserted correctly with
|
||||||
@ -434,7 +431,8 @@ describe("A Chat Message", function () {
|
|||||||
.c('composing', {'xmlns': Strophe.NS.CHATSTATES}).up()
|
.c('composing', {'xmlns': Strophe.NS.CHATSTATES}).up()
|
||||||
.tree();
|
.tree();
|
||||||
_converse.handleMessageStanza(msg);
|
_converse.handleMessageStanza(msg);
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
const csntext = await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent);
|
||||||
|
expect(csntext.trim()).toEqual('Mercutio is typing');
|
||||||
|
|
||||||
msg = $msg({
|
msg = $msg({
|
||||||
'id': _converse.connection.getUniqueId(),
|
'id': _converse.connection.getUniqueId(),
|
||||||
@ -446,7 +444,7 @@ describe("A Chat Message", function () {
|
|||||||
.c('body').t("latest message")
|
.c('body').t("latest message")
|
||||||
.tree();
|
.tree();
|
||||||
await _converse.handleMessageStanza(msg);
|
await _converse.handleMessageStanza(msg);
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
|
|
||||||
view.clearSpinner(); //cleanup
|
view.clearSpinner(); //cleanup
|
||||||
expect(view.content.querySelectorAll('.date-separator').length).toEqual(4);
|
expect(view.content.querySelectorAll('.date-separator').length).toEqual(4);
|
||||||
@ -473,7 +471,7 @@ describe("A Chat Message", function () {
|
|||||||
|
|
||||||
el = sizzle('.chat-msg:eq(1)', view.content).pop();
|
el = sizzle('.chat-msg:eq(1)', view.content).pop();
|
||||||
expect(el.querySelector('.chat-msg__text').textContent).toEqual('Inbetween message');
|
expect(el.querySelector('.chat-msg__text').textContent).toEqual('Inbetween message');
|
||||||
expect(el.nextElementSibling.querySelector('.chat-msg__text').textContent).toEqual('another inbetween message');
|
expect(el.parentElement.nextElementSibling.querySelector('.chat-msg__text').textContent).toEqual('another inbetween message');
|
||||||
el = sizzle('.chat-msg:eq(2)', view.content).pop();
|
el = sizzle('.chat-msg:eq(2)', view.content).pop();
|
||||||
expect(el.querySelector('.chat-msg__text').textContent)
|
expect(el.querySelector('.chat-msg__text').textContent)
|
||||||
.toEqual('another inbetween message');
|
.toEqual('another inbetween message');
|
||||||
@ -492,7 +490,7 @@ describe("A Chat Message", function () {
|
|||||||
|
|
||||||
el = sizzle('.chat-msg:eq(4)', view.content).pop();
|
el = sizzle('.chat-msg:eq(4)', view.content).pop();
|
||||||
expect(el.querySelector('.chat-msg__text').textContent).toEqual('message');
|
expect(el.querySelector('.chat-msg__text').textContent).toEqual('message');
|
||||||
expect(el.nextElementSibling.querySelector('.chat-msg__text').textContent).toEqual('newer message from the next day');
|
expect(el.parentElement.nextElementSibling.querySelector('.chat-msg__text').textContent).toEqual('newer message from the next day');
|
||||||
expect(u.hasClass('chat-msg--followup', el)).toBe(false);
|
expect(u.hasClass('chat-msg--followup', el)).toBe(false);
|
||||||
|
|
||||||
day = sizzle('.date-separator:last', view.content).pop();
|
day = sizzle('.date-separator:last', view.content).pop();
|
||||||
@ -624,8 +622,8 @@ describe("A Chat Message", function () {
|
|||||||
expect(msg_obj.get('sender')).toEqual('me');
|
expect(msg_obj.get('sender')).toEqual('me');
|
||||||
expect(msg_obj.get('is_delayed')).toEqual(false);
|
expect(msg_obj.get('is_delayed')).toEqual(false);
|
||||||
// Now check that the message appears inside the chatbox in the DOM
|
// Now check that the message appears inside the chatbox in the DOM
|
||||||
const msg_txt = view.el.querySelector('.chat-content .chat-msg .chat-msg__text').textContent;
|
const msg_el = await u.waitUntil(() => view.el.querySelector('.chat-content .chat-msg .chat-msg__text'));
|
||||||
expect(msg_txt).toEqual(msgtext);
|
expect(msg_el.textContent).toEqual(msgtext);
|
||||||
done();
|
done();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -751,7 +749,7 @@ describe("A Chat Message", function () {
|
|||||||
|
|
||||||
await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length);
|
await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length);
|
||||||
await mock.openChatBoxFor(_converse, contact_jid);
|
await mock.openChatBoxFor(_converse, contact_jid);
|
||||||
await mock.clearChatBoxMessages(_converse, contact_jid);
|
|
||||||
const one_day_ago = dayjs().subtract(1, 'day');
|
const one_day_ago = dayjs().subtract(1, 'day');
|
||||||
const chatbox = _converse.chatboxes.get(contact_jid);
|
const chatbox = _converse.chatboxes.get(contact_jid);
|
||||||
const view = _converse.chatboxviews.get(contact_jid);
|
const view = _converse.chatboxviews.get(contact_jid);
|
||||||
@ -766,7 +764,7 @@ describe("A Chat Message", function () {
|
|||||||
.c('delay', { xmlns:'urn:xmpp:delay', from: 'montague.lit', stamp: one_day_ago.toISOString() })
|
.c('delay', { xmlns:'urn:xmpp:delay', from: 'montague.lit', stamp: one_day_ago.toISOString() })
|
||||||
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
|
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
|
||||||
await _converse.handleMessageStanza(msg);
|
await _converse.handleMessageStanza(msg);
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
|
|
||||||
expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object));
|
expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object));
|
||||||
expect(chatbox.messages.length).toEqual(1);
|
expect(chatbox.messages.length).toEqual(1);
|
||||||
@ -798,7 +796,7 @@ describe("A Chat Message", function () {
|
|||||||
}).c('body').t(message).up()
|
}).c('body').t(message).up()
|
||||||
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
|
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
|
||||||
await _converse.handleMessageStanza(msg);
|
await _converse.handleMessageStanza(msg);
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
|
|
||||||
expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object));
|
expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object));
|
||||||
// Check that there is a <time> element, with the required props.
|
// Check that there is a <time> element, with the required props.
|
||||||
@ -823,9 +821,9 @@ describe("A Chat Message", function () {
|
|||||||
const msg_txt = sizzle('.chat-msg:last .chat-msg__text', view.msgs_container).pop().textContent;
|
const msg_txt = sizzle('.chat-msg:last .chat-msg__text', view.msgs_container).pop().textContent;
|
||||||
expect(msg_txt).toEqual(message);
|
expect(msg_txt).toEqual(message);
|
||||||
|
|
||||||
expect(view.msgs_container.querySelector('.chat-msg:last-child .chat-msg__text').textContent).toEqual(message);
|
expect(view.msgs_container.querySelector('converse-chat-message:last-child .chat-msg__text').textContent).toEqual(message);
|
||||||
expect(view.msgs_container.querySelector('.chat-msg:last-child .chat-msg__time').textContent.match(/^[0-9][0-9]:[0-9][0-9]/)).toBeTruthy();
|
expect(view.msgs_container.querySelector('converse-chat-message:last-child .chat-msg__time').textContent.match(/^[0-9][0-9]:[0-9][0-9]/)).toBeTruthy();
|
||||||
expect(view.msgs_container.querySelector('.chat-msg:last-child .chat-msg__author').textContent.trim()).toBe('Juliet Capulet');
|
expect(view.msgs_container.querySelector('converse-chat-message:last-child .chat-msg__author').textContent.trim()).toBe('Juliet Capulet');
|
||||||
done();
|
done();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -845,7 +843,7 @@ describe("A Chat Message", function () {
|
|||||||
expect(view.model.sendMessage).toHaveBeenCalled();
|
expect(view.model.sendMessage).toHaveBeenCalled();
|
||||||
const msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
|
const msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
|
||||||
expect(msg.textContent).toEqual(message);
|
expect(msg.textContent).toEqual(message);
|
||||||
expect(msg.innerHTML).toEqual('<p>This message contains <em>some</em> <b>markup</b></p>');
|
expect(msg.innerHTML.replace(/<!---->/g, '')).toEqual('<p>This message contains <em>some</em> <b>markup</b></p>');
|
||||||
done();
|
done();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -863,10 +861,10 @@ describe("A Chat Message", function () {
|
|||||||
spyOn(view.model, 'sendMessage').and.callThrough();
|
spyOn(view.model, 'sendMessage').and.callThrough();
|
||||||
mock.sendMessage(view, message);
|
mock.sendMessage(view, message);
|
||||||
expect(view.model.sendMessage).toHaveBeenCalled();
|
expect(view.model.sendMessage).toHaveBeenCalled();
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
const msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
|
const msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
|
||||||
expect(msg.textContent).toEqual(message);
|
expect(msg.textContent).toEqual(message);
|
||||||
expect(msg.innerHTML)
|
expect(msg.innerHTML.replace(/<!---->/g, ''))
|
||||||
.toEqual('This message contains a hyperlink: <a target="_blank" rel="noopener" href="http://www.opkode.com">www.opkode.com</a>');
|
.toEqual('This message contains a hyperlink: <a target="_blank" rel="noopener" href="http://www.opkode.com">www.opkode.com</a>');
|
||||||
done();
|
done();
|
||||||
}));
|
}));
|
||||||
@ -886,8 +884,8 @@ describe("A Chat Message", function () {
|
|||||||
<body>Hey\nHave you heard the news?</body>
|
<body>Hey\nHave you heard the news?</body>
|
||||||
</message>`);
|
</message>`);
|
||||||
_converse.connection._dataRecv(mock.createRequest(stanza));
|
_converse.connection._dataRecv(mock.createRequest(stanza));
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
expect(view.content.querySelector('.chat-msg__text').innerHTML).toBe('Hey<br>Have you heard the news?');
|
expect(view.content.querySelector('.chat-msg__text').innerHTML.replace(/<!---->/g, '')).toBe('Hey\nHave you heard the news?');
|
||||||
stanza = u.toStanza(`
|
stanza = u.toStanza(`
|
||||||
<message from="${contact_jid}"
|
<message from="${contact_jid}"
|
||||||
type="chat"
|
type="chat"
|
||||||
@ -895,8 +893,8 @@ describe("A Chat Message", function () {
|
|||||||
<body>Hey\n\n\nHave you heard the news?</body>
|
<body>Hey\n\n\nHave you heard the news?</body>
|
||||||
</message>`);
|
</message>`);
|
||||||
_converse.connection._dataRecv(mock.createRequest(stanza));
|
_converse.connection._dataRecv(mock.createRequest(stanza));
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
expect(view.content.querySelector('.message:last-child .chat-msg__text').innerHTML).toBe('Hey<br><br>Have you heard the news?');
|
expect(view.content.querySelector('converse-chat-message:last-child .chat-msg__text').innerHTML.replace(/<!---->/g, '')).toBe('Hey\n\nHave you heard the news?');
|
||||||
stanza = u.toStanza(`
|
stanza = u.toStanza(`
|
||||||
<message from="${contact_jid}"
|
<message from="${contact_jid}"
|
||||||
type="chat"
|
type="chat"
|
||||||
@ -904,8 +902,8 @@ describe("A Chat Message", function () {
|
|||||||
<body>Hey\nHave you heard\nthe news?</body>
|
<body>Hey\nHave you heard\nthe news?</body>
|
||||||
</message>`);
|
</message>`);
|
||||||
_converse.connection._dataRecv(mock.createRequest(stanza));
|
_converse.connection._dataRecv(mock.createRequest(stanza));
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
expect(view.content.querySelector('.message:last-child .chat-msg__text').innerHTML).toBe('Hey<br>Have you heard<br>the news?');
|
expect(view.content.querySelector('converse-chat-message:last-child .chat-msg__text').innerHTML.replace(/<!---->/g, '')).toBe('Hey\nHave you heard\nthe news?');
|
||||||
done();
|
done();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -925,16 +923,20 @@ describe("A Chat Message", function () {
|
|||||||
await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-image').length, 1000)
|
await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-image').length, 1000)
|
||||||
expect(view.model.sendMessage).toHaveBeenCalled();
|
expect(view.model.sendMessage).toHaveBeenCalled();
|
||||||
let msg = sizzle('.chat-content .chat-msg:last .chat-msg__text').pop();
|
let msg = sizzle('.chat-content .chat-msg:last .chat-msg__text').pop();
|
||||||
expect(msg.innerHTML.trim()).toEqual(
|
expect(msg.innerHTML.replace(/<!---->/g, '').trim()).toEqual(
|
||||||
`<a target="_blank" rel="noopener" href="${base_url}/logo/conversejs-filled.svg" class="chat-image__link"><img src="${message}" class="chat-image img-thumbnail"></a>`);
|
`<a class="chat-image__link" target="_blank" rel="noopener" href="${base_url}/logo/conversejs-filled.svg">`+
|
||||||
|
`<img class="chat-image img-thumbnail" src="${base_url}/logo/conversejs-filled.svg">`+
|
||||||
|
`</a>`);
|
||||||
|
|
||||||
message += "?param1=val1¶m2=val2";
|
message += "?param1=val1¶m2=val2";
|
||||||
mock.sendMessage(view, message);
|
mock.sendMessage(view, message);
|
||||||
await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-image').length === 2, 1000);
|
await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-image').length === 2, 1000);
|
||||||
expect(view.model.sendMessage).toHaveBeenCalled();
|
expect(view.model.sendMessage).toHaveBeenCalled();
|
||||||
msg = sizzle('.chat-content .chat-msg:last .chat-msg__text').pop();
|
msg = sizzle('.chat-content .chat-msg:last .chat-msg__text').pop();
|
||||||
expect(msg.innerHTML.trim()).toEqual(
|
expect(msg.innerHTML.replace(/<!---->/g, '').trim()).toEqual(
|
||||||
'<a target="_blank" rel="noopener" href="'+base_url+'/logo/conversejs-filled.svg?param1=val1&param2=val2" class="chat-image__link"><img'+
|
`<a class="chat-image__link" target="_blank" rel="noopener" href="${base_url}/logo/conversejs-filled.svg?param1=val1&param2=val2">`+
|
||||||
' src="'+message.replace(/&/g, '&')+'" class="chat-image img-thumbnail"></a>')
|
`<img class="chat-image img-thumbnail" src="${message.replace(/&/g, '&')}">`+
|
||||||
|
`</a>`);
|
||||||
|
|
||||||
// Test now with two images in one message
|
// Test now with two images in one message
|
||||||
message += ' hello world '+base_url+"/logo/conversejs-filled.svg";
|
message += ' hello world '+base_url+"/logo/conversejs-filled.svg";
|
||||||
@ -981,7 +983,7 @@ describe("A Chat Message", function () {
|
|||||||
|
|
||||||
it("will be correctly identified and rendered as a followup message",
|
it("will be correctly identified and rendered as a followup message",
|
||||||
mock.initConverse(
|
mock.initConverse(
|
||||||
['rosterGroupsFetched'], {},
|
['rosterGroupsFetched'], {'debounced_content_rendering': false},
|
||||||
async function (done, _converse) {
|
async function (done, _converse) {
|
||||||
|
|
||||||
await mock.waitForRoster(_converse, 'current');
|
await mock.waitForRoster(_converse, 'current');
|
||||||
@ -1006,7 +1008,7 @@ describe("A Chat Message", function () {
|
|||||||
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree());
|
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree());
|
||||||
await new Promise(resolve => _converse.on('chatBoxViewInitialized', resolve));
|
await new Promise(resolve => _converse.on('chatBoxViewInitialized', resolve));
|
||||||
const view = _converse.api.chatviews.get(sender_jid);
|
const view = _converse.api.chatviews.get(sender_jid);
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
|
|
||||||
jasmine.clock().tick(3*ONE_MINUTE_LATER);
|
jasmine.clock().tick(3*ONE_MINUTE_LATER);
|
||||||
_converse.handleMessageStanza($msg({
|
_converse.handleMessageStanza($msg({
|
||||||
@ -1016,7 +1018,7 @@ describe("A Chat Message", function () {
|
|||||||
'id': u.getUniqueId()
|
'id': u.getUniqueId()
|
||||||
}).c('body').t("Another message 3 minutes later").up()
|
}).c('body').t("Another message 3 minutes later").up()
|
||||||
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree());
|
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree());
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
|
|
||||||
jasmine.clock().tick(11*ONE_MINUTE_LATER);
|
jasmine.clock().tick(11*ONE_MINUTE_LATER);
|
||||||
_converse.handleMessageStanza($msg({
|
_converse.handleMessageStanza($msg({
|
||||||
@ -1026,7 +1028,7 @@ describe("A Chat Message", function () {
|
|||||||
'id': u.getUniqueId()
|
'id': u.getUniqueId()
|
||||||
}).c('body').t("Another message 14 minutes since we started").up()
|
}).c('body').t("Another message 14 minutes since we started").up()
|
||||||
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree());
|
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree());
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
|
|
||||||
jasmine.clock().tick(1*ONE_MINUTE_LATER);
|
jasmine.clock().tick(1*ONE_MINUTE_LATER);
|
||||||
|
|
||||||
@ -1037,26 +1039,29 @@ describe("A Chat Message", function () {
|
|||||||
'id': _converse.connection.getUniqueId()
|
'id': _converse.connection.getUniqueId()
|
||||||
}).c('body').t("Another message 1 minute and 1 second since the previous one").up()
|
}).c('body').t("Another message 1 minute and 1 second since the previous one").up()
|
||||||
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree());
|
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree());
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
|
|
||||||
jasmine.clock().tick(1*ONE_MINUTE_LATER);
|
jasmine.clock().tick(1*ONE_MINUTE_LATER);
|
||||||
await mock.sendMessage(view, "Another message within 10 minutes, but from a different person");
|
await mock.sendMessage(view, "Another message within 10 minutes, but from a different person");
|
||||||
|
|
||||||
expect(view.content.querySelectorAll('.message').length).toBe(6);
|
expect(view.content.querySelectorAll('.message').length).toBe(6);
|
||||||
expect(view.content.querySelectorAll('.chat-msg').length).toBe(5);
|
expect(view.content.querySelectorAll('.chat-msg').length).toBe(5);
|
||||||
expect(u.hasClass('chat-msg--followup', view.content.querySelector('.message:nth-child(2)'))).toBe(false);
|
|
||||||
expect(view.content.querySelector('.message:nth-child(2) .chat-msg__text').textContent).toBe("A message");
|
const nth_child = (n) => `converse-chat-message:nth-child(${n}) .chat-msg`;
|
||||||
expect(u.hasClass('chat-msg--followup', view.content.querySelector('.message:nth-child(3)'))).toBe(true);
|
expect(u.hasClass('chat-msg--followup', view.content.querySelector(nth_child(2)))).toBe(false);
|
||||||
expect(view.content.querySelector('.message:nth-child(3) .chat-msg__text').textContent).toBe(
|
expect(view.content.querySelector(`${nth_child(2)} .chat-msg__text`).textContent).toBe("A message");
|
||||||
|
|
||||||
|
expect(u.hasClass('chat-msg--followup', view.content.querySelector(nth_child(3)))).toBe(true);
|
||||||
|
expect(view.content.querySelector(`${nth_child(3)} .chat-msg__text`).textContent).toBe(
|
||||||
"Another message 3 minutes later");
|
"Another message 3 minutes later");
|
||||||
expect(u.hasClass('chat-msg--followup', view.content.querySelector('.message:nth-child(4)'))).toBe(false);
|
expect(u.hasClass('chat-msg--followup', view.content.querySelector(nth_child(4)))).toBe(false);
|
||||||
expect(view.content.querySelector('.message:nth-child(4) .chat-msg__text').textContent).toBe(
|
expect(view.content.querySelector(`${nth_child(4)} .chat-msg__text`).textContent).toBe(
|
||||||
"Another message 14 minutes since we started");
|
"Another message 14 minutes since we started");
|
||||||
expect(u.hasClass('chat-msg--followup', view.content.querySelector('.message:nth-child(5)'))).toBe(true);
|
expect(u.hasClass('chat-msg--followup', view.content.querySelector(nth_child(5)))).toBe(true);
|
||||||
expect(view.content.querySelector('.message:nth-child(5) .chat-msg__text').textContent).toBe(
|
expect(view.content.querySelector(`${nth_child(5)} .chat-msg__text`).textContent).toBe(
|
||||||
"Another message 1 minute and 1 second since the previous one");
|
"Another message 1 minute and 1 second since the previous one");
|
||||||
expect(u.hasClass('chat-msg--followup', view.content.querySelector('.message:nth-child(6)'))).toBe(false);
|
expect(u.hasClass('chat-msg--followup', view.content.querySelector(nth_child(6)))).toBe(false);
|
||||||
expect(view.content.querySelector('.message:nth-child(6) .chat-msg__text').textContent).toBe(
|
expect(view.content.querySelector(`${nth_child(6)} .chat-msg__text`).textContent).toBe(
|
||||||
"Another message within 10 minutes, but from a different person");
|
"Another message within 10 minutes, but from a different person");
|
||||||
|
|
||||||
// Let's add a delayed, inbetween message
|
// Let's add a delayed, inbetween message
|
||||||
@ -1070,26 +1075,32 @@ describe("A Chat Message", function () {
|
|||||||
}).c('body').t("A delayed message, sent 5 minutes since we started").up()
|
}).c('body').t("A delayed message, sent 5 minutes since we started").up()
|
||||||
.c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp': dayjs(base_time).add(5, 'minutes').toISOString()})
|
.c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp': dayjs(base_time).add(5, 'minutes').toISOString()})
|
||||||
.tree());
|
.tree());
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
|
|
||||||
expect(view.content.querySelectorAll('.message').length).toBe(7);
|
expect(view.content.querySelectorAll('.message').length).toBe(7);
|
||||||
expect(view.content.querySelectorAll('.chat-msg').length).toBe(6);
|
expect(view.content.querySelectorAll('.chat-msg').length).toBe(6);
|
||||||
expect(u.hasClass('chat-msg--followup', view.content.querySelector('.message:nth-child(2)'))).toBe(false);
|
expect(u.hasClass('chat-msg--followup', view.content.querySelector(nth_child(2)))).toBe(false);
|
||||||
expect(view.content.querySelector('.message:nth-child(2) .chat-msg__text').textContent).toBe("A message");
|
expect(view.content.querySelector(`${nth_child(2)} .chat-msg__text`).textContent).toBe("A message");
|
||||||
expect(u.hasClass('chat-msg--followup', view.content.querySelector('.message:nth-child(3)'))).toBe(true);
|
|
||||||
expect(view.content.querySelector('.message:nth-child(3) .chat-msg__text').textContent).toBe(
|
|
||||||
|
expect(u.hasClass('chat-msg--followup', view.content.querySelector(nth_child(3)))).toBe(true);
|
||||||
|
expect(view.content.querySelector(`${nth_child(3)} .chat-msg__text`).textContent).toBe(
|
||||||
"Another message 3 minutes later");
|
"Another message 3 minutes later");
|
||||||
expect(u.hasClass('chat-msg--followup', view.content.querySelector('.message:nth-child(4)'))).toBe(true);
|
expect(u.hasClass('chat-msg--followup', view.content.querySelector(nth_child(4)))).toBe(true);
|
||||||
expect(view.content.querySelector('.message:nth-child(4) .chat-msg__text').textContent).toBe(
|
expect(view.content.querySelector(`${nth_child(4)} .chat-msg__text`).textContent).toBe(
|
||||||
"A delayed message, sent 5 minutes since we started");
|
"A delayed message, sent 5 minutes since we started");
|
||||||
|
|
||||||
expect(u.hasClass('chat-msg--followup', view.content.querySelector('.message:nth-child(5)'))).toBe(false);
|
expect(u.hasClass('chat-msg--followup', view.content.querySelector(nth_child(5)))).toBe(true);
|
||||||
expect(view.content.querySelector('.message:nth-child(5) .chat-msg__text').textContent).toBe(
|
expect(view.content.querySelector(`${nth_child(5)} .chat-msg__text`).textContent).toBe(
|
||||||
"Another message 14 minutes since we started");
|
"Another message 14 minutes since we started");
|
||||||
expect(u.hasClass('chat-msg--followup', view.content.querySelector('.message:nth-child(6)'))).toBe(true);
|
|
||||||
expect(view.content.querySelector('.message:nth-child(6) .chat-msg__text').textContent).toBe(
|
expect(u.hasClass('chat-msg--followup', view.content.querySelector(nth_child(6)))).toBe(true);
|
||||||
|
expect(view.content.querySelector(`${nth_child(6)} .chat-msg__text`).textContent).toBe(
|
||||||
"Another message 1 minute and 1 second since the previous one");
|
"Another message 1 minute and 1 second since the previous one");
|
||||||
expect(u.hasClass('chat-msg--followup', view.content.querySelector('.message:nth-child(7)'))).toBe(false);
|
|
||||||
|
expect(u.hasClass('chat-msg--followup', view.content.querySelector(nth_child(7)))).toBe(false);
|
||||||
|
expect(view.content.querySelector(`${nth_child(7)} .chat-msg__text`).textContent).toBe(
|
||||||
|
"Another message within 10 minutes, but from a different person");
|
||||||
|
|
||||||
_converse.handleMessageStanza(
|
_converse.handleMessageStanza(
|
||||||
$msg({
|
$msg({
|
||||||
@ -1101,29 +1112,28 @@ describe("A Chat Message", function () {
|
|||||||
.c('body').t("A carbon message 4 minutes later").up()
|
.c('body').t("A carbon message 4 minutes later").up()
|
||||||
.c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':dayjs(base_time).add(4, 'minutes').toISOString()})
|
.c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':dayjs(base_time).add(4, 'minutes').toISOString()})
|
||||||
.tree());
|
.tree());
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
|
|
||||||
expect(view.content.querySelectorAll('.message').length).toBe(8);
|
|
||||||
expect(view.content.querySelectorAll('.chat-msg').length).toBe(7);
|
expect(view.content.querySelectorAll('.chat-msg').length).toBe(7);
|
||||||
expect(u.hasClass('chat-msg--followup', view.content.querySelector('.message:nth-child(2)'))).toBe(false);
|
expect(u.hasClass('chat-msg--followup', view.content.querySelector(nth_child(2)))).toBe(false);
|
||||||
expect(view.content.querySelector('.message:nth-child(2) .chat-msg__text').textContent).toBe("A message");
|
expect(view.content.querySelector(`${nth_child(2)} .chat-msg__text`).textContent).toBe("A message");
|
||||||
expect(u.hasClass('chat-msg--followup', view.content.querySelector('.message:nth-child(3)'))).toBe(true);
|
expect(u.hasClass('chat-msg--followup', view.content.querySelector(nth_child(3)))).toBe(true);
|
||||||
expect(view.content.querySelector('.message:nth-child(3) .chat-msg__text').textContent).toBe(
|
expect(view.content.querySelector(`${nth_child(3)} .chat-msg__text`).textContent).toBe(
|
||||||
"Another message 3 minutes later");
|
"Another message 3 minutes later");
|
||||||
expect(u.hasClass('chat-msg--followup', view.content.querySelector('.message:nth-child(4)'))).toBe(false);
|
expect(u.hasClass('chat-msg--followup', view.content.querySelector(nth_child(4)))).toBe(false);
|
||||||
expect(view.content.querySelector('.message:nth-child(4) .chat-msg__text').textContent).toBe(
|
expect(view.content.querySelector(`${nth_child(4)} .chat-msg__text`).textContent).toBe(
|
||||||
"A carbon message 4 minutes later");
|
"A carbon message 4 minutes later");
|
||||||
expect(u.hasClass('chat-msg--followup', view.content.querySelector('.message:nth-child(5)'))).toBe(false);
|
expect(u.hasClass('chat-msg--followup', view.content.querySelector(nth_child(5)))).toBe(false);
|
||||||
expect(view.content.querySelector('.message:nth-child(5) .chat-msg__text').textContent).toBe(
|
expect(view.content.querySelector(`${nth_child(5)} .chat-msg__text`).textContent).toBe(
|
||||||
"A delayed message, sent 5 minutes since we started");
|
"A delayed message, sent 5 minutes since we started");
|
||||||
expect(u.hasClass('chat-msg--followup', view.content.querySelector('.message:nth-child(6)'))).toBe(false);
|
expect(u.hasClass('chat-msg--followup', view.content.querySelector(nth_child(6)))).toBe(true);
|
||||||
expect(view.content.querySelector('.message:nth-child(6) .chat-msg__text').textContent).toBe(
|
expect(view.content.querySelector(`${nth_child(6)} .chat-msg__text`).textContent).toBe(
|
||||||
"Another message 14 minutes since we started");
|
"Another message 14 minutes since we started");
|
||||||
expect(u.hasClass('chat-msg--followup', view.content.querySelector('.message:nth-child(7)'))).toBe(true);
|
expect(u.hasClass('chat-msg--followup', view.content.querySelector(nth_child(7)))).toBe(true);
|
||||||
expect(view.content.querySelector('.message:nth-child(7) .chat-msg__text').textContent).toBe(
|
expect(view.content.querySelector(`${nth_child(7)} .chat-msg__text`).textContent).toBe(
|
||||||
"Another message 1 minute and 1 second since the previous one");
|
"Another message 1 minute and 1 second since the previous one");
|
||||||
expect(u.hasClass('chat-msg--followup', view.content.querySelector('.message:nth-child(8)'))).toBe(false);
|
expect(u.hasClass('chat-msg--followup', view.content.querySelector(nth_child(8)))).toBe(false);
|
||||||
expect(view.content.querySelector('.message:nth-child(8) .chat-msg__text').textContent).toBe(
|
expect(view.content.querySelector(`${nth_child(8)} .chat-msg__text`).textContent).toBe(
|
||||||
"Another message within 10 minutes, but from a different person");
|
"Another message within 10 minutes, but from a different person");
|
||||||
|
|
||||||
jasmine.clock().uninstall();
|
jasmine.clock().uninstall();
|
||||||
@ -1205,7 +1215,7 @@ describe("A Chat Message", function () {
|
|||||||
});
|
});
|
||||||
const chatbox = _converse.chatboxes.get(contact_jid);
|
const chatbox = _converse.chatboxes.get(contact_jid);
|
||||||
expect(chatbox).toBeDefined();
|
expect(chatbox).toBeDefined();
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
let msg_obj = chatbox.messages.models[0];
|
let msg_obj = chatbox.messages.models[0];
|
||||||
let msg_id = msg_obj.get('msgid');
|
let msg_id = msg_obj.get('msgid');
|
||||||
let msg = $msg({
|
let msg = $msg({
|
||||||
@ -1214,8 +1224,7 @@ describe("A Chat Message", function () {
|
|||||||
'id': u.getUniqueId(),
|
'id': u.getUniqueId(),
|
||||||
}).c('received', {'id': msg_id, xmlns: Strophe.NS.RECEIPTS}).up().tree();
|
}).c('received', {'id': msg_id, xmlns: Strophe.NS.RECEIPTS}).up().tree();
|
||||||
_converse.connection._dataRecv(mock.createRequest(msg));
|
_converse.connection._dataRecv(mock.createRequest(msg));
|
||||||
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__receipt').length === 1);
|
||||||
expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(1);
|
|
||||||
|
|
||||||
// Also handle receipts with type 'chat'. See #1353
|
// Also handle receipts with type 'chat'. See #1353
|
||||||
spyOn(_converse, 'handleMessageStanza').and.callThrough();
|
spyOn(_converse, 'handleMessageStanza').and.callThrough();
|
||||||
@ -1225,7 +1234,7 @@ describe("A Chat Message", function () {
|
|||||||
preventDefault: function preventDefault () {},
|
preventDefault: function preventDefault () {},
|
||||||
keyCode: 13 // Enter
|
keyCode: 13 // Enter
|
||||||
});
|
});
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
|
|
||||||
msg_obj = chatbox.messages.models[1];
|
msg_obj = chatbox.messages.models[1];
|
||||||
msg_id = msg_obj.get('msgid');
|
msg_id = msg_obj.get('msgid');
|
||||||
@ -1236,8 +1245,7 @@ describe("A Chat Message", function () {
|
|||||||
'id': u.getUniqueId(),
|
'id': u.getUniqueId(),
|
||||||
}).c('received', {'id': msg_id, xmlns: Strophe.NS.RECEIPTS}).up().tree();
|
}).c('received', {'id': msg_id, xmlns: Strophe.NS.RECEIPTS}).up().tree();
|
||||||
_converse.connection._dataRecv(mock.createRequest(msg));
|
_converse.connection._dataRecv(mock.createRequest(msg));
|
||||||
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__receipt').length === 2);
|
||||||
expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(2);
|
|
||||||
expect(_converse.handleMessageStanza.calls.count()).toBe(1);
|
expect(_converse.handleMessageStanza.calls.count()).toBe(1);
|
||||||
done();
|
done();
|
||||||
}));
|
}));
|
||||||
@ -1377,7 +1385,7 @@ describe("A Chat Message", function () {
|
|||||||
'type': 'chat',
|
'type': 'chat',
|
||||||
'id': msg_id,
|
'id': msg_id,
|
||||||
}).c('body').t('But soft, what light through yonder airlock breaks?').tree());
|
}).c('body').t('But soft, what light through yonder airlock breaks?').tree());
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
|
expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
|
||||||
expect(view.el.querySelector('.chat-msg__text').textContent)
|
expect(view.el.querySelector('.chat-msg__text').textContent)
|
||||||
.toBe('But soft, what light through yonder airlock breaks?');
|
.toBe('But soft, what light through yonder airlock breaks?');
|
||||||
@ -1411,7 +1419,7 @@ describe("A Chat Message", function () {
|
|||||||
expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
|
expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
|
||||||
expect(view.el.querySelectorAll('.chat-msg__content .fa-edit').length).toBe(1);
|
expect(view.el.querySelectorAll('.chat-msg__content .fa-edit').length).toBe(1);
|
||||||
view.el.querySelector('.chat-msg__content .fa-edit').click();
|
view.el.querySelector('.chat-msg__content .fa-edit').click();
|
||||||
const modal = view.model.messages.at(0).message_versions_modal;
|
const modal = await u.waitUntil(() => view.el.querySelector('converse-chat-message').message_versions_modal);
|
||||||
await u.waitUntil(() => u.isVisible(modal.el), 1000);
|
await u.waitUntil(() => u.isVisible(modal.el), 1000);
|
||||||
const older_msgs = modal.el.querySelectorAll('.older-msg');
|
const older_msgs = modal.el.querySelectorAll('.older-msg');
|
||||||
expect(older_msgs.length).toBe(2);
|
expect(older_msgs.length).toBe(2);
|
||||||
@ -1456,7 +1464,7 @@ describe("A Chat Message", function () {
|
|||||||
|
|
||||||
await _converse.handleMessageStanza(msg);
|
await _converse.handleMessageStanza(msg);
|
||||||
const view = await u.waitUntil(() => _converse.api.chatviews.get(sender_jid));
|
const view = await u.waitUntil(() => _converse.api.chatviews.get(sender_jid));
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object));
|
expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object));
|
||||||
|
|
||||||
// Check that the chatbox and its view now exist
|
// Check that the chatbox and its view now exist
|
||||||
@ -1508,7 +1516,7 @@ describe("A Chat Message", function () {
|
|||||||
_converse.allow_non_roster_messaging = true;
|
_converse.allow_non_roster_messaging = true;
|
||||||
await _converse.handleMessageStanza(msg);
|
await _converse.handleMessageStanza(msg);
|
||||||
view = _converse.chatboxviews.get(sender_jid);
|
view = _converse.chatboxviews.get(sender_jid);
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object));
|
expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object));
|
||||||
// Check that the chatbox and its view now exist
|
// Check that the chatbox and its view now exist
|
||||||
chatbox = await _converse.api.chats.get(sender_jid);
|
chatbox = await _converse.api.chats.get(sender_jid);
|
||||||
@ -1563,7 +1571,7 @@ describe("A Chat Message", function () {
|
|||||||
let msg_text = 'This message will not be sent, due to an error';
|
let msg_text = 'This message will not be sent, due to an error';
|
||||||
const view = _converse.api.chatviews.get(sender_jid);
|
const view = _converse.api.chatviews.get(sender_jid);
|
||||||
const message = await view.model.sendMessage(msg_text);
|
const message = await view.model.sendMessage(msg_text);
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
let msg_txt = sizzle('.chat-msg:last .chat-msg__text', view.content).pop().textContent;
|
let msg_txt = sizzle('.chat-msg:last .chat-msg__text', view.content).pop().textContent;
|
||||||
expect(msg_txt).toEqual(msg_text);
|
expect(msg_txt).toEqual(msg_text);
|
||||||
|
|
||||||
@ -1598,8 +1606,9 @@ describe("A Chat Message", function () {
|
|||||||
.c('text', { 'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas" })
|
.c('text', { 'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas" })
|
||||||
.t('Server-to-server connection failed: Connecting failed: connection timeout');
|
.t('Server-to-server connection failed: Connecting failed: connection timeout');
|
||||||
_converse.connection._dataRecv(mock.createRequest(stanza));
|
_converse.connection._dataRecv(mock.createRequest(stanza));
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await u.waitUntil(() => view.content.querySelector('.chat-msg__error').textContent.trim() === error_txt);
|
||||||
expect(view.content.querySelector('.chat-error').textContent.trim()).toEqual(error_txt);
|
|
||||||
|
const other_error_txt = 'Server-to-server connection failed: Connecting failed: connection timeout';
|
||||||
stanza = $msg({
|
stanza = $msg({
|
||||||
'to': _converse.connection.jid,
|
'to': _converse.connection.jid,
|
||||||
'type': 'error',
|
'type': 'error',
|
||||||
@ -1609,10 +1618,10 @@ describe("A Chat Message", function () {
|
|||||||
.c('error', {'type': 'cancel'})
|
.c('error', {'type': 'cancel'})
|
||||||
.c('remote-server-not-found', { 'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas" }).up()
|
.c('remote-server-not-found', { 'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas" }).up()
|
||||||
.c('text', { 'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas" })
|
.c('text', { 'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas" })
|
||||||
.t('Server-to-server connection failed: Connecting failed: connection timeout');
|
.t(other_error_txt);
|
||||||
_converse.connection._dataRecv(mock.createRequest(stanza));
|
_converse.connection._dataRecv(mock.createRequest(stanza));
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await u.waitUntil(() =>
|
||||||
expect(view.content.querySelectorAll('.chat-error').length).toEqual(2);
|
view.content.querySelector('converse-chat-message:last-child .chat-msg__error').textContent.trim() === other_error_txt);
|
||||||
|
|
||||||
// We don't render duplicates
|
// We don't render duplicates
|
||||||
stanza = $msg({
|
stanza = $msg({
|
||||||
@ -1626,13 +1635,11 @@ describe("A Chat Message", function () {
|
|||||||
.c('text', { 'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas" })
|
.c('text', { 'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas" })
|
||||||
.t('Server-to-server connection failed: Connecting failed: connection timeout');
|
.t('Server-to-server connection failed: Connecting failed: connection timeout');
|
||||||
_converse.connection._dataRecv(mock.createRequest(stanza));
|
_converse.connection._dataRecv(mock.createRequest(stanza));
|
||||||
expect(view.content.querySelectorAll('.chat-error').length).toEqual(2);
|
expect(view.content.querySelectorAll('.chat-msg__error').length).toEqual(2);
|
||||||
|
|
||||||
msg_text = 'This message will be sent, and also receive an error';
|
msg_text = 'This message will be sent, and also receive an error';
|
||||||
const third_message = await view.model.sendMessage(msg_text);
|
const third_message = await view.model.sendMessage(msg_text);
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await u.waitUntil(() => sizzle('converse-chat-message:last-child .chat-msg__text', view.content).pop()?.textContent === msg_text);
|
||||||
msg_txt = sizzle('.chat-msg:last .chat-msg__text', view.content).pop().textContent;
|
|
||||||
expect(msg_txt).toEqual(msg_text);
|
|
||||||
|
|
||||||
// A different error message will however render
|
// A different error message will however render
|
||||||
stanza = $msg({
|
stanza = $msg({
|
||||||
@ -1647,8 +1654,8 @@ describe("A Chat Message", function () {
|
|||||||
.t('Something else went wrong as well');
|
.t('Something else went wrong as well');
|
||||||
_converse.connection._dataRecv(mock.createRequest(stanza));
|
_converse.connection._dataRecv(mock.createRequest(stanza));
|
||||||
await u.waitUntil(() => view.model.messages.length > 3);
|
await u.waitUntil(() => view.model.messages.length > 3);
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
expect(view.content.querySelectorAll('.chat-error').length).toEqual(3);
|
expect(view.content.querySelectorAll('.chat-error').length).toEqual(1);
|
||||||
done();
|
done();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -1709,7 +1716,7 @@ describe("A Chat Message", function () {
|
|||||||
id: _converse.connection.getUniqueId(),
|
id: _converse.connection.getUniqueId(),
|
||||||
}).c('body').t('Message: '+i).up()
|
}).c('body').t('Message: '+i).up()
|
||||||
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree());
|
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree());
|
||||||
promises.push(new Promise(resolve => view.once('messageInserted', resolve)));
|
promises.push(new Promise(resolve => view.model.messages.once('rendered', resolve)));
|
||||||
}
|
}
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
// XXX Fails on Travis
|
// XXX Fails on Travis
|
||||||
@ -1728,7 +1735,7 @@ describe("A Chat Message", function () {
|
|||||||
id: u.getUniqueId()
|
id: u.getUniqueId()
|
||||||
}).c('body').t(message).up()
|
}).c('body').t(message).up()
|
||||||
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree());
|
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree());
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
await u.waitUntil(() => view.model.messages.length > 20, 1000);
|
await u.waitUntil(() => view.model.messages.length > 20, 1000);
|
||||||
// Now check that the message appears inside the chatbox in the DOM
|
// Now check that the message appears inside the chatbox in the DOM
|
||||||
const msg_txt = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop().textContent;
|
const msg_txt = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop().textContent;
|
||||||
@ -1813,16 +1820,16 @@ describe("A Chat Message", function () {
|
|||||||
<x xmlns="jabber:x:oob"><url>https://montague.lit/audio.mp3</url></x>
|
<x xmlns="jabber:x:oob"><url>https://montague.lit/audio.mp3</url></x>
|
||||||
</message>`)
|
</message>`)
|
||||||
_converse.connection._dataRecv(mock.createRequest(stanza));
|
_converse.connection._dataRecv(mock.createRequest(stanza));
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-msg audio').length, 1000);
|
await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-msg audio').length, 1000);
|
||||||
let msg = view.el.querySelector('.chat-msg .chat-msg__text');
|
let msg = view.el.querySelector('.chat-msg .chat-msg__text');
|
||||||
expect(msg.classList.length).toEqual(1);
|
expect(msg.classList.length).toEqual(1);
|
||||||
expect(u.hasClass('chat-msg__text', msg)).toBe(true);
|
expect(u.hasClass('chat-msg__text', msg)).toBe(true);
|
||||||
expect(msg.textContent).toEqual('Have you heard this funny audio?');
|
expect(msg.textContent).toEqual('Have you heard this funny audio?');
|
||||||
let media = view.el.querySelector('.chat-msg .chat-msg__media');
|
let media = view.el.querySelector('.chat-msg .chat-msg__media');
|
||||||
expect(media.innerHTML.replace(/(\r\n|\n|\r)/gm, "")).toEqual(
|
expect(media.innerHTML.replace(/<!---->/g, '').replace(/(\r\n|\n|\r)/gm, "").trim()).toEqual(
|
||||||
`<!----> <audio controls="" src="https://montague.lit/audio.mp3"></audio> `+
|
`<audio controls="" src="https://montague.lit/audio.mp3"></audio> `+
|
||||||
`<a target="_blank" rel="noopener" href="https://montague.lit/audio.mp3"><!---->Download audio file "audio.mp3"<!----></a><!---->`);
|
`<a target="_blank" rel="noopener" href="https://montague.lit/audio.mp3">Download audio file "audio.mp3"</a>`);
|
||||||
|
|
||||||
// If the <url> and <body> contents is the same, don't duplicate.
|
// If the <url> and <body> contents is the same, don't duplicate.
|
||||||
stanza = u.toStanza(`
|
stanza = u.toStanza(`
|
||||||
@ -1833,14 +1840,14 @@ describe("A Chat Message", function () {
|
|||||||
<x xmlns="jabber:x:oob"><url>https://montague.lit/audio.mp3</url></x>
|
<x xmlns="jabber:x:oob"><url>https://montague.lit/audio.mp3</url></x>
|
||||||
</message>`);
|
</message>`);
|
||||||
_converse.connection._dataRecv(mock.createRequest(stanza));
|
_converse.connection._dataRecv(mock.createRequest(stanza));
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
msg = view.el.querySelector('.chat-msg:last-child .chat-msg__text');
|
msg = view.el.querySelector('.chat-msg:last-child .chat-msg__text');
|
||||||
expect(msg.innerHTML).toEqual('<!-- message gets added here via renderMessage -->'); // Emtpy
|
expect(msg.innerHTML.replace(/<!---->/g, '')).toEqual('Have you heard this funny audio?'); // Emtpy
|
||||||
media = view.el.querySelector('.chat-msg:last-child .chat-msg__media');
|
media = view.el.querySelector('.chat-msg:last-child .chat-msg__media');
|
||||||
expect(media.innerHTML.replace(/(\r\n|\n|\r)/gm, "")).toEqual(
|
expect(media.innerHTML.replace(/<!---->/g, '').replace(/(\r\n|\n|\r)/gm, "").trim()).toEqual(
|
||||||
`<!----> <audio controls="" src="https://montague.lit/audio.mp3"></audio> `+
|
`<audio controls="" src="https://montague.lit/audio.mp3"></audio> `+
|
||||||
`<a target="_blank" rel="noopener" href="https://montague.lit/audio.mp3">`+
|
`<a target="_blank" rel="noopener" href="https://montague.lit/audio.mp3">`+
|
||||||
`<!---->Download audio file "audio.mp3"<!----></a><!---->`);
|
`Download audio file "audio.mp3"</a>`);
|
||||||
done();
|
done();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -1881,9 +1888,9 @@ describe("A Chat Message", function () {
|
|||||||
<x xmlns="jabber:x:oob"><url>https://montague.lit/video.mp4</url></x>
|
<x xmlns="jabber:x:oob"><url>https://montague.lit/video.mp4</url></x>
|
||||||
</message>`);
|
</message>`);
|
||||||
_converse.connection._dataRecv(mock.createRequest(stanza));
|
_converse.connection._dataRecv(mock.createRequest(stanza));
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
msg = view.el.querySelector('.chat-msg:last-child .chat-msg__text');
|
msg = view.el.querySelector('.chat-msg:last-child .chat-msg__text');
|
||||||
expect(msg.innerHTML).toEqual('<!-- message gets added here via renderMessage -->'); // Emtpy
|
expect(msg.innerHTML.replace(/<!---->/g, '')).toEqual('Have you seen this funny video?');
|
||||||
media = view.el.querySelector('.chat-msg:last-child .chat-msg__media');
|
media = view.el.querySelector('.chat-msg:last-child .chat-msg__media');
|
||||||
expect(media.innerHTML.replace(/(\r\n|\n|\r)/gm, "")).toEqual(
|
expect(media.innerHTML.replace(/(\r\n|\n|\r)/gm, "")).toEqual(
|
||||||
`<!----><video controls="" preload="metadata" style="max-height: 50vh" src="https://montague.lit/video.mp4"></video><!---->`);
|
`<!----><video controls="" preload="metadata" style="max-height: 50vh" src="https://montague.lit/video.mp4"></video><!---->`);
|
||||||
@ -1908,7 +1915,7 @@ describe("A Chat Message", function () {
|
|||||||
<x xmlns="jabber:x:oob"><url>https://montague.lit/funny.pdf</url></x>
|
<x xmlns="jabber:x:oob"><url>https://montague.lit/funny.pdf</url></x>
|
||||||
</message>`);
|
</message>`);
|
||||||
_converse.connection._dataRecv(mock.createRequest(stanza));
|
_converse.connection._dataRecv(mock.createRequest(stanza));
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-msg a').length, 1000);
|
await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-msg a').length, 1000);
|
||||||
const msg = view.el.querySelector('.chat-msg .chat-msg__text');
|
const msg = view.el.querySelector('.chat-msg .chat-msg__text');
|
||||||
expect(u.hasClass('chat-msg__text', msg)).toBe(true);
|
expect(u.hasClass('chat-msg__text', msg)).toBe(true);
|
||||||
@ -2048,7 +2055,7 @@ describe("A XEP-0333 Chat Marker", function () {
|
|||||||
<stanza-id xmlns="urn:xmpp:sid:0" id="IxVDLJ0RYbWcWvqC" by="${_converse.bare_jid}"/>
|
<stanza-id xmlns="urn:xmpp:sid:0" id="IxVDLJ0RYbWcWvqC" by="${_converse.bare_jid}"/>
|
||||||
</message>`);
|
</message>`);
|
||||||
_converse.connection._dataRecv(mock.createRequest(stanza));
|
_converse.connection._dataRecv(mock.createRequest(stanza));
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
|
expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
|
||||||
expect(view.model.messages.length).toBe(1);
|
expect(view.model.messages.length).toBe(1);
|
||||||
|
|
||||||
|
10
spec/mock.js
10
spec/mock.js
@ -4,6 +4,8 @@ let _converse, initConverse;
|
|||||||
|
|
||||||
const converseLoaded = new Promise(resolve => window.addEventListener('converse-loaded', resolve));
|
const converseLoaded = new Promise(resolve => window.addEventListener('converse-loaded', resolve));
|
||||||
|
|
||||||
|
jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000;
|
||||||
|
|
||||||
mock.initConverse = function (promise_names=[], settings=null, func) {
|
mock.initConverse = function (promise_names=[], settings=null, func) {
|
||||||
if (typeof promise_names === "function") {
|
if (typeof promise_names === "function") {
|
||||||
func = promise_names;
|
func = promise_names;
|
||||||
@ -337,12 +339,6 @@ window.addEventListener('converse-loaded', () => {
|
|||||||
await view.model.messages.fetched;
|
await view.model.messages.fetched;
|
||||||
};
|
};
|
||||||
|
|
||||||
mock.clearChatBoxMessages = function (converse, jid) {
|
|
||||||
const view = converse.chatboxviews.get(jid);
|
|
||||||
view.msgs_container.innerHTML = '';
|
|
||||||
return view.model.messages.clearStore();
|
|
||||||
};
|
|
||||||
|
|
||||||
mock.createContact = async function (_converse, name, ask, requesting, subscription) {
|
mock.createContact = async function (_converse, name, ask, requesting, subscription) {
|
||||||
const jid = name.replace(/ /g,'.').toLowerCase() + '@montague.lit';
|
const jid = name.replace(/ /g,'.').toLowerCase() + '@montague.lit';
|
||||||
if (_converse.roster.get(jid)) {
|
if (_converse.roster.get(jid)) {
|
||||||
@ -449,7 +445,7 @@ window.addEventListener('converse-loaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mock.sendMessage = function (view, message) {
|
mock.sendMessage = function (view, message) {
|
||||||
const promise = new Promise(resolve => view.once('messageInserted', resolve));
|
const promise = new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
view.el.querySelector('.chat-textarea').value = message;
|
view.el.querySelector('.chat-textarea').value = message;
|
||||||
view.onKeyDown({
|
view.onKeyDown({
|
||||||
target: view.el.querySelector('textarea.chat-textarea'),
|
target: view.el.querySelector('textarea.chat-textarea'),
|
||||||
|
143
spec/muc.js
143
spec/muc.js
@ -1,14 +1,14 @@
|
|||||||
/*global mock */
|
/*global mock */
|
||||||
|
|
||||||
const _ = converse.env._,
|
const _ = converse.env._;
|
||||||
$pres = converse.env.$pres,
|
const $pres = converse.env.$pres;
|
||||||
$iq = converse.env.$iq,
|
const $iq = converse.env.$iq;
|
||||||
$msg = converse.env.$msg,
|
const $msg = converse.env.$msg;
|
||||||
Model = converse.env.Model,
|
const Model = converse.env.Model;
|
||||||
Strophe = converse.env.Strophe,
|
const Strophe = converse.env.Strophe;
|
||||||
Promise = converse.env.Promise,
|
const Promise = converse.env.Promise;
|
||||||
sizzle = converse.env.sizzle,
|
const sizzle = converse.env.sizzle;
|
||||||
u = converse.env.utils;
|
const u = converse.env.utils;
|
||||||
|
|
||||||
describe("Groupchats", function () {
|
describe("Groupchats", function () {
|
||||||
|
|
||||||
@ -527,7 +527,7 @@ describe("Groupchats", function () {
|
|||||||
<body>This is a message</body>
|
<body>This is a message</body>
|
||||||
</message>`);
|
</message>`);
|
||||||
_converse.connection._dataRecv(mock.createRequest(stanza));
|
_converse.connection._dataRecv(mock.createRequest(stanza));
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
expect(sizzle('.chat-msg__subject', view.el).length).toBe(1);
|
expect(sizzle('.chat-msg__subject', view.el).length).toBe(1);
|
||||||
expect(sizzle('.chat-msg__subject', view.el).pop().textContent.trim()).toBe('This is a message subject');
|
expect(sizzle('.chat-msg__subject', view.el).pop().textContent.trim()).toBe('This is a message subject');
|
||||||
expect(sizzle('.chat-msg__text').length).toBe(1);
|
expect(sizzle('.chat-msg__text').length).toBe(1);
|
||||||
@ -562,7 +562,7 @@ describe("Groupchats", function () {
|
|||||||
<body>This is a message</body>
|
<body>This is a message</body>
|
||||||
</message>`);
|
</message>`);
|
||||||
_converse.connection._dataRecv(mock.createRequest(stanza));
|
_converse.connection._dataRecv(mock.createRequest(stanza));
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
expect(sizzle('.chat-msg__subject', view.el).length).toBe(1);
|
expect(sizzle('.chat-msg__subject', view.el).length).toBe(1);
|
||||||
expect(sizzle('.chat-msg__subject', view.el).pop().textContent.trim()).toBe('This is a message subject');
|
expect(sizzle('.chat-msg__subject', view.el).pop().textContent.trim()).toBe('This is a message subject');
|
||||||
expect(sizzle('.chat-msg__text').length).toBe(1);
|
expect(sizzle('.chat-msg__text').length).toBe(1);
|
||||||
@ -645,8 +645,7 @@ describe("Groupchats", function () {
|
|||||||
</message>`)));
|
</message>`)));
|
||||||
await u.waitUntil(() => view.model.handleSubjectChange.calls.count() === 2);
|
await u.waitUntil(() => view.model.handleSubjectChange.calls.count() === 2);
|
||||||
|
|
||||||
let el = sizzle('.chat-info__message', view.el).pop();
|
await u.waitUntil(() => sizzle('.chat-info__message', view.el).pop()?.textContent.trim() === 'Topic set by ralphm');
|
||||||
expect(el.textContent.trim()).toBe('Topic set by ralphm');
|
|
||||||
await u.waitUntil(() => desc.textContent.trim() === 'This is a new topic');
|
await u.waitUntil(() => desc.textContent.trim() === 'This is a new topic');
|
||||||
|
|
||||||
// Doesn't show multiple subsequent topic change notifications
|
// Doesn't show multiple subsequent topic change notifications
|
||||||
@ -666,7 +665,7 @@ describe("Groupchats", function () {
|
|||||||
await u.waitUntil(() => view.model.handleSubjectChange.calls.count() === 4);
|
await u.waitUntil(() => view.model.handleSubjectChange.calls.count() === 4);
|
||||||
await u.waitUntil(() => desc.textContent.trim() === "Some1's topic");
|
await u.waitUntil(() => desc.textContent.trim() === "Some1's topic");
|
||||||
expect(sizzle('.chat-info__message', view.el).length).toBe(2);
|
expect(sizzle('.chat-info__message', view.el).length).toBe(2);
|
||||||
el = sizzle('.chat-info__message', view.el).pop();
|
const el = sizzle('.chat-info__message', view.el).pop();
|
||||||
expect(el.textContent.trim()).toBe('Topic set by some1');
|
expect(el.textContent.trim()).toBe('Topic set by some1');
|
||||||
|
|
||||||
// Removes current topic
|
// Removes current topic
|
||||||
@ -676,8 +675,8 @@ describe("Groupchats", function () {
|
|||||||
</message>`);
|
</message>`);
|
||||||
_converse.connection._dataRecv(mock.createRequest(stanza));
|
_converse.connection._dataRecv(mock.createRequest(stanza));
|
||||||
await u.waitUntil(() => view.model.handleSubjectChange.calls.count() === 5);
|
await u.waitUntil(() => view.model.handleSubjectChange.calls.count() === 5);
|
||||||
await u.waitUntil(() => view.el.querySelector('.chat-head__desc') === null);
|
await u.waitUntil(() => view.el.querySelector('.chat-head__desc').textContent.replace(/<!---->/g, '') === '');
|
||||||
expect(view.el.querySelector('.chat-info:last-child').textContent.trim()).toBe("Topic cleared by some1");
|
await u.waitUntil(() => view.el.querySelector('converse-chat-message:last-child .chat-info').textContent.trim() === "Topic cleared by some1");
|
||||||
done();
|
done();
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
@ -701,12 +700,11 @@ describe("Groupchats", function () {
|
|||||||
}).c('body').t(message).tree();
|
}).c('body').t(message).tree();
|
||||||
|
|
||||||
await view.model.handleMessageStanza(msg);
|
await view.model.handleMessageStanza(msg);
|
||||||
|
|
||||||
spyOn(view.model, 'clearMessages').and.callThrough();
|
spyOn(view.model, 'clearMessages').and.callThrough();
|
||||||
await view.model.close();
|
await view.model.close();
|
||||||
await u.waitUntil(() => view.model.clearMessages.calls.count());
|
await u.waitUntil(() => view.model.clearMessages.calls.count());
|
||||||
expect(view.model.messages.length).toBe(0);
|
expect(view.model.messages.length).toBe(0);
|
||||||
expect(view.msgs_container.innerHTML).toBe('');
|
expect(view.el.querySelector('converse-chat-history')).toBe(null);
|
||||||
done()
|
done()
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -861,7 +859,7 @@ describe("Groupchats", function () {
|
|||||||
'type': 'groupchat'
|
'type': 'groupchat'
|
||||||
}).c('body').t('hello world').tree();
|
}).c('body').t('hello world').tree();
|
||||||
_converse.connection._dataRecv(mock.createRequest(msg));
|
_converse.connection._dataRecv(mock.createRequest(msg));
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
|
|
||||||
// Add another entrant, otherwise the above message will be
|
// Add another entrant, otherwise the above message will be
|
||||||
// collapsed if "newguy" leaves immediately again
|
// collapsed if "newguy" leaves immediately again
|
||||||
@ -1082,7 +1080,6 @@ describe("Groupchats", function () {
|
|||||||
</presence>`);
|
</presence>`);
|
||||||
_converse.connection._dataRecv(mock.createRequest(presence));
|
_converse.connection._dataRecv(mock.createRequest(presence));
|
||||||
await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === "romeo, fabio and Dele Olajide have entered the groupchat");
|
await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === "romeo, fabio and Dele Olajide have entered the groupchat");
|
||||||
|
|
||||||
presence = u.toStanza(
|
presence = u.toStanza(
|
||||||
`<presence xmlns="jabber:client" to="romeo@montague.lit/orchard" from="coven@chat.shakespeare.lit/jcbrand">
|
`<presence xmlns="jabber:client" to="romeo@montague.lit/orchard" from="coven@chat.shakespeare.lit/jcbrand">
|
||||||
<x xmlns="http://jabber.org/protocol/muc#user">
|
<x xmlns="http://jabber.org/protocol/muc#user">
|
||||||
@ -1158,10 +1155,6 @@ describe("Groupchats", function () {
|
|||||||
await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() ===
|
await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() ===
|
||||||
"romeo, jcbrand and others have entered the groupchat\nfuvuv has left the groupchat");
|
"romeo, jcbrand and others have entered the groupchat\nfuvuv has left the groupchat");
|
||||||
|
|
||||||
// XXX: hack so that we can test leave/enter of occupants
|
|
||||||
// who were already in the room when we joined.
|
|
||||||
view.msgs_container.innerHTML = '';
|
|
||||||
|
|
||||||
presence = u.toStanza(
|
presence = u.toStanza(
|
||||||
`<presence xmlns="jabber:client" to="romeo@montague.lit/orchard" type="unavailable" from="coven@chat.shakespeare.lit/fabio">
|
`<presence xmlns="jabber:client" to="romeo@montague.lit/orchard" type="unavailable" from="coven@chat.shakespeare.lit/fabio">
|
||||||
<status>Disconnected: closed</status>
|
<status>Disconnected: closed</status>
|
||||||
@ -2042,7 +2035,7 @@ describe("Groupchats", function () {
|
|||||||
preventDefault: function preventDefault () {},
|
preventDefault: function preventDefault () {},
|
||||||
keyCode: 13
|
keyCode: 13
|
||||||
});
|
});
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
|
|
||||||
expect(_converse.api.trigger).toHaveBeenCalledWith('messageSend', jasmine.any(_converse.Message));
|
expect(_converse.api.trigger).toHaveBeenCalledWith('messageSend', jasmine.any(_converse.Message));
|
||||||
expect(view.content.querySelectorAll('.chat-msg').length).toBe(1);
|
expect(view.content.querySelectorAll('.chat-msg').length).toBe(1);
|
||||||
@ -2102,7 +2095,7 @@ describe("Groupchats", function () {
|
|||||||
type: 'groupchat',
|
type: 'groupchat',
|
||||||
id: u.getUniqueId(),
|
id: u.getUniqueId(),
|
||||||
}).c('body').t(message).tree());
|
}).c('body').t(message).tree());
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
// Now check that the message appears inside the chatbox in the DOM
|
// Now check that the message appears inside the chatbox in the DOM
|
||||||
const msg_txt = sizzle('.chat-msg:last .chat-msg__text', view.content).pop().textContent;
|
const msg_txt = sizzle('.chat-msg:last .chat-msg__text', view.content).pop().textContent;
|
||||||
expect(msg_txt).toEqual(message);
|
expect(msg_txt).toEqual(message);
|
||||||
@ -2898,8 +2891,10 @@ describe("Groupchats", function () {
|
|||||||
textarea.value = '/help';
|
textarea.value = '/help';
|
||||||
view.onKeyDown(enter);
|
view.onKeyDown(enter);
|
||||||
|
|
||||||
let info_messages = sizzle('.chat-info:not(.chat-event)', view.el);
|
await u.waitUntil(() => sizzle('converse-chat-help .chat-info', view.el).length);
|
||||||
expect(info_messages.length).toBe(20);
|
const chat_help_el = view.el.querySelector('converse-chat-help');
|
||||||
|
let info_messages = sizzle('.chat-info', chat_help_el);
|
||||||
|
expect(info_messages.length).toBe(19);
|
||||||
expect(info_messages.pop().textContent.trim()).toBe('/voice: Allow muted user to post messages');
|
expect(info_messages.pop().textContent.trim()).toBe('/voice: Allow muted user to post messages');
|
||||||
expect(info_messages.pop().textContent.trim()).toBe('/topic: Set groupchat subject (alias for /subject)');
|
expect(info_messages.pop().textContent.trim()).toBe('/topic: Set groupchat subject (alias for /subject)');
|
||||||
expect(info_messages.pop().textContent.trim()).toBe('/subject: Set groupchat subject');
|
expect(info_messages.pop().textContent.trim()).toBe('/subject: Set groupchat subject');
|
||||||
@ -2919,47 +2914,49 @@ describe("Groupchats", function () {
|
|||||||
expect(info_messages.pop().textContent.trim()).toBe('/clear: Clear the chat area');
|
expect(info_messages.pop().textContent.trim()).toBe('/clear: Clear the chat area');
|
||||||
expect(info_messages.pop().textContent.trim()).toBe('/ban: Ban user by changing their affiliation to outcast');
|
expect(info_messages.pop().textContent.trim()).toBe('/ban: Ban user by changing their affiliation to outcast');
|
||||||
expect(info_messages.pop().textContent.trim()).toBe('/admin: Change user\'s affiliation to admin');
|
expect(info_messages.pop().textContent.trim()).toBe('/admin: Change user\'s affiliation to admin');
|
||||||
expect(info_messages.pop().textContent.trim()).toBe('You can run the following commands');
|
|
||||||
|
|
||||||
const occupant = view.model.occupants.findWhere({'jid': _converse.bare_jid});
|
const occupant = view.model.occupants.findWhere({'jid': _converse.bare_jid});
|
||||||
occupant.set('affiliation', 'admin');
|
occupant.set('affiliation', 'admin');
|
||||||
textarea = view.el.querySelector('.chat-textarea');
|
|
||||||
textarea.value = '/clear';
|
view.el.querySelector('.close-chat-help').click();
|
||||||
view.onKeyDown(enter);
|
await u.waitUntil(() => chat_help_el.hidden);
|
||||||
await u.waitUntil(() => sizzle('.chat-info:not(.chat-event)', view.el).length === 0);
|
|
||||||
|
|
||||||
textarea.value = '/help';
|
textarea.value = '/help';
|
||||||
view.onKeyDown(enter);
|
view.onKeyDown(enter);
|
||||||
info_messages = sizzle('.chat-info:not(.chat-event)', view.el);
|
await u.waitUntil(() => !chat_help_el.hidden);
|
||||||
expect(info_messages.length).toBe(19);
|
info_messages = sizzle('.chat-info', chat_help_el);
|
||||||
|
expect(info_messages.length).toBe(18);
|
||||||
let commands = info_messages.map(m => m.textContent.replace(/:.*$/, ''));
|
let commands = info_messages.map(m => m.textContent.replace(/:.*$/, ''));
|
||||||
expect(commands).toEqual([
|
expect(commands).toEqual([
|
||||||
"You can run the following commands",
|
|
||||||
"/admin", "/ban", "/clear", "/deop", "/destroy",
|
"/admin", "/ban", "/clear", "/deop", "/destroy",
|
||||||
"/help", "/kick", "/me", "/member", "/modtools", "/mute", "/nick",
|
"/help", "/kick", "/me", "/member", "/modtools", "/mute", "/nick",
|
||||||
"/op", "/register", "/revoke", "/subject", "/topic", "/voice"
|
"/op", "/register", "/revoke", "/subject", "/topic", "/voice"
|
||||||
]);
|
]);
|
||||||
occupant.set('affiliation', 'member');
|
occupant.set('affiliation', 'member');
|
||||||
textarea.value = '/clear';
|
view.el.querySelector('.close-chat-help').click();
|
||||||
view.onKeyDown(enter);
|
await u.waitUntil(() => chat_help_el.hidden);
|
||||||
await u.waitUntil(() => sizzle('.chat-info:not(.chat-event)', view.el).length === 0);
|
|
||||||
|
|
||||||
textarea.value = '/help';
|
textarea.value = '/help';
|
||||||
view.onKeyDown(enter);
|
view.onKeyDown(enter);
|
||||||
info_messages = sizzle('.chat-info', view.el).slice(1);
|
await u.waitUntil(() => !chat_help_el.hidden);
|
||||||
|
info_messages = sizzle('.chat-info', chat_help_el);
|
||||||
expect(info_messages.length).toBe(9);
|
expect(info_messages.length).toBe(9);
|
||||||
commands = info_messages.map(m => m.textContent.replace(/:.*$/, ''));
|
commands = info_messages.map(m => m.textContent.replace(/:.*$/, ''));
|
||||||
expect(commands).toEqual(["/clear", "/help", "/kick", "/me", "/modtools", "/mute", "/nick", "/register", "/voice"]);
|
expect(commands).toEqual(["/clear", "/help", "/kick", "/me", "/modtools", "/mute", "/nick", "/register", "/voice"]);
|
||||||
|
|
||||||
|
view.el.querySelector('.close-chat-help').click();
|
||||||
|
await u.waitUntil(() => chat_help_el.hidden);
|
||||||
|
expect(view.model.get('show_help_messages')).toBe(false);
|
||||||
|
|
||||||
occupant.set('role', 'participant');
|
occupant.set('role', 'participant');
|
||||||
|
// Role changes causes rerender, so we need to get the new textarea
|
||||||
textarea = view.el.querySelector('.chat-textarea');
|
textarea = view.el.querySelector('.chat-textarea');
|
||||||
textarea.value = '/clear';
|
|
||||||
view.onKeyDown(enter);
|
|
||||||
await u.waitUntil(() => sizzle('.chat-info:not(.chat-event)', view.el).length === 0);
|
|
||||||
|
|
||||||
textarea.value = '/help';
|
textarea.value = '/help';
|
||||||
view.onKeyDown(enter);
|
view.onKeyDown(enter);
|
||||||
info_messages = sizzle('.chat-info', view.el).slice(1);
|
await u.waitUntil(() => view.model.get('show_help_messages'));
|
||||||
|
await u.waitUntil(() => !chat_help_el.hidden);
|
||||||
|
info_messages = sizzle('.chat-info', chat_help_el);
|
||||||
expect(info_messages.length).toBe(5);
|
expect(info_messages.length).toBe(5);
|
||||||
commands = info_messages.map(m => m.textContent.replace(/:.*$/, ''));
|
commands = info_messages.map(m => m.textContent.replace(/:.*$/, ''));
|
||||||
expect(commands).toEqual(["/clear", "/help", "/me", "/nick", "/register"]);
|
expect(commands).toEqual(["/clear", "/help", "/me", "/nick", "/register"]);
|
||||||
@ -2967,13 +2964,13 @@ describe("Groupchats", function () {
|
|||||||
// Test that /topic is available if all users may change the subject
|
// Test that /topic is available if all users may change the subject
|
||||||
// Note: we're making a shortcut here, this value should never be set manually
|
// Note: we're making a shortcut here, this value should never be set manually
|
||||||
view.model.config.set('changesubject', true);
|
view.model.config.set('changesubject', true);
|
||||||
textarea.value = '/clear';
|
view.el.querySelector('.close-chat-help').click();
|
||||||
view.onKeyDown(enter);
|
await u.waitUntil(() => chat_help_el.hidden);
|
||||||
await u.waitUntil(() => sizzle('.chat-info:not(.chat-event)', view.el).length === 0);
|
|
||||||
|
|
||||||
textarea.value = '/help';
|
textarea.value = '/help';
|
||||||
view.onKeyDown(enter);
|
view.onKeyDown(enter);
|
||||||
info_messages = sizzle('.chat-info', view.el).slice(1);
|
await u.waitUntil(() => !chat_help_el.hidden, 1000);
|
||||||
|
info_messages = sizzle('.chat-info', chat_help_el);
|
||||||
expect(info_messages.length).toBe(7);
|
expect(info_messages.length).toBe(7);
|
||||||
commands = info_messages.map(m => m.textContent.replace(/:.*$/, ''));
|
commands = info_messages.map(m => m.textContent.replace(/:.*$/, ''));
|
||||||
expect(commands).toEqual(["/clear", "/help", "/me", "/nick", "/register", "/subject", "/topic"]);
|
expect(commands).toEqual(["/clear", "/help", "/me", "/nick", "/register", "/subject", "/topic"]);
|
||||||
@ -2995,8 +2992,9 @@ describe("Groupchats", function () {
|
|||||||
textarea.value = '/help';
|
textarea.value = '/help';
|
||||||
view.onKeyDown(enter);
|
view.onKeyDown(enter);
|
||||||
|
|
||||||
|
await u.waitUntil(() => sizzle('.chat-info:not(.chat-event)', view.el).length);
|
||||||
const info_messages = sizzle('.chat-info:not(.chat-event)', view.el);
|
const info_messages = sizzle('.chat-info:not(.chat-event)', view.el);
|
||||||
expect(info_messages.length).toBe(18);
|
expect(info_messages.length).toBe(17);
|
||||||
expect(info_messages.pop().textContent.trim()).toBe('/topic: Set groupchat subject (alias for /subject)');
|
expect(info_messages.pop().textContent.trim()).toBe('/topic: Set groupchat subject (alias for /subject)');
|
||||||
expect(info_messages.pop().textContent.trim()).toBe('/subject: Set groupchat subject');
|
expect(info_messages.pop().textContent.trim()).toBe('/subject: Set groupchat subject');
|
||||||
expect(info_messages.pop().textContent.trim()).toBe('/revoke: Revoke the user\'s current affiliation');
|
expect(info_messages.pop().textContent.trim()).toBe('/revoke: Revoke the user\'s current affiliation');
|
||||||
@ -3014,7 +3012,6 @@ describe("Groupchats", function () {
|
|||||||
expect(info_messages.pop().textContent.trim()).toBe('/clear: Clear the chat area');
|
expect(info_messages.pop().textContent.trim()).toBe('/clear: Clear the chat area');
|
||||||
expect(info_messages.pop().textContent.trim()).toBe('/ban: Ban user by changing their affiliation to outcast');
|
expect(info_messages.pop().textContent.trim()).toBe('/ban: Ban user by changing their affiliation to outcast');
|
||||||
expect(info_messages.pop().textContent.trim()).toBe('/admin: Change user\'s affiliation to admin');
|
expect(info_messages.pop().textContent.trim()).toBe('/admin: Change user\'s affiliation to admin');
|
||||||
expect(info_messages.pop().textContent.trim()).toBe('You can run the following commands');
|
|
||||||
done();
|
done();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -3432,7 +3429,7 @@ describe("Groupchats", function () {
|
|||||||
|
|
||||||
textarea.value = '/ban joe22';
|
textarea.value = '/ban joe22';
|
||||||
view.onFormSubmitted(new Event('submit'));
|
view.onFormSubmitted(new Event('submit'));
|
||||||
await u.waitUntil(() => view.el.querySelector('.message:last-child')?.textContent?.trim() ===
|
await u.waitUntil(() => view.el.querySelector('converse-chat-message:last-child')?.textContent?.trim() ===
|
||||||
"Error: couldn't find a groupchat participant based on your arguments");
|
"Error: couldn't find a groupchat participant based on your arguments");
|
||||||
done();
|
done();
|
||||||
}));
|
}));
|
||||||
@ -3520,6 +3517,7 @@ describe("Groupchats", function () {
|
|||||||
}).c('actor', {'nick': 'romeo'}).up()
|
}).c('actor', {'nick': 'romeo'}).up()
|
||||||
.c('reason').t("You're annoying").up().up()
|
.c('reason').t("You're annoying").up().up()
|
||||||
.c('status', {'code': '307'});
|
.c('status', {'code': '307'});
|
||||||
|
|
||||||
_converse.connection._dataRecv(mock.createRequest(presence));
|
_converse.connection._dataRecv(mock.createRequest(presence));
|
||||||
|
|
||||||
await u.waitUntil(() => view.el.querySelectorAll('.chat-info').length === 2);
|
await u.waitUntil(() => view.el.querySelectorAll('.chat-info').length === 2);
|
||||||
@ -4996,11 +4994,13 @@ describe("Groupchats", function () {
|
|||||||
|
|
||||||
// See XEP-0085 https://xmpp.org/extensions/xep-0085.html#definitions
|
// See XEP-0085 https://xmpp.org/extensions/xep-0085.html#definitions
|
||||||
|
|
||||||
const timeout_functions = [];
|
const remove_notifications_timeouts = [];
|
||||||
spyOn(window, 'setTimeout').and.callFake(f => {
|
const setTimeout = window.setTimeout;
|
||||||
|
spyOn(window, 'setTimeout').and.callFake((f, w) => {
|
||||||
if (f.toString() === "() => this.removeNotification(actor, state)") {
|
if (f.toString() === "() => this.removeNotification(actor, state)") {
|
||||||
timeout_functions.push(f)
|
remove_notifications_timeouts.push(f)
|
||||||
}
|
}
|
||||||
|
setTimeout(f, w);
|
||||||
});
|
});
|
||||||
|
|
||||||
// <composing> state
|
// <composing> state
|
||||||
@ -5014,7 +5014,7 @@ describe("Groupchats", function () {
|
|||||||
|
|
||||||
csntext = await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent);
|
csntext = await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent);
|
||||||
expect(csntext.trim()).toEqual('newguy is typing');
|
expect(csntext.trim()).toEqual('newguy is typing');
|
||||||
expect(timeout_functions.length).toBe(1);
|
expect(remove_notifications_timeouts.length).toBe(1);
|
||||||
|
|
||||||
expect(view.el.querySelector('.chat-content__notifications').textContent.trim()).toEqual('newguy is typing');
|
expect(view.el.querySelector('.chat-content__notifications').textContent.trim()).toEqual('newguy is typing');
|
||||||
|
|
||||||
@ -5048,7 +5048,6 @@ describe("Groupchats", function () {
|
|||||||
await view.model.handleMessageStanza(msg);
|
await view.model.handleMessageStanza(msg);
|
||||||
await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === 'newguy, nomorenicks and others are typing');
|
await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === 'newguy, nomorenicks and others are typing');
|
||||||
|
|
||||||
// Check that new messages appear under the chat state notifications
|
|
||||||
msg = $msg({
|
msg = $msg({
|
||||||
from: `${muc_jid}/some1`,
|
from: `${muc_jid}/some1`,
|
||||||
id: u.getUniqueId(),
|
id: u.getUniqueId(),
|
||||||
@ -5056,7 +5055,7 @@ describe("Groupchats", function () {
|
|||||||
type: 'groupchat'
|
type: 'groupchat'
|
||||||
}).c('body').t('hello world').tree();
|
}).c('body').t('hello world').tree();
|
||||||
await view.model.handleMessageStanza(msg);
|
await view.model.handleMessageStanza(msg);
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve), 1000);
|
||||||
|
|
||||||
const messages = view.el.querySelectorAll('.message');
|
const messages = view.el.querySelectorAll('.message');
|
||||||
expect(messages.length).toBe(2);
|
expect(messages.length).toBe(2);
|
||||||
@ -5064,7 +5063,7 @@ describe("Groupchats", function () {
|
|||||||
expect(view.el.querySelector('.chat-msg .chat-msg__text').textContent.trim()).toBe('hello world');
|
expect(view.el.querySelector('.chat-msg .chat-msg__text').textContent.trim()).toBe('hello world');
|
||||||
|
|
||||||
// Test that the composing notifications get removed via timeout.
|
// Test that the composing notifications get removed via timeout.
|
||||||
timeout_functions[0]();
|
remove_notifications_timeouts[0]();
|
||||||
await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === 'nomorenicks, majortom and groundcontrol are typing');
|
await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === 'nomorenicks, majortom and groundcontrol are typing');
|
||||||
done();
|
done();
|
||||||
}));
|
}));
|
||||||
@ -5186,34 +5185,42 @@ describe("Groupchats", function () {
|
|||||||
const textarea = view.el.querySelector('.chat-textarea');
|
const textarea = view.el.querySelector('.chat-textarea');
|
||||||
textarea.value = 'Hello world';
|
textarea.value = 'Hello world';
|
||||||
view.onFormSubmitted(new Event('submit'));
|
view.onFormSubmitted(new Event('submit'));
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
|
|
||||||
let stanza = u.toStanza(`
|
let stanza = u.toStanza(`
|
||||||
<message xmlns="jabber:client" type="error" to="troll@montague.lit/resource" from="trollbox@montague.lit">
|
<message id="${view.model.messages.at(0).get('msgid')}"
|
||||||
|
xmlns="jabber:client"
|
||||||
|
type="error"
|
||||||
|
to="troll@montague.lit/resource"
|
||||||
|
from="trollbox@montague.lit">
|
||||||
<error type="auth"><forbidden xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"/></error>
|
<error type="auth"><forbidden xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"/></error>
|
||||||
</message>`);
|
</message>`);
|
||||||
_converse.connection._dataRecv(mock.createRequest(stanza));
|
_converse.connection._dataRecv(mock.createRequest(stanza));
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await u.waitUntil(() => view.el.querySelector('.chat-msg__error')?.textContent.trim(), 1000);
|
||||||
expect(view.el.querySelector('.chat-error').textContent.trim()).toBe(
|
expect(view.el.querySelector('.chat-msg__error').textContent.trim()).toBe(
|
||||||
"Your message was not delivered because you weren't allowed to send it.");
|
"Your message was not delivered because you weren't allowed to send it.");
|
||||||
|
|
||||||
textarea.value = 'Hello again';
|
textarea.value = 'Hello again';
|
||||||
view.onFormSubmitted(new Event('submit'));
|
view.onFormSubmitted(new Event('submit'));
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__text').length === 2);
|
||||||
|
|
||||||
stanza = u.toStanza(`
|
stanza = u.toStanza(`
|
||||||
<message xmlns="jabber:client" type="error" to="troll@montague.lit/resource" from="trollbox@montague.lit">
|
<message id="${view.model.messages.at(1).get('msgid')}"
|
||||||
|
xmlns="jabber:client"
|
||||||
|
type="error"
|
||||||
|
to="troll@montague.lit/resource"
|
||||||
|
from="trollbox@montague.lit">
|
||||||
<error type="auth">
|
<error type="auth">
|
||||||
<forbidden xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"/>
|
<forbidden xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"/>
|
||||||
<text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas">Thou shalt not!</text>
|
<text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas">Thou shalt not!</text>
|
||||||
</error>
|
</error>
|
||||||
</message>`);
|
</message>`);
|
||||||
_converse.connection._dataRecv(mock.createRequest(stanza));
|
_converse.connection._dataRecv(mock.createRequest(stanza));
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
|
||||||
|
|
||||||
expect(view.el.querySelector('.message:last-child').textContent.trim()).toBe(
|
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__error').length === 2);
|
||||||
'Your message was not delivered because you weren\'t allowed to send it. '+
|
const sel = 'converse-message-history converse-chat-message:last-child .chat-msg__error';
|
||||||
'The message from the server is: "Thou shalt not!"')
|
await u.waitUntil(() => view.el.querySelector(sel)?.textContent.trim());
|
||||||
|
expect(view.el.querySelector(sel).textContent.trim()).toBe('Thou shalt not!')
|
||||||
done();
|
done();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -2,9 +2,12 @@
|
|||||||
|
|
||||||
const { Promise, Strophe, $msg, $pres, sizzle, stanza_utils } = converse.env;
|
const { Promise, Strophe, $msg, $pres, sizzle, stanza_utils } = converse.env;
|
||||||
const u = converse.env.utils;
|
const u = converse.env.utils;
|
||||||
|
const original_timeout = jasmine.DEFAULT_TIMEOUT_INTERVAL;
|
||||||
|
|
||||||
describe("A Groupchat Message", function () {
|
describe("A Groupchat Message", function () {
|
||||||
|
|
||||||
|
beforeEach(() => (jasmine.DEFAULT_TIMEOUT_INTERVAL = 7000));
|
||||||
|
afterEach(() => (jasmine.DEFAULT_TIMEOUT_INTERVAL = original_timeout));
|
||||||
|
|
||||||
describe("which is succeeded by an error message", function () {
|
describe("which is succeeded by an error message", function () {
|
||||||
|
|
||||||
@ -25,7 +28,7 @@ describe("A Groupchat Message", function () {
|
|||||||
'keyCode': 13 // Enter
|
'keyCode': 13 // Enter
|
||||||
}
|
}
|
||||||
view.onKeyDown(enter_event);
|
view.onKeyDown(enter_event);
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
|
|
||||||
const msg = view.model.messages.at(0);
|
const msg = view.model.messages.at(0);
|
||||||
const err_msg_text = "Message rejected because you're sending messages too quickly";
|
const err_msg_text = "Message rejected because you're sending messages too quickly";
|
||||||
@ -44,7 +47,7 @@ describe("A Groupchat Message", function () {
|
|||||||
const message = view.model.messages.at(0);
|
const message = view.model.messages.at(0);
|
||||||
expect(message.get('received')).toBeUndefined();
|
expect(message.get('received')).toBeUndefined();
|
||||||
expect(message.get('body')).toBe('hello world');
|
expect(message.get('body')).toBe('hello world');
|
||||||
expect(message.get('error')).toBe(err_msg_text);
|
expect(message.get('error_text')).toBe(err_msg_text);
|
||||||
done();
|
done();
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
@ -180,7 +183,7 @@ describe("A Groupchat Message", function () {
|
|||||||
.c('active', {'xmlns': "http://jabber.org/protocol/chatstates"})
|
.c('active', {'xmlns': "http://jabber.org/protocol/chatstates"})
|
||||||
.tree();
|
.tree();
|
||||||
await view.model.handleMessageStanza(msg);
|
await view.model.handleMessageStanza(msg);
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
expect(view.el.querySelector('.chat-msg')).not.toBe(null);
|
expect(view.el.querySelector('.chat-msg')).not.toBe(null);
|
||||||
done();
|
done();
|
||||||
}));
|
}));
|
||||||
@ -203,7 +206,7 @@ describe("A Groupchat Message", function () {
|
|||||||
type: 'groupchat'
|
type: 'groupchat'
|
||||||
}).c('body').t(message).tree();
|
}).c('body').t(message).tree();
|
||||||
await view.model.handleMessageStanza(msg);
|
await view.model.handleMessageStanza(msg);
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
expect(u.hasClass('mentioned', view.el.querySelector('.chat-msg'))).toBeTruthy();
|
expect(u.hasClass('mentioned', view.el.querySelector('.chat-msg'))).toBeTruthy();
|
||||||
done();
|
done();
|
||||||
}));
|
}));
|
||||||
@ -435,7 +438,7 @@ describe("A Groupchat Message", function () {
|
|||||||
type: 'groupchat'
|
type: 'groupchat'
|
||||||
}).c('body').t('Another message!').tree();
|
}).c('body').t('Another message!').tree();
|
||||||
await view.model.handleMessageStanza(msg);
|
await view.model.handleMessageStanza(msg);
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
expect(view.model.messages.last().occupant.get('affiliation')).toBe('member');
|
expect(view.model.messages.last().occupant.get('affiliation')).toBe('member');
|
||||||
expect(view.model.messages.last().occupant.get('role')).toBe('participant');
|
expect(view.model.messages.last().occupant.get('role')).toBe('participant');
|
||||||
expect(view.el.querySelectorAll('.chat-msg').length).toBe(2);
|
expect(view.el.querySelectorAll('.chat-msg').length).toBe(2);
|
||||||
@ -472,7 +475,7 @@ describe("A Groupchat Message", function () {
|
|||||||
type: 'groupchat'
|
type: 'groupchat'
|
||||||
}).c('body').t('Message from someone not in the MUC right now').tree();
|
}).c('body').t('Message from someone not in the MUC right now').tree();
|
||||||
await view.model.handleMessageStanza(msg);
|
await view.model.handleMessageStanza(msg);
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
expect(view.model.messages.last().occupant).toBeUndefined();
|
expect(view.model.messages.last().occupant).toBeUndefined();
|
||||||
// Check that there's a new "add" event handler, for when the occupant appears.
|
// Check that there's a new "add" event handler, for when the occupant appears.
|
||||||
expect(view.model.occupants._events.add.length).toBe(add_events+1);
|
expect(view.model.occupants._events.add.length).toBe(add_events+1);
|
||||||
@ -583,7 +586,7 @@ describe("A Groupchat Message", function () {
|
|||||||
await u.waitUntil(() => view.el.querySelector('.chat-msg__text').textContent ===
|
await u.waitUntil(() => view.el.querySelector('.chat-msg__text').textContent ===
|
||||||
'But soft, what light through yonder chimney breaks?', 500);
|
'But soft, what light through yonder chimney breaks?', 500);
|
||||||
expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
|
expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
|
||||||
expect(view.el.querySelectorAll('.chat-msg__content .fa-edit').length).toBe(1);
|
await u.waitUntil(() => view.el.querySelector('.chat-msg__content .fa-edit'));
|
||||||
|
|
||||||
await view.model.handleMessageStanza($msg({
|
await view.model.handleMessageStanza($msg({
|
||||||
'from': 'lounge@montague.lit/newguy',
|
'from': 'lounge@montague.lit/newguy',
|
||||||
@ -597,8 +600,9 @@ describe("A Groupchat Message", function () {
|
|||||||
'But soft, what light through yonder window breaks?', 500);
|
'But soft, what light through yonder window breaks?', 500);
|
||||||
expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
|
expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
|
||||||
expect(view.el.querySelectorAll('.chat-msg__content .fa-edit').length).toBe(1);
|
expect(view.el.querySelectorAll('.chat-msg__content .fa-edit').length).toBe(1);
|
||||||
view.el.querySelector('.chat-msg__content .fa-edit').click();
|
const edit = await u.waitUntil(() => view.el.querySelector('.chat-msg__content .fa-edit'));
|
||||||
const modal = view.model.messages.at(0).message_versions_modal;
|
edit.click();
|
||||||
|
const modal = await u.waitUntil(() => view.el.querySelector('converse-chat-message').message_versions_modal);
|
||||||
await u.waitUntil(() => u.isVisible(modal.el), 1000);
|
await u.waitUntil(() => u.isVisible(modal.el), 1000);
|
||||||
const older_msgs = modal.el.querySelectorAll('.older-msg');
|
const older_msgs = modal.el.querySelectorAll('.older-msg');
|
||||||
expect(older_msgs.length).toBe(2);
|
expect(older_msgs.length).toBe(2);
|
||||||
@ -641,11 +645,10 @@ describe("A Groupchat Message", function () {
|
|||||||
target: textarea,
|
target: textarea,
|
||||||
keyCode: 38 // Up arrow
|
keyCode: 38 // Up arrow
|
||||||
});
|
});
|
||||||
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
|
||||||
expect(textarea.value).toBe('But soft, what light through yonder airlock breaks?');
|
expect(textarea.value).toBe('But soft, what light through yonder airlock breaks?');
|
||||||
expect(view.model.messages.at(0).get('correcting')).toBe(true);
|
expect(view.model.messages.at(0).get('correcting')).toBe(true);
|
||||||
expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
|
expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
|
||||||
expect(u.hasClass('correcting', view.el.querySelector('.chat-msg'))).toBe(true);
|
await u.waitUntil(() => u.hasClass('correcting', view.el.querySelector('.chat-msg')));
|
||||||
|
|
||||||
spyOn(_converse.connection, 'send');
|
spyOn(_converse.connection, 'send');
|
||||||
textarea.value = 'But soft, what light through yonder window breaks?';
|
textarea.value = 'But soft, what light through yonder window breaks?';
|
||||||
@ -688,7 +691,7 @@ describe("A Groupchat Message", function () {
|
|||||||
'to': 'romeo@montague.lit',
|
'to': 'romeo@montague.lit',
|
||||||
'type': 'groupchat'
|
'type': 'groupchat'
|
||||||
}).c('body').t('Hello world').tree());
|
}).c('body').t('Hello world').tree());
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
expect(view.el.querySelectorAll('.chat-msg').length).toBe(2);
|
expect(view.el.querySelectorAll('.chat-msg').length).toBe(2);
|
||||||
|
|
||||||
// Test that pressing the down arrow cancels message correction
|
// Test that pressing the down arrow cancels message correction
|
||||||
@ -729,7 +732,7 @@ describe("A Groupchat Message", function () {
|
|||||||
preventDefault: function preventDefault () {},
|
preventDefault: function preventDefault () {},
|
||||||
keyCode: 13 // Enter
|
keyCode: 13 // Enter
|
||||||
});
|
});
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
expect(view.el.querySelectorAll('.chat-msg__body.chat-msg__body--received').length).toBe(0);
|
expect(view.el.querySelectorAll('.chat-msg__body.chat-msg__body--received').length).toBe(0);
|
||||||
|
|
||||||
const msg_obj = view.model.messages.at(0);
|
const msg_obj = view.model.messages.at(0);
|
||||||
@ -807,7 +810,7 @@ describe("A Groupchat Message", function () {
|
|||||||
preventDefault: function preventDefault () {},
|
preventDefault: function preventDefault () {},
|
||||||
keyCode: 13 // Enter
|
keyCode: 13 // Enter
|
||||||
});
|
});
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
|
expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
|
||||||
|
|
||||||
const msg_obj = view.model.messages.at(0);
|
const msg_obj = view.model.messages.at(0);
|
||||||
@ -841,7 +844,7 @@ describe("A Groupchat Message", function () {
|
|||||||
preventDefault: function preventDefault () {},
|
preventDefault: function preventDefault () {},
|
||||||
keyCode: 13 // Enter
|
keyCode: 13 // Enter
|
||||||
});
|
});
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
|
expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
|
||||||
expect(view.el.querySelector('.chat-msg .chat-msg__body').textContent.trim())
|
expect(view.el.querySelector('.chat-msg .chat-msg__body').textContent.trim())
|
||||||
.toBe("But soft, what light through yonder airlock breaks?");
|
.toBe("But soft, what light through yonder airlock breaks?");
|
||||||
@ -929,7 +932,7 @@ describe("A Groupchat Message", function () {
|
|||||||
await view.model.handleMessageStanza(msg);
|
await view.model.handleMessageStanza(msg);
|
||||||
const message = await u.waitUntil(() => view.el.querySelector('.chat-msg__text'));
|
const message = await u.waitUntil(() => view.el.querySelector('.chat-msg__text'));
|
||||||
expect(message.classList.length).toEqual(1);
|
expect(message.classList.length).toEqual(1);
|
||||||
expect(message.innerHTML).toBe(
|
expect(message.innerHTML.replace(/<!---->/g, '')).toBe(
|
||||||
'hello <span class="mention">z3r0</span> '+
|
'hello <span class="mention">z3r0</span> '+
|
||||||
'<span class="mention mention--self badge badge-info">tom</span> '+
|
'<span class="mention mention--self badge badge-info">tom</span> '+
|
||||||
'<span class="mention">mr.robot</span>, how are you?');
|
'<span class="mention">mr.robot</span>, how are you?');
|
||||||
@ -970,7 +973,7 @@ describe("A Groupchat Message", function () {
|
|||||||
await view.model.handleMessageStanza(msg);
|
await view.model.handleMessageStanza(msg);
|
||||||
const message = await u.waitUntil(() => view.el.querySelector('.chat-msg__text'));
|
const message = await u.waitUntil(() => view.el.querySelector('.chat-msg__text'));
|
||||||
expect(message.classList.length).toEqual(1);
|
expect(message.classList.length).toEqual(1);
|
||||||
expect(message.innerHTML).toBe(
|
expect(message.innerHTML.replace(/<!---->/g, '')).toBe(
|
||||||
'>hello <span class="mention">z3r0</span> '+
|
'>hello <span class="mention">z3r0</span> '+
|
||||||
'<span class="mention mention--self badge badge-info">tom</span> '+
|
'<span class="mention mention--self badge badge-info">tom</span> '+
|
||||||
'<span class="mention">mr.robot</span>, how are you?');
|
'<span class="mention">mr.robot</span>, how are you?');
|
||||||
@ -1144,7 +1147,7 @@ describe("A Groupchat Message", function () {
|
|||||||
}
|
}
|
||||||
spyOn(_converse.connection, 'send');
|
spyOn(_converse.connection, 'send');
|
||||||
view.onKeyDown(enter_event);
|
view.onKeyDown(enter_event);
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
const msg = _converse.connection.send.calls.all()[0].args[0];
|
const msg = _converse.connection.send.calls.all()[0].args[0];
|
||||||
expect(msg.toLocaleString())
|
expect(msg.toLocaleString())
|
||||||
.toBe(`<message from="romeo@montague.lit/orchard" id="${msg.nodeTree.getAttribute("id")}" `+
|
.toBe(`<message from="romeo@montague.lit/orchard" id="${msg.nodeTree.getAttribute("id")}" `+
|
||||||
@ -1191,7 +1194,14 @@ describe("A Groupchat Message", function () {
|
|||||||
}
|
}
|
||||||
spyOn(_converse.connection, 'send');
|
spyOn(_converse.connection, 'send');
|
||||||
view.onKeyDown(enter_event);
|
view.onKeyDown(enter_event);
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
|
|
||||||
|
const last_msg_sel = 'converse-chat-message:last-child .chat-msg__text';
|
||||||
|
await u.waitUntil(() =>
|
||||||
|
view.content.querySelector(last_msg_sel).innerHTML.replace(/<!---->/g, '') ===
|
||||||
|
'hello <span class="mention">z3r0</span> <span class="mention">gibson</span> <span class="mention">mr.robot</span>, how are you?'
|
||||||
|
);
|
||||||
|
|
||||||
const msg = _converse.connection.send.calls.all()[0].args[0];
|
const msg = _converse.connection.send.calls.all()[0].args[0];
|
||||||
expect(msg.toLocaleString())
|
expect(msg.toLocaleString())
|
||||||
.toBe(`<message from="romeo@montague.lit/orchard" id="${msg.nodeTree.getAttribute("id")}" `+
|
.toBe(`<message from="romeo@montague.lit/orchard" id="${msg.nodeTree.getAttribute("id")}" `+
|
||||||
@ -1269,7 +1279,7 @@ describe("A Groupchat Message", function () {
|
|||||||
'keyCode': 13 // Enter
|
'keyCode': 13 // Enter
|
||||||
}
|
}
|
||||||
view.onKeyDown(enter_event);
|
view.onKeyDown(enter_event);
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
|
|
||||||
const msg = _converse.connection.send.calls.all()[0].args[0];
|
const msg = _converse.connection.send.calls.all()[0].args[0];
|
||||||
expect(msg.toLocaleString())
|
expect(msg.toLocaleString())
|
||||||
|
@ -65,7 +65,7 @@ describe("Notifications", function () {
|
|||||||
type: 'groupchat'
|
type: 'groupchat'
|
||||||
}).c('body').t(message).tree();
|
}).c('body').t(message).tree();
|
||||||
_converse.connection._dataRecv(mock.createRequest(msg));
|
_converse.connection._dataRecv(mock.createRequest(msg));
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
|
|
||||||
await u.waitUntil(() => _converse.areDesktopNotificationsEnabled.calls.count() === 1);
|
await u.waitUntil(() => _converse.areDesktopNotificationsEnabled.calls.count() === 1);
|
||||||
expect(_converse.showMessageNotification).toHaveBeenCalled();
|
expect(_converse.showMessageNotification).toHaveBeenCalled();
|
||||||
@ -94,7 +94,7 @@ describe("Notifications", function () {
|
|||||||
_converse.connection._dataRecv(mock.createRequest(stanza));
|
_converse.connection._dataRecv(mock.createRequest(stanza));
|
||||||
await u.waitUntil(() => _converse.chatboxviews.keys().length);
|
await u.waitUntil(() => _converse.chatboxviews.keys().length);
|
||||||
const view = _converse.chatboxviews.get('notify.example.com');
|
const view = _converse.chatboxviews.get('notify.example.com');
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
expect(_converse.chatboxviews.keys().includes('notify.example.com')).toBeTruthy();
|
expect(_converse.chatboxviews.keys().includes('notify.example.com')).toBeTruthy();
|
||||||
expect(_converse.showMessageNotification).toHaveBeenCalled();
|
expect(_converse.showMessageNotification).toHaveBeenCalled();
|
||||||
done();
|
done();
|
||||||
|
@ -199,7 +199,7 @@ describe("The OMEMO module", function() {
|
|||||||
.up().up()
|
.up().up()
|
||||||
.c('payload').t(obj.payload);
|
.c('payload').t(obj.payload);
|
||||||
_converse.connection._dataRecv(mock.createRequest(stanza));
|
_converse.connection._dataRecv(mock.createRequest(stanza));
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
expect(view.model.messages.length).toBe(2);
|
expect(view.model.messages.length).toBe(2);
|
||||||
expect(view.el.querySelectorAll('.chat-msg__body')[1].textContent.trim())
|
expect(view.el.querySelectorAll('.chat-msg__body')[1].textContent.trim())
|
||||||
.toBe('This is an encrypted message from the contact');
|
.toBe('This is an encrypted message from the contact');
|
||||||
@ -218,7 +218,7 @@ describe("The OMEMO module", function() {
|
|||||||
.up().up()
|
.up().up()
|
||||||
.c('payload').t(obj.payload);
|
.c('payload').t(obj.payload);
|
||||||
_converse.connection._dataRecv(mock.createRequest(stanza));
|
_converse.connection._dataRecv(mock.createRequest(stanza));
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
await u.waitUntil(() => view.model.messages.length > 1);
|
await u.waitUntil(() => view.model.messages.length > 1);
|
||||||
expect(view.model.messages.length).toBe(3);
|
expect(view.model.messages.length).toBe(3);
|
||||||
expect(view.el.querySelectorAll('.chat-msg__body')[2].textContent.trim())
|
expect(view.el.querySelectorAll('.chat-msg__body')[2].textContent.trim())
|
||||||
@ -435,7 +435,7 @@ describe("The OMEMO module", function() {
|
|||||||
</message>
|
</message>
|
||||||
`);
|
`);
|
||||||
_converse.connection._dataRecv(mock.createRequest(carbon));
|
_converse.connection._dataRecv(mock.createRequest(carbon));
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
expect(view.model.messages.length).toBe(1);
|
expect(view.model.messages.length).toBe(1);
|
||||||
expect(view.el.querySelector('.chat-msg__body').textContent.trim())
|
expect(view.el.querySelector('.chat-msg__body').textContent.trim())
|
||||||
.toBe('This is an encrypted carbon message from another device of mine');
|
.toBe('This is an encrypted carbon message from another device of mine');
|
||||||
@ -1258,7 +1258,7 @@ describe("The OMEMO module", function() {
|
|||||||
|
|
||||||
it("adds a toolbar button for starting an encrypted groupchat session",
|
it("adds a toolbar button for starting an encrypted groupchat session",
|
||||||
mock.initConverse(
|
mock.initConverse(
|
||||||
['rosterGroupsFetched', 'chatBoxesFetched'], {'view_mode': 'fullscreen'},
|
['rosterGroupsFetched', 'chatBoxesFetched'], {},
|
||||||
async function (done, _converse) {
|
async function (done, _converse) {
|
||||||
|
|
||||||
await mock.waitUntilDiscoConfirmed(
|
await mock.waitUntilDiscoConfirmed(
|
||||||
@ -1416,8 +1416,7 @@ describe("The OMEMO module", function() {
|
|||||||
_converse.connection._dataRecv(mock.createRequest(stanza));
|
_converse.connection._dataRecv(mock.createRequest(stanza));
|
||||||
|
|
||||||
await u.waitUntil(() => !view.model.get('omemo_supported'));
|
await u.waitUntil(() => !view.model.get('omemo_supported'));
|
||||||
|
await u.waitUntil(() => view.el.querySelector('.chat-error .chat-info__message')?.textContent.trim() ===
|
||||||
expect(view.el.querySelector('.chat-error').textContent.trim()).toBe(
|
|
||||||
"oldguy doesn't appear to have a client that supports OMEMO. "+
|
"oldguy doesn't appear to have a client that supports OMEMO. "+
|
||||||
"Encrypted chat will no longer be possible in this grouchat."
|
"Encrypted chat will no longer be possible in this grouchat."
|
||||||
);
|
);
|
||||||
|
@ -5,9 +5,13 @@ const Strophe = converse.env.Strophe;
|
|||||||
const _ = converse.env._;
|
const _ = converse.env._;
|
||||||
const sizzle = converse.env.sizzle;
|
const sizzle = converse.env.sizzle;
|
||||||
const u = converse.env.utils;
|
const u = converse.env.utils;
|
||||||
|
const original_timeout = jasmine.DEFAULT_TIMEOUT_INTERVAL;
|
||||||
|
|
||||||
describe("XEP-0357 Push Notifications", function () {
|
describe("XEP-0357 Push Notifications", function () {
|
||||||
|
|
||||||
|
beforeEach(() => (jasmine.DEFAULT_TIMEOUT_INTERVAL = 7000));
|
||||||
|
afterEach(() => (jasmine.DEFAULT_TIMEOUT_INTERVAL = original_timeout));
|
||||||
|
|
||||||
it("can be enabled",
|
it("can be enabled",
|
||||||
mock.initConverse(
|
mock.initConverse(
|
||||||
['rosterGroupsFetched'], {
|
['rosterGroupsFetched'], {
|
||||||
|
@ -180,6 +180,7 @@ describe("Message Retractions", function () {
|
|||||||
_converse.connection._dataRecv(mock.createRequest(received_stanza));
|
_converse.connection._dataRecv(mock.createRequest(received_stanza));
|
||||||
await u.waitUntil(() => view.model.handleModeration.calls.count() === 2);
|
await u.waitUntil(() => view.model.handleModeration.calls.count() === 2);
|
||||||
|
|
||||||
|
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length);
|
||||||
expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
|
expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
|
||||||
expect(view.model.messages.length).toBe(1);
|
expect(view.model.messages.length).toBe(1);
|
||||||
|
|
||||||
@ -221,10 +222,8 @@ describe("Message Retractions", function () {
|
|||||||
</message>
|
</message>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const promise = new Promise(resolve => _converse.api.listen.on('messageAdded', resolve));
|
|
||||||
_converse.connection._dataRecv(mock.createRequest(retraction_stanza));
|
_converse.connection._dataRecv(mock.createRequest(retraction_stanza));
|
||||||
await u.waitUntil(() => view.model.messages.length === 1);
|
await u.waitUntil(() => view.model.messages.length === 1);
|
||||||
await promise;
|
|
||||||
const message = view.model.messages.at(0);
|
const message = view.model.messages.at(0);
|
||||||
expect(message.get('dangling_retraction')).toBe(true);
|
expect(message.get('dangling_retraction')).toBe(true);
|
||||||
expect(message.get('is_ephemeral')).toBe(false);
|
expect(message.get('is_ephemeral')).toBe(false);
|
||||||
@ -628,8 +627,8 @@ describe("Message Retractions", function () {
|
|||||||
`</apply-to>`+
|
`</apply-to>`+
|
||||||
`</message>`);
|
`</message>`);
|
||||||
|
|
||||||
|
await u.waitUntil(() => view.model.messages.last().get('retracted'));
|
||||||
const message = view.model.messages.last();
|
const message = view.model.messages.last();
|
||||||
expect(message.get('retracted')).toBeTruthy();
|
|
||||||
expect(message.get('is_ephemeral')).toBe(false);
|
expect(message.get('is_ephemeral')).toBe(false);
|
||||||
expect(message.get('editable')).toBeFalsy();
|
expect(message.get('editable')).toBeFalsy();
|
||||||
|
|
||||||
@ -648,7 +647,7 @@ describe("Message Retractions", function () {
|
|||||||
_converse.connection._dataRecv(mock.createRequest(reflection));
|
_converse.connection._dataRecv(mock.createRequest(reflection));
|
||||||
await u.waitUntil(() => view.model.handleRetraction.calls.count() === 1);
|
await u.waitUntil(() => view.model.handleRetraction.calls.count() === 1);
|
||||||
|
|
||||||
expect(view.model.messages.length).toBe(1);
|
await u.waitUntil(() => view.model.messages.length === 1);
|
||||||
expect(view.model.messages.last().get('retracted')).toBeTruthy();
|
expect(view.model.messages.last().get('retracted')).toBeTruthy();
|
||||||
expect(view.model.messages.last().get('is_ephemeral')).toBe(false);
|
expect(view.model.messages.last().get('is_ephemeral')).toBe(false);
|
||||||
expect(view.model.messages.last().get('editable')).toBe(false);
|
expect(view.model.messages.last().get('editable')).toBe(false);
|
||||||
@ -675,7 +674,7 @@ describe("Message Retractions", function () {
|
|||||||
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1);
|
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1);
|
||||||
|
|
||||||
expect(view.model.messages.length).toBe(1);
|
expect(view.model.messages.length).toBe(1);
|
||||||
expect(view.model.messages.last().get('retracted')).toBeTruthy();
|
await u.waitUntil(() => view.model.messages.last().get('retracted'));
|
||||||
const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div');
|
const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div');
|
||||||
expect(el.textContent.trim()).toBe('romeo has removed this message');
|
expect(el.textContent.trim()).toBe('romeo has removed this message');
|
||||||
|
|
||||||
@ -695,20 +694,15 @@ describe("Message Retractions", function () {
|
|||||||
</message>`);
|
</message>`);
|
||||||
|
|
||||||
_converse.connection._dataRecv(mock.createRequest(error));
|
_converse.connection._dataRecv(mock.createRequest(error));
|
||||||
await u.waitUntil(() => view.el.querySelectorAll('.chat-error').length === 1);
|
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__error').length === 1);
|
||||||
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 0);
|
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 0);
|
||||||
expect(view.model.messages.length).toBe(2);
|
expect(view.model.messages.length).toBe(1);
|
||||||
expect(view.model.messages.at(0).get('retracted')).toBeFalsy();
|
expect(view.model.messages.at(0).get('retracted')).toBeFalsy();
|
||||||
expect(view.model.messages.at(0).get('is_ephemeral')).toBeFalsy();
|
expect(view.model.messages.at(0).get('is_ephemeral')).toBeFalsy();
|
||||||
expect(view.model.messages.at(0).get('editable')).toBeTruthy();
|
expect(view.model.messages.at(0).get('editable')).toBeTruthy();
|
||||||
|
|
||||||
const err_msg = "Sorry, something went wrong while trying to retract your message."
|
const errmsg = view.el.querySelector('.chat-msg__error');
|
||||||
expect(view.model.messages.at(1).get('message')).toBe(err_msg);
|
expect(errmsg.textContent.trim()).toBe("You're not allowed to retract your message.");
|
||||||
expect(view.model.messages.at(1).get('type')).toBe('error');
|
|
||||||
|
|
||||||
expect(view.el.querySelectorAll('.chat-error').length).toBe(1);
|
|
||||||
const errmsg = view.el.querySelector('.chat-error');
|
|
||||||
expect(errmsg.textContent.trim()).toBe("Sorry, something went wrong while trying to retract your message.");
|
|
||||||
done();
|
done();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -728,25 +722,23 @@ describe("Message Retractions", function () {
|
|||||||
occupant.save('role', 'member');
|
occupant.save('role', 'member');
|
||||||
await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.includes("romeo is no longer a moderator"))
|
await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.includes("romeo is no longer a moderator"))
|
||||||
await sendAndThenRetractMessage(_converse, view);
|
await sendAndThenRetractMessage(_converse, view);
|
||||||
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1);
|
|
||||||
|
|
||||||
expect(view.model.messages.length).toBe(1);
|
expect(view.model.messages.length).toBe(1);
|
||||||
expect(view.model.messages.last().get('retracted')).toBeTruthy();
|
expect(view.model.messages.last().get('retracted')).toBeTruthy();
|
||||||
|
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1);
|
||||||
const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div');
|
const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div');
|
||||||
expect(el.textContent.trim()).toBe('romeo has removed this message');
|
expect(el.textContent.trim()).toBe('romeo has removed this message');
|
||||||
|
|
||||||
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1);
|
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1);
|
||||||
|
|
||||||
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 0);
|
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 0);
|
||||||
expect(view.model.messages.length).toBe(3);
|
expect(view.model.messages.length).toBe(1);
|
||||||
expect(view.model.messages.at(0).get('retracted')).toBeFalsy();
|
expect(view.model.messages.at(0).get('retracted')).toBeFalsy();
|
||||||
expect(view.model.messages.at(0).get('is_ephemeral')).toBeFalsy();
|
expect(view.model.messages.at(0).get('is_ephemeral')).toBeFalsy();
|
||||||
expect(view.model.messages.at(0).get('editable')).toBeTruthy();
|
expect(view.model.messages.at(0).get('editable')).toBeTruthy();
|
||||||
|
|
||||||
const error_messages = view.el.querySelectorAll('.chat-error');
|
const error_messages = view.el.querySelectorAll('.chat-msg__error');
|
||||||
expect(error_messages.length).toBe(2);
|
expect(error_messages.length).toBe(1);
|
||||||
expect(error_messages[0].textContent.trim()).toBe("Sorry, something went wrong while trying to retract your message.");
|
expect(error_messages[0].textContent.trim()).toBe('A timeout happened while while trying to retract your message.');
|
||||||
expect(error_messages[1].textContent.trim()).toBe("Timeout Error: No response from server");
|
|
||||||
done();
|
done();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -1009,7 +1001,6 @@ describe("Message Retractions", function () {
|
|||||||
</message>
|
</message>
|
||||||
`);
|
`);
|
||||||
spyOn(view.model, 'handleRetraction').and.callThrough();
|
spyOn(view.model, 'handleRetraction').and.callThrough();
|
||||||
const promise = new Promise(resolve => _converse.api.listen.once('messageAdded', resolve));
|
|
||||||
_converse.connection._dataRecv(mock.createRequest(tombstone));
|
_converse.connection._dataRecv(mock.createRequest(tombstone));
|
||||||
|
|
||||||
const last_id = u.getUniqueId();
|
const last_id = u.getUniqueId();
|
||||||
@ -1037,8 +1028,7 @@ describe("Message Retractions", function () {
|
|||||||
.c('count').t('2');
|
.c('count').t('2');
|
||||||
_converse.connection._dataRecv(mock.createRequest(iq_result));
|
_converse.connection._dataRecv(mock.createRequest(iq_result));
|
||||||
|
|
||||||
await promise;
|
await u.waitUntil(() => view.model.messages.length === 1);
|
||||||
expect(view.model.messages.length).toBe(1);
|
|
||||||
let message = view.model.messages.at(0);
|
let message = view.model.messages.at(0);
|
||||||
expect(message.get('retracted')).toBeTruthy();
|
expect(message.get('retracted')).toBeTruthy();
|
||||||
expect(message.get('is_tombstone')).toBe(true);
|
expect(message.get('is_tombstone')).toBe(true);
|
||||||
@ -1050,6 +1040,7 @@ describe("Message Retractions", function () {
|
|||||||
message = view.model.messages.at(0);
|
message = view.model.messages.at(0);
|
||||||
expect(message.get('retracted')).toBeTruthy();
|
expect(message.get('retracted')).toBeTruthy();
|
||||||
expect(message.get('is_tombstone')).toBe(true);
|
expect(message.get('is_tombstone')).toBe(true);
|
||||||
|
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length);
|
||||||
expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
|
expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
|
||||||
expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1);
|
expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1);
|
||||||
const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div');
|
const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div');
|
||||||
@ -1088,7 +1079,6 @@ describe("Message Retractions", function () {
|
|||||||
</message>
|
</message>
|
||||||
`);
|
`);
|
||||||
spyOn(view.model, 'handleModeration').and.callThrough();
|
spyOn(view.model, 'handleModeration').and.callThrough();
|
||||||
const promise = new Promise(resolve => _converse.api.listen.once('messageAdded', resolve));
|
|
||||||
_converse.connection._dataRecv(mock.createRequest(tombstone));
|
_converse.connection._dataRecv(mock.createRequest(tombstone));
|
||||||
|
|
||||||
const last_id = u.getUniqueId();
|
const last_id = u.getUniqueId();
|
||||||
@ -1119,10 +1109,10 @@ describe("Message Retractions", function () {
|
|||||||
.c('count').t('2');
|
.c('count').t('2');
|
||||||
_converse.connection._dataRecv(mock.createRequest(iq_result));
|
_converse.connection._dataRecv(mock.createRequest(iq_result));
|
||||||
|
|
||||||
await promise;
|
await u.waitUntil(() => view.model.messages.length);
|
||||||
expect(view.model.messages.length).toBe(1);
|
expect(view.model.messages.length).toBe(1);
|
||||||
let message = view.model.messages.at(0);
|
let message = view.model.messages.at(0);
|
||||||
expect(message.get('retracted')).toBeTruthy();
|
await u.waitUntil(() => message.get('retracted'));
|
||||||
expect(message.get('is_tombstone')).toBe(true);
|
expect(message.get('is_tombstone')).toBe(true);
|
||||||
|
|
||||||
await u.waitUntil(() => view.model.handleModeration.calls.count() === 2);
|
await u.waitUntil(() => view.model.handleModeration.calls.count() === 2);
|
||||||
@ -1134,6 +1124,8 @@ describe("Message Retractions", function () {
|
|||||||
expect(message.get('retracted')).toBeTruthy();
|
expect(message.get('retracted')).toBeTruthy();
|
||||||
expect(message.get('is_tombstone')).toBe(true);
|
expect(message.get('is_tombstone')).toBe(true);
|
||||||
expect(message.get('moderation_reason')).toBe("This message contains inappropriate content");
|
expect(message.get('moderation_reason')).toBe("This message contains inappropriate content");
|
||||||
|
|
||||||
|
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length, 500);
|
||||||
expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
|
expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
|
||||||
|
|
||||||
expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1);
|
expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1);
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
/* global mock */
|
/* global mock */
|
||||||
|
|
||||||
|
const original_timeout = jasmine.DEFAULT_TIMEOUT_INTERVAL;
|
||||||
|
|
||||||
describe("A spoiler message", function () {
|
describe("A spoiler message", function () {
|
||||||
|
|
||||||
|
beforeEach(() => (jasmine.DEFAULT_TIMEOUT_INTERVAL = 7000));
|
||||||
|
afterEach(() => (jasmine.DEFAULT_TIMEOUT_INTERVAL = original_timeout));
|
||||||
|
|
||||||
it("can be received with a hint",
|
it("can be received with a hint",
|
||||||
mock.initConverse(
|
mock.initConverse(
|
||||||
['rosterGroupsFetched', 'chatBoxesFetched'], {},
|
['rosterGroupsFetched', 'chatBoxesFetched'], {},
|
||||||
@ -32,11 +37,11 @@ describe("A spoiler message", function () {
|
|||||||
_converse.connection._dataRecv(mock.createRequest(msg));
|
_converse.connection._dataRecv(mock.createRequest(msg));
|
||||||
await new Promise(resolve => _converse.api.listen.once('chatBoxViewInitialized', resolve));
|
await new Promise(resolve => _converse.api.listen.once('chatBoxViewInitialized', resolve));
|
||||||
const view = _converse.chatboxviews.get(sender_jid);
|
const view = _converse.chatboxviews.get(sender_jid);
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
await u.waitUntil(() => view.model.vcard.get('fullname') === 'Mercutio')
|
await u.waitUntil(() => view.model.vcard.get('fullname') === 'Mercutio')
|
||||||
expect(view.el.querySelector('.chat-msg__author').textContent.trim()).toBe('Mercutio');
|
expect(view.el.querySelector('.chat-msg__author').textContent.trim()).toBe('Mercutio');
|
||||||
const message_content = view.el.querySelector('.chat-msg__text');
|
const message_content = view.el.querySelector('.chat-msg__text');
|
||||||
expect(message_content.textContent).toBe(spoiler);
|
await u.waitUntil(() => message_content.textContent === spoiler);
|
||||||
const spoiler_hint_el = view.el.querySelector('.spoiler-hint');
|
const spoiler_hint_el = view.el.querySelector('.spoiler-hint');
|
||||||
expect(spoiler_hint_el.textContent).toBe(spoiler_hint);
|
expect(spoiler_hint_el.textContent).toBe(spoiler_hint);
|
||||||
done();
|
done();
|
||||||
@ -72,9 +77,10 @@ describe("A spoiler message", function () {
|
|||||||
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
await u.waitUntil(() => u.isVisible(view.el));
|
await u.waitUntil(() => u.isVisible(view.el));
|
||||||
await u.waitUntil(() => view.model.vcard.get('fullname') === 'Mercutio')
|
await u.waitUntil(() => view.model.vcard.get('fullname') === 'Mercutio')
|
||||||
|
await u.waitUntil(() => u.isVisible(view.el.querySelector('.chat-msg__author')));
|
||||||
expect(view.el.querySelector('.chat-msg__author').textContent.includes('Mercutio')).toBeTruthy();
|
expect(view.el.querySelector('.chat-msg__author').textContent.includes('Mercutio')).toBeTruthy();
|
||||||
const message_content = view.el.querySelector('.chat-msg__text');
|
const message_content = view.el.querySelector('.chat-msg__text');
|
||||||
expect(message_content.textContent).toBe(spoiler);
|
await u.waitUntil(() => message_content.textContent === spoiler);
|
||||||
const spoiler_hint_el = view.el.querySelector('.spoiler-hint');
|
const spoiler_hint_el = view.el.querySelector('.spoiler-hint');
|
||||||
expect(spoiler_hint_el.textContent).toBe('');
|
expect(spoiler_hint_el.textContent).toBe('');
|
||||||
done();
|
done();
|
||||||
@ -117,7 +123,7 @@ describe("A spoiler message", function () {
|
|||||||
preventDefault: function preventDefault () {},
|
preventDefault: function preventDefault () {},
|
||||||
keyCode: 13
|
keyCode: 13
|
||||||
});
|
});
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
|
|
||||||
/* Test the XML stanza
|
/* Test the XML stanza
|
||||||
*
|
*
|
||||||
@ -136,23 +142,26 @@ describe("A spoiler message", function () {
|
|||||||
expect(spoiler_el === null).toBeFalsy();
|
expect(spoiler_el === null).toBeFalsy();
|
||||||
expect(spoiler_el.textContent).toBe('');
|
expect(spoiler_el.textContent).toBe('');
|
||||||
|
|
||||||
|
const spoiler = 'This is the spoiler';
|
||||||
const body_el = stanza.querySelector('body');
|
const body_el = stanza.querySelector('body');
|
||||||
expect(body_el.textContent).toBe('This is the spoiler');
|
expect(body_el.textContent).toBe(spoiler);
|
||||||
|
|
||||||
/* Test the HTML spoiler message */
|
/* Test the HTML spoiler message */
|
||||||
expect(view.el.querySelector('.chat-msg__author').textContent.trim()).toBe('Romeo Montague');
|
expect(view.el.querySelector('.chat-msg__author').textContent.trim()).toBe('Romeo Montague');
|
||||||
|
|
||||||
|
const message_content = view.el.querySelector('.chat-msg__text');
|
||||||
|
await u.waitUntil(() => message_content.textContent === spoiler);
|
||||||
|
|
||||||
const spoiler_msg_el = view.el.querySelector('.chat-msg__text.spoiler');
|
const spoiler_msg_el = view.el.querySelector('.chat-msg__text.spoiler');
|
||||||
expect(spoiler_msg_el.textContent).toBe('This is the spoiler');
|
|
||||||
expect(Array.from(spoiler_msg_el.classList).includes('collapsed')).toBeTruthy();
|
expect(Array.from(spoiler_msg_el.classList).includes('collapsed')).toBeTruthy();
|
||||||
|
|
||||||
spoiler_toggle = view.el.querySelector('.spoiler-toggle');
|
spoiler_toggle = view.el.querySelector('.spoiler-toggle');
|
||||||
expect(spoiler_toggle.textContent).toBe('Show more');
|
expect(spoiler_toggle.textContent.trim()).toBe('Show more');
|
||||||
spoiler_toggle.click();
|
spoiler_toggle.click();
|
||||||
expect(Array.from(spoiler_msg_el.classList).includes('collapsed')).toBeFalsy();
|
await u.waitUntil(() => !Array.from(spoiler_msg_el.classList).includes('collapsed'));
|
||||||
expect(spoiler_toggle.textContent).toBe('Show less');
|
expect(spoiler_toggle.textContent.trim()).toBe('Show less');
|
||||||
spoiler_toggle.click();
|
spoiler_toggle.click();
|
||||||
expect(Array.from(spoiler_msg_el.classList).includes('collapsed')).toBeTruthy();
|
await u.waitUntil(() => Array.from(spoiler_msg_el.classList).includes('collapsed'));
|
||||||
done();
|
done();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -197,7 +206,7 @@ describe("A spoiler message", function () {
|
|||||||
preventDefault: function preventDefault () {},
|
preventDefault: function preventDefault () {},
|
||||||
keyCode: 13
|
keyCode: 13
|
||||||
});
|
});
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||||
|
|
||||||
/* Test the XML stanza
|
/* Test the XML stanza
|
||||||
*
|
*
|
||||||
@ -217,23 +226,26 @@ describe("A spoiler message", function () {
|
|||||||
expect(spoiler_el === null).toBeFalsy();
|
expect(spoiler_el === null).toBeFalsy();
|
||||||
expect(spoiler_el.textContent).toBe('This is the hint');
|
expect(spoiler_el.textContent).toBe('This is the hint');
|
||||||
|
|
||||||
|
const spoiler = 'This is the spoiler'
|
||||||
const body_el = stanza.querySelector('body');
|
const body_el = stanza.querySelector('body');
|
||||||
expect(body_el.textContent).toBe('This is the spoiler');
|
expect(body_el.textContent).toBe(spoiler);
|
||||||
|
|
||||||
/* Test the HTML spoiler message */
|
/* Test the HTML spoiler message */
|
||||||
expect(view.el.querySelector('.chat-msg__author').textContent.trim()).toBe('Romeo Montague');
|
expect(view.el.querySelector('.chat-msg__author').textContent.trim()).toBe('Romeo Montague');
|
||||||
|
|
||||||
|
const message_content = view.el.querySelector('.chat-msg__text');
|
||||||
|
await u.waitUntil(() => message_content.textContent === spoiler);
|
||||||
|
|
||||||
const spoiler_msg_el = view.el.querySelector('.chat-msg__text.spoiler');
|
const spoiler_msg_el = view.el.querySelector('.chat-msg__text.spoiler');
|
||||||
expect(spoiler_msg_el.textContent).toBe('This is the spoiler');
|
|
||||||
expect(Array.from(spoiler_msg_el.classList).includes('collapsed')).toBeTruthy();
|
expect(Array.from(spoiler_msg_el.classList).includes('collapsed')).toBeTruthy();
|
||||||
|
|
||||||
spoiler_toggle = view.el.querySelector('.spoiler-toggle');
|
spoiler_toggle = view.el.querySelector('.spoiler-toggle');
|
||||||
expect(spoiler_toggle.textContent).toBe('Show more');
|
expect(spoiler_toggle.textContent.trim()).toBe('Show more');
|
||||||
spoiler_toggle.click();
|
spoiler_toggle.click();
|
||||||
expect(Array.from(spoiler_msg_el.classList).includes('collapsed')).toBeFalsy();
|
await u.waitUntil(() => !Array.from(spoiler_msg_el.classList).includes('collapsed'));
|
||||||
expect(spoiler_toggle.textContent).toBe('Show less');
|
expect(spoiler_toggle.textContent.trim()).toBe('Show less');
|
||||||
spoiler_toggle.click();
|
spoiler_toggle.click();
|
||||||
expect(Array.from(spoiler_msg_el.classList).includes('collapsed')).toBeTruthy();
|
await u.waitUntil(() => Array.from(spoiler_msg_el.classList).includes('collapsed'));
|
||||||
done();
|
done();
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
48
spec/xss.js
48
spec/xss.js
@ -24,44 +24,44 @@ describe("XSS", function () {
|
|||||||
await mock.sendMessage(view, message);
|
await mock.sendMessage(view, message);
|
||||||
let msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
|
let msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
|
||||||
expect(msg.textContent).toEqual(message);
|
expect(msg.textContent).toEqual(message);
|
||||||
expect(msg.innerHTML).toEqual("<img src=x onerror=alert('XSS');>");
|
expect(msg.innerHTML.replace(/<!---->/g, '')).toEqual("<img src=x onerror=alert('XSS');>");
|
||||||
expect(window.alert).not.toHaveBeenCalled();
|
expect(window.alert).not.toHaveBeenCalled();
|
||||||
|
|
||||||
message = "<img src=x onerror=alert('XSS')//";
|
message = "<img src=x onerror=alert('XSS')//";
|
||||||
await mock.sendMessage(view, message);
|
await mock.sendMessage(view, message);
|
||||||
msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
|
msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
|
||||||
expect(msg.textContent).toEqual(message);
|
expect(msg.textContent).toEqual(message);
|
||||||
expect(msg.innerHTML).toEqual("<img src=x onerror=alert('XSS')//");
|
expect(msg.innerHTML.replace(/<!---->/g, '')).toEqual("<img src=x onerror=alert('XSS')//");
|
||||||
|
|
||||||
message = "<img src=x onerror=alert(String.fromCharCode(88,83,83));>";
|
message = "<img src=x onerror=alert(String.fromCharCode(88,83,83));>";
|
||||||
await mock.sendMessage(view, message);
|
await mock.sendMessage(view, message);
|
||||||
msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
|
msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
|
||||||
expect(msg.textContent).toEqual(message);
|
expect(msg.textContent).toEqual(message);
|
||||||
expect(msg.innerHTML).toEqual("<img src=x onerror=alert(String.fromCharCode(88,83,83));>");
|
expect(msg.innerHTML.replace(/<!---->/g, '')).toEqual("<img src=x onerror=alert(String.fromCharCode(88,83,83));>");
|
||||||
|
|
||||||
message = "<img src=x oneonerrorrror=alert(String.fromCharCode(88,83,83));>";
|
message = "<img src=x oneonerrorrror=alert(String.fromCharCode(88,83,83));>";
|
||||||
await mock.sendMessage(view, message);
|
await mock.sendMessage(view, message);
|
||||||
msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
|
msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
|
||||||
expect(msg.textContent).toEqual(message);
|
expect(msg.textContent).toEqual(message);
|
||||||
expect(msg.innerHTML).toEqual("<img src=x oneonerrorrror=alert(String.fromCharCode(88,83,83));>");
|
expect(msg.innerHTML.replace(/<!---->/g, '')).toEqual("<img src=x oneonerrorrror=alert(String.fromCharCode(88,83,83));>");
|
||||||
|
|
||||||
message = "<img src=x:alert(alt) onerror=eval(src) alt=xss>";
|
message = "<img src=x:alert(alt) onerror=eval(src) alt=xss>";
|
||||||
await mock.sendMessage(view, message);
|
await mock.sendMessage(view, message);
|
||||||
msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
|
msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
|
||||||
expect(msg.textContent).toEqual(message);
|
expect(msg.textContent).toEqual(message);
|
||||||
expect(msg.innerHTML).toEqual("<img src=x:alert(alt) onerror=eval(src) alt=xss>");
|
expect(msg.innerHTML.replace(/<!---->/g, '')).toEqual("<img src=x:alert(alt) onerror=eval(src) alt=xss>");
|
||||||
|
|
||||||
message = "><img src=x onerror=alert('XSS');>";
|
message = "><img src=x onerror=alert('XSS');>";
|
||||||
await mock.sendMessage(view, message);
|
await mock.sendMessage(view, message);
|
||||||
msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
|
msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
|
||||||
expect(msg.textContent).toEqual(message);
|
expect(msg.textContent).toEqual(message);
|
||||||
expect(msg.innerHTML).toEqual("><img src=x onerror=alert('XSS');>");
|
expect(msg.innerHTML.replace(/<!---->/g, '')).toEqual("><img src=x onerror=alert('XSS');>");
|
||||||
|
|
||||||
message = "><img src=x onerror=alert(String.fromCharCode(88,83,83));>";
|
message = "><img src=x onerror=alert(String.fromCharCode(88,83,83));>";
|
||||||
await mock.sendMessage(view, message);
|
await mock.sendMessage(view, message);
|
||||||
msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
|
msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
|
||||||
expect(msg.textContent).toEqual(message);
|
expect(msg.textContent).toEqual(message);
|
||||||
expect(msg.innerHTML).toEqual("><img src=x onerror=alert(String.fromCharCode(88,83,83));>");
|
expect(msg.innerHTML.replace(/<!---->/g, '')).toEqual("><img src=x onerror=alert(String.fromCharCode(88,83,83));>");
|
||||||
|
|
||||||
expect(window.alert).not.toHaveBeenCalled();
|
expect(window.alert).not.toHaveBeenCalled();
|
||||||
done();
|
done();
|
||||||
@ -84,43 +84,43 @@ describe("XSS", function () {
|
|||||||
await mock.sendMessage(view, message);
|
await mock.sendMessage(view, message);
|
||||||
let msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
|
let msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
|
||||||
expect(msg.textContent).toEqual(message);
|
expect(msg.textContent).toEqual(message);
|
||||||
expect(msg.innerHTML).toEqual('<svgonload=alert(1)>');
|
expect(msg.innerHTML.replace(/<!---->/g, '')).toEqual('<svgonload=alert(1)>');
|
||||||
|
|
||||||
message = "<svg/onload=alert('XSS')>";
|
message = "<svg/onload=alert('XSS')>";
|
||||||
await mock.sendMessage(view, message);
|
await mock.sendMessage(view, message);
|
||||||
msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
|
msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
|
||||||
expect(msg.textContent).toEqual(message);
|
expect(msg.textContent).toEqual(message);
|
||||||
expect(msg.innerHTML).toEqual("<svg/onload=alert('XSS')>");
|
expect(msg.innerHTML.replace(/<!---->/g, '')).toEqual("<svg/onload=alert('XSS')>");
|
||||||
|
|
||||||
message = "<svg onload=alert(1)//";
|
message = "<svg onload=alert(1)//";
|
||||||
await mock.sendMessage(view, message);
|
await mock.sendMessage(view, message);
|
||||||
msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
|
msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
|
||||||
expect(msg.textContent).toEqual(message);
|
expect(msg.textContent).toEqual(message);
|
||||||
expect(msg.innerHTML).toEqual("<svg onload=alert(1)//");
|
expect(msg.innerHTML.replace(/<!---->/g, '')).toEqual("<svg onload=alert(1)//");
|
||||||
|
|
||||||
message = "<svg/onload=alert(String.fromCharCode(88,83,83))>";
|
message = "<svg/onload=alert(String.fromCharCode(88,83,83))>";
|
||||||
await mock.sendMessage(view, message);
|
await mock.sendMessage(view, message);
|
||||||
msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
|
msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
|
||||||
expect(msg.textContent).toEqual(message);
|
expect(msg.textContent).toEqual(message);
|
||||||
expect(msg.innerHTML).toEqual("<svg/onload=alert(String.fromCharCode(88,83,83))>");
|
expect(msg.innerHTML.replace(/<!---->/g, '')).toEqual("<svg/onload=alert(String.fromCharCode(88,83,83))>");
|
||||||
|
|
||||||
message = "<svg id=alert(1) onload=eval(id)>";
|
message = "<svg id=alert(1) onload=eval(id)>";
|
||||||
await mock.sendMessage(view, message);
|
await mock.sendMessage(view, message);
|
||||||
msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
|
msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
|
||||||
expect(msg.textContent).toEqual(message);
|
expect(msg.textContent).toEqual(message);
|
||||||
expect(msg.innerHTML).toEqual("<svg id=alert(1) onload=eval(id)>");
|
expect(msg.innerHTML.replace(/<!---->/g, '')).toEqual("<svg id=alert(1) onload=eval(id)>");
|
||||||
|
|
||||||
message = '"><svg/onload=alert(String.fromCharCode(88,83,83))>';
|
message = '"><svg/onload=alert(String.fromCharCode(88,83,83))>';
|
||||||
await mock.sendMessage(view, message);
|
await mock.sendMessage(view, message);
|
||||||
msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
|
msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
|
||||||
expect(msg.textContent).toEqual(message);
|
expect(msg.textContent).toEqual(message);
|
||||||
expect(msg.innerHTML).toEqual('"><svg/onload=alert(String.fromCharCode(88,83,83))>');
|
expect(msg.innerHTML.replace(/<!---->/g, '')).toEqual('"><svg/onload=alert(String.fromCharCode(88,83,83))>');
|
||||||
|
|
||||||
message = '"><svg/onload=alert(/XSS/)';
|
message = '"><svg/onload=alert(/XSS/)';
|
||||||
await mock.sendMessage(view, message);
|
await mock.sendMessage(view, message);
|
||||||
msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
|
msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
|
||||||
expect(msg.textContent).toEqual(message);
|
expect(msg.textContent).toEqual(message);
|
||||||
expect(msg.innerHTML).toEqual('"><svg/onload=alert(/XSS/)');
|
expect(msg.innerHTML.replace(/<!---->/g, '')).toEqual('"><svg/onload=alert(/XSS/)');
|
||||||
|
|
||||||
expect(window.alert).not.toHaveBeenCalled();
|
expect(window.alert).not.toHaveBeenCalled();
|
||||||
done();
|
done();
|
||||||
@ -143,7 +143,7 @@ describe("XSS", function () {
|
|||||||
|
|
||||||
let msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
|
let msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
|
||||||
expect(msg.textContent).toEqual(message);
|
expect(msg.textContent).toEqual(message);
|
||||||
expect(msg.innerHTML)
|
expect(msg.innerHTML.replace(/<!---->/g, ''))
|
||||||
.toEqual('<a target="_blank" rel="noopener" href="http://www.opkode.com/%27onmouseover=%27alert%281%29%27whatever">http://www.opkode.com/\'onmouseover=\'alert(1)\'whatever</a>');
|
.toEqual('<a target="_blank" rel="noopener" href="http://www.opkode.com/%27onmouseover=%27alert%281%29%27whatever">http://www.opkode.com/\'onmouseover=\'alert(1)\'whatever</a>');
|
||||||
|
|
||||||
message = 'http://www.opkode.com/"onmouseover="alert(1)"whatever';
|
message = 'http://www.opkode.com/"onmouseover="alert(1)"whatever';
|
||||||
@ -151,21 +151,21 @@ describe("XSS", function () {
|
|||||||
|
|
||||||
msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
|
msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
|
||||||
expect(msg.textContent).toEqual(message);
|
expect(msg.textContent).toEqual(message);
|
||||||
expect(msg.innerHTML).toEqual('<a target="_blank" rel="noopener" href="http://www.opkode.com/%22onmouseover=%22alert%281%29%22whatever">http://www.opkode.com/"onmouseover="alert(1)"whatever</a>');
|
expect(msg.innerHTML.replace(/<!---->/g, '')).toEqual('<a target="_blank" rel="noopener" href="http://www.opkode.com/%22onmouseover=%22alert%281%29%22whatever">http://www.opkode.com/"onmouseover="alert(1)"whatever</a>');
|
||||||
|
|
||||||
message = "https://en.wikipedia.org/wiki/Ender's_Game";
|
message = "https://en.wikipedia.org/wiki/Ender's_Game";
|
||||||
await mock.sendMessage(view, message);
|
await mock.sendMessage(view, message);
|
||||||
|
|
||||||
msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
|
msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
|
||||||
expect(msg.textContent).toEqual(message);
|
expect(msg.textContent).toEqual(message);
|
||||||
expect(msg.innerHTML).toEqual('<a target="_blank" rel="noopener" href="https://en.wikipedia.org/wiki/Ender%27s_Game">'+message+'</a>');
|
expect(msg.innerHTML.replace(/<!---->/g, '')).toEqual('<a target="_blank" rel="noopener" href="https://en.wikipedia.org/wiki/Ender%27s_Game">'+message+'</a>');
|
||||||
|
|
||||||
message = "<https://bugs.documentfoundation.org/show_bug.cgi?id=123737>";
|
message = "<https://bugs.documentfoundation.org/show_bug.cgi?id=123737>";
|
||||||
await mock.sendMessage(view, message);
|
await mock.sendMessage(view, message);
|
||||||
|
|
||||||
msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
|
msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
|
||||||
expect(msg.textContent).toEqual(message);
|
expect(msg.textContent).toEqual(message);
|
||||||
expect(msg.innerHTML).toEqual(
|
expect(msg.innerHTML.replace(/<!---->/g, '')).toEqual(
|
||||||
`<<a target="_blank" rel="noopener" href="https://bugs.documentfoundation.org/show_bug.cgi?id=123737">https://bugs.documentfoundation.org/show_bug.cgi?id=123737</a>>`);
|
`<<a target="_blank" rel="noopener" href="https://bugs.documentfoundation.org/show_bug.cgi?id=123737">https://bugs.documentfoundation.org/show_bug.cgi?id=123737</a>>`);
|
||||||
|
|
||||||
message = '<http://www.opkode.com/"onmouseover="alert(1)"whatever>';
|
message = '<http://www.opkode.com/"onmouseover="alert(1)"whatever>';
|
||||||
@ -173,7 +173,7 @@ describe("XSS", function () {
|
|||||||
|
|
||||||
msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
|
msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
|
||||||
expect(msg.textContent).toEqual(message);
|
expect(msg.textContent).toEqual(message);
|
||||||
expect(msg.innerHTML).toEqual(
|
expect(msg.innerHTML.replace(/<!---->/g, '')).toEqual(
|
||||||
'<<a target="_blank" rel="noopener" href="http://www.opkode.com/%22onmouseover=%22alert%281%29%22whatever">http://www.opkode.com/"onmouseover="alert(1)"whatever</a>>');
|
'<<a target="_blank" rel="noopener" href="http://www.opkode.com/%22onmouseover=%22alert%281%29%22whatever">http://www.opkode.com/"onmouseover="alert(1)"whatever</a>>');
|
||||||
|
|
||||||
message = `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`
|
message = `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`
|
||||||
@ -181,7 +181,7 @@ describe("XSS", function () {
|
|||||||
|
|
||||||
msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
|
msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
|
||||||
expect(msg.textContent).toEqual(message);
|
expect(msg.textContent).toEqual(message);
|
||||||
expect(msg.innerHTML).toEqual(
|
expect(msg.innerHTML.replace(/<!---->/g, '')).toEqual(
|
||||||
`<a target="_blank" rel="noopener" href="https://www.google.com/maps/place/Kochstraat+6,+2041+CE+Zandvoort/@52.3775999,4.548971,3a,15y,170.85h,88.39t/data=%213m6%211e1%213m4%211sQ7SdHo_bPLPlLlU8GSGWaQ%212e0%217i13312%218i6656%214m5%213m4%211s0x47c5ec1e56f845ad:0x1de0bc4a5771fb08%218m2%213d52.3773668%214d4.5489388%215m1%211e2">https://www.google.com/maps/place/Kochstraat+6,+2041+CE+Zandvoort/@52.3775999,4.548971,3a,15y,170.85h,88.39t/data=!3m6!1e1!3m4!1sQ7SdHo_bPLPlLlU8GSGWaQ!2e0!7i13312!8i6656!4m5!3m4!1s0x47c5ec1e56f845ad:0x1de0bc4a5771fb08!8m2!3d52.3773668!4d4.5489388!5m1!1e2</a>`);
|
`<a target="_blank" rel="noopener" href="https://www.google.com/maps/place/Kochstraat+6,+2041+CE+Zandvoort/@52.3775999,4.548971,3a,15y,170.85h,88.39t/data=%213m6%211e1%213m4%211sQ7SdHo_bPLPlLlU8GSGWaQ%212e0%217i13312%218i6656%214m5%213m4%211s0x47c5ec1e56f845ad:0x1de0bc4a5771fb08%218m2%213d52.3773668%214d4.5489388%215m1%211e2">https://www.google.com/maps/place/Kochstraat+6,+2041+CE+Zandvoort/@52.3775999,4.548971,3a,15y,170.85h,88.39t/data=!3m6!1e1!3m4!1sQ7SdHo_bPLPlLlU8GSGWaQ!2e0!7i13312!8i6656!4m5!3m4!1s0x47c5ec1e56f845ad:0x1de0bc4a5771fb08!8m2!3d52.3773668!4d4.5489388!5m1!1e2</a>`);
|
||||||
done();
|
done();
|
||||||
}));
|
}));
|
||||||
@ -226,19 +226,19 @@ describe("XSS", function () {
|
|||||||
function checkNonParsedURL (url) {
|
function checkNonParsedURL (url) {
|
||||||
const msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
|
const msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
|
||||||
expect(msg.textContent).toEqual(url);
|
expect(msg.textContent).toEqual(url);
|
||||||
expect(msg.innerHTML).toEqual(url);
|
expect(msg.innerHTML.replace(/<!---->/g, '')).toEqual(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkParsedURL ({ entered, href }) {
|
function checkParsedURL ({ entered, href }) {
|
||||||
const msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
|
const msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
|
||||||
expect(msg.textContent).toEqual(entered);
|
expect(msg.textContent).toEqual(entered);
|
||||||
expect(msg.innerHTML).toEqual(`<a target="_blank" rel="noopener" href="${href}">${entered}</a>`);
|
expect(msg.innerHTML.replace(/<!---->/g, '')).toEqual(`<a target="_blank" rel="noopener" href="${href}">${entered}</a>`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkParsedXMPPURL ({ entered, href }) {
|
function checkParsedXMPPURL ({ entered, href }) {
|
||||||
const msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
|
const msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop();
|
||||||
expect(msg.textContent).toEqual(entered);
|
expect(msg.textContent.trim()).toEqual(entered);
|
||||||
expect(msg.innerHTML).toEqual(`<a target="_blank" rel="noopener" class="open-chatroom" href="${href}">${entered}</a>`);
|
expect(msg.innerHTML.replace(/<!---->/g, '').trim()).toEqual(`<a target="_blank" rel="noopener" href="${href}">${entered}</a>`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await mock.sendMessage(view, bad_urls[0]);
|
await mock.sendMessage(view, bad_urls[0]);
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import "./autocomplete.js"
|
import "./autocomplete.js"
|
||||||
|
import log from "@converse/headless/log";
|
||||||
|
import sizzle from "sizzle";
|
||||||
import { CustomElement } from './element.js';
|
import { CustomElement } from './element.js';
|
||||||
import { __ } from '@converse/headless/i18n';
|
import { __ } from '@converse/headless/i18n';
|
||||||
import { api, converse } from "@converse/headless/converse-core";
|
import { api, converse } from "@converse/headless/converse-core";
|
||||||
import { html } from "lit-html";
|
import { html } from "lit-html";
|
||||||
import { unsafeHTML } from 'lit-html/directives/unsafe-html.js';
|
import { unsafeHTML } from 'lit-html/directives/unsafe-html.js';
|
||||||
import log from "@converse/headless/log";
|
|
||||||
import sizzle from "sizzle";
|
|
||||||
|
|
||||||
const { Strophe, $iq } = converse.env;
|
const { Strophe, $iq } = converse.env;
|
||||||
const u = converse.env.utils;
|
const u = converse.env.utils;
|
||||||
|
41
src/components/chat_content.js
Normal file
41
src/components/chat_content.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import "../components/message-history";
|
||||||
|
import xss from "xss/dist/xss";
|
||||||
|
import { CustomElement } from './element.js';
|
||||||
|
import { html } from 'lit-element';
|
||||||
|
import { unsafeHTML } from 'lit-html/directives/unsafe-html.js';
|
||||||
|
|
||||||
|
|
||||||
|
class ChatContent extends CustomElement {
|
||||||
|
|
||||||
|
static get properties () {
|
||||||
|
return {
|
||||||
|
chatview: { type: Object},
|
||||||
|
messages: { type: Array},
|
||||||
|
notifications: { type: String }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const notifications = xss.filterXSS(this.notifications, {'whiteList': {}});
|
||||||
|
return html`
|
||||||
|
<converse-message-history
|
||||||
|
.chatview=${this.chatview}
|
||||||
|
.messages=${this.messages}>
|
||||||
|
</converse-message-history>
|
||||||
|
<div class="chat-content__notifications">${unsafeHTML(notifications)}</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollDown () {
|
||||||
|
if (!this.chatview.model.get('scrolled')) {
|
||||||
|
this.parentElement.scrollTop = this.parentElement.scrollHeight;
|
||||||
|
}
|
||||||
|
this.parentElement.scrollTop = this.parentElement.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
updated () {
|
||||||
|
this.scrollDown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('converse-chat-content', ChatContent);
|
@ -1,8 +1,8 @@
|
|||||||
import { html } from 'lit-element';
|
|
||||||
import { CustomElement } from './element.js';
|
|
||||||
import { until } from 'lit-html/directives/until.js';
|
|
||||||
import DOMNavigator from "../dom-navigator";
|
import DOMNavigator from "../dom-navigator";
|
||||||
|
import { CustomElement } from './element.js';
|
||||||
import { converse } from "@converse/headless/converse-core";
|
import { converse } from "@converse/headless/converse-core";
|
||||||
|
import { html } from 'lit-element';
|
||||||
|
import { until } from 'lit-html/directives/until.js';
|
||||||
|
|
||||||
const u = converse.env.utils;
|
const u = converse.env.utils;
|
||||||
|
|
||||||
|
43
src/components/help_messages.js
Normal file
43
src/components/help_messages.js
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import 'fa-icons';
|
||||||
|
import xss from "xss/dist/xss";
|
||||||
|
import { CustomElement } from './element.js';
|
||||||
|
import { _converse, converse } from "@converse/headless/converse-core";
|
||||||
|
import { html } from 'lit-element';
|
||||||
|
import { unsafeHTML } from 'lit-html/directives/unsafe-html.js';
|
||||||
|
|
||||||
|
const u = converse.env.utils;
|
||||||
|
|
||||||
|
|
||||||
|
class ChatHelp extends CustomElement {
|
||||||
|
|
||||||
|
static get properties () {
|
||||||
|
return {
|
||||||
|
chat_type: { type: String },
|
||||||
|
messages: { type: Array },
|
||||||
|
model: { type: Object },
|
||||||
|
type: { type: String }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const icon_color = this.chat_type === _converse.CHATROOMS_TYPE ? 'var(--chatroom-head-bg-color)' : 'var(--chat-head-color)';
|
||||||
|
const isodate = (new Date()).toISOString();
|
||||||
|
return [
|
||||||
|
html`<fa-icon class="fas fa-times close-chat-help" @click=${this.close} path-prefix="dist" color="${icon_color}" size="1em"></fa-icon>`,
|
||||||
|
...this.messages.map(m => this.renderHelpMessage({
|
||||||
|
isodate,
|
||||||
|
'markup': xss.filterXSS(m, {'whiteList': {'strong': []}})
|
||||||
|
}))
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
close () {
|
||||||
|
this.model.set({'show_help_messages': false});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderHelpMessage (o) {
|
||||||
|
return html`<div class="message chat-${this.type}" data-isodate="${o.isodate}">${unsafeHTML(o.markup)}</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('converse-chat-help', ChatHelp);
|
29
src/components/message-body.js
Normal file
29
src/components/message-body.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { CustomElement } from './element.js';
|
||||||
|
import { renderBodyText } from './../templates/directives/body';
|
||||||
|
import { html } from 'lit-element';
|
||||||
|
|
||||||
|
|
||||||
|
class MessageBody extends CustomElement {
|
||||||
|
|
||||||
|
static get properties () {
|
||||||
|
return {
|
||||||
|
is_only_emojis: { type: Boolean },
|
||||||
|
is_spoiler: { type: Boolean },
|
||||||
|
is_spoiler_visible: { type: Boolean },
|
||||||
|
is_me_message: { type: Boolean },
|
||||||
|
model: { type: Object },
|
||||||
|
text: { type: String },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const spoiler_classes = this.is_spoiler ? `spoiler ${this.is_spoiler_visible ? '' : 'collapsed'}` : '';
|
||||||
|
return html`
|
||||||
|
<div class="chat-msg__text ${this.is_only_emojis ? 'chat-msg__text--larger' : ''} ${spoiler_classes}"
|
||||||
|
>${renderBodyText(this)}</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('converse-chat-message-body', MessageBody);
|
124
src/components/message-history.js
Normal file
124
src/components/message-history.js
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
import "../components/message";
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import tpl_new_day from "../templates//new_day.js";
|
||||||
|
import { CustomElement } from './element.js';
|
||||||
|
import { __ } from '@converse/headless/i18n';
|
||||||
|
import { api } from "@converse/headless/converse-core";
|
||||||
|
import { html } from 'lit-element';
|
||||||
|
import { repeat } from 'lit-html/directives/repeat.js';
|
||||||
|
|
||||||
|
const i18n_no_history = __('No message history available.');
|
||||||
|
|
||||||
|
const tpl_message = (o) => html`
|
||||||
|
<converse-chat-message
|
||||||
|
.chatview=${o.chatview}
|
||||||
|
.hats=${o.hats}
|
||||||
|
.model=${o.model}
|
||||||
|
?allow_retry=${o.retry}
|
||||||
|
?correcting=${o.correcting}
|
||||||
|
?editable=${o.editable}
|
||||||
|
?has_mentions=${o.has_mentions}
|
||||||
|
?is_delayed=${o.is_delayed}
|
||||||
|
?is_encrypted=${o.is_encrypted}
|
||||||
|
?is_me_message=${o.is_me_message}
|
||||||
|
?is_only_emojis=${o.is_only_emojis}
|
||||||
|
?is_retracted=${o.is_retracted}
|
||||||
|
?is_spoiler=${o.is_spoiler}
|
||||||
|
?is_spoiler_visible=${o.is_spoiler_visible}
|
||||||
|
?retractable=${o.retractable}
|
||||||
|
edited=${o.edited || ''}
|
||||||
|
error=${o.error || ''}
|
||||||
|
error_text=${o.error_text || ''}
|
||||||
|
filename=${o.filename || ''}
|
||||||
|
filesize=${o.filesize || ''}
|
||||||
|
from=${o.from}
|
||||||
|
message_type=${o.type || ''}
|
||||||
|
moderated_by=${o.moderated_by || ''}
|
||||||
|
moderation_reason=${o.moderation_reason || ''}
|
||||||
|
msgid=${o.msgid}
|
||||||
|
occupant_affiliation=${o.model.occupant ? o.model.occupant.get('affiliation') : ''}
|
||||||
|
occupant_role=${o.model.occupant ? o.model.occupant.get('role') : ''}
|
||||||
|
oob_url=${o.oob_url || ''}
|
||||||
|
pretty_type=${o.pretty_type}
|
||||||
|
progress=${o.progress || 0 }
|
||||||
|
reason=${o.reason || ''}
|
||||||
|
received=${o.received || ''}
|
||||||
|
sender=${o.sender}
|
||||||
|
spoiler_hint=${o.spoiler_hint || ''}
|
||||||
|
subject=${o.subject || ''}
|
||||||
|
time=${o.time}
|
||||||
|
username=${o.username}></converse-chat-message>
|
||||||
|
`;
|
||||||
|
|
||||||
|
|
||||||
|
// Return a TemplateResult indicating a new day if the passed in message is
|
||||||
|
// more than a day later than its predecessor.
|
||||||
|
function getDayIndicator (model) {
|
||||||
|
const models = model.collection.models;
|
||||||
|
const idx = models.indexOf(model);
|
||||||
|
const prev_model = models[idx-1];
|
||||||
|
if (!prev_model || dayjs(model.get('time')).isAfter(dayjs(prev_model.get('time')), 'day')) {
|
||||||
|
const day_date = dayjs(model.get('time')).startOf('day');
|
||||||
|
return tpl_new_day({
|
||||||
|
'type': 'date',
|
||||||
|
'time': day_date.toISOString(),
|
||||||
|
'datestring': day_date.format("dddd MMM Do YYYY")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class MessageHistory extends CustomElement {
|
||||||
|
|
||||||
|
static get properties () {
|
||||||
|
return {
|
||||||
|
chatview: { type: Object},
|
||||||
|
messages: { type: Array}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const msgs = this.messages;
|
||||||
|
return msgs.length ?
|
||||||
|
html`${repeat(msgs, m => m.get('id'), m => this.renderMessage(m)) }` :
|
||||||
|
html`<div class="empty-history-feedback form-help"><span>${i18n_no_history}</span></div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderMessage (model) {
|
||||||
|
// XXX: leaky abstraction "is_only_key" from converse-omemo
|
||||||
|
if (model.get('dangling_retraction') || model.get('is_only_key')) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const day = getDayIndicator(model);
|
||||||
|
const templates = day ? [day] : [];
|
||||||
|
const is_retracted = model.get('retracted') || model.get('moderated') === 'retracted';
|
||||||
|
const is_groupchat = model.get('type') === 'groupchat';
|
||||||
|
|
||||||
|
let hats = [];
|
||||||
|
if (is_groupchat) {
|
||||||
|
if (api.settings.get('muc_hats_from_vcard')) {
|
||||||
|
const role = model.vcard ? model.vcard.get('role') : null;
|
||||||
|
hats = role ? role.split(',') : [];
|
||||||
|
} else {
|
||||||
|
hats = model.occupant?.get('hats') || [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const chatbox = this.chatview.model;
|
||||||
|
const has_mentions = is_groupchat && model.get('sender') === 'them' && chatbox.isUserMentioned(model);
|
||||||
|
const message = tpl_message(
|
||||||
|
Object.assign(model.toJSON(), {
|
||||||
|
'chatview': this.chatview,
|
||||||
|
'is_me_message': model.isMeCommand(),
|
||||||
|
'occupant': model.occupant,
|
||||||
|
'username': model.getDisplayName(),
|
||||||
|
has_mentions,
|
||||||
|
hats,
|
||||||
|
is_retracted,
|
||||||
|
model,
|
||||||
|
}));
|
||||||
|
return [...templates, message];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('converse-message-history', MessageHistory);
|
288
src/components/message.js
Normal file
288
src/components/message.js
Normal file
@ -0,0 +1,288 @@
|
|||||||
|
import "./message-body.js";
|
||||||
|
import MessageVersionsModal from '../modals/message-versions.js';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import filesize from "filesize";
|
||||||
|
import tpl_spinner from '../templates/spinner.js';
|
||||||
|
import { CustomElement } from './element.js';
|
||||||
|
import { __ } from '@converse/headless/i18n';
|
||||||
|
import { _converse, api, converse } from "@converse/headless/converse-core";
|
||||||
|
import { html } from 'lit-element';
|
||||||
|
import { renderAvatar } from './../templates/directives/avatar';
|
||||||
|
import { renderRetractionLink } from './../templates/directives/retraction';
|
||||||
|
|
||||||
|
const { Strophe } = converse.env;
|
||||||
|
const u = converse.env.utils;
|
||||||
|
|
||||||
|
const i18n_edit_message = __('Edit this message');
|
||||||
|
const i18n_edited = __('This message has been edited');
|
||||||
|
const i18n_show = __('Show more');
|
||||||
|
const i18n_show_less = __('Show less');
|
||||||
|
const i18n_uploading = __('Uploading file:')
|
||||||
|
|
||||||
|
|
||||||
|
class Message extends CustomElement {
|
||||||
|
|
||||||
|
static get properties () {
|
||||||
|
return {
|
||||||
|
allow_retry: { type: Boolean },
|
||||||
|
chatview: { type: Object},
|
||||||
|
correcting: { type: Boolean },
|
||||||
|
editable: { type: Boolean },
|
||||||
|
error: { type: String },
|
||||||
|
error_text: { type: String },
|
||||||
|
first_unread: { type: Boolean },
|
||||||
|
from: { type: String },
|
||||||
|
has_mentions: { type: Boolean },
|
||||||
|
hats: { type: Array },
|
||||||
|
is_delayed: { type: Boolean },
|
||||||
|
is_encrypted: { type: Boolean },
|
||||||
|
is_me_message: { type: Boolean },
|
||||||
|
is_only_emojis: { type: Boolean },
|
||||||
|
is_retracted: { type: Boolean },
|
||||||
|
is_spoiler: { type: Boolean },
|
||||||
|
is_spoiler_visible: { type: Boolean },
|
||||||
|
message_type: { type: String },
|
||||||
|
edited: { type: String },
|
||||||
|
model: { type: Object },
|
||||||
|
moderated_by: { type: String },
|
||||||
|
moderation_reason: { type: String },
|
||||||
|
msgid: { type: String },
|
||||||
|
occupant_affiliation: { type: String },
|
||||||
|
occupant_role: { type: String },
|
||||||
|
oob_url: { type: String },
|
||||||
|
progress: { type: Number },
|
||||||
|
reason: { type: String },
|
||||||
|
received: { type: String },
|
||||||
|
retractable: { type: Boolean },
|
||||||
|
sender: { type: String },
|
||||||
|
show_spinner: { type: Boolean },
|
||||||
|
spoiler_hint: { type: String },
|
||||||
|
subject: { type: String },
|
||||||
|
time: { type: String },
|
||||||
|
username: { type: String }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const format = api.settings.get('time_format');
|
||||||
|
this.pretty_time = dayjs(this.time).format(format);
|
||||||
|
if (this.show_spinner) {
|
||||||
|
return tpl_spinner();
|
||||||
|
} else if (this.model.get('file') && !this.model.get('oob_url')) {
|
||||||
|
return this.renderFileProgress();
|
||||||
|
} else if (['error', 'info'].includes(this.message_type)) {
|
||||||
|
return this.renderInfoMessage();
|
||||||
|
} else {
|
||||||
|
return this.renderChatMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updated () {
|
||||||
|
// XXX: This is ugly but tests rely on this event.
|
||||||
|
// For "normal" chat messages the event is fired in
|
||||||
|
// src/templates/directives/body.js
|
||||||
|
if (
|
||||||
|
this.show_spinner ||
|
||||||
|
(this.model.get('file') && !this.model.get('oob_url')) ||
|
||||||
|
(['error', 'info'].includes(this.message_type))
|
||||||
|
) {
|
||||||
|
this.model.collection?.trigger('rendered', this.model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderInfoMessage () {
|
||||||
|
const isodate = dayjs(this.model.get('time')).toISOString();
|
||||||
|
const i18n_retry = __('Retry');
|
||||||
|
return html`
|
||||||
|
<div class="message chat-info chat-${this.message_type}"
|
||||||
|
data-isodate="${isodate}"
|
||||||
|
data-type="${this.data_name}"
|
||||||
|
data-value="${this.data_value}">
|
||||||
|
|
||||||
|
<div class="chat-info__message">
|
||||||
|
${ this.model.getMessageText() }
|
||||||
|
</div>
|
||||||
|
${ this.reason ? html`<q class="reason">${this.reason}</q>` : `` }
|
||||||
|
${ this.error_text ? html`<q class="reason">${this.error_text}</q>` : `` }
|
||||||
|
${ this.allow_retry ? html`<a class="retry" @click=${this.onRetryClicked}>${i18n_retry}</a>` : '' }
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderFileProgress () {
|
||||||
|
const filename = this.model.file.name;
|
||||||
|
const size = filesize(this.model.file.size);
|
||||||
|
return html`
|
||||||
|
<div class="message chat-msg">
|
||||||
|
${ renderAvatar(this) }
|
||||||
|
<div class="chat-msg__content">
|
||||||
|
<span class="chat-msg__text">${i18n_uploading} <strong>${filename}</strong>, ${size}</span>
|
||||||
|
<progress value="${this.progress}"/>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderChatMessage () {
|
||||||
|
const is_groupchat_message = (this.message_type === 'groupchat');
|
||||||
|
return html`
|
||||||
|
<div class="message chat-msg ${this.message_type} ${this.getExtraMessageClasses()}
|
||||||
|
${ this.is_me_message ? 'chat-msg--action' : '' }
|
||||||
|
${this.isFollowup() ? 'chat-msg--followup' : ''}"
|
||||||
|
data-isodate="${this.time}" data-msgid="${this.msgid}" data-from="${this.from}" data-encrypted="${this.is_encrypted}">
|
||||||
|
|
||||||
|
${ renderAvatar(this) }
|
||||||
|
<div class="chat-msg__content chat-msg__content--${this.sender} ${this.is_me_message ? 'chat-msg__content--action' : ''}">
|
||||||
|
${this.first_unread ? html`<div class="message date-separator"><hr class="separator"><span class="separator-text">{{{this.__('unread messages')}}}</span></div>` : '' }
|
||||||
|
<span class="chat-msg__heading">
|
||||||
|
${ (this.is_me_message) ? html`
|
||||||
|
<time timestamp="${this.time}" class="chat-msg__time">${this.pretty_time}</time>
|
||||||
|
${this.hats.map(hat => html`<span class="badge badge-secondary">${hat}</span>`)}
|
||||||
|
` : '' }
|
||||||
|
<span class="chat-msg__author">${ this.is_me_message ? '**' : ''}${this.username}</span>
|
||||||
|
${ !this.is_me_message ? this.renderAvatarByline() : '' }
|
||||||
|
${ this.is_encrypted ? html`<span class="fa fa-lock"></span>` : '' }
|
||||||
|
</span>
|
||||||
|
<div class="chat-msg__body chat-msg__body--${this.message_type} ${this.received ? 'chat-msg__body--received' : '' } ${this.is_delayed ? 'chat-msg__body--delayed' : '' }">
|
||||||
|
<div class="chat-msg__message">
|
||||||
|
${ this.is_retracted ? this.renderRetraction() : this.renderMessageText() }
|
||||||
|
</div>
|
||||||
|
${ (this.received && !this.is_me_message && !is_groupchat_message) ? html`<span class="fa fa-check chat-msg__receipt"></span>` : '' }
|
||||||
|
${ (this.edited) ? html`<i title="${ i18n_edited }" class="fa fa-edit chat-msg__edit-modal" @click=${this.showMessageVersionsModal}></i>` : '' }
|
||||||
|
<div class="chat-msg__actions">
|
||||||
|
${ this.editable ?
|
||||||
|
html`<button
|
||||||
|
class="chat-msg__action chat-msg__action-edit"
|
||||||
|
title="${i18n_edit_message}"
|
||||||
|
@click=${this.onMessageEditButtonClicked}
|
||||||
|
>
|
||||||
|
<fa-icon class="fas fa-pencil-alt" path-prefix="dist" color="var(--text-color-lighten-15-percent)" size="1em"></fa-icon>
|
||||||
|
</button>` : '' }
|
||||||
|
${ renderRetractionLink(this) }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async onRetryClicked () {
|
||||||
|
this.show_spinner = true;
|
||||||
|
await this.model.error.retry();
|
||||||
|
this.model.destroy();
|
||||||
|
this.parentElement.removeChild(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMessageRetractButtonClicked (ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
this.chatview.onMessageRetractButtonClicked(this.model);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMessageEditButtonClicked (ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
this.chatview.onMessageEditButtonClicked(this.model);
|
||||||
|
}
|
||||||
|
|
||||||
|
isFollowup () {
|
||||||
|
const messages = this.model.collection.models;
|
||||||
|
const idx = messages.indexOf(this.model);
|
||||||
|
const prev_model = idx ? messages[idx-1] : null;
|
||||||
|
if (prev_model === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const date = dayjs(this.time);
|
||||||
|
return this.from === prev_model.get('from') &&
|
||||||
|
!this.is_me_message &&
|
||||||
|
!prev_model.isMeCommand() &&
|
||||||
|
this.message_type !== 'info' &&
|
||||||
|
prev_model.get('type') !== 'info' &&
|
||||||
|
date.isBefore(dayjs(prev_model.get('time')).add(10, 'minutes')) &&
|
||||||
|
this.is_encrypted === prev_model.get('is_encrypted');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
getExtraMessageClasses () {
|
||||||
|
const extra_classes = [
|
||||||
|
...(this.is_delayed ? ['delayed'] : []),
|
||||||
|
...(this.is_retracted ? ['chat-msg--retracted'] : [])
|
||||||
|
];
|
||||||
|
if (this.message_type === 'groupchat') {
|
||||||
|
this.occupant_role && extra_classes.push(this.occupant_role);
|
||||||
|
this.occupant_affiliation && extra_classes.push(this.occupant_affiliation);
|
||||||
|
if (this.sender === 'them' && this.has_mentions) {
|
||||||
|
extra_classes.push('mentioned');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.correcting && extra_classes.push('correcting');
|
||||||
|
return extra_classes.filter(c => c).join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
getRetractionText () {
|
||||||
|
if (this.message_type === 'groupchat' && this.moderated_by) {
|
||||||
|
const retracted_by_mod = this.moderated_by;
|
||||||
|
const chatbox = this.model.collection.chatbox;
|
||||||
|
if (!this.model.mod) {
|
||||||
|
this.model.mod =
|
||||||
|
chatbox.occupants.findOccupant({'jid': retracted_by_mod}) ||
|
||||||
|
chatbox.occupants.findOccupant({'nick': Strophe.getResourceFromJid(retracted_by_mod)});
|
||||||
|
}
|
||||||
|
const modname = this.model.mod ? this.model.mod.getDisplayName() : 'A moderator';
|
||||||
|
return __('%1$s has removed this message', modname);
|
||||||
|
} else {
|
||||||
|
return __('%1$s has removed this message', this.model.getDisplayName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderRetraction () {
|
||||||
|
const retraction_text = this.is_retracted ? this.getRetractionText() : null;
|
||||||
|
return html`
|
||||||
|
<div>${retraction_text}</div>
|
||||||
|
${ this.moderation_reason ? html`<q class="chat-msg--retracted__reason">${this.moderation_reason}</q>` : '' }
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderMessageText () {
|
||||||
|
const tpl_spoiler_hint = html`
|
||||||
|
<div class="chat-msg__spoiler-hint">
|
||||||
|
<span class="spoiler-hint">${this.spoiler_hint}</span>
|
||||||
|
<a class="badge badge-info spoiler-toggle" href="#" @click=${this.toggleSpoilerMessage}>
|
||||||
|
<i class="fa ${this.is_spoiler_visible ? 'fa-eye-slash' : 'fa-eye'}"></i>
|
||||||
|
${ this.is_spoiler_visible ? i18n_show_less : i18n_show }
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return html`
|
||||||
|
${ this.is_spoiler ? tpl_spoiler_hint : '' }
|
||||||
|
${ this.subject ? html`<div class="chat-msg__subject">${this.subject}</div>` : '' }
|
||||||
|
<converse-chat-message-body
|
||||||
|
.model="${this.model}"
|
||||||
|
?is_me_message="${this.is_me_message}"
|
||||||
|
?is_only_emojis="${this.is_only_emojis}"
|
||||||
|
?is_spoiler="${this.is_spoiler}"
|
||||||
|
?is_spoiler_visible="${this.is_spoiler_visible}"
|
||||||
|
text="${this.model.getMessageText()}"></converse-chat-message-body>
|
||||||
|
${ this.oob_url ? html`<div class="chat-msg__media">${u.getOOBURLMarkup(_converse, this.oob_url)}</div>` : '' }
|
||||||
|
<div class="chat-msg__error">${ this.error_text || this.error }</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderAvatarByline () {
|
||||||
|
return html`
|
||||||
|
${ this.hats.map(h => html`<span class="badge badge-secondary">${h.title}</span>`) }
|
||||||
|
<time timestamp="${this.time}" class="chat-msg__time">${this.pretty_time}</time>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
showMessageVersionsModal (ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
if (this.message_versions_modal === undefined) {
|
||||||
|
this.message_versions_modal = new MessageVersionsModal({'model': this.model});
|
||||||
|
}
|
||||||
|
this.message_versions_modal.show(ev);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleSpoilerMessage (ev) {
|
||||||
|
ev?.preventDefault();
|
||||||
|
this.model.save({'is_spoiler_visible': !this.model.get('is_spoiler_visible')});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('converse-chat-message', Message);
|
@ -3,31 +3,29 @@
|
|||||||
* @copyright 2020, the Converse.js contributors
|
* @copyright 2020, the Converse.js contributors
|
||||||
* @license Mozilla Public License (MPLv2)
|
* @license Mozilla Public License (MPLv2)
|
||||||
*/
|
*/
|
||||||
|
import "./components/chat_content.js";
|
||||||
|
import "./components/help_messages.js";
|
||||||
import "converse-chatboxviews";
|
import "converse-chatboxviews";
|
||||||
import "converse-message-view";
|
|
||||||
import "converse-modal";
|
import "converse-modal";
|
||||||
import log from "@converse/headless/log";
|
import log from "@converse/headless/log";
|
||||||
import tpl_chatbox from "templates/chatbox.js";
|
import tpl_chatbox from "templates/chatbox.js";
|
||||||
import tpl_chatbox_head from "templates/chatbox_head.js";
|
import tpl_chatbox_head from "templates/chatbox_head.js";
|
||||||
import tpl_chatbox_message_form from "templates/chatbox_message_form.html";
|
import tpl_chatbox_message_form from "templates/chatbox_message_form.html";
|
||||||
import tpl_help_message from "templates/help_message.html";
|
|
||||||
import tpl_info from "templates/info.html";
|
|
||||||
import tpl_new_day from "templates/new_day.html";
|
import tpl_new_day from "templates/new_day.html";
|
||||||
import tpl_spinner from "templates/spinner.html";
|
import tpl_spinner from "templates/spinner.html";
|
||||||
import tpl_spoiler_button from "templates/spoiler_button.html";
|
import tpl_spoiler_button from "templates/spoiler_button.html";
|
||||||
import tpl_toolbar from "templates/toolbar.html";
|
import tpl_toolbar from "templates/toolbar.html";
|
||||||
import tpl_toolbar_fileupload from "templates/toolbar_fileupload.html";
|
import tpl_toolbar_fileupload from "templates/toolbar_fileupload.html";
|
||||||
import tpl_user_details_modal from "templates/user_details_modal.js";
|
import tpl_user_details_modal from "templates/user_details_modal.js";
|
||||||
import xss from "xss/dist/xss";
|
|
||||||
import { BootstrapModal } from "./converse-modal.js";
|
import { BootstrapModal } from "./converse-modal.js";
|
||||||
import { Overview } from "skeletor.js/src/overview";
|
import { View } from 'skeletor.js/src/view.js';
|
||||||
import { __ } from '@converse/headless/i18n';
|
import { __ } from '@converse/headless/i18n';
|
||||||
import { _converse, api, converse } from "@converse/headless/converse-core";
|
import { _converse, api, converse } from "@converse/headless/converse-core";
|
||||||
import { debounce, isString } from "lodash";
|
import { debounce, isString } from "lodash";
|
||||||
import { html, render } from "lit-html";
|
import { html, render } from "lit-html";
|
||||||
|
|
||||||
|
|
||||||
const { Strophe, sizzle, dayjs } = converse.env;
|
const { Strophe, dayjs } = converse.env;
|
||||||
const u = converse.env.utils;
|
const u = converse.env.utils;
|
||||||
|
|
||||||
|
|
||||||
@ -46,7 +44,6 @@ converse.plugins.add('converse-chatview', {
|
|||||||
"converse-chatboxviews",
|
"converse-chatboxviews",
|
||||||
"converse-chat",
|
"converse-chat",
|
||||||
"converse-disco",
|
"converse-disco",
|
||||||
"converse-message-view",
|
|
||||||
"converse-modal"
|
"converse-modal"
|
||||||
],
|
],
|
||||||
|
|
||||||
@ -57,9 +54,13 @@ converse.plugins.add('converse-chatview', {
|
|||||||
api.settings.update({
|
api.settings.update({
|
||||||
'auto_focus': true,
|
'auto_focus': true,
|
||||||
'message_limit': 0,
|
'message_limit': 0,
|
||||||
'show_send_button': true,
|
'muc_hats_from_vcard': false,
|
||||||
|
'show_images_inline': true,
|
||||||
'show_retraction_warning': true,
|
'show_retraction_warning': true,
|
||||||
|
'show_send_button': true,
|
||||||
'show_toolbar': true,
|
'show_toolbar': true,
|
||||||
|
'time_format': 'HH:mm',
|
||||||
|
'debounced_content_rendering': true,
|
||||||
'visible_toolbar_buttons': {
|
'visible_toolbar_buttons': {
|
||||||
'call': false,
|
'call': false,
|
||||||
'clear': true,
|
'clear': true,
|
||||||
@ -163,19 +164,16 @@ converse.plugins.add('converse-chatview', {
|
|||||||
* @namespace _converse.ChatBoxView
|
* @namespace _converse.ChatBoxView
|
||||||
* @memberOf _converse
|
* @memberOf _converse
|
||||||
*/
|
*/
|
||||||
_converse.ChatBoxView = Overview.extend({
|
_converse.ChatBoxView = View.extend({
|
||||||
length: 200,
|
length: 200,
|
||||||
className: 'chatbox hidden',
|
className: 'chatbox hidden',
|
||||||
is_chatroom: false, // Leaky abstraction from MUC
|
is_chatroom: false, // Leaky abstraction from MUC
|
||||||
|
|
||||||
events: {
|
events: {
|
||||||
'change input.fileupload': 'onFileSelection',
|
'change input.fileupload': 'onFileSelection',
|
||||||
'click .chat-msg__action-edit': 'onMessageEditButtonClicked',
|
|
||||||
'click .chat-msg__action-retract': 'onMessageRetractButtonClicked',
|
|
||||||
'click .chatbox-navback': 'showControlBox',
|
'click .chatbox-navback': 'showControlBox',
|
||||||
'click .new-msgs-indicator': 'viewUnreadMessages',
|
'click .new-msgs-indicator': 'viewUnreadMessages',
|
||||||
'click .send-button': 'onFormSubmitted',
|
'click .send-button': 'onFormSubmitted',
|
||||||
'click .spoiler-toggle': 'toggleSpoilerMessage',
|
|
||||||
'click .toggle-call': 'toggleCall',
|
'click .toggle-call': 'toggleCall',
|
||||||
'click .toggle-clear': 'clearMessages',
|
'click .toggle-clear': 'clearMessages',
|
||||||
'click .toggle-compose-spoiler': 'toggleComposeSpoilerMessage',
|
'click .toggle-compose-spoiler': 'toggleComposeSpoilerMessage',
|
||||||
@ -191,15 +189,6 @@ converse.plugins.add('converse-chatview', {
|
|||||||
async initialize () {
|
async initialize () {
|
||||||
this.initDebounced();
|
this.initDebounced();
|
||||||
|
|
||||||
this.listenTo(this.model.messages, 'add', this.onMessageAdded);
|
|
||||||
this.listenTo(this.model.messages, 'rendered', this.scrollDown);
|
|
||||||
this.model.messages.on('reset', () => {
|
|
||||||
this.msgs_container.innerHTML = '';
|
|
||||||
this.removeAll();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.listenTo(this.model.notifications, 'change', this.renderChatStateNotification);
|
|
||||||
|
|
||||||
this.listenTo(this.model, 'change:status', this.onStatusMessageChanged);
|
this.listenTo(this.model, 'change:status', this.onStatusMessageChanged);
|
||||||
this.listenTo(this.model, 'destroy', this.remove);
|
this.listenTo(this.model, 'destroy', this.remove);
|
||||||
this.listenTo(this.model, 'show', this.show);
|
this.listenTo(this.model, 'show', this.show);
|
||||||
@ -217,6 +206,14 @@ converse.plugins.add('converse-chatview', {
|
|||||||
|
|
||||||
this.listenTo(this.model.presence, 'change:show', this.onPresenceChanged);
|
this.listenTo(this.model.presence, 'change:show', this.onPresenceChanged);
|
||||||
this.render();
|
this.render();
|
||||||
|
|
||||||
|
// Need to be registered after render has been called.
|
||||||
|
this.listenTo(this.model.messages, 'add', this.onMessageAdded);
|
||||||
|
this.listenTo(this.model.messages, 'change', this.renderChatHistory);
|
||||||
|
this.listenTo(this.model.messages, 'reset', this.renderChatHistory);
|
||||||
|
this.listenTo(this.model.notifications, 'change', this.renderNotifications);
|
||||||
|
this.listenTo(this.model, 'change:show_help_messages', this.renderHelpMessages);
|
||||||
|
|
||||||
await this.updateAfterMessagesFetched();
|
await this.updateAfterMessagesFetched();
|
||||||
this.model.maybeShow();
|
this.model.maybeShow();
|
||||||
/**
|
/**
|
||||||
@ -229,11 +226,19 @@ converse.plugins.add('converse-chatview', {
|
|||||||
},
|
},
|
||||||
|
|
||||||
initDebounced () {
|
initDebounced () {
|
||||||
this.scrollDown = debounce(this._scrollDown, 50);
|
|
||||||
this.markScrolled = debounce(this._markScrolled, 100);
|
this.markScrolled = debounce(this._markScrolled, 100);
|
||||||
|
// For tests that use Jasmine.Clock we want to turn of
|
||||||
|
// debouncing, since setTimeout breaks.
|
||||||
|
if (api.settings.get('debounced_content_rendering')) {
|
||||||
|
this.renderChatHistory = debounce(() => this.renderChatContent(false), 100);
|
||||||
|
this.renderNotifications = debounce(() => this.renderChatContent(true), 100);
|
||||||
|
} else {
|
||||||
|
this.renderChatHistory = () => this.renderChatContent(false);
|
||||||
|
this.renderNotifications = () => this.renderChatContent(true);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
render () {
|
async render () {
|
||||||
const result = tpl_chatbox(
|
const result = tpl_chatbox(
|
||||||
Object.assign(
|
Object.assign(
|
||||||
this.model.toJSON(), {
|
this.model.toJSON(), {
|
||||||
@ -244,26 +249,87 @@ converse.plugins.add('converse-chatview', {
|
|||||||
);
|
);
|
||||||
render(result, this.el);
|
render(result, this.el);
|
||||||
this.content = this.el.querySelector('.chat-content');
|
this.content = this.el.querySelector('.chat-content');
|
||||||
|
|
||||||
this.notifications = this.el.querySelector('.chat-content__notifications');
|
this.notifications = this.el.querySelector('.chat-content__notifications');
|
||||||
this.msgs_container = this.el.querySelector('.chat-content__messages');
|
this.msgs_container = this.el.querySelector('.chat-content__messages');
|
||||||
this.renderChatStateNotification();
|
this.help_container = this.el.querySelector('.chat-content__help');
|
||||||
|
|
||||||
|
await api.waitUntil('emojisInitialized');
|
||||||
|
this.renderChatContent();
|
||||||
this.renderMessageForm();
|
this.renderMessageForm();
|
||||||
this.renderHeading();
|
this.renderHeading();
|
||||||
return this;
|
return this;
|
||||||
},
|
},
|
||||||
|
|
||||||
renderChatStateNotification () {
|
onMessageAdded (message) {
|
||||||
if (this.model.notifications.get('chat_state') === _converse.COMPOSING) {
|
this.renderChatHistory();
|
||||||
this.notifications.innerText = __('%1$s is typing', this.model.getDisplayName());
|
|
||||||
} else if (this.model.notifications.get('chat_state') === _converse.PAUSED) {
|
if (u.isNewMessage(message)) {
|
||||||
this.notifications.innerText = __('%1$s has stopped typing', this.model.getDisplayName());
|
if (message.get('sender') === 'me') {
|
||||||
} else if (this.model.notifications.get('chat_state') === _converse.GONE) {
|
// We remove the "scrolled" flag so that the chat area
|
||||||
this.notifications.innerText = __('%1$s has gone away', this.model.getDisplayName());
|
// gets scrolled down. We always want to scroll down
|
||||||
} else {
|
// when the user writes a message as opposed to when a
|
||||||
this.notifications.innerText = '';
|
// message is received.
|
||||||
|
this.model.set('scrolled', false);
|
||||||
|
} else if (this.model.get('scrolled', true)) {
|
||||||
|
this.showNewMessagesIndicator();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getNotifications () {
|
||||||
|
if (this.model.notifications.get('chat_state') === _converse.COMPOSING) {
|
||||||
|
return __('%1$s is typing', this.model.getDisplayName());
|
||||||
|
} else if (this.model.notifications.get('chat_state') === _converse.PAUSED) {
|
||||||
|
return __('%1$s has stopped typing', this.model.getDisplayName());
|
||||||
|
} else if (this.model.notifications.get('chat_state') === _converse.GONE) {
|
||||||
|
return __('%1$s has gone away', this.model.getDisplayName());
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getHelpMessages () {
|
||||||
|
return [
|
||||||
|
`<strong>/clear</strong>: ${__('Remove messages')}`,
|
||||||
|
`<strong>/close</strong>: ${__('Close this chat')}`,
|
||||||
|
`<strong>/me</strong>: ${__('Write in the third person')}`,
|
||||||
|
`<strong>/help</strong>: ${__('Show this menu')}`
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHelpMessages () {
|
||||||
|
render(
|
||||||
|
html`<converse-chat-help
|
||||||
|
.model=${this.model}
|
||||||
|
.messages=${this.getHelpMessages()}
|
||||||
|
?hidden=${!this.model.get('show_help_messages')}
|
||||||
|
type="info"
|
||||||
|
chat_type="${this.model.get('type')}"></converse-chat-help>`,
|
||||||
|
|
||||||
|
this.help_container
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderChatContent (msgs_by_ref=false) {
|
||||||
|
if (!this.tpl_chat_content) {
|
||||||
|
this.tpl_chat_content = (o) => {
|
||||||
|
return html`
|
||||||
|
<converse-chat-content
|
||||||
|
.chatview=${this}
|
||||||
|
.messages=${o.messages}
|
||||||
|
notifications=${o.notifications}>
|
||||||
|
</converse-chat-content>`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const msg_models = this.model.messages.models;
|
||||||
|
const messages = msgs_by_ref ? msg_models : Array.from(msg_models);
|
||||||
|
render(
|
||||||
|
this.tpl_chat_content({ messages, 'notifications': this.getNotifications() }),
|
||||||
|
this.msgs_container
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
renderToolbar () {
|
renderToolbar () {
|
||||||
if (!api.settings.get('show_toolbar')) {
|
if (!api.settings.get('show_toolbar')) {
|
||||||
return this;
|
return this;
|
||||||
@ -473,10 +539,9 @@ converse.plugins.add('converse-chatview', {
|
|||||||
|
|
||||||
async updateAfterMessagesFetched () {
|
async updateAfterMessagesFetched () {
|
||||||
await this.model.messages.fetched;
|
await this.model.messages.fetched;
|
||||||
await Promise.all(this.model.messages.map(m => this.onMessageAdded(m)));
|
this.renderChatContent();
|
||||||
this.insertIntoDOM();
|
this.insertIntoDOM();
|
||||||
this.scrollDown();
|
this.scrollDown();
|
||||||
this.content.addEventListener('scroll', () => this.markScrolled());
|
|
||||||
/**
|
/**
|
||||||
* Triggered whenever a `_converse.ChatBox` instance has fetched its messages from
|
* Triggered whenever a `_converse.ChatBox` instance has fetched its messages from
|
||||||
* `sessionStorage` but **NOT** from the server.
|
* `sessionStorage` but **NOT** from the server.
|
||||||
@ -484,7 +549,12 @@ converse.plugins.add('converse-chatview', {
|
|||||||
* @type {_converse.ChatBoxView | _converse.ChatRoomView}
|
* @type {_converse.ChatBoxView | _converse.ChatRoomView}
|
||||||
* @example _converse.api.listen.on('afterMessagesFetched', view => { ... });
|
* @example _converse.api.listen.on('afterMessagesFetched', view => { ... });
|
||||||
*/
|
*/
|
||||||
api.trigger('afterMessagesFetched', this);
|
api.trigger('afterMessagesFetched', this.model);
|
||||||
|
},
|
||||||
|
|
||||||
|
scrollDown () {
|
||||||
|
const el = this.msgs_container.firstElementChild;
|
||||||
|
el && el.scrollDown();
|
||||||
},
|
},
|
||||||
|
|
||||||
insertIntoDOM () {
|
insertIntoDOM () {
|
||||||
@ -499,20 +569,6 @@ converse.plugins.add('converse-chatview', {
|
|||||||
return this;
|
return this;
|
||||||
},
|
},
|
||||||
|
|
||||||
showChatEvent (message) {
|
|
||||||
const isodate = (new Date()).toISOString();
|
|
||||||
this.msgs_container.insertAdjacentHTML(
|
|
||||||
'beforeend',
|
|
||||||
tpl_info({
|
|
||||||
'extra_classes': 'chat-event',
|
|
||||||
'message': message,
|
|
||||||
'isodate': isodate,
|
|
||||||
}));
|
|
||||||
this.insertDayIndicator(this.msgs_container.lastElementChild);
|
|
||||||
this.scrollDown();
|
|
||||||
return isodate;
|
|
||||||
},
|
|
||||||
|
|
||||||
addSpinner (append=false) {
|
addSpinner (append=false) {
|
||||||
if (this.el.querySelector('.spinner') === null) {
|
if (this.el.querySelector('.spinner') === null) {
|
||||||
if (append) {
|
if (append) {
|
||||||
@ -557,47 +613,6 @@ converse.plugins.add('converse-chatview', {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the ISO8601 format date of the latest message.
|
|
||||||
* @private
|
|
||||||
* @method _converse.ChatBoxView#getLastMessageDate
|
|
||||||
* @param { Date } cutoff - Moment Date cutoff date. The last
|
|
||||||
* message received cutoff this date will be returned.
|
|
||||||
* @returns { Date }
|
|
||||||
*/
|
|
||||||
getLastMessageDate (cutoff) {
|
|
||||||
const first_msg = u.getFirstChildElement(this.msgs_container, '.message:not(.chat-state-notification)');
|
|
||||||
const oldest_date = first_msg ? first_msg.getAttribute('data-isodate') : null;
|
|
||||||
if (oldest_date !== null && dayjs(oldest_date).isAfter(cutoff)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const last_msg = u.getLastChildElement(this.msgs_container, '.message:not(.chat-state-notification)');
|
|
||||||
const most_recent_date = last_msg ? last_msg.getAttribute('data-isodate') : null;
|
|
||||||
if (most_recent_date === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (dayjs(most_recent_date).isBefore(cutoff)) {
|
|
||||||
return dayjs(most_recent_date).toDate();
|
|
||||||
}
|
|
||||||
/* XXX: We avoid .chat-state-notification messages, since they are
|
|
||||||
* temporary and get removed once a new element is
|
|
||||||
* inserted into the chat area, so we don't query for
|
|
||||||
* them here, otherwise we get a null reference later
|
|
||||||
* upon element insertion.
|
|
||||||
*/
|
|
||||||
const sel = '.message:not(.chat-state-notification)';
|
|
||||||
const msg_dates = sizzle(sel, this.msgs_container).map(e => e.getAttribute('data-isodate'));
|
|
||||||
const cutoff_iso = cutoff.toISOString();
|
|
||||||
msg_dates.push(cutoff_iso);
|
|
||||||
msg_dates.sort();
|
|
||||||
const idx = msg_dates.lastIndexOf(cutoff_iso);
|
|
||||||
if (idx === 0) {
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
return dayjs(msg_dates[idx-1]).toDate();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
setScrollPosition (message_el) {
|
setScrollPosition (message_el) {
|
||||||
/* Given a newly inserted message, determine whether we
|
/* Given a newly inserted message, determine whether we
|
||||||
* should keep the scrollbar in place (so as to not scroll
|
* should keep the scrollbar in place (so as to not scroll
|
||||||
@ -637,63 +652,10 @@ converse.plugins.add('converse-chatview', {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
showHelpMessages (msgs, type='info', spinner) {
|
|
||||||
msgs.forEach(msg => {
|
|
||||||
this.msgs_container.insertAdjacentHTML(
|
|
||||||
'beforeend',
|
|
||||||
tpl_help_message({
|
|
||||||
'isodate': (new Date()).toISOString(),
|
|
||||||
'type': type,
|
|
||||||
'message': xss.filterXSS(msg, {'whiteList': {'strong': []}})
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
if (spinner === true) {
|
|
||||||
this.addSpinner();
|
|
||||||
} else if (spinner === false) {
|
|
||||||
this.clearSpinner();
|
|
||||||
}
|
|
||||||
return this.scrollDown();
|
|
||||||
},
|
|
||||||
|
|
||||||
shouldShowOnTextMessage () {
|
shouldShowOnTextMessage () {
|
||||||
return !u.isVisible(this.el);
|
return !u.isVisible(this.el);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Given a view representing a message, insert it into the
|
|
||||||
* content area of the chat box.
|
|
||||||
* @private
|
|
||||||
* @method _converse.ChatBoxView#insertMessage
|
|
||||||
* @param { View } message - The message View
|
|
||||||
*/
|
|
||||||
insertMessage (view) {
|
|
||||||
if (view.model.get('type') === 'error') {
|
|
||||||
const previous_msg_el = this.msgs_container.querySelector(`[data-msgid="${view.model.get('msgid')}"]`);
|
|
||||||
if (previous_msg_el) {
|
|
||||||
previous_msg_el.insertAdjacentElement('afterend', view.el);
|
|
||||||
return this.trigger('messageInserted', view.el);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const current_msg_date = dayjs(view.model.get('time')).toDate() || new Date();
|
|
||||||
const previous_msg_date = this.getLastMessageDate(current_msg_date);
|
|
||||||
|
|
||||||
if (previous_msg_date === null) {
|
|
||||||
this.msgs_container.insertAdjacentElement('afterbegin', view.el);
|
|
||||||
} else {
|
|
||||||
const previous_msg_el = sizzle(`[data-isodate="${previous_msg_date.toISOString()}"]:last`, this.msgs_container).pop();
|
|
||||||
if (view.model.get('type') === 'error' &&
|
|
||||||
u.hasClass('chat-error', previous_msg_el) &&
|
|
||||||
previous_msg_el.textContent === view.model.get('message')) {
|
|
||||||
// We don't show a duplicate error message
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
previous_msg_el.insertAdjacentElement('afterend', view.el);
|
|
||||||
this.markFollowups(view.el);
|
|
||||||
}
|
|
||||||
return this.trigger('messageInserted', view.el);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a message element, determine wether it should be
|
* Given a message element, determine wether it should be
|
||||||
* marked as a followup message to the previous element.
|
* marked as a followup message to the previous element.
|
||||||
@ -733,71 +695,6 @@ converse.plugins.add('converse-chatview', {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Inserts a chat message into the content area of the chat box.
|
|
||||||
* Will also insert a new day indicator if the message is on a different day.
|
|
||||||
* @private
|
|
||||||
* @method _converse.ChatBoxView#showMessage
|
|
||||||
* @param { _converse.Message } message - The message object
|
|
||||||
*/
|
|
||||||
async showMessage (message) {
|
|
||||||
await message.initialized;
|
|
||||||
const view = this.add(message.get('id'), new _converse.MessageView({'model': message}));
|
|
||||||
await view.render();
|
|
||||||
this.insertMessage(view);
|
|
||||||
this.insertDayIndicator(view.el);
|
|
||||||
this.setScrollPosition(view.el);
|
|
||||||
|
|
||||||
if (u.isNewMessage(message)) {
|
|
||||||
if (message.get('sender') === 'me') {
|
|
||||||
// We remove the "scrolled" flag so that the chat area
|
|
||||||
// gets scrolled down. We always want to scroll down
|
|
||||||
// when the user writes a message as opposed to when a
|
|
||||||
// message is received.
|
|
||||||
this.model.set('scrolled', false);
|
|
||||||
} else if (this.model.get('scrolled', true)) {
|
|
||||||
this.showNewMessagesIndicator();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (this.shouldShowOnTextMessage()) {
|
|
||||||
this.show();
|
|
||||||
} else {
|
|
||||||
this.scrollDown();
|
|
||||||
}
|
|
||||||
if (message.get('correcting')) {
|
|
||||||
this.insertIntoTextArea(message.get('message'), true, true);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handler that gets called when a new message object is created.
|
|
||||||
* @private
|
|
||||||
* @method _converse.ChatBoxView#onMessageAdded
|
|
||||||
* @param { object } message - The message object that was added.
|
|
||||||
*/
|
|
||||||
async onMessageAdded (message) {
|
|
||||||
const id = message.get('id');
|
|
||||||
if (id && this.get(id)) {
|
|
||||||
// We already have a view for this message
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!message.get('dangling_retraction')) {
|
|
||||||
await this.showMessage(message);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Triggered once a message has been added to a chatbox.
|
|
||||||
* @event _converse#messageAdded
|
|
||||||
* @type {object}
|
|
||||||
* @property { _converse.Message } message - The message instance
|
|
||||||
* @property { _converse.ChatBox | _converse.ChatRoom } chatbox - The chat model
|
|
||||||
* @example _converse.api.listen.on('messageAdded', data => { ... });
|
|
||||||
*/
|
|
||||||
api.trigger('messageAdded', {
|
|
||||||
'message': message,
|
|
||||||
'chatbox': this.model
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
parseMessageForCommands (text) {
|
parseMessageForCommands (text) {
|
||||||
const match = text.replace(/^\s*/, "").match(/^\/(.*)\s*$/);
|
const match = text.replace(/^\s*/, "").match(/^\/(.*)\s*$/);
|
||||||
if (match) {
|
if (match) {
|
||||||
@ -808,13 +705,7 @@ converse.plugins.add('converse-chatview', {
|
|||||||
this.close();
|
this.close();
|
||||||
return true;
|
return true;
|
||||||
} else if (match[1] === "help") {
|
} else if (match[1] === "help") {
|
||||||
const msgs = [
|
this.model.set({'show_help_messages': true});
|
||||||
`<strong>/clear</strong>: ${__('Remove messages')}`,
|
|
||||||
`<strong>/close</strong>: ${__('Close this chat')}`,
|
|
||||||
`<strong>/me</strong>: ${__('Write in the third person')}`,
|
|
||||||
`<strong>/help</strong>: ${__('Show this menu')}`
|
|
||||||
];
|
|
||||||
this.showHelpMessages(msgs);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -829,10 +720,8 @@ converse.plugins.add('converse-chatview', {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!_converse.connection.authenticated) {
|
if (!_converse.connection.authenticated) {
|
||||||
this.showHelpMessages(
|
const err_msg = __('Sorry, the connection has been lost, and your message could not be sent');
|
||||||
['Sorry, the connection has been lost, and your message could not be sent'],
|
api.alert('error', __('Error'), err_msg);
|
||||||
'error'
|
|
||||||
);
|
|
||||||
api.connection.reconnect();
|
api.connection.reconnect();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -977,14 +866,9 @@ converse.plugins.add('converse-chatview', {
|
|||||||
this.insertIntoTextArea('', true, false);
|
this.insertIntoTextArea('', true, false);
|
||||||
},
|
},
|
||||||
|
|
||||||
async onMessageRetractButtonClicked (ev) {
|
async onMessageRetractButtonClicked (message) {
|
||||||
ev.preventDefault();
|
|
||||||
const msg_el = u.ancestor(ev.target, '.message');
|
|
||||||
const msgid = msg_el.getAttribute('data-msgid');
|
|
||||||
const time = msg_el.getAttribute('data-isodate');
|
|
||||||
const message = this.model.messages.findWhere({msgid, time});
|
|
||||||
if (message.get('sender') !== 'me') {
|
if (message.get('sender') !== 'me') {
|
||||||
return log.error("onMessageEditButtonClicked called for someone else's message!");
|
return log.error("onMessageRetractButtonClicked called for someone else's message!");
|
||||||
}
|
}
|
||||||
const retraction_warning =
|
const retraction_warning =
|
||||||
__("Be aware that other XMPP/Jabber clients (and servers) may "+
|
__("Be aware that other XMPP/Jabber clients (and servers) may "+
|
||||||
@ -1001,26 +885,17 @@ converse.plugins.add('converse-chatview', {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onMessageEditButtonClicked (ev) {
|
onMessageEditButtonClicked (message) {
|
||||||
ev.preventDefault();
|
const currently_correcting = this.model.messages.findWhere('correcting');
|
||||||
|
const unsent_text = this.el.querySelector('.chat-textarea')?.value;
|
||||||
const idx = this.model.messages.findLastIndex('correcting'),
|
if (unsent_text && (!currently_correcting || currently_correcting.get('message') !== unsent_text)) {
|
||||||
currently_correcting = idx >=0 ? this.model.messages.at(idx) : null,
|
|
||||||
message_el = u.ancestor(ev.target, '.chat-msg'),
|
|
||||||
message = this.model.messages.findWhere({'msgid': message_el.getAttribute('data-msgid')});
|
|
||||||
|
|
||||||
const textarea = this.el.querySelector('.chat-textarea');
|
|
||||||
if (textarea.value &&
|
|
||||||
((currently_correcting === null) || currently_correcting.get('message') !== textarea.value)) {
|
|
||||||
if (! confirm(__("You have an unsent message which will be lost if you continue. Are you sure?"))) {
|
if (! confirm(__("You have an unsent message which will be lost if you continue. Are you sure?"))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currently_correcting !== message) {
|
if (currently_correcting !== message) {
|
||||||
if (currently_correcting !== null) {
|
currently_correcting?.save('correcting', false);
|
||||||
currently_correcting.save('correcting', false);
|
|
||||||
}
|
|
||||||
message.save('correcting', true);
|
message.save('correcting', true);
|
||||||
this.insertIntoTextArea(u.prefixMentions(message), true, true);
|
this.insertIntoTextArea(u.prefixMentions(message), true, true);
|
||||||
} else {
|
} else {
|
||||||
@ -1150,34 +1025,9 @@ converse.plugins.add('converse-chatview', {
|
|||||||
this.focus();
|
this.focus();
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleSpoilerMessage (ev) {
|
|
||||||
if (ev && ev.preventDefault) {
|
|
||||||
ev.preventDefault();
|
|
||||||
}
|
|
||||||
const toggle_el = ev.target,
|
|
||||||
icon_el = toggle_el.firstElementChild;
|
|
||||||
|
|
||||||
u.slideToggleElement(
|
|
||||||
toggle_el.parentElement.parentElement.querySelector('.spoiler')
|
|
||||||
);
|
|
||||||
if (toggle_el.getAttribute("data-toggle-state") == "closed") {
|
|
||||||
toggle_el.textContent = 'Show less';
|
|
||||||
icon_el.classList.remove("fa-eye");
|
|
||||||
icon_el.classList.add("fa-eye-slash");
|
|
||||||
toggle_el.insertAdjacentElement('afterBegin', icon_el);
|
|
||||||
toggle_el.setAttribute("data-toggle-state", "open");
|
|
||||||
} else {
|
|
||||||
toggle_el.textContent = 'Show more';
|
|
||||||
icon_el.classList.remove("fa-eye-slash");
|
|
||||||
icon_el.classList.add("fa-eye");
|
|
||||||
toggle_el.insertAdjacentElement('afterBegin', icon_el);
|
|
||||||
toggle_el.setAttribute("data-toggle-state", "closed");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onPresenceChanged (item) {
|
onPresenceChanged (item) {
|
||||||
const show = item.get('show'),
|
const show = item.get('show');
|
||||||
fullname = this.model.getDisplayName();
|
const fullname = this.model.getDisplayName();
|
||||||
|
|
||||||
let text;
|
let text;
|
||||||
if (u.isVisible(this.el)) {
|
if (u.isVisible(this.el)) {
|
||||||
@ -1333,21 +1183,6 @@ converse.plugins.add('converse-chatview', {
|
|||||||
this.scrollDown();
|
this.scrollDown();
|
||||||
},
|
},
|
||||||
|
|
||||||
_scrollDown () {
|
|
||||||
/* Inner method that gets debounced */
|
|
||||||
if (this.content === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (u.isVisible(this.content) && !this.model.get('scrolled')) {
|
|
||||||
if ((this.content.scrollTop === 0 || this.content.scrollTop < this.content.scrollHeight/2)) {
|
|
||||||
u.removeClass('smooth-scroll', this.content);
|
|
||||||
} else if (api.settings.get('animate')) {
|
|
||||||
u.addClass('smooth-scroll', this.content);
|
|
||||||
}
|
|
||||||
this.content.scrollTop = this.content.scrollHeight;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onScrolledDown () {
|
onScrolledDown () {
|
||||||
this.hideNewMessagesIndicator();
|
this.hideNewMessagesIndicator();
|
||||||
if (_converse.windowState !== 'hidden') {
|
if (_converse.windowState !== 'hidden') {
|
||||||
|
@ -132,7 +132,7 @@ converse.plugins.add('converse-headlines-view', {
|
|||||||
this.initDebounced();
|
this.initDebounced();
|
||||||
|
|
||||||
this.model.disable_mam = true; // Don't do MAM queries for this box
|
this.model.disable_mam = true; // Don't do MAM queries for this box
|
||||||
this.listenTo(this.model.messages, 'add', this.onMessageAdded);
|
this.listenTo(this.model.messages, 'add', this.renderChatHistory);
|
||||||
this.listenTo(this.model, 'show', this.show);
|
this.listenTo(this.model, 'show', this.show);
|
||||||
this.listenTo(this.model, 'destroy', this.hide);
|
this.listenTo(this.model, 'destroy', this.hide);
|
||||||
this.listenTo(this.model, 'change:minimized', this.onMinimizedChanged);
|
this.listenTo(this.model, 'change:minimized', this.onMinimizedChanged);
|
||||||
@ -168,6 +168,12 @@ converse.plugins.add('converse-headlines-view', {
|
|||||||
return this;
|
return this;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getNotifications () {
|
||||||
|
// Override method in ChatBox. We don't show notifications for
|
||||||
|
// headlines boxes.
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a list of objects which represent buttons for the headlines header.
|
* Returns a list of objects which represent buttons for the headlines header.
|
||||||
* @async
|
* @async
|
||||||
|
@ -1,386 +0,0 @@
|
|||||||
/**
|
|
||||||
* @module converse-message-view
|
|
||||||
* @copyright 2020, the Converse.js contributors
|
|
||||||
* @license Mozilla Public License (MPLv2)
|
|
||||||
*/
|
|
||||||
import "./utils/html";
|
|
||||||
import "@converse/headless/converse-emoji";
|
|
||||||
import URI from "urijs";
|
|
||||||
import filesize from "filesize";
|
|
||||||
import log from "@converse/headless/log";
|
|
||||||
import tpl_file_progress from "templates/file_progress.html";
|
|
||||||
import tpl_info from "templates/info.html";
|
|
||||||
import tpl_message from "templates/message.html";
|
|
||||||
import tpl_message_versions_modal from "templates/message_versions_modal.js";
|
|
||||||
import tpl_spinner from "templates/spinner.html";
|
|
||||||
import xss from "xss/dist/xss";
|
|
||||||
import { BootstrapModal } from "./converse-modal.js";
|
|
||||||
import { __ } from '@converse/headless/i18n';
|
|
||||||
import { _converse, api, converse } from "@converse/headless/converse-core";
|
|
||||||
import { debounce } from 'lodash'
|
|
||||||
import { render } from "lit-html";
|
|
||||||
|
|
||||||
const { Strophe, dayjs } = converse.env;
|
|
||||||
const u = converse.env.utils;
|
|
||||||
|
|
||||||
|
|
||||||
converse.plugins.add('converse-message-view', {
|
|
||||||
|
|
||||||
dependencies: ["converse-modal", "converse-chatboxviews"],
|
|
||||||
|
|
||||||
initialize () {
|
|
||||||
/* The initialize function gets called as soon as the plugin is
|
|
||||||
* loaded by converse.js's plugin machinery.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function onTagFoundDuringXSSFilter (tag, html, options) {
|
|
||||||
/* This function gets called by the XSS library whenever it finds
|
|
||||||
* what it thinks is a new HTML tag.
|
|
||||||
*
|
|
||||||
* It thinks that something like <https://example.com> is an HTML
|
|
||||||
* tag and then escapes the <> chars.
|
|
||||||
*
|
|
||||||
* We want to avoid this, because it prevents these URLs from being
|
|
||||||
* shown properly (whithout the trailing >).
|
|
||||||
*
|
|
||||||
* The URI lib correctly trims a trailing >, but not a trailing >
|
|
||||||
*/
|
|
||||||
if (options.isClosing) {
|
|
||||||
// Closing tags don't match our use-case
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const uri = new URI(tag);
|
|
||||||
const protocol = uri.protocol().toLowerCase();
|
|
||||||
if (!["https", "http", "xmpp", "ftp"].includes(protocol)) {
|
|
||||||
// Not a URL, the tag will get filtered as usual
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (uri.equals(tag) && `<${tag}>` === html.toLocaleLowerCase()) {
|
|
||||||
// We have something like <https://example.com>, and don't want
|
|
||||||
// to filter it.
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
api.settings.update({
|
|
||||||
'muc_hats_from_vcard': false,
|
|
||||||
'show_images_inline': true,
|
|
||||||
'time_format': 'HH:mm',
|
|
||||||
});
|
|
||||||
|
|
||||||
_converse.MessageVersionsModal = BootstrapModal.extend({
|
|
||||||
id: "message-versions-modal",
|
|
||||||
toHTML () {
|
|
||||||
return tpl_message_versions_modal(this.model.toJSON());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @class
|
|
||||||
* @namespace _converse.MessageView
|
|
||||||
* @memberOf _converse
|
|
||||||
*/
|
|
||||||
_converse.MessageView = _converse.ViewWithAvatar.extend({
|
|
||||||
events: {
|
|
||||||
'click .chat-msg__edit-modal': 'showMessageVersionsModal',
|
|
||||||
'click .retry': 'onRetryClicked'
|
|
||||||
},
|
|
||||||
|
|
||||||
initialize () {
|
|
||||||
this.debouncedRender = debounce(() => {
|
|
||||||
// If the model gets destroyed in the meantime,
|
|
||||||
// it no longer has a collection
|
|
||||||
if (this.model.collection) {
|
|
||||||
this.render();
|
|
||||||
}
|
|
||||||
}, 50);
|
|
||||||
|
|
||||||
if (this.model.rosterContactAdded) {
|
|
||||||
this.model.rosterContactAdded.then(() => {
|
|
||||||
this.listenTo(this.model.contact, 'change:nickname', this.debouncedRender);
|
|
||||||
this.debouncedRender();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.model.occupant && this.addOccupantListeners();
|
|
||||||
this.listenTo(this.model, 'change', this.onChanged);
|
|
||||||
this.listenTo(this.model, 'destroy', this.fadeOut);
|
|
||||||
this.listenTo(this.model, 'occupantAdded', () => {
|
|
||||||
this.addOccupantListeners();
|
|
||||||
this.debouncedRender();
|
|
||||||
});
|
|
||||||
this.listenTo(this.model, 'vcard:change', this.debouncedRender);
|
|
||||||
this.debouncedRender();
|
|
||||||
},
|
|
||||||
|
|
||||||
async render () {
|
|
||||||
const is_followup = u.hasClass('chat-msg--followup', this.el);
|
|
||||||
if (this.model.get('file') && !this.model.get('oob_url')) {
|
|
||||||
if (!this.model.file) {
|
|
||||||
log.error("Attempted to render a file upload message with no file data");
|
|
||||||
return this.el;
|
|
||||||
}
|
|
||||||
this.renderFileUploadProgresBar();
|
|
||||||
} else if (this.model.get('type') === 'error') {
|
|
||||||
this.renderErrorMessage();
|
|
||||||
} else if (this.model.get('type') === 'info') {
|
|
||||||
this.renderInfoMessage();
|
|
||||||
} else {
|
|
||||||
await this.renderChatMessage();
|
|
||||||
}
|
|
||||||
is_followup && u.addClass('chat-msg--followup', this.el);
|
|
||||||
return this.el;
|
|
||||||
},
|
|
||||||
|
|
||||||
async onChanged (item) {
|
|
||||||
// Jot down whether it was edited because the `changed`
|
|
||||||
// attr gets removed when this.render() gets called further down.
|
|
||||||
const edited = item.changed.edited;
|
|
||||||
if (this.model.changed.progress) {
|
|
||||||
return this.renderFileUploadProgresBar();
|
|
||||||
}
|
|
||||||
// TODO: We can remove this once we render messages via lit-html
|
|
||||||
const isValidChange = prop => Object.prototype.hasOwnProperty.call(this.model.changed, prop);
|
|
||||||
const props = [
|
|
||||||
'correcting',
|
|
||||||
'editable',
|
|
||||||
'error',
|
|
||||||
'message',
|
|
||||||
'moderated',
|
|
||||||
'received',
|
|
||||||
'retracted',
|
|
||||||
'type',
|
|
||||||
'upload',
|
|
||||||
];
|
|
||||||
if (props.filter(isValidChange).length) {
|
|
||||||
await this.debouncedRender();
|
|
||||||
}
|
|
||||||
if (edited) {
|
|
||||||
this.onMessageEdited();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
addOccupantListeners () {
|
|
||||||
this.listenTo(this.model.occupant, 'change:affiliation', this.debouncedRender);
|
|
||||||
this.listenTo(this.model.occupant, 'change:hats', this.debouncedRender);
|
|
||||||
this.listenTo(this.model.occupant, 'change:role', this.debouncedRender);
|
|
||||||
},
|
|
||||||
|
|
||||||
fadeOut () {
|
|
||||||
if (api.settings.get('animate')) {
|
|
||||||
setTimeout(() => this.remove(), 600);
|
|
||||||
u.addClass('fade-out', this.el);
|
|
||||||
} else {
|
|
||||||
this.remove();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async onRetryClicked () {
|
|
||||||
this.showSpinner();
|
|
||||||
await this.model.error.retry();
|
|
||||||
this.model.destroy();
|
|
||||||
},
|
|
||||||
|
|
||||||
showSpinner () {
|
|
||||||
this.el.innerHTML = tpl_spinner();
|
|
||||||
},
|
|
||||||
|
|
||||||
onMessageEdited () {
|
|
||||||
if (this.model.get('is_archived')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.el.addEventListener(
|
|
||||||
'animationend',
|
|
||||||
() => u.removeClass('onload', this.el),
|
|
||||||
{'once': true}
|
|
||||||
);
|
|
||||||
u.addClass('onload', this.el);
|
|
||||||
},
|
|
||||||
|
|
||||||
replaceElement (msg) {
|
|
||||||
if (this.el.parentElement) {
|
|
||||||
this.el.parentElement.replaceChild(msg, this.el);
|
|
||||||
}
|
|
||||||
this.setElement(msg);
|
|
||||||
return this.el;
|
|
||||||
},
|
|
||||||
|
|
||||||
transformOOBURL (url) {
|
|
||||||
return u.getOOBURLMarkup(_converse, url);
|
|
||||||
},
|
|
||||||
|
|
||||||
async transformBodyText (text) {
|
|
||||||
/**
|
|
||||||
* Synchronous event which provides a hook for transforming a chat message's body text
|
|
||||||
* before the default transformations have been applied.
|
|
||||||
* @event _converse#beforeMessageBodyTransformed
|
|
||||||
* @param { _converse.MessageView } view - The view representing the message
|
|
||||||
* @param { string } text - The message text
|
|
||||||
* @example _converse.api.listen.on('beforeMessageBodyTransformed', (view, text) => { ... });
|
|
||||||
*/
|
|
||||||
await api.trigger('beforeMessageBodyTransformed', this, text, {'Synchronous': true});
|
|
||||||
text = this.model.isMeCommand() ? text.substring(4) : text;
|
|
||||||
text = xss.filterXSS(text, {'whiteList': {}, 'onTag': onTagFoundDuringXSSFilter});
|
|
||||||
text = u.geoUriToHttp(text, api.settings.get("geouri_replacement"));
|
|
||||||
text = u.addMentionsMarkup(text, this.model.get('references'), this.model.collection.chatbox);
|
|
||||||
text = u.addHyperlinks(text);
|
|
||||||
text = u.renderNewLines(text);
|
|
||||||
text = u.addEmoji(text);
|
|
||||||
/**
|
|
||||||
* Synchronous event which provides a hook for transforming a chat message's body text
|
|
||||||
* after the default transformations have been applied.
|
|
||||||
* @event _converse#afterMessageBodyTransformed
|
|
||||||
* @param { _converse.MessageView } view - The view representing the message
|
|
||||||
* @param { string } text - The message text
|
|
||||||
* @example _converse.api.listen.on('afterMessageBodyTransformed', (view, text) => { ... });
|
|
||||||
*/
|
|
||||||
await api.trigger('afterMessageBodyTransformed', this, text, {'Synchronous': true});
|
|
||||||
return text;
|
|
||||||
},
|
|
||||||
|
|
||||||
async renderChatMessage () {
|
|
||||||
await api.waitUntil('emojisInitialized');
|
|
||||||
const time = dayjs(this.model.get('time'));
|
|
||||||
const is_retracted = this.model.get('retracted') || this.model.get('moderated') === 'retracted';
|
|
||||||
const may_be_moderated = this.model.get('type') === 'groupchat' && await this.model.mayBeModerated();
|
|
||||||
const retractable= !is_retracted && (this.model.mayBeRetracted() || may_be_moderated);
|
|
||||||
const is_groupchat_message = this.model.get('type') === 'groupchat';
|
|
||||||
|
|
||||||
let hats = [];
|
|
||||||
if (is_groupchat_message) {
|
|
||||||
if (api.settings.get('muc_hats_from_vcard')) {
|
|
||||||
const role = this.model.vcard ? this.model.vcard.get('role') : null;
|
|
||||||
hats = role ? role.split(',') : [];
|
|
||||||
} else {
|
|
||||||
hats = this.model.occupant?.get('hats') || [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const msg = u.stringToElement(tpl_message(
|
|
||||||
Object.assign(
|
|
||||||
this.model.toJSON(), {
|
|
||||||
__,
|
|
||||||
hats,
|
|
||||||
is_groupchat_message,
|
|
||||||
is_retracted,
|
|
||||||
retractable,
|
|
||||||
'extra_classes': this.getExtraMessageClasses(),
|
|
||||||
'is_me_message': this.model.isMeCommand(),
|
|
||||||
'label_show': __('Show more'),
|
|
||||||
'occupant': this.model.occupant,
|
|
||||||
'pretty_time': time.format(api.settings.get('time_format')),
|
|
||||||
'retraction_text': is_retracted ? this.getRetractionText() : null,
|
|
||||||
'time': time.toISOString(),
|
|
||||||
'username': this.model.getDisplayName()
|
|
||||||
})
|
|
||||||
));
|
|
||||||
|
|
||||||
const url = this.model.get('oob_url');
|
|
||||||
url && render(this.transformOOBURL(url), msg.querySelector('.chat-msg__media'));
|
|
||||||
|
|
||||||
if (!is_retracted) {
|
|
||||||
const text = this.model.getMessageText();
|
|
||||||
const msg_content = msg.querySelector('.chat-msg__text');
|
|
||||||
if (text && text !== url) {
|
|
||||||
msg_content.innerHTML = await this.transformBodyText(text);
|
|
||||||
if (api.settings.get('show_images_inline')) {
|
|
||||||
u.renderImageURLs(_converse, msg_content).then(() => this.triggerRendered());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (this.model.get('type') !== 'headline') {
|
|
||||||
this.renderAvatar(msg);
|
|
||||||
}
|
|
||||||
this.replaceElement(msg);
|
|
||||||
this.triggerRendered();
|
|
||||||
},
|
|
||||||
|
|
||||||
triggerRendered () {
|
|
||||||
if (this.model.collection) {
|
|
||||||
// If the model gets destroyed in the meantime, it no
|
|
||||||
// longer has a collection.
|
|
||||||
this.model.collection.trigger('rendered', this);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
renderInfoMessage () {
|
|
||||||
const msg = u.stringToElement(
|
|
||||||
tpl_info(Object.assign(this.model.toJSON(), {
|
|
||||||
'extra_classes': 'chat-info',
|
|
||||||
'isodate': dayjs(this.model.get('time')).toISOString()
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
return this.replaceElement(msg);
|
|
||||||
},
|
|
||||||
|
|
||||||
getRetractionText () {
|
|
||||||
if (this.model.get('type') === 'groupchat' && this.model.get('moderated_by')) {
|
|
||||||
const retracted_by_mod = this.model.get('moderated_by');
|
|
||||||
const chatbox = this.model.collection.chatbox;
|
|
||||||
if (!this.model.mod) {
|
|
||||||
this.model.mod =
|
|
||||||
chatbox.occupants.findOccupant({'jid': retracted_by_mod}) ||
|
|
||||||
chatbox.occupants.findOccupant({'nick': Strophe.getResourceFromJid(retracted_by_mod)});
|
|
||||||
}
|
|
||||||
const modname = this.model.mod ? this.model.mod.getDisplayName() : 'A moderator';
|
|
||||||
return __('%1$s has removed this message', modname);
|
|
||||||
} else {
|
|
||||||
return __('%1$s has removed this message', this.model.getDisplayName());
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
renderErrorMessage () {
|
|
||||||
const msg = u.stringToElement(
|
|
||||||
tpl_info(Object.assign(this.model.toJSON(), {
|
|
||||||
'extra_classes': 'chat-error',
|
|
||||||
'isodate': dayjs(this.model.get('time')).toISOString()
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
return this.replaceElement(msg);
|
|
||||||
},
|
|
||||||
|
|
||||||
renderFileUploadProgresBar () {
|
|
||||||
const msg = u.stringToElement(tpl_file_progress(
|
|
||||||
Object.assign(this.model.toJSON(), {
|
|
||||||
'__': __,
|
|
||||||
'filename': this.model.file.name,
|
|
||||||
'filesize': filesize(this.model.file.size)
|
|
||||||
})));
|
|
||||||
this.replaceElement(msg);
|
|
||||||
this.renderAvatar();
|
|
||||||
},
|
|
||||||
|
|
||||||
showMessageVersionsModal (ev) {
|
|
||||||
ev.preventDefault();
|
|
||||||
if (this.model.message_versions_modal === undefined) {
|
|
||||||
this.model.message_versions_modal = new _converse.MessageVersionsModal({'model': this.model});
|
|
||||||
}
|
|
||||||
this.model.message_versions_modal.show(ev);
|
|
||||||
},
|
|
||||||
|
|
||||||
getExtraMessageClasses () {
|
|
||||||
const is_retracted = this.model.get('retracted') || this.model.get('moderated') === 'retracted';
|
|
||||||
const extra_classes = [
|
|
||||||
...(this.model.get('is_delayed') ? ['delayed'] : []), ...(is_retracted ? ['chat-msg--retracted'] : [])
|
|
||||||
];
|
|
||||||
if (this.model.get('type') === 'groupchat') {
|
|
||||||
if (this.model.occupant) {
|
|
||||||
extra_classes.push(this.model.occupant.get('role'));
|
|
||||||
extra_classes.push(this.model.occupant.get('affiliation'));
|
|
||||||
}
|
|
||||||
if (this.model.get('sender') === 'them' && this.model.collection.chatbox.isUserMentioned(this.model)) {
|
|
||||||
// Add special class to mark groupchat messages
|
|
||||||
// in which we are mentioned.
|
|
||||||
extra_classes.push('mentioned');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (this.model.get('correcting')) {
|
|
||||||
extra_classes.push('correcting');
|
|
||||||
}
|
|
||||||
return extra_classes.filter(c => c).join(" ");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
@ -17,7 +17,6 @@ import tpl_chatroom_destroyed from "templates/chatroom_destroyed.html";
|
|||||||
import tpl_chatroom_disconnect from "templates/chatroom_disconnect.html";
|
import tpl_chatroom_disconnect from "templates/chatroom_disconnect.html";
|
||||||
import tpl_chatroom_head from "templates/chatroom_head.js";
|
import tpl_chatroom_head from "templates/chatroom_head.js";
|
||||||
import tpl_chatroom_nickname_form from "templates/chatroom_nickname_form.html";
|
import tpl_chatroom_nickname_form from "templates/chatroom_nickname_form.html";
|
||||||
import tpl_info from "templates/info.html";
|
|
||||||
import tpl_list_chatrooms_modal from "templates/list_chatrooms_modal.js";
|
import tpl_list_chatrooms_modal from "templates/list_chatrooms_modal.js";
|
||||||
import tpl_muc_config_form from "templates/muc_config_form.js";
|
import tpl_muc_config_form from "templates/muc_config_form.js";
|
||||||
import tpl_muc_invite_modal from "templates/muc_invite_modal.js";
|
import tpl_muc_invite_modal from "templates/muc_invite_modal.js";
|
||||||
@ -438,8 +437,6 @@ converse.plugins.add('converse-muc-views', {
|
|||||||
is_chatroom: true,
|
is_chatroom: true,
|
||||||
events: {
|
events: {
|
||||||
'change input.fileupload': 'onFileSelection',
|
'change input.fileupload': 'onFileSelection',
|
||||||
'click .chat-msg__action-edit': 'onMessageEditButtonClicked',
|
|
||||||
'click .chat-msg__action-retract': 'onMessageRetractButtonClicked',
|
|
||||||
'click .chatbox-navback': 'showControlBox',
|
'click .chatbox-navback': 'showControlBox',
|
||||||
'click .hide-occupants': 'hideOccupants',
|
'click .hide-occupants': 'hideOccupants',
|
||||||
'click .new-msgs-indicator': 'viewUnreadMessages',
|
'click .new-msgs-indicator': 'viewUnreadMessages',
|
||||||
@ -463,24 +460,15 @@ converse.plugins.add('converse-muc-views', {
|
|||||||
async initialize () {
|
async initialize () {
|
||||||
this.initDebounced();
|
this.initDebounced();
|
||||||
|
|
||||||
this.listenTo(this.model.messages, 'add', this.onMessageAdded);
|
|
||||||
this.listenTo(this.model.messages, 'change:edited', this.onMessageEdited);
|
|
||||||
this.listenTo(this.model.messages, 'rendered', this.scrollDown);
|
|
||||||
this.model.messages.on('reset', () => {
|
|
||||||
this.msgs_container.innerHTML = '';
|
|
||||||
this.removeAll();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.listenTo(this.model.session, 'change:connection_status', this.onConnectionStatusChanged);
|
|
||||||
|
|
||||||
this.listenTo(this.model, 'change', debounce(() => this.renderHeading(), 250));
|
this.listenTo(this.model, 'change', debounce(() => this.renderHeading(), 250));
|
||||||
this.listenTo(this.model, 'change:hidden_occupants', this.updateOccupantsToggle);
|
this.listenTo(this.model, 'change:hidden_occupants', this.updateOccupantsToggle);
|
||||||
this.listenTo(this.model, 'configurationNeeded', this.getAndRenderConfigurationForm);
|
this.listenTo(this.model, 'configurationNeeded', this.getAndRenderConfigurationForm);
|
||||||
this.listenTo(this.model, 'destroy', this.hide);
|
this.listenTo(this.model, 'destroy', this.hide);
|
||||||
this.listenTo(this.model, 'show', this.show);
|
this.listenTo(this.model, 'show', this.show);
|
||||||
|
|
||||||
this.listenTo(this.model.features, 'change:moderated', this.renderBottomPanel);
|
this.listenTo(this.model.features, 'change:moderated', this.renderBottomPanel);
|
||||||
this.listenTo(this.model.features, 'change:open', this.renderHeading);
|
this.listenTo(this.model.features, 'change:open', this.renderHeading);
|
||||||
|
this.listenTo(this.model.messages, 'rendered', this.scrollDown);
|
||||||
|
this.listenTo(this.model.session, 'change:connection_status', this.onConnectionStatusChanged);
|
||||||
|
|
||||||
// Bind so that we can pass it to addEventListener and removeEventListener
|
// Bind so that we can pass it to addEventListener and removeEventListener
|
||||||
this.onMouseMove = this.onMouseMove.bind(this);
|
this.onMouseMove = this.onMouseMove.bind(this);
|
||||||
@ -489,13 +477,19 @@ converse.plugins.add('converse-muc-views', {
|
|||||||
await this.render();
|
await this.render();
|
||||||
|
|
||||||
// Need to be registered after render has been called.
|
// Need to be registered after render has been called.
|
||||||
|
this.listenTo(this.model, 'change:show_help_messages', this.renderHelpMessages);
|
||||||
|
this.listenTo(this.model.messages, 'add', this.onMessageAdded);
|
||||||
|
this.listenTo(this.model.messages, 'change', this.renderChatHistory);
|
||||||
|
this.listenTo(this.model.messages, 'reset', this.renderChatHistory);
|
||||||
|
this.listenTo(this.model.notifications, 'change', this.renderNotifications);
|
||||||
|
|
||||||
this.model.occupants.forEach(o => this.onOccupantAdded(o));
|
this.model.occupants.forEach(o => this.onOccupantAdded(o));
|
||||||
this.listenTo(this.model.occupants, 'add', this.onOccupantAdded);
|
this.listenTo(this.model.occupants, 'add', this.onOccupantAdded);
|
||||||
this.listenTo(this.model.occupants, 'remove', this.onOccupantRemoved);
|
this.listenTo(this.model.occupants, 'change', this.renderChatHistory);
|
||||||
this.listenTo(this.model.occupants, 'change:show', this.showJoinOrLeaveNotification);
|
|
||||||
this.listenTo(this.model.occupants, 'change:role', this.onOccupantRoleChanged);
|
|
||||||
this.listenTo(this.model.occupants, 'change:affiliation', this.onOccupantAffiliationChanged);
|
this.listenTo(this.model.occupants, 'change:affiliation', this.onOccupantAffiliationChanged);
|
||||||
this.listenTo(this.model.notifications, 'change', this.renderNotifications);
|
this.listenTo(this.model.occupants, 'change:role', this.onOccupantRoleChanged);
|
||||||
|
this.listenTo(this.model.occupants, 'change:show', this.showJoinOrLeaveNotification);
|
||||||
|
this.listenTo(this.model.occupants, 'remove', this.onOccupantRemoved);
|
||||||
|
|
||||||
this.createSidebarView();
|
this.createSidebarView();
|
||||||
await this.updateAfterMessagesFetched();
|
await this.updateAfterMessagesFetched();
|
||||||
@ -522,9 +516,11 @@ converse.plugins.add('converse-muc-views', {
|
|||||||
'muc_show_logs_before_join': _converse.muc_show_logs_before_join,
|
'muc_show_logs_before_join': _converse.muc_show_logs_before_join,
|
||||||
'show_send_button': _converse.show_send_button
|
'show_send_button': _converse.show_send_button
|
||||||
}), this.el);
|
}), this.el);
|
||||||
|
|
||||||
this.notifications = this.el.querySelector('.chat-content__notifications');
|
this.notifications = this.el.querySelector('.chat-content__notifications');
|
||||||
this.content = this.el.querySelector('.chat-content');
|
this.content = this.el.querySelector('.chat-content');
|
||||||
this.msgs_container = this.el.querySelector('.chat-content__messages');
|
this.msgs_container = this.el.querySelector('.chat-content__messages');
|
||||||
|
this.help_container = this.el.querySelector('.chat-content__help');
|
||||||
|
|
||||||
this.renderBottomPanel();
|
this.renderBottomPanel();
|
||||||
if (!_converse.muc_show_logs_before_join &&
|
if (!_converse.muc_show_logs_before_join &&
|
||||||
@ -538,13 +534,13 @@ converse.plugins.add('converse-muc-views', {
|
|||||||
!this.model.get('hidden') && this.show();
|
!this.model.get('hidden') && this.show();
|
||||||
},
|
},
|
||||||
|
|
||||||
renderNotifications () {
|
getNotifications () {
|
||||||
const actors_per_state = this.model.notifications.toJSON();
|
const actors_per_state = this.model.notifications.toJSON();
|
||||||
const states = api.settings.get('muc_show_join_leave') ?
|
const states = api.settings.get('muc_show_join_leave') ?
|
||||||
[...converse.CHAT_STATES, ...converse.MUC_TRAFFIC_STATES, ...converse.MUC_ROLE_CHANGES] :
|
[...converse.CHAT_STATES, ...converse.MUC_TRAFFIC_STATES, ...converse.MUC_ROLE_CHANGES] :
|
||||||
converse.CHAT_STATES;
|
converse.CHAT_STATES;
|
||||||
|
|
||||||
const message = states.reduce((result, state) => {
|
return states.reduce((result, state) => {
|
||||||
const existing_actors = actors_per_state[state];
|
const existing_actors = actors_per_state[state];
|
||||||
if (!(existing_actors?.length)) {
|
if (!(existing_actors?.length)) {
|
||||||
return result;
|
return result;
|
||||||
@ -601,8 +597,34 @@ converse.plugins.add('converse-muc-views', {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}, '');
|
}, '');
|
||||||
this.notifications.innerHTML = message;
|
},
|
||||||
message.includes('\n') && this.scrollDown();
|
|
||||||
|
getHelpMessages () {
|
||||||
|
const setting = api.settings.get("muc_disable_slash_commands");
|
||||||
|
const disabled_commands = Array.isArray(setting) ? setting : [];
|
||||||
|
return [
|
||||||
|
`<strong>/admin</strong>: ${__("Change user's affiliation to admin")}`,
|
||||||
|
`<strong>/ban</strong>: ${__('Ban user by changing their affiliation to outcast')}`,
|
||||||
|
`<strong>/clear</strong>: ${__('Clear the chat area')}`,
|
||||||
|
`<strong>/close</strong>: ${__('Close this groupchat')}`,
|
||||||
|
`<strong>/deop</strong>: ${__('Change user role to participant')}`,
|
||||||
|
`<strong>/destroy</strong>: ${__('Remove this groupchat')}`,
|
||||||
|
`<strong>/help</strong>: ${__('Show this menu')}`,
|
||||||
|
`<strong>/kick</strong>: ${__('Kick user from groupchat')}`,
|
||||||
|
`<strong>/me</strong>: ${__('Write in 3rd person')}`,
|
||||||
|
`<strong>/member</strong>: ${__('Grant membership to a user')}`,
|
||||||
|
`<strong>/modtools</strong>: ${__('Opens up the moderator tools GUI')}`,
|
||||||
|
`<strong>/mute</strong>: ${__("Remove user's ability to post messages")}`,
|
||||||
|
`<strong>/nick</strong>: ${__('Change your nickname')}`,
|
||||||
|
`<strong>/op</strong>: ${__('Grant moderator role to user')}`,
|
||||||
|
`<strong>/owner</strong>: ${__('Grant ownership of this groupchat')}`,
|
||||||
|
`<strong>/register</strong>: ${__("Register your nickname")}`,
|
||||||
|
`<strong>/revoke</strong>: ${__("Revoke the user's current affiliation")}`,
|
||||||
|
`<strong>/subject</strong>: ${__('Set groupchat subject')}`,
|
||||||
|
`<strong>/topic</strong>: ${__('Set groupchat subject (alias for /subject)')}`,
|
||||||
|
`<strong>/voice</strong>: ${__('Allow muted user to post messages')}`
|
||||||
|
].filter(line => disabled_commands.every(c => (!line.startsWith(c+'<', 9))))
|
||||||
|
.filter(line => this.getAllowedCommands().some(c => line.startsWith(c+'<', 9)));
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -785,12 +807,7 @@ converse.plugins.add('converse-muc-views', {
|
|||||||
return _converse.ChatBoxView.prototype.onKeyUp.call(this, ev);
|
return _converse.ChatBoxView.prototype.onKeyUp.call(this, ev);
|
||||||
},
|
},
|
||||||
|
|
||||||
async onMessageRetractButtonClicked (ev) {
|
async onMessageRetractButtonClicked (message) {
|
||||||
ev.preventDefault();
|
|
||||||
const msg_el = u.ancestor(ev.target, '.message');
|
|
||||||
const msgid = msg_el.getAttribute('data-msgid');
|
|
||||||
const time = msg_el.getAttribute('data-isodate');
|
|
||||||
const message = this.model.messages.findWhere({msgid, time});
|
|
||||||
const retraction_warning =
|
const retraction_warning =
|
||||||
__("Be aware that other XMPP/Jabber clients (and servers) may "+
|
__("Be aware that other XMPP/Jabber clients (and servers) may "+
|
||||||
"not yet support retractions and that this message may not "+
|
"not yet support retractions and that this message may not "+
|
||||||
@ -801,7 +818,7 @@ converse.plugins.add('converse-muc-views', {
|
|||||||
if (_converse.show_retraction_warning) {
|
if (_converse.show_retraction_warning) {
|
||||||
messages[1] = retraction_warning;
|
messages[1] = retraction_warning;
|
||||||
}
|
}
|
||||||
!!(await api.confirm(__('Confirm'), messages)) && this.retractOwnMessage(message);
|
!!(await api.confirm(__('Confirm'), messages)) && this.model.retractOwnMessage(message);
|
||||||
} else if (await message.mayBeModerated()) {
|
} else if (await message.mayBeModerated()) {
|
||||||
if (message.get('sender') === 'me') {
|
if (message.get('sender') === 'me') {
|
||||||
let messages = [__('Are you sure you want to retract this message?')];
|
let messages = [__('Are you sure you want to retract this message?')];
|
||||||
@ -830,22 +847,6 @@ converse.plugins.add('converse-muc-views', {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Retract one of your messages in this groupchat.
|
|
||||||
* @private
|
|
||||||
* @method _converse.ChatRoomView#retractOwnMessage
|
|
||||||
* @param { _converse.Message } message - The message which we're retracting.
|
|
||||||
*/
|
|
||||||
retractOwnMessage(message) {
|
|
||||||
this.model.retractOwnMessage(message)
|
|
||||||
.catch(e => {
|
|
||||||
const message = __('Sorry, something went wrong while trying to retract your message.');
|
|
||||||
this.model.createMessage({message, 'type': 'error'});
|
|
||||||
!u.isErrorStanza(e) && this.model.createMessage({'message': e.message, 'type': 'error'});
|
|
||||||
log.error(e);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retract someone else's message in this groupchat.
|
* Retract someone else's message in this groupchat.
|
||||||
* @private
|
* @private
|
||||||
@ -1363,10 +1364,7 @@ converse.plugins.add('converse-muc-views', {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const args = text.slice(('/'+command).length+1).trim();
|
const args = text.slice(('/'+command).length+1).trim();
|
||||||
const disabled_commands = Array.isArray(_converse.muc_disable_slash_commands) ?
|
if (!this.getAllowedCommands().includes(command)) {
|
||||||
_converse.muc_disable_slash_commands : [];
|
|
||||||
const allowed_commands = this.getAllowedCommands();
|
|
||||||
if (!allowed_commands.includes(command)) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1401,31 +1399,7 @@ converse.plugins.add('converse-muc-views', {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'help': {
|
case 'help': {
|
||||||
this.showHelpMessages([`<strong>${__("You can run the following commands")}</strong>`]);
|
this.model.set({'show_help_messages': true});
|
||||||
this.showHelpMessages([
|
|
||||||
`<strong>/admin</strong>: ${__("Change user's affiliation to admin")}`,
|
|
||||||
`<strong>/ban</strong>: ${__('Ban user by changing their affiliation to outcast')}`,
|
|
||||||
`<strong>/clear</strong>: ${__('Clear the chat area')}`,
|
|
||||||
`<strong>/close</strong>: ${__('Close this groupchat')}`,
|
|
||||||
`<strong>/deop</strong>: ${__('Change user role to participant')}`,
|
|
||||||
`<strong>/destroy</strong>: ${__('Remove this groupchat')}`,
|
|
||||||
`<strong>/help</strong>: ${__('Show this menu')}`,
|
|
||||||
`<strong>/kick</strong>: ${__('Kick user from groupchat')}`,
|
|
||||||
`<strong>/me</strong>: ${__('Write in 3rd person')}`,
|
|
||||||
`<strong>/member</strong>: ${__('Grant membership to a user')}`,
|
|
||||||
`<strong>/modtools</strong>: ${__('Opens up the moderator tools GUI')}`,
|
|
||||||
`<strong>/mute</strong>: ${__("Remove user's ability to post messages")}`,
|
|
||||||
`<strong>/nick</strong>: ${__('Change your nickname')}`,
|
|
||||||
`<strong>/op</strong>: ${__('Grant moderator role to user')}`,
|
|
||||||
`<strong>/owner</strong>: ${__('Grant ownership of this groupchat')}`,
|
|
||||||
`<strong>/register</strong>: ${__("Register your nickname")}`,
|
|
||||||
`<strong>/revoke</strong>: ${__("Revoke the user's current affiliation")}`,
|
|
||||||
`<strong>/subject</strong>: ${__('Set groupchat subject')}`,
|
|
||||||
`<strong>/topic</strong>: ${__('Set groupchat subject (alias for /subject)')}`,
|
|
||||||
`<strong>/voice</strong>: ${__('Allow muted user to post messages')}`
|
|
||||||
].filter(line => disabled_commands.every(c => (!line.startsWith(c+'<', 9))))
|
|
||||||
.filter(line => allowed_commands.some(c => line.startsWith(c+'<', 9)))
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
} case 'kick': {
|
} case 'kick': {
|
||||||
this.setRole(command, args, [], ['moderator']);
|
this.setRole(command, args, [], ['moderator']);
|
||||||
@ -1673,35 +1647,6 @@ converse.plugins.add('converse-muc-views', {
|
|||||||
u.showElement(container);
|
u.showElement(container);
|
||||||
},
|
},
|
||||||
|
|
||||||
removeEmptyHistoryFeedback () {
|
|
||||||
const el = this.msgs_container.firstElementChild;
|
|
||||||
if (_converse.muc_show_logs_before_join && el && el.matches('.empty-history-feedback')) {
|
|
||||||
this.msgs_container.removeChild(this.msgs_container.firstElementChild);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
insertDayIndicator () {
|
|
||||||
this.removeEmptyHistoryFeedback();
|
|
||||||
return _converse.ChatBoxView.prototype.insertDayIndicator.apply(this, arguments);
|
|
||||||
},
|
|
||||||
|
|
||||||
insertMessage (view) {
|
|
||||||
this.removeEmptyHistoryFeedback();
|
|
||||||
return _converse.ChatBoxView.prototype.insertMessage.call(this, view);
|
|
||||||
},
|
|
||||||
|
|
||||||
insertNotification (message) {
|
|
||||||
this.removeEmptyHistoryFeedback();
|
|
||||||
this.msgs_container.insertAdjacentHTML(
|
|
||||||
'beforeend',
|
|
||||||
tpl_info({
|
|
||||||
'isodate': (new Date()).toISOString(),
|
|
||||||
'extra_classes': 'chat-event',
|
|
||||||
'message': message
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
onOccupantAdded (occupant) {
|
onOccupantAdded (occupant) {
|
||||||
if (occupant.get('jid') === _converse.bare_jid) {
|
if (occupant.get('jid') === _converse.bare_jid) {
|
||||||
this.renderHeading();
|
this.renderHeading();
|
||||||
|
@ -194,13 +194,6 @@ converse.plugins.add('converse-omemo', {
|
|||||||
this.__super__.initialize.apply(this, arguments);
|
this.__super__.initialize.apply(this, arguments);
|
||||||
this.listenTo(this.model, 'change:omemo_active', this.renderOMEMOToolbarButton);
|
this.listenTo(this.model, 'change:omemo_active', this.renderOMEMOToolbarButton);
|
||||||
this.listenTo(this.model, 'change:omemo_supported', this.onOMEMOSupportedDetermined);
|
this.listenTo(this.model, 'change:omemo_supported', this.onOMEMOSupportedDetermined);
|
||||||
},
|
|
||||||
|
|
||||||
showMessage (message) {
|
|
||||||
// We don't show a message if it's only keying material
|
|
||||||
if (!message.get('is_only_key')) {
|
|
||||||
return this.__super__.showMessage.apply(this, arguments);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -45,7 +45,6 @@ const WHITELISTED_PLUGINS = [
|
|||||||
'converse-emoji-views',
|
'converse-emoji-views',
|
||||||
'converse-fullscreen',
|
'converse-fullscreen',
|
||||||
'converse-mam-views',
|
'converse-mam-views',
|
||||||
'converse-message-view',
|
|
||||||
'converse-minimize',
|
'converse-minimize',
|
||||||
'converse-modal',
|
'converse-modal',
|
||||||
'converse-muc-views',
|
'converse-muc-views',
|
||||||
|
@ -331,8 +331,8 @@ converse.plugins.add('converse-chat', {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.set({'box_id': `box-${btoa(jid)}`});
|
this.set({'box_id': `box-${btoa(jid)}`});
|
||||||
this.initMessages();
|
|
||||||
this.initNotifications();
|
this.initNotifications();
|
||||||
|
this.initMessages();
|
||||||
|
|
||||||
if (this.get('type') === _converse.PRIVATE_CHAT_TYPE) {
|
if (this.get('type') === _converse.PRIVATE_CHAT_TYPE) {
|
||||||
this.presence = _converse.presences.findWhere({'jid': jid}) || _converse.presences.create({'jid': jid});
|
this.presence = _converse.presences.findWhere({'jid': jid}) || _converse.presences.create({'jid': jid});
|
||||||
@ -395,9 +395,39 @@ converse.plugins.add('converse-chat', {
|
|||||||
return this.messages.fetched;
|
return this.messages.fetched;
|
||||||
},
|
},
|
||||||
|
|
||||||
async handleErrormessageStanza (stanza) {
|
async handleErrorMessageStanza (stanza) {
|
||||||
if (await this.shouldShowErrorMessage(stanza)) {
|
const attrs = await st.parseMessage(stanza, _converse);
|
||||||
this.createMessage(await st.parseMessage(stanza, _converse));
|
if (!await this.shouldShowErrorMessage(attrs)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const message = this.getMessageReferencedByError(attrs);
|
||||||
|
if (message) {
|
||||||
|
const new_attrs = {
|
||||||
|
'error': attrs.error,
|
||||||
|
'error_condition': attrs.error_condition,
|
||||||
|
'error_text': attrs.error_text,
|
||||||
|
'error_type': attrs.error_type,
|
||||||
|
};
|
||||||
|
if (attrs.msgid === message.get('retraction_id')) {
|
||||||
|
// The error message refers to a retraction
|
||||||
|
new_attrs.retraction_id = undefined;
|
||||||
|
if (!attrs.error) {
|
||||||
|
if (attrs.error_condition === 'forbidden') {
|
||||||
|
new_attrs.error = __("You're not allowed to retract your message.");
|
||||||
|
} else {
|
||||||
|
new_attrs.error = __('Sorry, an error occurred while trying to retract your message.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (!attrs.error) {
|
||||||
|
if (attrs.error_condition === 'forbidden') {
|
||||||
|
new_attrs.error = __("You're not allowed to send a message.");
|
||||||
|
} else {
|
||||||
|
new_attrs.error = __('Sorry, an error occurred while trying to send your message.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
message.save(new_attrs);
|
||||||
|
} else {
|
||||||
|
this.createMessage(attrs);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -510,7 +540,11 @@ converse.plugins.add('converse-chat', {
|
|||||||
|
|
||||||
async createMessageFromError (error) {
|
async createMessageFromError (error) {
|
||||||
if (error instanceof _converse.TimeoutError) {
|
if (error instanceof _converse.TimeoutError) {
|
||||||
const msg = await this.createMessage({'type': 'error', 'message': error.message, 'retry': true});
|
const msg = await this.createMessage({
|
||||||
|
'type': 'error',
|
||||||
|
'message': error.message,
|
||||||
|
'retry': true
|
||||||
|
});
|
||||||
msg.error = error;
|
msg.error = error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -579,27 +613,29 @@ converse.plugins.add('converse-chat', {
|
|||||||
return this;
|
return this;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an error `<message>` stanza's attributes, find the saved message model which is
|
||||||
|
* referenced by that error.
|
||||||
|
* @param { Object } attrs
|
||||||
|
*/
|
||||||
|
getMessageReferencedByError (attrs) {
|
||||||
|
const id = attrs.msgid;
|
||||||
|
return id && this.messages.models.find(m => [m.get('msgid'), m.get('retraction_id')].includes(id));
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
* @method _converse.ChatBox#shouldShowErrorMessage
|
* @method _converse.ChatBox#shouldShowErrorMessage
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
shouldShowErrorMessage (stanza) {
|
shouldShowErrorMessage (attrs) {
|
||||||
const id = stanza.getAttribute('id');
|
const msg = this.getMessageReferencedByError(attrs);
|
||||||
if (id) {
|
if (!msg && attrs.body === null) {
|
||||||
const msgs = this.messages.where({'msgid': id});
|
// If the error refers to a message not included in our store,
|
||||||
const referenced_msgs = msgs.filter(m => m.get('type') !== 'error');
|
// and it doesn't have a <body> tag, we assume that this was a
|
||||||
if (!referenced_msgs.length && stanza.querySelector('body') === null) {
|
// CSI message (which we don't store).
|
||||||
// If the error refers to a message not included in our store,
|
// See https://github.com/conversejs/converse.js/issues/1317
|
||||||
// and it doesn't have a <body> tag, we assume that this was a
|
return;
|
||||||
// CSI message (which we don't store).
|
|
||||||
// See https://github.com/conversejs/converse.js/issues/1317
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const dupes = msgs.filter(m => m.get('type') === 'error');
|
|
||||||
if (dupes.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Gets overridden in ChatRoom
|
// Gets overridden in ChatRoom
|
||||||
return true;
|
return true;
|
||||||
@ -765,6 +801,7 @@ converse.plugins.add('converse-chat', {
|
|||||||
message.save({
|
message.save({
|
||||||
'retracted': (new Date()).toISOString(),
|
'retracted': (new Date()).toISOString(),
|
||||||
'retracted_id': message.get('origin_id'),
|
'retracted_id': message.get('origin_id'),
|
||||||
|
'retraction_id': message.get('id'),
|
||||||
'is_ephemeral': true,
|
'is_ephemeral': true,
|
||||||
'editable': false
|
'editable': false
|
||||||
});
|
});
|
||||||
@ -1044,9 +1081,9 @@ converse.plugins.add('converse-chat', {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const data = item.dataforms.where({'FORM_TYPE': {'value': Strophe.NS.HTTPUPLOAD, 'type': "hidden"}}).pop(),
|
const data = item.dataforms.where({'FORM_TYPE': {'value': Strophe.NS.HTTPUPLOAD, 'type': "hidden"}}).pop();
|
||||||
max_file_size = window.parseInt((data?.attributes || {})['max-file-size']?.value),
|
const max_file_size = window.parseInt((data?.attributes || {})['max-file-size']?.value);
|
||||||
slot_request_url = item?.id;
|
const slot_request_url = item?.id;
|
||||||
|
|
||||||
if (!slot_request_url) {
|
if (!slot_request_url) {
|
||||||
this.createMessage({
|
this.createMessage({
|
||||||
@ -1147,7 +1184,7 @@ converse.plugins.add('converse-chat', {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const chatbox = await api.chatboxes.get(from_jid);
|
const chatbox = await api.chatboxes.get(from_jid);
|
||||||
chatbox?.handleErrormessageStanza(stanza);
|
chatbox?.handleErrorMessageStanza(stanza);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -382,8 +382,8 @@ converse.plugins.add('converse-muc', {
|
|||||||
this.initialized = u.getResolveablePromise();
|
this.initialized = u.getResolveablePromise();
|
||||||
this.debouncedRejoin = debounce(this.rejoin, 250);
|
this.debouncedRejoin = debounce(this.rejoin, 250);
|
||||||
this.set('box_id', `box-${btoa(this.get('jid'))}`);
|
this.set('box_id', `box-${btoa(this.get('jid'))}`);
|
||||||
this.initMessages();
|
|
||||||
this.initNotifications();
|
this.initNotifications();
|
||||||
|
this.initMessages();
|
||||||
this.initOccupants();
|
this.initOccupants();
|
||||||
this.initDiscoModels(); // sendChatState depends on this.features
|
this.initDiscoModels(); // sendChatState depends on this.features
|
||||||
this.registerHandlers();
|
this.registerHandlers();
|
||||||
@ -618,15 +618,43 @@ converse.plugins.add('converse-muc', {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async handleErrormessageStanza (stanza) {
|
async handleErrorMessageStanza (stanza) {
|
||||||
if (await this.shouldShowErrorMessage(stanza)) {
|
const attrs = await st.parseMUCMessage(stanza, this, _converse);
|
||||||
const attrs = await st.parseMUCMessage(stanza, this, _converse);
|
if (!await this.shouldShowErrorMessage(attrs)) {
|
||||||
const message = attrs.msgid && this.messages.findWhere({'msgid': attrs.msgid});
|
return;
|
||||||
if (message) {
|
}
|
||||||
message.save({'error': attrs.error});
|
const message = this.getMessageReferencedByError(attrs);
|
||||||
} else {
|
if (message) {
|
||||||
this.createMessage(attrs);
|
const new_attrs = {
|
||||||
|
'error': attrs.error,
|
||||||
|
'error_condition': attrs.error_condition,
|
||||||
|
'error_text': attrs.error_text,
|
||||||
|
'error_type': attrs.error_type,
|
||||||
|
};
|
||||||
|
if (attrs.msgid === message.get('retraction_id')) {
|
||||||
|
// The error message refers to a retraction
|
||||||
|
new_attrs.retraction_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.");
|
||||||
|
} else {
|
||||||
|
new_attrs.error = __('Sorry, an error occurred while trying to retract your message.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (!attrs.error) {
|
||||||
|
if (attrs.error_condition === 'forbidden') {
|
||||||
|
new_attrs.error = __("Your message was not delivered because you weren't allowed to send it.");
|
||||||
|
} else if (attrs.error_condition === 'not-acceptable') {
|
||||||
|
new_attrs.error = __("Your message was not delivered because you're not present in the groupchat.");
|
||||||
|
} else {
|
||||||
|
new_attrs.error = __('Sorry, an error occurred while trying to send your message.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
message.save(new_attrs);
|
||||||
|
} else {
|
||||||
|
this.createMessage(attrs);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -749,20 +777,38 @@ converse.plugins.add('converse-muc', {
|
|||||||
* @param { _converse.Message } message - The message which we're retracting.
|
* @param { _converse.Message } message - The message which we're retracting.
|
||||||
*/
|
*/
|
||||||
async retractOwnMessage(message) {
|
async retractOwnMessage(message) {
|
||||||
|
const origin_id = message.get('origin_id');
|
||||||
|
if (!origin_id) {
|
||||||
|
throw new Error("Can't retract message without a XEP-0359 Origin ID");
|
||||||
|
}
|
||||||
const editable = message.get('editable');
|
const editable = message.get('editable');
|
||||||
|
const stanza = $msg({
|
||||||
|
'id': u.getUniqueId(),
|
||||||
|
'to': this.get('jid'),
|
||||||
|
'type': "groupchat"
|
||||||
|
})
|
||||||
|
.c('store', {xmlns: Strophe.NS.HINTS}).up()
|
||||||
|
.c("apply-to", {
|
||||||
|
'id': origin_id,
|
||||||
|
'xmlns': Strophe.NS.FASTEN
|
||||||
|
}).c('retract', {xmlns: Strophe.NS.RETRACT});
|
||||||
|
|
||||||
// Optimistic save
|
// Optimistic save
|
||||||
message.save({
|
message.set({
|
||||||
'retracted': (new Date()).toISOString(),
|
'retracted': (new Date()).toISOString(),
|
||||||
'retracted_id': message.get('origin_id'),
|
'retracted_id': origin_id,
|
||||||
|
'retraction_id': stanza.nodeTree.getAttribute('id'),
|
||||||
'editable': false
|
'editable': false
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
await this.sendRetractionMessage(message)
|
await this.sendTimedMessage(stanza);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
message.save({
|
message.save({
|
||||||
editable,
|
editable,
|
||||||
|
'error_type': 'timeout',
|
||||||
|
'error': __('A timeout happened while while trying to retract your message.'),
|
||||||
'retracted': undefined,
|
'retracted': undefined,
|
||||||
'retracted_id': undefined,
|
'retracted_id': undefined
|
||||||
});
|
});
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
@ -799,30 +845,6 @@ converse.plugins.add('converse-muc', {
|
|||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends a message stanza to retract a message in this groupchat.
|
|
||||||
* @private
|
|
||||||
* @method _converse.ChatRoom#sendRetractionMessage
|
|
||||||
* @param { _converse.Message } message - The message which we're retracting.
|
|
||||||
*/
|
|
||||||
sendRetractionMessage (message) {
|
|
||||||
const origin_id = message.get('origin_id');
|
|
||||||
if (!origin_id) {
|
|
||||||
throw new Error("Can't retract message without a XEP-0359 Origin ID");
|
|
||||||
}
|
|
||||||
const msg = $msg({
|
|
||||||
'id': u.getUniqueId(),
|
|
||||||
'to': this.get('jid'),
|
|
||||||
'type': "groupchat"
|
|
||||||
})
|
|
||||||
.c('store', {xmlns: Strophe.NS.HINTS}).up()
|
|
||||||
.c("apply-to", {
|
|
||||||
'id': origin_id,
|
|
||||||
'xmlns': Strophe.NS.FASTEN
|
|
||||||
}).c('retract', {xmlns: Strophe.NS.RETRACT});
|
|
||||||
return this.sendTimedMessage(msg);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends an IQ stanza to the XMPP server to retract a message in this groupchat.
|
* Sends an IQ stanza to the XMPP server to retract a message in this groupchat.
|
||||||
* @private
|
* @private
|
||||||
@ -1815,13 +1837,11 @@ converse.plugins.add('converse-muc', {
|
|||||||
* @method _converse.ChatRoom#shouldShowErrorMessage
|
* @method _converse.ChatRoom#shouldShowErrorMessage
|
||||||
* @returns {Promise<boolean>}
|
* @returns {Promise<boolean>}
|
||||||
*/
|
*/
|
||||||
async shouldShowErrorMessage (stanza) {
|
async shouldShowErrorMessage (attrs) {
|
||||||
if (sizzle(`not-acceptable[xmlns="${Strophe.NS.STANZAS}"]`, stanza).length) {
|
if (attrs['error_condition'] === 'not-acceptable' && await this.rejoinIfNecessary()) {
|
||||||
if (await this.rejoinIfNecessary()) {
|
return false;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return _converse.ChatBox.prototype.shouldShowErrorMessage.call(this, stanza);
|
return _converse.ChatBox.prototype.shouldShowErrorMessage.call(this, attrs);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -463,16 +463,6 @@ u.triggerEvent = function (el, name, type="Event", bubbles=true, cancelable=true
|
|||||||
el.dispatchEvent(evt);
|
el.dispatchEvent(evt);
|
||||||
};
|
};
|
||||||
|
|
||||||
u.geoUriToHttp = function(text, geouri_replacement) {
|
|
||||||
const regex = /geo:([\-0-9.]+),([\-0-9.]+)(?:,([\-0-9.]+))?(?:\?(.*))?/g;
|
|
||||||
return text.replace(regex, geouri_replacement);
|
|
||||||
};
|
|
||||||
|
|
||||||
u.httpToGeoUri = function(text, _converse) {
|
|
||||||
const replacement = 'geo:$1,$2';
|
|
||||||
return text.replace(_converse.api.settings.get("geouri_regex"), replacement);
|
|
||||||
};
|
|
||||||
|
|
||||||
u.getSelectValues = function (select) {
|
u.getSelectValues = function (select) {
|
||||||
const result = [];
|
const result = [];
|
||||||
const options = select && select.options;
|
const options = select && select.options;
|
||||||
|
@ -3,7 +3,6 @@ import dayjs from 'dayjs';
|
|||||||
import sizzle from 'sizzle';
|
import sizzle from 'sizzle';
|
||||||
import u from '@converse/headless/utils/core';
|
import u from '@converse/headless/utils/core';
|
||||||
import log from "../log";
|
import log from "../log";
|
||||||
import { __ } from '@converse/headless/i18n';
|
|
||||||
import { api } from "@converse/headless/converse-core";
|
import { api } from "@converse/headless/converse-core";
|
||||||
|
|
||||||
const Strophe = strophe.default.Strophe;
|
const Strophe = strophe.default.Strophe;
|
||||||
@ -243,20 +242,6 @@ function getReferences (stanza) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the human readable error message contained in an message stanza of type 'error'.
|
|
||||||
* @private
|
|
||||||
* @param { XMLElement } stanza - The message stanza
|
|
||||||
*/
|
|
||||||
function getErrorMessage (stanza) {
|
|
||||||
if (stanza.getAttribute('type') === 'error') {
|
|
||||||
const error = stanza.querySelector('error');
|
|
||||||
return error.querySelector('text')?.textContent ||
|
|
||||||
__('Sorry, an error occurred:') + ' ' + error.innerHTML;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function rejectMessage (stanza, text) {
|
function rejectMessage (stanza, text) {
|
||||||
// Reject an incoming message by replying with an error message of type "cancel".
|
// Reject an incoming message by replying with an error message of type "cancel".
|
||||||
api.send(
|
api.send(
|
||||||
@ -278,20 +263,18 @@ function rejectMessage (stanza, text) {
|
|||||||
* @private
|
* @private
|
||||||
* @param { XMLElement } stanza - The message stanza
|
* @param { XMLElement } stanza - The message stanza
|
||||||
*/
|
*/
|
||||||
function getMUCErrorMessage (stanza) {
|
function getErrorAttributes (stanza) {
|
||||||
if (stanza.getAttribute('type') === 'error') {
|
if (stanza.getAttribute('type') === 'error') {
|
||||||
const forbidden = sizzle(`error forbidden[xmlns="${Strophe.NS.STANZAS}"]`, stanza).pop();
|
const error = stanza.querySelector('error');
|
||||||
const text = sizzle(`error text[xmlns="${Strophe.NS.STANZAS}"]`, stanza).pop();
|
const text = sizzle(`text[xmlns="${Strophe.NS.STANZAS}"]`, error).pop();
|
||||||
if (forbidden) {
|
return {
|
||||||
const msg = __("Your message was not delivered because you weren't allowed to send it.");
|
'is_error': true,
|
||||||
const server_msg = text ? __('The message from the server is: "%1$s"', text.textContent) : '';
|
'error_text': text?.textContent,
|
||||||
return server_msg ? `${msg} ${server_msg}` : msg;
|
'error_type': error.getAttribute('type'),
|
||||||
} else if (sizzle(`not-acceptable[xmlns="${Strophe.NS.STANZAS}"]`, stanza).length) {
|
'error_condition': error.firstElementChild.nodeName
|
||||||
return __("Your message was not delivered because you're not present in the groupchat.");
|
|
||||||
} else {
|
|
||||||
return text?.textContent;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -458,6 +441,7 @@ const st = {
|
|||||||
* @property { Boolean } is_carbon - Is this message a XEP-0280 Carbon?
|
* @property { Boolean } is_carbon - Is this message a XEP-0280 Carbon?
|
||||||
* @property { Boolean } is_delayed - Was delivery of this message was delayed as per XEP-0203?
|
* @property { Boolean } is_delayed - Was delivery of this message was delayed as per XEP-0203?
|
||||||
* @property { Boolean } is_encrypted - Is this message XEP-0384 encrypted?
|
* @property { Boolean } is_encrypted - Is this message XEP-0384 encrypted?
|
||||||
|
* @property { Boolean } is_error - Whether an error was received for this message
|
||||||
* @property { Boolean } is_headline - Is this a "headline" message?
|
* @property { Boolean } is_headline - Is this a "headline" message?
|
||||||
* @property { Boolean } is_markable - Can this message be marked with a XEP-0333 chat marker?
|
* @property { Boolean } is_markable - Can this message be marked with a XEP-0333 chat marker?
|
||||||
* @property { Boolean } is_marker - Is this message a XEP-0333 Chat Marker?
|
* @property { Boolean } is_marker - Is this message a XEP-0333 Chat Marker?
|
||||||
@ -469,8 +453,10 @@ const st = {
|
|||||||
* @property { String } body - The contents of the <body> tag of the message stanza
|
* @property { String } body - The contents of the <body> tag of the message stanza
|
||||||
* @property { String } chat_state - The XEP-0085 chat state notification contained in this message
|
* @property { String } chat_state - The XEP-0085 chat state notification contained in this message
|
||||||
* @property { String } contact_jid - The JID of the other person or entity
|
* @property { String } contact_jid - The JID of the other person or entity
|
||||||
* @property { String } edit - An ISO8601 string recording the time that the message was edited per XEP-0308
|
* @property { String } edited - An ISO8601 string recording the time that the message was edited per XEP-0308
|
||||||
* @property { String } error - The error message, in case it's an error stanza
|
* @property { String } error_condition - The defined error condition
|
||||||
|
* @property { String } error_text - The error text received from the server
|
||||||
|
* @property { String } error_type - The type of error received from the server
|
||||||
* @property { String } from - The sender JID
|
* @property { String } from - The sender JID
|
||||||
* @property { String } fullname - The full name of the sender
|
* @property { String } fullname - The full name of the sender
|
||||||
* @property { String } marker - The XEP-0333 Chat Marker value
|
* @property { String } marker - The XEP-0333 Chat Marker value
|
||||||
@ -503,7 +489,6 @@ const st = {
|
|||||||
is_server_message,
|
is_server_message,
|
||||||
'body': stanza.querySelector('body')?.textContent?.trim(),
|
'body': stanza.querySelector('body')?.textContent?.trim(),
|
||||||
'chat_state': getChatState(stanza),
|
'chat_state': getChatState(stanza),
|
||||||
'error': getErrorMessage(stanza),
|
|
||||||
'from': Strophe.getBareJidFromJid(stanza.getAttribute('from')),
|
'from': Strophe.getBareJidFromJid(stanza.getAttribute('from')),
|
||||||
'is_archived': st.isArchived(original_stanza),
|
'is_archived': st.isArchived(original_stanza),
|
||||||
'is_carbon': isCarbon(original_stanza),
|
'is_carbon': isCarbon(original_stanza),
|
||||||
@ -523,6 +508,7 @@ const st = {
|
|||||||
'to': stanza.getAttribute('to'),
|
'to': stanza.getAttribute('to'),
|
||||||
'type': stanza.getAttribute('type')
|
'type': stanza.getAttribute('type')
|
||||||
},
|
},
|
||||||
|
getErrorAttributes(stanza),
|
||||||
getOutOfBandAttributes(stanza),
|
getOutOfBandAttributes(stanza),
|
||||||
getSpoilerAttributes(stanza),
|
getSpoilerAttributes(stanza),
|
||||||
getCorrectionAttributes(stanza, original_stanza),
|
getCorrectionAttributes(stanza, original_stanza),
|
||||||
@ -589,6 +575,7 @@ const st = {
|
|||||||
* @property { Boolean } is_carbon - Is this message a XEP-0280 Carbon?
|
* @property { Boolean } is_carbon - Is this message a XEP-0280 Carbon?
|
||||||
* @property { Boolean } is_delayed - Was delivery of this message was delayed as per XEP-0203?
|
* @property { Boolean } is_delayed - Was delivery of this message was delayed as per XEP-0203?
|
||||||
* @property { Boolean } is_encrypted - Is this message XEP-0384 encrypted?
|
* @property { Boolean } is_encrypted - Is this message XEP-0384 encrypted?
|
||||||
|
* @property { Boolean } is_error - Whether an error was received for this message
|
||||||
* @property { Boolean } is_headline - Is this a "headline" message?
|
* @property { Boolean } is_headline - Is this a "headline" message?
|
||||||
* @property { Boolean } is_markable - Can this message be marked with a XEP-0333 chat marker?
|
* @property { Boolean } is_markable - Can this message be marked with a XEP-0333 chat marker?
|
||||||
* @property { Boolean } is_marker - Is this message a XEP-0333 Chat Marker?
|
* @property { Boolean } is_marker - Is this message a XEP-0333 Chat Marker?
|
||||||
@ -599,8 +586,10 @@ const st = {
|
|||||||
* @property { Object } encrypted - XEP-0384 encryption payload attributes
|
* @property { Object } encrypted - XEP-0384 encryption payload attributes
|
||||||
* @property { String } body - The contents of the <body> tag of the message stanza
|
* @property { String } body - The contents of the <body> tag of the message stanza
|
||||||
* @property { String } chat_state - The XEP-0085 chat state notification contained in this message
|
* @property { String } chat_state - The XEP-0085 chat state notification contained in this message
|
||||||
* @property { String } edit - An ISO8601 string recording the time that the message was edited per XEP-0308
|
* @property { String } edited - An ISO8601 string recording the time that the message was edited per XEP-0308
|
||||||
* @property { String } error - The error message, in case it's an error stanza
|
* @property { String } error_condition - The defined error condition
|
||||||
|
* @property { String } error_text - The error text received from the server
|
||||||
|
* @property { String } error_type - The type of error received from the server
|
||||||
* @property { String } from - The sender JID
|
* @property { String } from - The sender JID
|
||||||
* @property { String } from_muc - The JID of the MUC from which this message was sent
|
* @property { String } from_muc - The JID of the MUC from which this message was sent
|
||||||
* @property { String } fullname - The full name of the sender
|
* @property { String } fullname - The full name of the sender
|
||||||
@ -632,7 +621,6 @@ const st = {
|
|||||||
from,
|
from,
|
||||||
'body': stanza.querySelector('body')?.textContent?.trim(),
|
'body': stanza.querySelector('body')?.textContent?.trim(),
|
||||||
'chat_state': getChatState(stanza),
|
'chat_state': getChatState(stanza),
|
||||||
'error': getMUCErrorMessage(stanza),
|
|
||||||
'from_muc': Strophe.getBareJidFromJid(from),
|
'from_muc': Strophe.getBareJidFromJid(from),
|
||||||
'is_archived': st.isArchived(original_stanza),
|
'is_archived': st.isArchived(original_stanza),
|
||||||
'is_carbon': isCarbon(original_stanza),
|
'is_carbon': isCarbon(original_stanza),
|
||||||
@ -652,6 +640,7 @@ const st = {
|
|||||||
'to': stanza.getAttribute('to'),
|
'to': stanza.getAttribute('to'),
|
||||||
'type': stanza.getAttribute('type'),
|
'type': stanza.getAttribute('type'),
|
||||||
},
|
},
|
||||||
|
getErrorAttributes(stanza),
|
||||||
getOutOfBandAttributes(stanza),
|
getOutOfBandAttributes(stanza),
|
||||||
getSpoilerAttributes(stanza),
|
getSpoilerAttributes(stanza),
|
||||||
getCorrectionAttributes(stanza, original_stanza),
|
getCorrectionAttributes(stanza, original_stanza),
|
||||||
|
11
src/modals/message-versions.js
Normal file
11
src/modals/message-versions.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { BootstrapModal } from "../converse-modal.js";
|
||||||
|
import tpl_message_versions_modal from "../templates/message_versions_modal.js";
|
||||||
|
|
||||||
|
|
||||||
|
export default BootstrapModal.extend({
|
||||||
|
// FIXME: this isn't globally unique
|
||||||
|
id: "message-versions-modal",
|
||||||
|
toHTML () {
|
||||||
|
return tpl_message_versions_modal(this.model.toJSON());
|
||||||
|
}
|
||||||
|
});
|
@ -1,5 +1,6 @@
|
|||||||
import { html } from "lit-html";
|
import { html } from "lit-html";
|
||||||
|
|
||||||
export default (o) => html`
|
export default (o) => html`
|
||||||
<img alt="${o.alt_text}" class="avatar align-self-center ${o.extra_classes}"
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="${o.classes}" width="${o.width}" height="${o.height}">
|
||||||
height="${o.height}" width="${o.width}" src="data:${o.image_type};base64,${o.image}"/>`;
|
<image width="${o.width}" height="${o.height}" preserveAspectRatio="xMidYMid meet" xlink:href="${o.image}"/>
|
||||||
|
</svg>`;
|
||||||
|
@ -4,9 +4,9 @@ export default (o) => html`
|
|||||||
<div class="flyout box-flyout">
|
<div class="flyout box-flyout">
|
||||||
<div class="chat-head chat-head-chatbox row no-gutters"></div>
|
<div class="chat-head chat-head-chatbox row no-gutters"></div>
|
||||||
<div class="chat-body">
|
<div class="chat-body">
|
||||||
<div class="chat-content ${ o.show_send_button ? 'chat-content-sendbutton' : '' }" @scroll=${o.markScrolled} aria-live="polite">
|
<div class="chat-content ${ o.show_send_button ? 'chat-content-sendbutton' : '' }" aria-live="polite">
|
||||||
<div class="chat-content__messages"></div>
|
<div class="chat-content__messages smooth-scroll" @scroll=${o.markScrolled}></div>
|
||||||
<div class="chat-content__notifications"></div>
|
<div class="chat-content__help"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bottom-panel">
|
<div class="bottom-panel">
|
||||||
<div class="emoji-picker__container dropup"></div>
|
<div class="emoji-picker__container dropup"></div>
|
||||||
|
@ -1,8 +1,4 @@
|
|||||||
import { html } from "lit-html";
|
import { html } from "lit-html";
|
||||||
import { __ } from '@converse/headless/i18n';
|
|
||||||
|
|
||||||
const i18n_no_history = __('No message history available.');
|
|
||||||
|
|
||||||
|
|
||||||
export default (o) => html`
|
export default (o) => html`
|
||||||
<div class="flyout box-flyout">
|
<div class="flyout box-flyout">
|
||||||
@ -10,10 +6,8 @@ export default (o) => html`
|
|||||||
<div class="chat-body chatroom-body row no-gutters">
|
<div class="chat-body chatroom-body row no-gutters">
|
||||||
<div class="chat-area col">
|
<div class="chat-area col">
|
||||||
<div class="chat-content ${ o.show_send_button ? 'chat-content-sendbutton' : '' }" aria-live="polite">
|
<div class="chat-content ${ o.show_send_button ? 'chat-content-sendbutton' : '' }" aria-live="polite">
|
||||||
<div class="chat-content__messages">
|
<div class="chat-content__messages smooth-scroll" @scroll=${o.markScrolled}></div>
|
||||||
${ o.muc_show_logs_before_join ? html`<div class="empty-history-feedback"><span>${ i18n_no_history }</span></div>` : '' }
|
<div class="chat-content__help"></div>
|
||||||
</div>
|
|
||||||
<div class="chat-content__notifications"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="bottom-panel"></div>
|
<div class="bottom-panel"></div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
import '../components/dropdown.js';
|
import '../components/dropdown.js';
|
||||||
import { __ } from '@converse/headless/i18n';
|
import { __ } from '@converse/headless/i18n';
|
||||||
import { html } from "lit-html";
|
import { html } from "lit-html";
|
||||||
import { unsafeHTML } from 'lit-html/directives/unsafe-html.js';
|
|
||||||
import { until } from 'lit-html/directives/until.js';
|
import { until } from 'lit-html/directives/until.js';
|
||||||
import { converse } from "@converse/headless/converse-core";
|
import { converse } from "@converse/headless/converse-core";
|
||||||
import xss from "xss/dist/xss";
|
|
||||||
|
|
||||||
const u = converse.env.utils;
|
const u = converse.env.utils;
|
||||||
const i18n_hide_topic = __('Hide the groupchat topic');
|
const i18n_hide_topic = __('Hide the groupchat topic');
|
||||||
@ -15,7 +13,7 @@ const tpl_standalone_btns = (o) => o.standalone_btns.reverse().map(b => until(b,
|
|||||||
|
|
||||||
|
|
||||||
export default (o) => {
|
export default (o) => {
|
||||||
const subject = o.subject ? u.addHyperlinks(xss.filterXSS(o.subject.text, {'whiteList': {}})) : '';
|
const subject = o.subject ? u.addHyperlinks(o.subject.text) : '';
|
||||||
const show_subject = (subject && !o.subject_hidden);
|
const show_subject = (subject && !o.subject_hidden);
|
||||||
return html`
|
return html`
|
||||||
<div class="chatbox-title ${ show_subject ? '' : "chatbox-title--no-desc"}">
|
<div class="chatbox-title ${ show_subject ? '' : "chatbox-title--no-desc"}">
|
||||||
@ -28,6 +26,6 @@ export default (o) => {
|
|||||||
${ o.dropdown_btns.length ? html`<converse-dropdown .items=${o.dropdown_btns}></converse-dropdown>` : '' }
|
${ o.dropdown_btns.length ? html`<converse-dropdown .items=${o.dropdown_btns}></converse-dropdown>` : '' }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
${ show_subject ? html`<p class="chat-head__desc" title="${i18n_hide_topic}">${unsafeHTML(subject)}</p>` : '' }
|
${ show_subject ? html`<p class="chat-head__desc" title="${i18n_hide_topic}">${subject}</p>` : '' }
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
31
src/templates/directives/avatar.js
Normal file
31
src/templates/directives/avatar.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import tpl_avatar from "templates/avatar.svg";
|
||||||
|
import xss from "xss/dist/xss";
|
||||||
|
import { directive, html } from "lit-html";
|
||||||
|
import { unsafeHTML } from 'lit-html/directives/unsafe-html.js';
|
||||||
|
|
||||||
|
|
||||||
|
export const renderAvatar = directive(o => part => {
|
||||||
|
if (o.type === 'headline' || o.is_me_message) {
|
||||||
|
part.setValue('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (o.model.vcard) {
|
||||||
|
const data = {
|
||||||
|
'classes': 'avatar chat-msg__avatar',
|
||||||
|
'width': 36,
|
||||||
|
'height': 36,
|
||||||
|
}
|
||||||
|
const image_type = o.model.vcard.get('image_type');
|
||||||
|
const image = o.model.vcard.get('image');
|
||||||
|
data['image'] = "data:" + image_type + ";base64," + image;
|
||||||
|
const avatar = tpl_avatar(data);
|
||||||
|
const opts = {
|
||||||
|
'whiteList': {
|
||||||
|
'svg': ['xmlns', 'xmlns:xlink', 'class', 'width', 'height'],
|
||||||
|
'image': ['width', 'height', 'preserveAspectRatio', 'xlink:href']
|
||||||
|
}
|
||||||
|
};
|
||||||
|
part.setValue(html`${unsafeHTML(xss.filterXSS(avatar, opts))}`);
|
||||||
|
}
|
||||||
|
});
|
111
src/templates/directives/body.js
Normal file
111
src/templates/directives/body.js
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import { _converse, api, converse } from "@converse/headless/converse-core";
|
||||||
|
import { directive, html } from "lit-html";
|
||||||
|
import { isString } from "lodash";
|
||||||
|
|
||||||
|
const u = converse.env.utils;
|
||||||
|
|
||||||
|
|
||||||
|
class MessageBodyRenderer extends String {
|
||||||
|
|
||||||
|
constructor (component) {
|
||||||
|
super();
|
||||||
|
this.text = component.model.getMessageText();
|
||||||
|
this.model = component.model;
|
||||||
|
this.component = component;
|
||||||
|
}
|
||||||
|
|
||||||
|
async transform () {
|
||||||
|
/**
|
||||||
|
* Synchronous event which provides a hook for transforming a chat message's body text
|
||||||
|
* before the default transformations have been applied.
|
||||||
|
* @event _converse#beforeMessageBodyTransformed
|
||||||
|
* @param { _converse.Message } model - The model representing the message
|
||||||
|
* @param { string } text - The message text
|
||||||
|
* @example _converse.api.listen.on('beforeMessageBodyTransformed', (view, text) => { ... });
|
||||||
|
*/
|
||||||
|
await api.trigger('beforeMessageBodyTransformed', this.model, this.text, {'Synchronous': true});
|
||||||
|
|
||||||
|
let text = this.component.is_me_message ? this.text.substring(4) : this.text;
|
||||||
|
// Collapse multiple line breaks into at most two
|
||||||
|
text = text.replace(/\n\n+/g, '\n\n');
|
||||||
|
text = u.geoUriToHttp(text, _converse.geouri_replacement);
|
||||||
|
|
||||||
|
const process = (text) => {
|
||||||
|
text = u.addEmoji(text);
|
||||||
|
return addMentionsMarkup(text, this.model.get('references'), this.model.collection.chatbox);
|
||||||
|
}
|
||||||
|
const list = await Promise.all(u.addHyperlinks(text));
|
||||||
|
this.list = list.reduce((acc, i) => isString(i) ? [...acc, ...process(i)] : [...acc, i], []);
|
||||||
|
/**
|
||||||
|
* Synchronous event which provides a hook for transforming a chat message's body text
|
||||||
|
* after the default transformations have been applied.
|
||||||
|
* @event _converse#afterMessageBodyTransformed
|
||||||
|
* @param { _converse.Message } model - The model representing the message
|
||||||
|
* @param { string } text - The message text
|
||||||
|
* @example _converse.api.listen.on('afterMessageBodyTransformed', (view, text) => { ... });
|
||||||
|
*/
|
||||||
|
await api.trigger('afterMessageBodyTransformed', this.model, text, {'Synchronous': true});
|
||||||
|
|
||||||
|
return this.list;
|
||||||
|
}
|
||||||
|
|
||||||
|
async render () {
|
||||||
|
return html`${await this.transform()}`
|
||||||
|
}
|
||||||
|
|
||||||
|
get length () {
|
||||||
|
return this.text.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
toString () {
|
||||||
|
return "" + this.text;
|
||||||
|
}
|
||||||
|
|
||||||
|
textOf () {
|
||||||
|
return this.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tpl_mention_with_nick = (o) => html`<span class="mention mention--self badge badge-info">${o.mention}</span>`;
|
||||||
|
const tpl_mention = (o) => html`<span class="mention">${o.mention}</span>`;
|
||||||
|
|
||||||
|
|
||||||
|
function addMentionsMarkup (text, references, chatbox) {
|
||||||
|
if (chatbox.get('message_type') === 'groupchat' && references.length) {
|
||||||
|
let list = [text];
|
||||||
|
const nick = chatbox.get('nick');
|
||||||
|
references
|
||||||
|
.sort((a, b) => b.begin - a.begin)
|
||||||
|
.forEach(ref => {
|
||||||
|
const text = list.shift();
|
||||||
|
const mention = text.slice(ref.begin, ref.end);
|
||||||
|
if (mention === nick) {
|
||||||
|
list = [
|
||||||
|
text.slice(0, ref.begin),
|
||||||
|
tpl_mention_with_nick({mention}),
|
||||||
|
text.slice(ref.end),
|
||||||
|
...list
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
list = [
|
||||||
|
text.slice(0, ref.begin),
|
||||||
|
tpl_mention({mention}),
|
||||||
|
text.slice(ref.end),
|
||||||
|
...list
|
||||||
|
];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return list;
|
||||||
|
} else {
|
||||||
|
return [text];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const renderBodyText = directive(component => async part => {
|
||||||
|
const model = component.model;
|
||||||
|
const renderer = new MessageBodyRenderer(component);
|
||||||
|
part.setValue(await renderer.render());
|
||||||
|
part.commit();
|
||||||
|
model.collection?.trigger('rendered', model);
|
||||||
|
});
|
23
src/templates/directives/retraction.js
Normal file
23
src/templates/directives/retraction.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { directive, html } from "lit-html";
|
||||||
|
import { __ } from '@converse/headless/i18n';
|
||||||
|
|
||||||
|
|
||||||
|
const i18n_retract_message = __('Retract this message');
|
||||||
|
const tpl_retract = (o) => html`
|
||||||
|
<button class="chat-msg__action chat-msg__action-retract" title="${i18n_retract_message}" @click=${o.onMessageRetractButtonClicked}>
|
||||||
|
<fa-icon class="fas fa-trash-alt" path-prefix="/dist" color="var(--text-color-lighten-15-percent)" size="1em"></fa-icon>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
|
||||||
|
export const renderRetractionLink = directive(o => async part => {
|
||||||
|
const may_be_moderated = o.model.get('type') === 'groupchat' && await o.model.mayBeModerated();
|
||||||
|
const retractable = !o.is_retracted && (o.model.mayBeRetracted() || may_be_moderated);
|
||||||
|
|
||||||
|
if (retractable) {
|
||||||
|
part.setValue(tpl_retract(o));
|
||||||
|
} else {
|
||||||
|
part.setValue('');
|
||||||
|
}
|
||||||
|
part.commit();
|
||||||
|
});
|
@ -1,7 +0,0 @@
|
|||||||
<div class="message chat-msg" data-isodate="{{{o.time}}}" data-msgid="{{{o.msgid}}}">
|
|
||||||
<canvas class="avatar chat-msg__avatar" height="36" width="36"></canvas>
|
|
||||||
<div class="chat-msg__content">
|
|
||||||
<span class="chat-msg__text">{{{o.__('Uploading file:')}}} <strong>{{{o.filename}}}</strong>, {{{o.filesize}}}</span>
|
|
||||||
<progress value="{{{o.progress}}}"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
16
src/templates/file_progress.js
Normal file
16
src/templates/file_progress.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { __ } from '@converse/headless/i18n';
|
||||||
|
import { html } from "lit-html";
|
||||||
|
import { renderAvatar } from './../templates/directives/avatar';
|
||||||
|
|
||||||
|
const i18n_uploading = __('Uploading file:')
|
||||||
|
|
||||||
|
|
||||||
|
export default (o) => html`
|
||||||
|
<div class="message chat-msg" data-isodate="${o.time}" data-msgid="${o.msgid}">
|
||||||
|
${ renderAvatar(this) }
|
||||||
|
<div class="chat-msg__content">
|
||||||
|
<span class="chat-msg__text">${i18n_uploading} <strong>${o.filename}</strong>, ${o.filesize}</span>
|
||||||
|
<progress value="${o.progress}"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
@ -1 +0,0 @@
|
|||||||
<div class="message chat-info {[ if (o.type !== 'info') { ]} chat-{{{o.type}}} {[ } ]}" data-isodate="{{{o.isodate}}}">{{o.message}}</div>
|
|
@ -1,13 +0,0 @@
|
|||||||
<div class="message chat-info {{{o.extra_classes}}}" data-isodate="{{{o.isodate}}}" {[ if (o.data_name) { ]} data-{{{o.data_name}}}="{{{o.data_value}}}"{[ } ]}>
|
|
||||||
{[ if (o.render_message) {
|
|
||||||
// XXX: Should only ever be rendered if the message text has been sanitized already
|
|
||||||
]}
|
|
||||||
{{o.message}}
|
|
||||||
{[ } else { ]}
|
|
||||||
<div class="chat-info__message">{{{o.message}}}</div>
|
|
||||||
{[ if (o.reason) { ]}<q class="reason">{{{o.reason}}}</q>{[ } ]}
|
|
||||||
{[ } ]}
|
|
||||||
{[ if (o.retry) { ]}
|
|
||||||
<a class="retry">Retry</a>
|
|
||||||
{[ } ]}
|
|
||||||
</div>
|
|
@ -1,52 +0,0 @@
|
|||||||
<div class="message chat-msg {{{o.type}}} {{{o.extra_classes}}} {[ if (o.is_me_message) { ]} chat-msg--action {[ } ]}"
|
|
||||||
data-isodate="{{{o.time}}}" data-msgid="{{{o.msgid}}}" data-from="{{{o.from}}}" data-encrypted="{{{o.is_encrypted}}}">
|
|
||||||
{[ if (o.type !== 'headline' && !o.is_me_message) { ]}
|
|
||||||
<canvas class="avatar chat-msg__avatar" height="36" width="36"></canvas>
|
|
||||||
{[ } ]}
|
|
||||||
<div class="chat-msg__content chat-msg__content--{{{o.sender}}} {{{o.is_me_message ? 'chat-msg__content--action' : ''}}}">
|
|
||||||
{[ if (o.first_unread) { ]}
|
|
||||||
<div class="message date-separator"><hr class="separator"><span class="separator-text">{{{o.__('unread messages')}}}</span></div>
|
|
||||||
{[ } ]}
|
|
||||||
<span class="chat-msg__heading">{[ if (o.is_me_message) { ]}
|
|
||||||
<time timestamp="{{{o.isodate}}}" class="chat-msg__time">{{{o.pretty_time}}}</time>
|
|
||||||
{[ } ]}<span class="chat-msg__author">{[ if (o.is_me_message) { ]}**{[ }; ]}{{{o.username}}}</span>
|
|
||||||
{[ if (!o.is_me_message) { ]}{[o.hats.forEach(function (hat) { ]}<span class="badge badge-secondary">{{{hat.title}}}</span>
|
|
||||||
{[ }); ]}<time timestamp="{{{o.isodate}}}" class="chat-msg__time">{{{o.pretty_time}}}</time>{[ } ]}{[ if (o.is_encrypted) { ]}
|
|
||||||
<span class="fa fa-lock"></span>
|
|
||||||
{[ } ]}</span>
|
|
||||||
<div class="chat-msg__body chat-msg__body--{{{o.type}}} {{{o.received ? 'chat-msg__body--received' : '' }}} {{{o.is_delayed ? 'chat-msg__body--delayed' : '' }}}">
|
|
||||||
<div class="chat-msg__message">
|
|
||||||
{[ if (o.is_retracted) { ]}
|
|
||||||
<div>{{{o.retraction_text}}}</div>
|
|
||||||
{[ if (o.moderation_reason) { ]}<q class="chat-msg--retracted__reason">{{{o.moderation_reason}}}</q>{[ } ]}
|
|
||||||
{[ } else { ]}
|
|
||||||
{[ if (o.is_spoiler) { ]}
|
|
||||||
<div class="chat-msg__spoiler-hint">
|
|
||||||
<span class="spoiler-hint">{{{o.spoiler_hint}}}</span>
|
|
||||||
<a class="badge badge-info spoiler-toggle" data-toggle-state="closed" href="#"><i class="fa fa-eye"></i>{{{o.label_show}}}</a>
|
|
||||||
</div>
|
|
||||||
{[ } ]}
|
|
||||||
|
|
||||||
{[ if (o.subject) { ]}
|
|
||||||
<div class="chat-msg__subject">{{{ o.subject }}}</div>
|
|
||||||
{[ } ]}
|
|
||||||
<div class="chat-msg__text
|
|
||||||
{[ if (o.is_only_emojis) { ]} chat-msg__text--larger{[ } ]}
|
|
||||||
{[ if (o.is_spoiler) { ]} spoiler collapsed{[ } ]}"><!-- message gets added here via renderMessage --></div>
|
|
||||||
<div class="chat-msg__media"></div>
|
|
||||||
<div class="chat-msg__error">{{{o.error}}}</div>
|
|
||||||
{[ } ]}
|
|
||||||
</div>
|
|
||||||
{[ if (o.received && !o.is_me_message && !o.is_groupchat_message) { ]} <span class="fa fa-check chat-msg__receipt"></span> {[ } ]}
|
|
||||||
{[ if (o.edited) { ]} <i title="{{{o.__('This message has been edited')}}}" class="fa fa-edit chat-msg__edit-modal"></i> {[ } ]}
|
|
||||||
<div class="chat-msg__actions">
|
|
||||||
{[ if (o.editable) { ]}
|
|
||||||
<button class="chat-msg__action chat-msg__action-edit fa fa-pencil-alt" title="{{{o.__('Edit this message')}}}"></button>
|
|
||||||
{[ } ]}
|
|
||||||
{[ if (o.retractable) { ]}
|
|
||||||
<button class="chat-msg__action chat-msg__action-retract fa fa-trash-alt" title="{{{o.__('Retract this message')}}}"></button>
|
|
||||||
{[ } ]}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
9
src/templates/new_day.js
Normal file
9
src/templates/new_day.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { html } from "lit-html";
|
||||||
|
|
||||||
|
|
||||||
|
export default (o) => html`
|
||||||
|
<div class="message date-separator" data-isodate="${o.time}">
|
||||||
|
<hr class="separator"/>
|
||||||
|
<time class="separator-text" datetime="${o.time}"><span>${o.datestring}</span></time>
|
||||||
|
</div>
|
||||||
|
`;
|
@ -4,7 +4,6 @@
|
|||||||
* @description This is the DOM/HTML utilities module.
|
* @description This is the DOM/HTML utilities module.
|
||||||
*/
|
*/
|
||||||
import URI from "urijs";
|
import URI from "urijs";
|
||||||
import { isFunction } from "lodash";
|
|
||||||
import log from '@converse/headless/log';
|
import log from '@converse/headless/log';
|
||||||
import sizzle from "sizzle";
|
import sizzle from "sizzle";
|
||||||
import tpl_audio from "../templates/audio.js";
|
import tpl_audio from "../templates/audio.js";
|
||||||
@ -20,8 +19,10 @@ import tpl_image from "../templates/image.js";
|
|||||||
import tpl_select_option from "../templates/select_option.html";
|
import tpl_select_option from "../templates/select_option.html";
|
||||||
import tpl_video from "../templates/video.js";
|
import tpl_video from "../templates/video.js";
|
||||||
import u from "../headless/utils/core";
|
import u from "../headless/utils/core";
|
||||||
|
import { api } from "@converse/headless/converse-core";
|
||||||
|
import { html } from "lit-html";
|
||||||
|
import { isFunction } from "lodash";
|
||||||
|
|
||||||
const URL_REGEX = /\b(https?\:\/\/|www\.|https?:\/\/www\.)[^\s<>]{2,200}\b\/?/g;
|
|
||||||
const APPROVED_URL_PROTOCOLS = ['http', 'https', 'xmpp', 'mailto'];
|
const APPROVED_URL_PROTOCOLS = ['http', 'https', 'xmpp', 'mailto'];
|
||||||
|
|
||||||
function getAutoCompleteProperty (name, options) {
|
function getAutoCompleteProperty (name, options) {
|
||||||
@ -96,7 +97,7 @@ function renderAudioURL (_converse, uri) {
|
|||||||
|
|
||||||
function renderImageURL (_converse, uri) {
|
function renderImageURL (_converse, uri) {
|
||||||
if (!_converse.api.settings.get('show_images_inline')) {
|
if (!_converse.api.settings.get('show_images_inline')) {
|
||||||
return u.convertUriToHyperlink(uri);
|
return u.convertURIoHyperlink(uri);
|
||||||
}
|
}
|
||||||
const { __ } = _converse;
|
const { __ } = _converse;
|
||||||
return tpl_image({
|
return tpl_image({
|
||||||
@ -179,60 +180,6 @@ function loadImage (url) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function renderImage (img_url, link_url, el, callback) {
|
|
||||||
if (u.isImageURL(img_url)) {
|
|
||||||
let img;
|
|
||||||
try {
|
|
||||||
img = await loadImage(img_url);
|
|
||||||
} catch (e) {
|
|
||||||
log.error(e);
|
|
||||||
return callback();
|
|
||||||
}
|
|
||||||
sizzle(`a[href="${link_url}"]`, el).forEach(a => {
|
|
||||||
a.innerHTML = "";
|
|
||||||
u.addClass('chat-image__link', a);
|
|
||||||
u.addClass('chat-image', img);
|
|
||||||
u.addClass('img-thumbnail', img);
|
|
||||||
a.insertAdjacentElement('afterBegin', img);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a Promise which resolves once all images have been loaded.
|
|
||||||
* @method u#renderImageURLs
|
|
||||||
* @param { _converse }
|
|
||||||
* @param { HTMLElement }
|
|
||||||
* @returns { Promise }
|
|
||||||
*/
|
|
||||||
u.renderImageURLs = function (_converse, el) {
|
|
||||||
if (!_converse.api.settings.get('show_images_inline')) {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
const list = el.textContent.match(URL_REGEX) || [];
|
|
||||||
return Promise.all(
|
|
||||||
list.map(url =>
|
|
||||||
new Promise(resolve => {
|
|
||||||
let image_url = getURI(url);
|
|
||||||
if (['imgur.com', 'pbs.twimg.com'].includes(image_url.hostname()) && !u.isImageURL(url)) {
|
|
||||||
const format = (image_url.hostname() === 'pbs.twimg.com') ? image_url.search(true).format : 'png';
|
|
||||||
image_url = image_url.removeSearch(/.*/).toString() + `.${format}`;
|
|
||||||
renderImage(image_url, url, el, resolve);
|
|
||||||
} else {
|
|
||||||
renderImage(url, url, el, resolve);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
u.renderNewLines = function (text) {
|
|
||||||
return text.replace(/\n\n+/g, '<br/><br/>').replace(/\n/g, '<br/>');
|
|
||||||
};
|
|
||||||
|
|
||||||
u.calculateElementHeight = function (el) {
|
u.calculateElementHeight = function (el) {
|
||||||
/* Return the height of the passed in DOM element,
|
/* Return the height of the passed in DOM element,
|
||||||
* based on the heights of its children.
|
* based on the heights of its children.
|
||||||
@ -364,42 +311,43 @@ u.escapeHTML = function (string) {
|
|||||||
.replace(/"/g, """);
|
.replace(/"/g, """);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
u.convertToImageTag = async function (url) {
|
||||||
u.addMentionsMarkup = function (text, references, chatbox) {
|
const uri = getURI(url);
|
||||||
if (chatbox.get('message_type') !== 'groupchat') {
|
const img_url_without_ext = ['imgur.com', 'pbs.twimg.com'].includes(uri.hostname());
|
||||||
return text;
|
let src;
|
||||||
|
if (u.isImageURL(url) || img_url_without_ext) {
|
||||||
|
if (img_url_without_ext) {
|
||||||
|
const format = (uri.hostname() === 'pbs.twimg.com') ? uri.search(true).format : 'png';
|
||||||
|
src = uri.removeSearch(/.*/).toString() + `.${format}`;
|
||||||
|
} else {
|
||||||
|
src = url;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await loadImage(src);
|
||||||
|
} catch (e) {
|
||||||
|
log.error(e);
|
||||||
|
return u.convertUrlToHyperlink(url);
|
||||||
|
}
|
||||||
|
return tpl_image({url, src});
|
||||||
}
|
}
|
||||||
const nick = chatbox.get('nick');
|
}
|
||||||
references
|
|
||||||
.sort((a, b) => b.begin - a.begin)
|
|
||||||
.forEach(ref => {
|
|
||||||
const prefix = text.slice(0, ref.begin);
|
|
||||||
const offset = ((prefix.match(/</g) || []).length + (prefix.match(/>/g) || []).length) * 3;
|
|
||||||
const begin = parseInt(ref.begin, 10) + parseInt(offset, 10);
|
|
||||||
const end = parseInt(ref.end, 10) + parseInt(offset, 10);
|
|
||||||
const mention = text.slice(begin, end)
|
|
||||||
chatbox;
|
|
||||||
|
|
||||||
if (mention === nick) {
|
|
||||||
text = text.slice(0, begin) + `<span class="mention mention--self badge badge-info">${mention}</span>` + text.slice(end);
|
|
||||||
} else {
|
|
||||||
text = text.slice(0, begin) + `<span class="mention">${mention}</span>` + text.slice(end);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return text;
|
|
||||||
};
|
|
||||||
|
|
||||||
u.convertUriToHyperlink = function (uri, urlAsTyped) {
|
u.convertURIoHyperlink = function (uri, urlAsTyped) {
|
||||||
let normalizedUrl = uri.normalize()._string;
|
let normalized_url = uri.normalize()._string;
|
||||||
const pretty_url = uri._parts.urn ? normalizedUrl : uri.readable();
|
const pretty_url = uri._parts.urn ? normalized_url : uri.readable();
|
||||||
const visibleUrl = u.escapeHTML(urlAsTyped || pretty_url);
|
const visible_url = urlAsTyped || pretty_url;
|
||||||
if (!uri._parts.protocol && !normalizedUrl.startsWith('http://') && !normalizedUrl.startsWith('https://')) {
|
if (!uri._parts.protocol && !normalized_url.startsWith('http://') && !normalized_url.startsWith('https://')) {
|
||||||
normalizedUrl = 'http://' + normalizedUrl;
|
normalized_url = 'http://' + normalized_url;
|
||||||
}
|
}
|
||||||
if (uri._parts.protocol === 'xmpp' && uri._parts.query === 'join') {
|
if (uri._parts.protocol === 'xmpp' && uri._parts.query === 'join') {
|
||||||
return `<a target="_blank" rel="noopener" class="open-chatroom" href="${normalizedUrl}">${visibleUrl}</a>`;
|
return html`
|
||||||
|
<a target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
@click=${ev => api.rooms.open(ev.target.href)}
|
||||||
|
href="${normalized_url}">${visible_url}</a>`;
|
||||||
}
|
}
|
||||||
return `<a target="_blank" rel="noopener" href="${normalizedUrl}">${visibleUrl}</a>`;
|
return html`<a target="_blank" rel="noopener" href="${normalized_url}">${visible_url}</a>`;
|
||||||
};
|
};
|
||||||
|
|
||||||
function isProtocolApproved (protocol, safeProtocolsList = APPROVED_URL_PROTOCOLS) {
|
function isProtocolApproved (protocol, safeProtocolsList = APPROVED_URL_PROTOCOLS) {
|
||||||
@ -417,27 +365,59 @@ function isUrlValid (urlString) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
u.convertUrlToHyperlink = function (url) {
|
u.convertUrlToHyperlink = function (url) {
|
||||||
const urlWithProtocol = RegExp('^w{3}.', 'ig').test(url) ? `http://${url}` : url;
|
const http_url = RegExp('^w{3}.', 'ig').test(url) ? `http://${url}` : url;
|
||||||
const uri = getURI(url);
|
const uri = getURI(url);
|
||||||
if (uri !== null && isUrlValid(urlWithProtocol) && (isProtocolApproved(uri._parts.protocol) || !uri._parts.protocol)) {
|
if (uri !== null && isUrlValid(http_url) && (isProtocolApproved(uri._parts.protocol) || !uri._parts.protocol)) {
|
||||||
const hyperlink = this.convertUriToHyperlink(uri, url);
|
return this.convertURIoHyperlink(uri, url);
|
||||||
return hyperlink;
|
|
||||||
}
|
}
|
||||||
return url;
|
return url;
|
||||||
};
|
};
|
||||||
|
|
||||||
u.addHyperlinks = function (text) {
|
u.addHyperlinks = function (text) {
|
||||||
|
const objs = [];
|
||||||
|
const parse_options = { 'start': /\b(?:([a-z][a-z0-9.+-]*:\/\/)|xmpp:|mailto:|www\.)/gi };
|
||||||
try {
|
try {
|
||||||
const parse_options = {
|
URI.withinString(text, (url, start, end) => {
|
||||||
'start': /\b(?:([a-z][a-z0-9.+-]*:\/\/)|xmpp:|mailto:|www\.)/gi
|
objs.push({url, start, end})
|
||||||
};
|
return url;
|
||||||
return URI.withinString(text, url => u.convertUrlToHyperlink(url), parse_options);
|
} , parse_options);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.debug(error);
|
log.debug(error);
|
||||||
return text;
|
return [text];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const show_images = api.settings.get('show_images_inline');
|
||||||
|
|
||||||
|
let list = [text];
|
||||||
|
if (objs.length) {
|
||||||
|
objs.sort((a, b) => b.start - a.start)
|
||||||
|
.forEach(url_obj => {
|
||||||
|
const text = list.shift();
|
||||||
|
const url_text = text.slice(url_obj.start, url_obj.end);
|
||||||
|
list = [
|
||||||
|
text.slice(0, url_obj.start),
|
||||||
|
show_images && u.isImageURL(url_text) ?
|
||||||
|
u.convertToImageTag(url_text) :
|
||||||
|
u.convertUrlToHyperlink(url_text),
|
||||||
|
text.slice(url_obj.end),
|
||||||
|
...list
|
||||||
|
];
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
list = [text];
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
u.geoUriToHttp = function(text, geouri_replacement) {
|
||||||
|
const regex = /geo:([\-0-9.]+),([\-0-9.]+)(?:,([\-0-9.]+))?(?:\?(.*))?/g;
|
||||||
|
return text.replace(regex, geouri_replacement);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
u.httpToGeoUri = function(text, _converse) {
|
||||||
|
const replacement = 'geo:$1,$2';
|
||||||
|
return text.replace(_converse.api.settings.get("geouri_regex"), replacement);
|
||||||
|
};
|
||||||
|
|
||||||
u.slideInAllElements = function (elements, duration=300) {
|
u.slideInAllElements = function (elements, duration=300) {
|
||||||
return Promise.all(Array.from(elements).map(e => u.slideIn(e, duration)));
|
return Promise.all(Array.from(elements).map(e => u.slideIn(e, duration)));
|
||||||
|
@ -20,6 +20,7 @@ module.exports = merge(common, {
|
|||||||
new MiniCssExtractPlugin({filename: '../dist/converse.min.css'}),
|
new MiniCssExtractPlugin({filename: '../dist/converse.min.css'}),
|
||||||
new CopyWebpackPlugin([
|
new CopyWebpackPlugin([
|
||||||
{from: 'sounds'},
|
{from: 'sounds'},
|
||||||
|
{from: 'node_modules/@fortawesome/fontawesome-free/sprites/solid.svg', to: '@fortawesome/fontawesome-free/sprites/solid.svg'},
|
||||||
{from: 'images/favicon.ico', to: 'images/favicon.ico'},
|
{from: 'images/favicon.ico', to: 'images/favicon.ico'},
|
||||||
{from: 'images/custom_emojis', to: 'images/custom_emojis'},
|
{from: 'images/custom_emojis', to: 'images/custom_emojis'},
|
||||||
{from: 'logo/conversejs-filled-192.png', to: 'images/logo'},
|
{from: 'logo/conversejs-filled-192.png', to: 'images/logo'},
|
||||||
|
Loading…
Reference in New Issue
Block a user