Handle and render chat state notifications separately from messages
This commit is contained in:
parent
283a810d6b
commit
b5d57f0ef8
@ -230,10 +230,21 @@
|
|||||||
width: 100%
|
width: 100%
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-content-sendbutton {
|
.chat-content-sendbutton {
|
||||||
height: calc(100% - (var(--chat-textarea-height) + var(--send-button-height) + 2 * var(--send-button-margin)));
|
height: calc(100% - (var(--chat-textarea-height) + var(--send-button-height) + 2 * var(--send-button-margin)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat-state-notifications {
|
||||||
|
white-space: pre;
|
||||||
|
background-color: var(--chat-content-background-color);
|
||||||
|
color: var(--subdued-color);
|
||||||
|
font-size: 90%;
|
||||||
|
font-style: italic;
|
||||||
|
line-height: var(--line-height-small);
|
||||||
|
padding: 0 1em 0.3em;
|
||||||
|
}
|
||||||
|
|
||||||
.dropdown { /* status dropdown styles */
|
.dropdown { /* status dropdown styles */
|
||||||
background-color: var(--light-background-color);
|
background-color: var(--light-background-color);
|
||||||
dd {
|
dd {
|
||||||
|
130
spec/chatbox.js
130
spec/chatbox.js
@ -686,31 +686,37 @@
|
|||||||
id: u.getUniqueId()
|
id: u.getUniqueId()
|
||||||
}).c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
|
}).c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
|
||||||
|
|
||||||
spyOn(_converse.api, "trigger").and.callThrough();
|
|
||||||
_converse.connection._dataRecv(test_utils.createRequest(msg));
|
_converse.connection._dataRecv(test_utils.createRequest(msg));
|
||||||
await u.waitUntil(() => _converse.api.trigger.calls.count());
|
|
||||||
expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object));
|
|
||||||
const view = _converse.chatboxviews.get(sender_jid);
|
const view = _converse.chatboxviews.get(sender_jid);
|
||||||
expect(view).toBeDefined();
|
let csn = mock.cur_names[1] + ' is typing';
|
||||||
|
await u.waitUntil( () => view.el.querySelector('.chat-state-notifications').innerText === csn);
|
||||||
|
expect(view.model.messages.length).toEqual(0);
|
||||||
|
|
||||||
const event = await u.waitUntil(() => view.el.querySelector('.chat-state-notification'));
|
// <paused> state
|
||||||
expect(event.textContent).toEqual(mock.cur_names[1] + ' is typing');
|
|
||||||
|
|
||||||
// Check that it doesn't appear twice
|
|
||||||
msg = $msg({
|
msg = $msg({
|
||||||
from: sender_jid,
|
from: sender_jid,
|
||||||
to: _converse.connection.jid,
|
to: _converse.connection.jid,
|
||||||
type: 'chat',
|
type: 'chat',
|
||||||
id: u.getUniqueId()
|
id: u.getUniqueId()
|
||||||
}).c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
|
}).c('paused', {'xmlns': Strophe.NS.CHATSTATES}).tree();
|
||||||
|
_converse.connection._dataRecv(test_utils.createRequest(msg));
|
||||||
|
csn = mock.cur_names[1] + ' has stopped typing';
|
||||||
|
await u.waitUntil( () => view.el.querySelector('.chat-state-notifications').innerText === csn);
|
||||||
|
|
||||||
|
msg = $msg({
|
||||||
|
from: sender_jid,
|
||||||
|
to: _converse.connection.jid,
|
||||||
|
type: 'chat',
|
||||||
|
id: u.getUniqueId()
|
||||||
|
}).c('body').t('hello world').tree();
|
||||||
await _converse.handleMessageStanza(msg);
|
await _converse.handleMessageStanza(msg);
|
||||||
const events = view.el.querySelectorAll('.chat-state-notification');
|
const msg_el = await u.waitUntil(() => view.content.querySelector('.chat-msg'));
|
||||||
expect(events.length).toBe(1);
|
await u.waitUntil( () => view.el.querySelector('.chat-state-notifications').innerText === '');
|
||||||
expect(events[0].textContent).toEqual(mock.cur_names[1] + ' is typing');
|
expect(msg_el.querySelector('.chat-msg__text').textContent).toBe('hello world');
|
||||||
done();
|
done();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it("can be a composing carbon message that this user sent from a different client",
|
it("is ignored if it's a composing carbon message sent by this user from a different client",
|
||||||
mock.initConverse(
|
mock.initConverse(
|
||||||
['rosterGroupsFetched', 'chatBoxesFetched'], {},
|
['rosterGroupsFetched', 'chatBoxesFetched'], {},
|
||||||
async function (done, _converse) {
|
async function (done, _converse) {
|
||||||
@ -721,6 +727,9 @@
|
|||||||
// Send a message from a different resource
|
// Send a message from a different resource
|
||||||
const recipient_jid = mock.cur_names[5].replace(/ /g,'.').toLowerCase() + '@montague.lit';
|
const recipient_jid = mock.cur_names[5].replace(/ /g,'.').toLowerCase() + '@montague.lit';
|
||||||
const view = await test_utils.openChatBoxFor(_converse, recipient_jid);
|
const view = await test_utils.openChatBoxFor(_converse, recipient_jid);
|
||||||
|
|
||||||
|
spyOn(u, 'shouldCreateMessage').and.callThrough();
|
||||||
|
|
||||||
const msg = $msg({
|
const msg = $msg({
|
||||||
'from': _converse.bare_jid,
|
'from': _converse.bare_jid,
|
||||||
'id': u.getUniqueId(),
|
'id': u.getUniqueId(),
|
||||||
@ -735,20 +744,12 @@
|
|||||||
'to': recipient_jid,
|
'to': recipient_jid,
|
||||||
'type': 'chat'
|
'type': 'chat'
|
||||||
}).c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
|
}).c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
|
||||||
await _converse.handleMessageStanza(msg);
|
_converse.connection._dataRecv(test_utils.createRequest(msg));
|
||||||
await u.waitUntil(() => view.model.messages.length);
|
|
||||||
// Check that the chatbox and its view now exist
|
await u.waitUntil(() => u.shouldCreateMessage.calls.count());
|
||||||
const chatbox = _converse.chatboxes.get(recipient_jid);
|
expect(view.model.messages.length).toEqual(0);
|
||||||
const chatboxview = _converse.chatboxviews.get(recipient_jid);
|
const el = view.el.querySelector('.chat-state-notifications');
|
||||||
// Check that the message was received and check the message parameters
|
expect(el.textContent).toBe('');
|
||||||
expect(chatbox.messages.length).toEqual(1);
|
|
||||||
const msg_obj = chatbox.messages.models[0];
|
|
||||||
expect(msg_obj.get('sender')).toEqual('me');
|
|
||||||
expect(msg_obj.get('is_delayed')).toEqual(false);
|
|
||||||
const chat_content = chatboxview.el.querySelector('.chat-content');
|
|
||||||
const el = await u.waitUntil(() => chat_content.querySelector('.chat-info.chat-state-notification'));
|
|
||||||
const status_text = el.textContent;
|
|
||||||
expect(status_text).toBe('Typing from another device');
|
|
||||||
done();
|
done();
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
@ -829,15 +830,15 @@
|
|||||||
type: 'chat',
|
type: 'chat',
|
||||||
id: u.getUniqueId()
|
id: u.getUniqueId()
|
||||||
}).c('paused', {'xmlns': Strophe.NS.CHATSTATES}).tree();
|
}).c('paused', {'xmlns': Strophe.NS.CHATSTATES}).tree();
|
||||||
await _converse.handleMessageStanza(msg);
|
|
||||||
expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object));
|
_converse.connection._dataRecv(test_utils.createRequest(msg));
|
||||||
await u.waitUntil(() => view.model.vcard.get('fullname') === mock.cur_names[1])
|
const csn = mock.cur_names[1] + ' has stopped typing';
|
||||||
const event = await u.waitUntil(() => view.el.querySelector('.chat-state-notification'));
|
await u.waitUntil( () => view.el.querySelector('.chat-state-notifications').innerText === csn);
|
||||||
expect(event.textContent).toEqual(mock.cur_names[1] + ' has stopped typing');
|
expect(view.model.messages.length).toEqual(0);
|
||||||
done();
|
done();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it("can be a paused carbon message that this user sent from a different client",
|
it("will not be shown if it's a paused carbon message that this user sent from a different client",
|
||||||
mock.initConverse(
|
mock.initConverse(
|
||||||
['rosterGroupsFetched', 'chatBoxesFetched'], {},
|
['rosterGroupsFetched', 'chatBoxesFetched'], {},
|
||||||
async function (done, _converse) {
|
async function (done, _converse) {
|
||||||
@ -847,6 +848,7 @@
|
|||||||
await test_utils.waitForRoster(_converse, 'current');
|
await test_utils.waitForRoster(_converse, 'current');
|
||||||
// Send a message from a different resource
|
// Send a message from a different resource
|
||||||
const recipient_jid = mock.cur_names[5].replace(/ /g,'.').toLowerCase() + '@montague.lit';
|
const recipient_jid = mock.cur_names[5].replace(/ /g,'.').toLowerCase() + '@montague.lit';
|
||||||
|
spyOn(u, 'shouldCreateMessage').and.callThrough();
|
||||||
const view = await test_utils.openChatBoxFor(_converse, recipient_jid);
|
const view = await test_utils.openChatBoxFor(_converse, recipient_jid);
|
||||||
const msg = $msg({
|
const msg = $msg({
|
||||||
'from': _converse.bare_jid,
|
'from': _converse.bare_jid,
|
||||||
@ -862,18 +864,12 @@
|
|||||||
'to': recipient_jid,
|
'to': recipient_jid,
|
||||||
'type': 'chat'
|
'type': 'chat'
|
||||||
}).c('paused', {'xmlns': Strophe.NS.CHATSTATES}).tree();
|
}).c('paused', {'xmlns': Strophe.NS.CHATSTATES}).tree();
|
||||||
await _converse.handleMessageStanza(msg);
|
_converse.connection._dataRecv(test_utils.createRequest(msg));
|
||||||
await u.waitUntil(() => view.model.messages.length);
|
await u.waitUntil(() => u.shouldCreateMessage.calls.count());
|
||||||
// Check that the chatbox and its view now exist
|
expect(view.model.messages.length).toEqual(0);
|
||||||
const chatbox = _converse.chatboxes.get(recipient_jid);
|
const el = view.el.querySelector('.chat-state-notifications');
|
||||||
const chatboxview = _converse.chatboxviews.get(recipient_jid);
|
expect(el.textContent).toBe('');
|
||||||
// Check that the message was received and check the message parameters
|
done();
|
||||||
expect(chatbox.messages.length).toEqual(1);
|
|
||||||
const msg_obj = chatbox.messages.models[0];
|
|
||||||
expect(msg_obj.get('sender')).toEqual('me');
|
|
||||||
expect(msg_obj.get('is_delayed')).toEqual(false);
|
|
||||||
const el = await u.waitUntil(() => chatboxview.el.querySelector('.chat-info.chat-state-notification'));
|
|
||||||
expect(el.textContent).toBe('Stopped typing on the other device');
|
|
||||||
done();
|
done();
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
@ -997,7 +993,6 @@
|
|||||||
await test_utils.openControlBox(_converse);
|
await test_utils.openControlBox(_converse);
|
||||||
const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit';
|
const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit';
|
||||||
// See XEP-0085 https://xmpp.org/extensions/xep-0085.html#definitions
|
// See XEP-0085 https://xmpp.org/extensions/xep-0085.html#definitions
|
||||||
spyOn(_converse.api, "trigger").and.callThrough();
|
|
||||||
await test_utils.openChatBoxFor(_converse, sender_jid);
|
await test_utils.openChatBoxFor(_converse, sender_jid);
|
||||||
const view = _converse.chatboxviews.get(sender_jid);
|
const view = _converse.chatboxviews.get(sender_jid);
|
||||||
expect(view.el.querySelectorAll('.chat-event').length).toBe(0);
|
expect(view.el.querySelectorAll('.chat-event').length).toBe(0);
|
||||||
@ -1011,20 +1006,20 @@
|
|||||||
'type': 'chat'})
|
'type': 'chat'})
|
||||||
.c('composing', {'xmlns': Strophe.NS.CHATSTATES}).up()
|
.c('composing', {'xmlns': Strophe.NS.CHATSTATES}).up()
|
||||||
.tree();
|
.tree();
|
||||||
await _converse.handleMessageStanza(msg);
|
_converse.connection._dataRecv(test_utils.createRequest(msg));
|
||||||
await u.waitUntil(() => view.model.messages.length);
|
const csntext = await u.waitUntil(() => view.el.querySelector('.chat-state-notifications').textContent);
|
||||||
await u.waitUntil(() => view.el.querySelector('.chat-state-notification'));
|
expect(csntext).toEqual(mock.cur_names[1] + ' is typing');
|
||||||
expect(view.el.querySelectorAll('.chat-state-notification').length).toBe(1);
|
expect(view.model.messages.length).toBe(0);
|
||||||
|
|
||||||
msg = $msg({
|
msg = $msg({
|
||||||
from: sender_jid,
|
from: sender_jid,
|
||||||
to: _converse.connection.jid,
|
to: _converse.connection.jid,
|
||||||
type: 'chat',
|
type: 'chat',
|
||||||
id: u.getUniqueId()
|
id: u.getUniqueId()
|
||||||
}).c('inactive', {'xmlns': Strophe.NS.CHATSTATES}).tree();
|
}).c('inactive', {'xmlns': Strophe.NS.CHATSTATES}).tree();
|
||||||
await _converse.handleMessageStanza(msg);
|
_converse.connection._dataRecv(test_utils.createRequest(msg));
|
||||||
await u.waitUntil(() => (view.model.messages.length > 1));
|
|
||||||
expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object));
|
await u.waitUntil(() => !view.el.querySelector('.chat-state-notifications').textContent);
|
||||||
await u.waitUntil(() => view.el.querySelectorAll('.chat-state-notification').length === 0);
|
|
||||||
done();
|
done();
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
@ -1038,28 +1033,27 @@
|
|||||||
|
|
||||||
await test_utils.waitForRoster(_converse, 'current', 3);
|
await test_utils.waitForRoster(_converse, 'current', 3);
|
||||||
await test_utils.openControlBox(_converse);
|
await test_utils.openControlBox(_converse);
|
||||||
|
|
||||||
spyOn(_converse.api, "trigger").and.callThrough();
|
|
||||||
const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit';
|
const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit';
|
||||||
// <paused> state
|
await test_utils.openChatBoxFor(_converse, sender_jid);
|
||||||
|
|
||||||
const msg = $msg({
|
const msg = $msg({
|
||||||
from: sender_jid,
|
from: sender_jid,
|
||||||
to: _converse.connection.jid,
|
to: _converse.connection.jid,
|
||||||
type: 'chat',
|
type: 'chat',
|
||||||
id: u.getUniqueId()
|
id: u.getUniqueId()
|
||||||
}).c('body').c('gone', {'xmlns': Strophe.NS.CHATSTATES}).tree();
|
}).c('body').c('gone', {'xmlns': Strophe.NS.CHATSTATES}).tree();
|
||||||
await _converse.handleMessageStanza(msg);
|
_converse.connection._dataRecv(test_utils.createRequest(msg));
|
||||||
expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object));
|
|
||||||
const view = _converse.chatboxviews.get(sender_jid);
|
const view = _converse.chatboxviews.get(sender_jid);
|
||||||
await u.waitUntil(() => view.model.vcard.get('fullname') === mock.cur_names[1]);
|
const csntext = await u.waitUntil(() => view.el.querySelector('.chat-state-notifications').textContent);
|
||||||
const event = await u.waitUntil(() => view.el.querySelector('.chat-state-notification'));
|
expect(csntext).toEqual(mock.cur_names[1] + ' has gone away');
|
||||||
expect(event.textContent).toEqual(mock.cur_names[1] + ' has gone away');
|
|
||||||
done();
|
done();
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("On receiving a message correction", function () {
|
describe("On receiving a message correction", function () {
|
||||||
it("will be updated",
|
|
||||||
|
it("will be removed",
|
||||||
mock.initConverse(
|
mock.initConverse(
|
||||||
['rosterGroupsFetched'], {},
|
['rosterGroupsFetched'], {},
|
||||||
async function (done, _converse) {
|
async function (done, _converse) {
|
||||||
@ -1096,10 +1090,10 @@
|
|||||||
type: 'chat',
|
type: 'chat',
|
||||||
id: u.getUniqueId()
|
id: u.getUniqueId()
|
||||||
}).c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
|
}).c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
|
||||||
|
_converse.connection._dataRecv(test_utils.createRequest(msg));
|
||||||
|
|
||||||
await _converse.handleMessageStanza(msg);
|
const csntext = await u.waitUntil(() => view.el.querySelector('.chat-state-notifications').textContent);
|
||||||
const event = await u.waitUntil(() => view.el.querySelector('.chat-state-notification'));
|
expect(csntext).toEqual(mock.cur_names[1] + ' is typing');
|
||||||
expect(event.textContent).toEqual(mock.cur_names[1] + ' is typing');
|
|
||||||
|
|
||||||
// Edited message
|
// Edited message
|
||||||
const edited = $msg({
|
const edited = $msg({
|
||||||
@ -1113,9 +1107,7 @@
|
|||||||
.c('replace', {'xmlns': Strophe.NS.MESSAGE_CORRECT, 'id': original_id }).tree();
|
.c('replace', {'xmlns': Strophe.NS.MESSAGE_CORRECT, 'id': original_id }).tree();
|
||||||
|
|
||||||
await _converse.handleMessageStanza(edited);
|
await _converse.handleMessageStanza(edited);
|
||||||
|
await u.waitUntil(() => !view.el.querySelector('.chat-state-notifications').textContent);
|
||||||
const events = view.el.querySelectorAll('.chat-state-notification');
|
|
||||||
expect(events.length).toBe(0);
|
|
||||||
done();
|
done();
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
@ -1108,21 +1108,8 @@
|
|||||||
.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.once('messageInserted', resolve));
|
||||||
|
|
||||||
jasmine.clock().tick(1000);
|
|
||||||
// Insert <composing> message, to also check that
|
|
||||||
// text messages are inserted correctly with
|
|
||||||
// temporary chat events in the chat contents.
|
|
||||||
_converse.handleMessageStanza($msg({
|
|
||||||
'id': 'aeb219',
|
|
||||||
'to': _converse.bare_jid,
|
|
||||||
'xmlns': 'jabber:client',
|
|
||||||
'from': sender_jid,
|
|
||||||
'type': 'chat'})
|
|
||||||
.c('composing', {'xmlns': Strophe.NS.CHATSTATES}).up()
|
|
||||||
.tree());
|
|
||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
|
||||||
|
|
||||||
jasmine.clock().tick(1*ONE_MINUTE_LATER);
|
jasmine.clock().tick(1*ONE_MINUTE_LATER);
|
||||||
|
|
||||||
_converse.handleMessageStanza($msg({
|
_converse.handleMessageStanza($msg({
|
||||||
'from': sender_jid,
|
'from': sender_jid,
|
||||||
'to': _converse.connection.jid,
|
'to': _converse.connection.jid,
|
||||||
|
180
spec/muc.js
180
spec/muc.js
@ -5063,7 +5063,11 @@
|
|||||||
async function (done, _converse) {
|
async function (done, _converse) {
|
||||||
|
|
||||||
const muc_jid = 'coven@chat.shakespeare.lit';
|
const muc_jid = 'coven@chat.shakespeare.lit';
|
||||||
await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'some1');
|
const members = [
|
||||||
|
{'affiliation': 'member', 'nick': 'majortom', 'jid': 'majortom@example.org'},
|
||||||
|
{'affiliation': 'admin', 'nick': 'groundcontrol', 'jid': 'groundcontrol@example.org'}
|
||||||
|
];
|
||||||
|
await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'some1', [], members);
|
||||||
const view = _converse.api.chatviews.get(muc_jid);
|
const view = _converse.api.chatviews.get(muc_jid);
|
||||||
const chat_content = view.el.querySelector('.chat-content');
|
const chat_content = view.el.querySelector('.chat-content');
|
||||||
|
|
||||||
@ -5102,6 +5106,13 @@
|
|||||||
|
|
||||||
// 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 = [];
|
||||||
|
spyOn(window, 'setTimeout').and.callFake(f => {
|
||||||
|
if (f.toString() === "() => this.removeCSNFor(actor, state)") {
|
||||||
|
timeout_functions.push(f)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// <composing> state
|
// <composing> state
|
||||||
let msg = $msg({
|
let msg = $msg({
|
||||||
from: muc_jid+'/newguy',
|
from: muc_jid+'/newguy',
|
||||||
@ -5109,43 +5120,19 @@
|
|||||||
to: 'romeo@montague.lit',
|
to: 'romeo@montague.lit',
|
||||||
type: 'groupchat'
|
type: 'groupchat'
|
||||||
}).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
|
}).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
|
||||||
|
_converse.connection._dataRecv(test_utils.createRequest(msg));
|
||||||
|
|
||||||
await view.model.queueMessage(msg);
|
const csntext = await u.waitUntil(() => view.el.querySelector('.chat-state-notifications').textContent);
|
||||||
await u.waitUntil(() => view.el.querySelectorAll('.chat-state-notification').length);
|
expect(csntext.trim()).toEqual('newguy is typing');
|
||||||
|
expect(timeout_functions.length).toBe(1);
|
||||||
|
|
||||||
// Check that the notification appears inside the chatbox in the DOM
|
// Check that the notification appears inside the chatbox in the DOM
|
||||||
let events = view.el.querySelectorAll('.chat-event');
|
const events = view.el.querySelectorAll('.chat-event');
|
||||||
expect(events.length).toBe(3);
|
expect(events.length).toBe(3);
|
||||||
expect(events[0].textContent.trim()).toEqual('some1 has entered the groupchat');
|
expect(events[0].textContent.trim()).toEqual('some1 has entered the groupchat');
|
||||||
expect(events[1].textContent.trim()).toEqual('newguy has entered the groupchat');
|
expect(events[1].textContent.trim()).toEqual('newguy has entered the groupchat');
|
||||||
expect(events[2].textContent.trim()).toEqual('nomorenicks has entered the groupchat');
|
expect(events[2].textContent.trim()).toEqual('nomorenicks has entered the groupchat');
|
||||||
|
expect(view.el.querySelector('.chat-state-notifications').textContent.trim()).toEqual('newguy is typing');
|
||||||
let notifications = view.el.querySelectorAll('.chat-state-notification');
|
|
||||||
expect(notifications.length).toBe(1);
|
|
||||||
expect(notifications[0].textContent.trim()).toEqual('newguy is typing');
|
|
||||||
|
|
||||||
const timeout_functions = [];
|
|
||||||
spyOn(window, 'setTimeout').and.callFake(f => timeout_functions.push(f));
|
|
||||||
|
|
||||||
// Check that it doesn't appear twice
|
|
||||||
msg = $msg({
|
|
||||||
from: muc_jid+'/newguy',
|
|
||||||
id: u.getUniqueId(),
|
|
||||||
to: 'romeo@montague.lit',
|
|
||||||
type: 'groupchat'
|
|
||||||
}).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
|
|
||||||
await view.model.queueMessage(msg);
|
|
||||||
|
|
||||||
events = view.el.querySelectorAll('.chat-event');
|
|
||||||
expect(events.length).toBe(3);
|
|
||||||
expect(events[0].textContent.trim()).toEqual('some1 has entered the groupchat');
|
|
||||||
expect(events[1].textContent.trim()).toEqual('newguy has entered the groupchat');
|
|
||||||
expect(events[2].textContent.trim()).toEqual('nomorenicks has entered the groupchat');
|
|
||||||
|
|
||||||
notifications = view.el.querySelectorAll('.chat-state-notification');
|
|
||||||
expect(notifications.length).toBe(1);
|
|
||||||
expect(notifications[0].textContent.trim()).toEqual('newguy is typing');
|
|
||||||
expect(timeout_functions.length).toBe(1);
|
|
||||||
|
|
||||||
// <composing> state for a different occupant
|
// <composing> state for a different occupant
|
||||||
msg = $msg({
|
msg = $msg({
|
||||||
@ -5155,17 +5142,27 @@
|
|||||||
type: 'groupchat'
|
type: 'groupchat'
|
||||||
}).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
|
}).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
|
||||||
await view.model.queueMessage(msg);
|
await view.model.queueMessage(msg);
|
||||||
events = view.el.querySelectorAll('.chat-event');
|
await u.waitUntil(() => view.el.querySelector('.chat-state-notifications').textContent.trim() === 'newguy and nomorenicks are typing');
|
||||||
expect(events.length).toBe(3);
|
|
||||||
expect(events[0].textContent.trim()).toEqual('some1 has entered the groupchat');
|
|
||||||
expect(events[1].textContent.trim()).toEqual('newguy has entered the groupchat');
|
|
||||||
expect(events[2].textContent.trim()).toEqual('nomorenicks has entered the groupchat');
|
|
||||||
|
|
||||||
await u.waitUntil(() => (view.el.querySelectorAll('.chat-state-notification').length === 2));
|
// <composing> state for a different occupant
|
||||||
notifications = view.el.querySelectorAll('.chat-state-notification');
|
msg = $msg({
|
||||||
expect(notifications.length).toBe(2);
|
from: muc_jid+'/majortom',
|
||||||
expect(notifications[0].textContent.trim()).toEqual('nomorenicks is typing');
|
id: u.getUniqueId(),
|
||||||
expect(notifications[1].textContent.trim()).toEqual('newguy is typing');
|
to: 'romeo@montague.lit',
|
||||||
|
type: 'groupchat'
|
||||||
|
}).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
|
||||||
|
await view.model.queueMessage(msg);
|
||||||
|
await u.waitUntil(() => view.el.querySelector('.chat-state-notifications').textContent.trim() === 'newguy, nomorenicks and majortom are typing');
|
||||||
|
|
||||||
|
// <composing> state for a different occupant
|
||||||
|
msg = $msg({
|
||||||
|
from: muc_jid+'/groundcontrol',
|
||||||
|
id: u.getUniqueId(),
|
||||||
|
to: 'romeo@montague.lit',
|
||||||
|
type: 'groupchat'
|
||||||
|
}).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
|
||||||
|
await view.model.queueMessage(msg);
|
||||||
|
await u.waitUntil(() => view.el.querySelector('.chat-state-notifications').textContent.trim() === 'newguy, nomorenicks and others are typing');
|
||||||
|
|
||||||
// Check that new messages appear under the chat state notifications
|
// Check that new messages appear under the chat state notifications
|
||||||
msg = $msg({
|
msg = $msg({
|
||||||
@ -5178,32 +5175,13 @@
|
|||||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
await new Promise(resolve => view.once('messageInserted', resolve));
|
||||||
|
|
||||||
const messages = view.el.querySelectorAll('.message');
|
const messages = view.el.querySelectorAll('.message');
|
||||||
expect(messages.length).toBe(7);
|
expect(messages.length).toBe(5);
|
||||||
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__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]();
|
timeout_functions[0]();
|
||||||
events = view.el.querySelectorAll('.chat-event');
|
await u.waitUntil(() => view.el.querySelector('.chat-state-notifications').textContent.trim() === 'nomorenicks, majortom and groundcontrol are typing');
|
||||||
expect(events.length).toBe(3);
|
|
||||||
expect(events[0].textContent.trim()).toEqual('some1 has entered the groupchat');
|
|
||||||
expect(events[1].textContent.trim()).toEqual('newguy has entered the groupchat');
|
|
||||||
expect(events[2].textContent.trim()).toEqual('nomorenicks has entered the groupchat');
|
|
||||||
|
|
||||||
notifications = view.el.querySelectorAll('.chat-state-notification');
|
|
||||||
expect(notifications.length).toBe(1);
|
|
||||||
expect(notifications[0].textContent.trim()).toEqual('nomorenicks is typing');
|
|
||||||
|
|
||||||
timeout_functions.filter(f => f.name === 'bound safeDestroy').pop()();
|
|
||||||
|
|
||||||
events = view.el.querySelectorAll('.chat-event');
|
|
||||||
expect(events.length).toBe(3);
|
|
||||||
expect(events[0].textContent.trim()).toEqual('some1 has entered the groupchat');
|
|
||||||
expect(events[1].textContent.trim()).toEqual('newguy has entered the groupchat');
|
|
||||||
expect(events[2].textContent.trim()).toEqual('nomorenicks has entered the groupchat');
|
|
||||||
|
|
||||||
notifications = view.el.querySelectorAll('.chat-state-notification');
|
|
||||||
expect(notifications.length).toBe(0);
|
|
||||||
done();
|
done();
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
@ -5221,13 +5199,13 @@
|
|||||||
const chat_content = view.el.querySelector('.chat-content');
|
const chat_content = view.el.querySelector('.chat-content');
|
||||||
|
|
||||||
/* <presence to="romeo@montague.lit/_converse.js-29092160"
|
/* <presence to="romeo@montague.lit/_converse.js-29092160"
|
||||||
* from="coven@chat.shakespeare.lit/some1">
|
* from="coven@chat.shakespeare.lit/some1">
|
||||||
* <x xmlns="http://jabber.org/protocol/muc#user">
|
* <x xmlns="http://jabber.org/protocol/muc#user">
|
||||||
* <item affiliation="owner" jid="romeo@montague.lit/_converse.js-29092160" role="moderator"/>
|
* <item affiliation="owner" jid="romeo@montague.lit/_converse.js-29092160" role="moderator"/>
|
||||||
* <status code="110"/>
|
* <status code="110"/>
|
||||||
* </x>
|
* </x>
|
||||||
* </presence></body>
|
* </presence></body>
|
||||||
*/
|
*/
|
||||||
let presence = $pres({
|
let presence = $pres({
|
||||||
to: 'romeo@montague.lit/_converse.js-29092160',
|
to: 'romeo@montague.lit/_converse.js-29092160',
|
||||||
from: 'coven@chat.shakespeare.lit/some1'
|
from: 'coven@chat.shakespeare.lit/some1'
|
||||||
@ -5282,37 +5260,8 @@
|
|||||||
type: 'groupchat'
|
type: 'groupchat'
|
||||||
}).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
|
}).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
|
||||||
await view.model.queueMessage(msg);
|
await view.model.queueMessage(msg);
|
||||||
|
await u.waitUntil(() => view.el.querySelector('.chat-state-notifications').textContent);
|
||||||
// Check that the notification appears inside the chatbox in the DOM
|
expect(view.el.querySelector('.chat-state-notifications').textContent.trim()).toBe('newguy is typing');
|
||||||
var events = view.el.querySelectorAll('.chat-event');
|
|
||||||
expect(events.length).toBe(3);
|
|
||||||
expect(events[0].textContent.trim()).toEqual('some1 has entered the groupchat');
|
|
||||||
expect(events[1].textContent.trim()).toEqual('newguy has entered the groupchat');
|
|
||||||
expect(events[2].textContent.trim()).toEqual('nomorenicks has entered the groupchat');
|
|
||||||
|
|
||||||
await u.waitUntil(() => view.el.querySelectorAll('.chat-state-notification').length);
|
|
||||||
let notifications = view.el.querySelectorAll('.chat-state-notification');
|
|
||||||
expect(notifications.length).toBe(1);
|
|
||||||
expect(notifications[0].textContent.trim()).toEqual('newguy is typing');
|
|
||||||
|
|
||||||
// Check that it doesn't appear twice
|
|
||||||
msg = $msg({
|
|
||||||
from: muc_jid+'/newguy',
|
|
||||||
id: u.getUniqueId(),
|
|
||||||
to: 'romeo@montague.lit',
|
|
||||||
type: 'groupchat'
|
|
||||||
}).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
|
|
||||||
await view.model.queueMessage(msg);
|
|
||||||
|
|
||||||
events = view.el.querySelectorAll('.chat-event');
|
|
||||||
expect(events.length).toBe(3);
|
|
||||||
expect(events[0].textContent.trim()).toEqual('some1 has entered the groupchat');
|
|
||||||
expect(events[1].textContent.trim()).toEqual('newguy has entered the groupchat');
|
|
||||||
expect(events[2].textContent.trim()).toEqual('nomorenicks has entered the groupchat');
|
|
||||||
|
|
||||||
notifications = view.el.querySelectorAll('.chat-state-notification');
|
|
||||||
expect(notifications.length).toBe(1);
|
|
||||||
expect(notifications[0].textContent.trim()).toEqual('newguy is typing');
|
|
||||||
|
|
||||||
// <composing> state for a different occupant
|
// <composing> state for a different occupant
|
||||||
msg = $msg({
|
msg = $msg({
|
||||||
@ -5322,20 +5271,8 @@
|
|||||||
type: 'groupchat'
|
type: 'groupchat'
|
||||||
}).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
|
}).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
|
||||||
await view.model.queueMessage(msg);
|
await view.model.queueMessage(msg);
|
||||||
events = view.el.querySelectorAll('.chat-event');
|
|
||||||
expect(events.length).toBe(3);
|
|
||||||
expect(events[0].textContent.trim()).toEqual('some1 has entered the groupchat');
|
|
||||||
expect(events[1].textContent.trim()).toEqual('newguy has entered the groupchat');
|
|
||||||
expect(events[2].textContent.trim()).toEqual('nomorenicks has entered the groupchat');
|
|
||||||
|
|
||||||
await u.waitUntil(() => view.el.querySelectorAll('.chat-state-notification').length === 2);
|
await u.waitUntil(() => view.el.querySelector('.chat-state-notifications').textContent.trim() == 'newguy and nomorenicks are typing');
|
||||||
notifications = view.el.querySelectorAll('.chat-state-notification');
|
|
||||||
// We check for the messages' text without assuming order,
|
|
||||||
// because it can be variable since getLastMessageDate
|
|
||||||
// ignore CSN messages.
|
|
||||||
let text = _.map(notifications, 'textContent').join(' ');
|
|
||||||
expect(text.includes('newguy is typing')).toBe(true);
|
|
||||||
expect(text.includes('nomorenicks is typing')).toBe(true);
|
|
||||||
|
|
||||||
// <paused> state from occupant who typed first
|
// <paused> state from occupant who typed first
|
||||||
msg = $msg({
|
msg = $msg({
|
||||||
@ -5345,22 +5282,7 @@
|
|||||||
type: 'groupchat'
|
type: 'groupchat'
|
||||||
}).c('body').c('paused', {'xmlns': Strophe.NS.CHATSTATES}).tree();
|
}).c('body').c('paused', {'xmlns': Strophe.NS.CHATSTATES}).tree();
|
||||||
await view.model.queueMessage(msg);
|
await view.model.queueMessage(msg);
|
||||||
events = view.el.querySelectorAll('.chat-event');
|
await u.waitUntil(() => view.el.querySelector('.chat-state-notifications').textContent.trim() == 'nomorenicks is typing\n newguy has stopped typing');
|
||||||
expect(events.length).toBe(3);
|
|
||||||
expect(events[0].textContent.trim()).toEqual('some1 has entered the groupchat');
|
|
||||||
expect(events[1].textContent.trim()).toEqual('newguy has entered the groupchat');
|
|
||||||
expect(events[2].textContent.trim()).toEqual('nomorenicks has entered the groupchat');
|
|
||||||
|
|
||||||
await u.waitUntil(() => {
|
|
||||||
return _.map(
|
|
||||||
view.el.querySelectorAll('.chat-state-notification'), 'textContent')
|
|
||||||
.join(' ').includes('stopped typing')
|
|
||||||
});
|
|
||||||
notifications = view.el.querySelectorAll('.chat-state-notification');
|
|
||||||
expect(notifications.length).toBe(2);
|
|
||||||
text = _.map(notifications, 'textContent').join(' ');
|
|
||||||
expect(text.includes('newguy has stopped typing')).toBe(true);
|
|
||||||
expect(text.includes('nomorenicks is typing')).toBe(true);
|
|
||||||
done();
|
done();
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
@ -197,13 +197,14 @@ converse.plugins.add('converse-chatview', {
|
|||||||
this.initDebounced();
|
this.initDebounced();
|
||||||
|
|
||||||
this.listenTo(this.model.messages, 'add', this.onMessageAdded);
|
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.listenTo(this.model.messages, 'rendered', this.scrollDown);
|
||||||
this.model.messages.on('reset', () => {
|
this.model.messages.on('reset', () => {
|
||||||
this.content.innerHTML = '';
|
this.content.innerHTML = '';
|
||||||
this.removeAll();
|
this.removeAll();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.listenTo(this.model.csn, '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);
|
||||||
@ -248,11 +249,25 @@ 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.csn = this.el.querySelector('.chat-state-notifications');
|
||||||
|
this.renderChatStateNotification();
|
||||||
this.renderMessageForm();
|
this.renderMessageForm();
|
||||||
this.renderHeading();
|
this.renderHeading();
|
||||||
return this;
|
return this;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
renderChatStateNotification () {
|
||||||
|
if (this.model.csn.get('chat_state') === _converse.COMPOSING) {
|
||||||
|
this.csn.innerText = __('%1$s is typing', this.model.getDisplayName());
|
||||||
|
} else if (this.model.csn.get('chat_state') === _converse.PAUSED) {
|
||||||
|
this.csn.innerText = __('%1$s has stopped typing', this.model.getDisplayName());
|
||||||
|
} else if (this.model.csn.get('chat_state') === _converse.GONE) {
|
||||||
|
this.csn.innerText = __('%1$s has gone away', this.model.getDisplayName());
|
||||||
|
} else {
|
||||||
|
this.csn.innerText = '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
renderToolbar () {
|
renderToolbar () {
|
||||||
if (!_converse.show_toolbar) {
|
if (!_converse.show_toolbar) {
|
||||||
return this;
|
return this;
|
||||||
@ -729,7 +744,6 @@ converse.plugins.add('converse-chatview', {
|
|||||||
await message.initialized;
|
await message.initialized;
|
||||||
const view = this.add(message.get('id'), new _converse.MessageView({'model': message}));
|
const view = this.add(message.get('id'), new _converse.MessageView({'model': message}));
|
||||||
await view.render();
|
await view.render();
|
||||||
this.clearChatStateForSender(message.get('from'));
|
|
||||||
this.insertMessage(view);
|
this.insertMessage(view);
|
||||||
this.insertDayIndicator(view.el);
|
this.insertDayIndicator(view.el);
|
||||||
this.setScrollPosition(view.el);
|
this.setScrollPosition(view.el);
|
||||||
@ -741,7 +755,7 @@ converse.plugins.add('converse-chatview', {
|
|||||||
// when the user writes a message as opposed to when a
|
// when the user writes a message as opposed to when a
|
||||||
// message is received.
|
// message is received.
|
||||||
this.model.set('scrolled', false);
|
this.model.set('scrolled', false);
|
||||||
} else if (this.model.get('scrolled', true) && !u.isOnlyChatStateNotification(message)) {
|
} else if (this.model.get('scrolled', true)) {
|
||||||
this.showNewMessagesIndicator();
|
this.showNewMessagesIndicator();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -784,16 +798,6 @@ converse.plugins.add('converse-chatview', {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Handler that gets called when a message object has been edited via LMC.
|
|
||||||
* @private
|
|
||||||
* @method _converse.ChatBoxView#onMessageEdited
|
|
||||||
* @param { object } message - The updated message object.
|
|
||||||
*/
|
|
||||||
onMessageEdited (message) {
|
|
||||||
this.clearChatStateForSender(message.get('from'));
|
|
||||||
},
|
|
||||||
|
|
||||||
parseMessageForCommands (text) {
|
parseMessageForCommands (text) {
|
||||||
const match = text.replace(/^\s*/, "").match(/^\/(.*)\s*$/);
|
const match = text.replace(/^\s*/, "").match(/^\/(.*)\s*$/);
|
||||||
if (match) {
|
if (match) {
|
||||||
@ -1086,16 +1090,6 @@ converse.plugins.add('converse-chatview', {
|
|||||||
return this;
|
return this;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove chat state notifications for a given sender JID.
|
|
||||||
* @private
|
|
||||||
* @method _converse.ChatBoxView#clearChatStateForSender
|
|
||||||
* @param {string} sender - The sender of the chat state
|
|
||||||
*/
|
|
||||||
clearChatStateForSender (sender) {
|
|
||||||
sizzle(`.chat-state-notification[data-csn="${sender}"]`, this.content).forEach(u.removeElement);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Insert a particular string value into the textarea of this chat box.
|
* Insert a particular string value into the textarea of this chat box.
|
||||||
* @private
|
* @private
|
||||||
|
@ -12,7 +12,6 @@ import { debounce } from 'lodash'
|
|||||||
import { render } from "lit-html";
|
import { render } from "lit-html";
|
||||||
import filesize from "filesize";
|
import filesize from "filesize";
|
||||||
import log from "@converse/headless/log";
|
import log from "@converse/headless/log";
|
||||||
import tpl_csn from "templates/csn.html";
|
|
||||||
import tpl_file_progress from "templates/file_progress.html";
|
import tpl_file_progress from "templates/file_progress.html";
|
||||||
import tpl_info from "templates/info.html";
|
import tpl_info from "templates/info.html";
|
||||||
import tpl_message from "templates/message.html";
|
import tpl_message from "templates/message.html";
|
||||||
@ -119,9 +118,7 @@ converse.plugins.add('converse-message-view', {
|
|||||||
|
|
||||||
async render () {
|
async render () {
|
||||||
const is_followup = u.hasClass('chat-msg--followup', this.el);
|
const is_followup = u.hasClass('chat-msg--followup', this.el);
|
||||||
if (this.model.isOnlyChatStateNotification()) {
|
if (this.model.get('file') && !this.model.get('oob_url')) {
|
||||||
this.renderChatStateNotification()
|
|
||||||
} else if (this.model.get('file') && !this.model.get('oob_url')) {
|
|
||||||
if (!this.model.file) {
|
if (!this.model.file) {
|
||||||
log.error("Attempted to render a file upload message with no file data");
|
log.error("Attempted to render a file upload message with no file data");
|
||||||
return this.el;
|
return this.el;
|
||||||
@ -327,38 +324,6 @@ converse.plugins.add('converse-message-view', {
|
|||||||
return this.replaceElement(msg);
|
return this.replaceElement(msg);
|
||||||
},
|
},
|
||||||
|
|
||||||
renderChatStateNotification () {
|
|
||||||
let text;
|
|
||||||
const from = this.model.get('from');
|
|
||||||
const name = this.model.getDisplayName();
|
|
||||||
|
|
||||||
if (this.model.get('chat_state') === _converse.COMPOSING) {
|
|
||||||
if (this.model.get('sender') === 'me') {
|
|
||||||
text = __('Typing from another device');
|
|
||||||
} else {
|
|
||||||
text = __('%1$s is typing', name);
|
|
||||||
}
|
|
||||||
} else if (this.model.get('chat_state') === _converse.PAUSED) {
|
|
||||||
if (this.model.get('sender') === 'me') {
|
|
||||||
text = __('Stopped typing on the other device');
|
|
||||||
} else {
|
|
||||||
text = __('%1$s has stopped typing', name);
|
|
||||||
}
|
|
||||||
} else if (this.model.get('chat_state') === _converse.GONE) {
|
|
||||||
text = __('%1$s has gone away', name);
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const isodate = (new Date()).toISOString();
|
|
||||||
this.replaceElement(
|
|
||||||
u.stringToElement(
|
|
||||||
tpl_csn({
|
|
||||||
'message': text,
|
|
||||||
'from': from,
|
|
||||||
'isodate': isodate
|
|
||||||
})));
|
|
||||||
},
|
|
||||||
|
|
||||||
renderFileUploadProgresBar () {
|
renderFileUploadProgresBar () {
|
||||||
const msg = u.stringToElement(tpl_file_progress(
|
const msg = u.stringToElement(tpl_file_progress(
|
||||||
Object.assign(this.model.toJSON(), {
|
Object.assign(this.model.toJSON(), {
|
||||||
|
@ -15,7 +15,6 @@ import { __ } from '@converse/headless/i18n';
|
|||||||
import converse from "@converse/headless/converse-core";
|
import converse from "@converse/headless/converse-core";
|
||||||
import log from "@converse/headless/log";
|
import log from "@converse/headless/log";
|
||||||
import tpl_add_chatroom_modal from "templates/add_chatroom_modal.js";
|
import tpl_add_chatroom_modal from "templates/add_chatroom_modal.js";
|
||||||
import tpl_chatarea from "templates/chatarea.html";
|
|
||||||
import tpl_chatroom from "templates/chatroom.js";
|
import tpl_chatroom from "templates/chatroom.js";
|
||||||
import tpl_chatroom_bottom_panel from "templates/chatroom_bottom_panel.html";
|
import tpl_chatroom_bottom_panel from "templates/chatroom_bottom_panel.html";
|
||||||
import tpl_chatroom_destroyed from "templates/chatroom_destroyed.html";
|
import tpl_chatroom_destroyed from "templates/chatroom_destroyed.html";
|
||||||
@ -708,8 +707,10 @@ converse.plugins.add('converse-muc-views', {
|
|||||||
this.removeAll();
|
this.removeAll();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.listenTo(this.model, 'change', this.renderHeading);
|
this.listenTo(this.model.csn, 'change', this.renderChatStateNotifications);
|
||||||
this.listenTo(this.model.session, 'change:connection_status', this.onConnectionStatusChanged);
|
this.listenTo(this.model.session, 'change:connection_status', this.onConnectionStatusChanged);
|
||||||
|
|
||||||
|
this.listenTo(this.model, 'change', this.renderHeading);
|
||||||
this.listenTo(this.model, 'change:hidden_occupants', this.updateOccupantsToggle);
|
this.listenTo(this.model, 'change:hidden_occupants', this.updateOccupantsToggle);
|
||||||
this.listenTo(this.model, 'change:subject', this.setChatRoomSubject);
|
this.listenTo(this.model, 'change:subject', this.setChatRoomSubject);
|
||||||
this.listenTo(this.model, 'configurationNeeded', this.getAndRenderConfigurationForm);
|
this.listenTo(this.model, 'configurationNeeded', this.getAndRenderConfigurationForm);
|
||||||
@ -745,10 +746,14 @@ converse.plugins.add('converse-muc-views', {
|
|||||||
|
|
||||||
render () {
|
render () {
|
||||||
this.el.setAttribute('id', this.model.get('box_id'));
|
this.el.setAttribute('id', this.model.get('box_id'));
|
||||||
render(tpl_chatroom(), this.el);
|
render(tpl_chatroom({
|
||||||
|
'muc_show_logs_before_join': _converse.muc_show_logs_before_join,
|
||||||
|
'show_send_button': _converse.show_send_button
|
||||||
|
}), this.el);
|
||||||
this.renderHeading();
|
this.renderHeading();
|
||||||
this.renderChatArea();
|
|
||||||
this.renderBottomPanel();
|
this.renderBottomPanel();
|
||||||
|
this.content = this.el.querySelector('.chat-content');
|
||||||
|
this.csn = this.el.querySelector('.chat-state-notifications');
|
||||||
if (!_converse.muc_show_logs_before_join) {
|
if (!_converse.muc_show_logs_before_join) {
|
||||||
this.model.session.get('connection_status') !== converse.ROOMSTATUS.ENTERED && this.showSpinner();
|
this.model.session.get('connection_status') !== converse.ROOMSTATUS.ENTERED && this.showSpinner();
|
||||||
}
|
}
|
||||||
@ -758,6 +763,47 @@ converse.plugins.add('converse-muc-views', {
|
|||||||
return this;
|
return this;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
renderChatStateNotifications () {
|
||||||
|
const actors_per_state = this.model.csn.toJSON();
|
||||||
|
const message = converse.CHAT_STATES.reduce((result, state) => {
|
||||||
|
const existing_actors = actors_per_state[state];
|
||||||
|
if (!existing_actors) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
const actors = existing_actors
|
||||||
|
.map(a => this.model.getOccupant(a))
|
||||||
|
.filter(a => a)
|
||||||
|
.map(a => a.getDisplayName());
|
||||||
|
|
||||||
|
if (actors.length === 1) {
|
||||||
|
if (state === 'composing') {
|
||||||
|
return `${result} ${__('%1$s is typing', actors[0])}\n`;
|
||||||
|
} else if (state === 'paused') {
|
||||||
|
return `${result} ${__('%1$s has stopped typing', actors[0])}\n`;
|
||||||
|
} else if (state === _converse.GONE) {
|
||||||
|
return `${result} ${__('%1$s has gone away', actors[0])}\n`;
|
||||||
|
}
|
||||||
|
} else if (actors.length > 1) {
|
||||||
|
let actors_str;
|
||||||
|
if (actors.length > 3) {
|
||||||
|
actors_str = `${Array.from(actors).slice(0, 2).join(', ')} and others`;
|
||||||
|
} else {
|
||||||
|
const last_actor = actors.pop();
|
||||||
|
actors_str = __('%1$s and %2$s', actors.join(', '), last_actor);
|
||||||
|
}
|
||||||
|
if (state === 'composing') {
|
||||||
|
return `${result} ${__('%1$s are typing', actors_str)}\n`;
|
||||||
|
} else if (state === 'paused') {
|
||||||
|
return `${result} ${__('%1$s have stopped typing', actors_str)}\n`;
|
||||||
|
} else if (state === _converse.GONE) {
|
||||||
|
return `${result} ${__('%1$s have gone away', actors_str)}\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}, '');
|
||||||
|
this.csn.innerHTML = message;
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the MUC heading if any relevant attributes have changed.
|
* Renders the MUC heading if any relevant attributes have changed.
|
||||||
* @private
|
* @private
|
||||||
@ -780,23 +826,6 @@ converse.plugins.add('converse-muc-views', {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
renderChatArea () {
|
|
||||||
// Render the UI container in which groupchat messages will appear.
|
|
||||||
if (this.el.querySelector('.chat-area') === null) {
|
|
||||||
const container_el = this.el.querySelector('.chatroom-body');
|
|
||||||
container_el.insertAdjacentHTML(
|
|
||||||
'beforeend',
|
|
||||||
tpl_chatarea({
|
|
||||||
__,
|
|
||||||
'muc_show_logs_before_join': _converse.muc_show_logs_before_join,
|
|
||||||
'show_send_button': _converse.show_send_button
|
|
||||||
})
|
|
||||||
);
|
|
||||||
this.content = this.el.querySelector('.chat-content');
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
|
|
||||||
createSidebarView () {
|
createSidebarView () {
|
||||||
this.model.occupants.chatroomview = this;
|
this.model.occupants.chatroomview = this;
|
||||||
this.sidebar_view = new _converse.MUCSidebar({'model': this.model.occupants});
|
this.sidebar_view = new _converse.MUCSidebar({'model': this.model.occupants});
|
||||||
|
@ -150,12 +150,8 @@ converse.plugins.add('converse-chat', {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
isOnlyChatStateNotification () {
|
|
||||||
return u.isOnlyChatStateNotification(this);
|
|
||||||
},
|
|
||||||
|
|
||||||
isEphemeral () {
|
isEphemeral () {
|
||||||
return this.get('is_ephemeral') || u.isOnlyChatStateNotification(this);
|
return this.get('is_ephemeral');
|
||||||
},
|
},
|
||||||
|
|
||||||
getDisplayName () {
|
getDisplayName () {
|
||||||
@ -325,6 +321,7 @@ converse.plugins.add('converse-chat', {
|
|||||||
}
|
}
|
||||||
this.set({'box_id': `box-${btoa(jid)}`});
|
this.set({'box_id': `box-${btoa(jid)}`});
|
||||||
this.initMessages();
|
this.initMessages();
|
||||||
|
this.initCSN();
|
||||||
|
|
||||||
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});
|
||||||
@ -357,6 +354,10 @@ converse.plugins.add('converse-chat', {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
initCSN () {
|
||||||
|
this.csn = new Model();
|
||||||
|
},
|
||||||
|
|
||||||
afterMessagesFetched () {
|
afterMessagesFetched () {
|
||||||
/**
|
/**
|
||||||
* Triggered whenever a `_converse.ChatBox` instance has fetched its messages from
|
* Triggered whenever a `_converse.ChatBox` instance has fetched its messages from
|
||||||
@ -409,8 +410,13 @@ converse.plugins.add('converse-chat', {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.setEditable(attrs, attrs.time, stanza);
|
this.setEditable(attrs, attrs.time, stanza);
|
||||||
|
|
||||||
|
if (attrs['chat_state'] && attrs.sender === 'them') {
|
||||||
|
this.csn.set('chat_state', attrs.chat_state);
|
||||||
|
}
|
||||||
if (u.shouldCreateMessage(attrs)) {
|
if (u.shouldCreateMessage(attrs)) {
|
||||||
const msg = this.handleCorrection(attrs) || await this.createMessage(attrs);
|
const msg = this.handleCorrection(attrs) || await this.createMessage(attrs);
|
||||||
|
this.csn.set({'chat_state': null});
|
||||||
this.incrementUnreadMsgCounter(msg);
|
this.incrementUnreadMsgCounter(msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -945,12 +951,18 @@ converse.plugins.add('converse-chat', {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @async
|
||||||
|
* @private
|
||||||
|
* @method _converse.ChatBox#createMessage
|
||||||
|
*/
|
||||||
createMessage (attrs, options) {
|
createMessage (attrs, options) {
|
||||||
return this.messages.create(attrs, Object.assign({'wait': true, 'promise':true}, options));
|
return this.messages.create(attrs, Object.assign({'wait': true, 'promise':true}, options));
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Responsible for sending off a text message inside an ongoing chat conversation.
|
* Responsible for sending off a text message inside an ongoing chat conversation.
|
||||||
|
* @private
|
||||||
* @method _converse.ChatBox#sendMessage
|
* @method _converse.ChatBox#sendMessage
|
||||||
* @memberOf _converse.ChatBox
|
* @memberOf _converse.ChatBox
|
||||||
* @param { String } text - The chat message text
|
* @param { String } text - The chat message text
|
||||||
@ -1073,7 +1085,6 @@ converse.plugins.add('converse-chat', {
|
|||||||
},
|
},
|
||||||
|
|
||||||
maybeShow () {
|
maybeShow () {
|
||||||
// Returns the chatbox
|
|
||||||
return this.trigger("show");
|
return this.trigger("show");
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -1691,6 +1691,9 @@ window.converse = window.converse || {};
|
|||||||
* @namespace converse
|
* @namespace converse
|
||||||
*/
|
*/
|
||||||
Object.assign(window.converse, {
|
Object.assign(window.converse, {
|
||||||
|
|
||||||
|
CHAT_STATES: ['active', 'composing', 'gone', 'inactive', 'paused'],
|
||||||
|
|
||||||
keycodes: {
|
keycodes: {
|
||||||
TAB: 9,
|
TAB: 9,
|
||||||
ENTER: 13,
|
ENTER: 13,
|
||||||
|
@ -354,6 +354,7 @@ converse.plugins.add('converse-muc', {
|
|||||||
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.initMessages();
|
||||||
|
this.initCSN();
|
||||||
this.initOccupants();
|
this.initOccupants();
|
||||||
this.initDiscoModels(); // sendChatState depends on this.features
|
this.initDiscoModels(); // sendChatState depends on this.features
|
||||||
this.registerHandlers();
|
this.registerHandlers();
|
||||||
@ -1818,6 +1819,36 @@ converse.plugins.add('converse-muc', {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
removeCSNFor (actor, state) {
|
||||||
|
const actors_per_state = this.csn.toJSON();
|
||||||
|
const existing_actors = Array.from(actors_per_state[state]) || [];
|
||||||
|
if (existing_actors.includes(actor)) {
|
||||||
|
const idx = existing_actors.indexOf(actor);
|
||||||
|
existing_actors.splice(idx, 1);
|
||||||
|
this.csn.set(state, Array.from(existing_actors));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateCSN (attrs) {
|
||||||
|
const actor = attrs.nick;
|
||||||
|
const state = attrs.chat_state;
|
||||||
|
const actors_per_state = this.csn.toJSON();
|
||||||
|
const existing_actors = actors_per_state[state] || [];
|
||||||
|
if (existing_actors.includes(actor)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const new_actors_per_state = converse.CHAT_STATES.reduce((out, s) => {
|
||||||
|
if (s === state) {
|
||||||
|
out[s] = [...existing_actors, actor];
|
||||||
|
} else {
|
||||||
|
out[s] = (actors_per_state[s] || []).filter(a => a !== actor);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}, {});
|
||||||
|
this.csn.set(new_actors_per_state);
|
||||||
|
window.setTimeout(() => this.removeCSNFor(actor, state), 10000);
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handler for all MUC messages sent to this groupchat. This method
|
* Handler for all MUC messages sent to this groupchat. This method
|
||||||
* shouldn't be called directly, instead {@link _converse.ChatRoom#queueMessage}
|
* shouldn't be called directly, instead {@link _converse.ChatRoom#queueMessage}
|
||||||
@ -1865,7 +1896,9 @@ converse.plugins.add('converse-muc', {
|
|||||||
}
|
}
|
||||||
this.setEditable(attrs, attrs.time);
|
this.setEditable(attrs, attrs.time);
|
||||||
|
|
||||||
if (u.shouldCreateGroupchatMessage(attrs)) {
|
if (attrs['chat_state']) {
|
||||||
|
this.updateCSN(attrs);
|
||||||
|
} else if (u.shouldCreateGroupchatMessage(attrs)) {
|
||||||
const msg = this.handleCorrection(attrs) || await this.createMessage(attrs);
|
const msg = this.handleCorrection(attrs) || await this.createMessage(attrs);
|
||||||
this.incrementUnreadMsgCounter(msg);
|
this.incrementUnreadMsgCounter(msg);
|
||||||
}
|
}
|
||||||
|
@ -124,8 +124,7 @@ u.isNewMessage = function (message) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
u.shouldCreateMessage = function (attrs) {
|
u.shouldCreateMessage = function (attrs) {
|
||||||
return attrs['chat_state'] ||
|
return attrs['retracted'] || // Retraction received *before* the message
|
||||||
attrs['retracted'] || // Retraction received *before* the message
|
|
||||||
!u.isEmptyMessage(attrs);
|
!u.isEmptyMessage(attrs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
<div class="chat-area col">
|
|
||||||
<div class="chat-content {[ if (o.show_send_button) { ]}chat-content-sendbutton{[ } ]}" aria-live="polite">
|
|
||||||
{[ if (o.muc_show_logs_before_join) { ]}
|
|
||||||
<div class="empty-history-feedback"><span>{{{o.__('No message history available.')}}}</span></div>
|
|
||||||
{[ } ]}
|
|
||||||
</div>
|
|
||||||
<div class="bottom-panel"></div>
|
|
||||||
</div>
|
|
@ -4,9 +4,8 @@ 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' : '' }"
|
<div class="chat-content ${ o.show_send_button ? 'chat-content-sendbutton' : '' }" @scroll=${o.markScrolled} aria-live="polite"></div>
|
||||||
@scroll=${o.markScrolled}
|
<div class="chat-state-notifications"></div>
|
||||||
aria-live="polite"></div>
|
|
||||||
<div class="bottom-panel">
|
<div class="bottom-panel">
|
||||||
<div class="emoji-picker__container dropup"></div>
|
<div class="emoji-picker__container dropup"></div>
|
||||||
<div class="message-form-container">
|
<div class="message-form-container">
|
||||||
|
@ -1,10 +1,20 @@
|
|||||||
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 () => html`
|
export default (o) => html`
|
||||||
<div class="flyout box-flyout">
|
<div class="flyout box-flyout">
|
||||||
<div class="chat-head chat-head-chatroom row no-gutters"></div>
|
<div class="chat-head chat-head-chatroom row no-gutters"></div>
|
||||||
<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-content ${ o.show_send_button ? 'chat-content-sendbutton' : '' }" aria-live="polite">
|
||||||
|
${ o.muc_show_logs_before_join ? html`<div class="empty-history-feedback"><span>${ i18n_no_history }</span></div>` : '' }
|
||||||
|
</div>
|
||||||
|
<div class="chat-state-notifications"></div>
|
||||||
|
<div class="bottom-panel"></div>
|
||||||
|
</div>
|
||||||
<div class="disconnect-container hidden"></div>
|
<div class="disconnect-container hidden"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
<div class="message chat-info chat-state-notification"
|
|
||||||
data-isodate="{{{o.isodate}}}"
|
|
||||||
data-csn="{{{o.from}}}">{{{o.message}}}</div>
|
|
Loading…
Reference in New Issue
Block a user