Re-add support for a new messages indicator

Fixes #2040
This commit is contained in:
JC Brand 2020-06-01 16:05:00 +02:00
parent ccd817cce1
commit ac36adddfe
8 changed files with 64 additions and 45 deletions

View File

@ -1353,7 +1353,7 @@ describe("Chatboxes", function () {
done(); done();
})); }));
it("is incremeted when message is received, chatbox is scrolled down and the window is not focused", it("is incremented when message is received, chatbox is scrolled down and the window is not focused",
mock.initConverse(['rosterGroupsFetched', 'chatBoxesFetched'], {}, mock.initConverse(['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) { async function (done, _converse) {
@ -1375,7 +1375,7 @@ describe("Chatboxes", function () {
done(); done();
})); }));
it("is incremeted when message is received, chatbox is scrolled up and the window is not focused", it("is incremented when message is received, chatbox is scrolled up and the window is not focused",
mock.initConverse( mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {}, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) { async function (done, _converse) {

View File

@ -6,6 +6,30 @@ const u = converse.env.utils;
describe("A Chat Message", function () { describe("A Chat Message", function () {
it("will be demarcated if it's the first newly received message",
mock.initConverse(['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) {
await mock.waitForRoster(_converse, 'current', 1);
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await mock.openChatBoxFor(_converse, contact_jid);
const view = _converse.api.chatviews.get(contact_jid);
await _converse.handleMessageStanza(mock.createChatMessage(_converse, contact_jid, 'This message will be read'));
_converse.windowState = 'hidden';
await _converse.handleMessageStanza(mock.createChatMessage(_converse, contact_jid, 'This message will be new'));
await u.waitUntil(() => view.model.messages.length);
expect(view.model.get('num_unread')).toBe(1);
expect(view.model.get('first_unread_id')).toBe(view.model.messages.last().get('id'));
await u.waitUntil(() => view.el.querySelectorAll('converse-chat-message').length === 2);
const last_msg_el = view.el.querySelector('converse-chat-message:last-child');
expect(last_msg_el.firstElementChild?.textContent).toBe('New messages');
done();
}));
it("is rejected if it's an unencapsulated forwarded message", it("is rejected if it's an unencapsulated forwarded message",
mock.initConverse( mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {}, ['rosterGroupsFetched', 'chatBoxesFetched'], {},

View File

@ -4,7 +4,7 @@ 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; jasmine.DEFAULT_TIMEOUT_INTERVAL = 7000;
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") {

View File

@ -74,7 +74,8 @@ describe("A sent presence stanza", function () {
spyOn(_converse.connection, 'send').and.callThrough(); spyOn(_converse.connection, 'send').and.callThrough();
const cbview = _converse.chatboxviews.get('controlbox'); const cbview = _converse.chatboxviews.get('controlbox');
cbview.el.querySelector('.change-status').click() const change_status_el = await u.waitUntil(() => cbview.el.querySelector('.change-status'));
change_status_el.click()
const modal = _converse.xmppstatusview.status_modal; const modal = _converse.xmppstatusview.status_modal;
await u.waitUntil(() => u.isVisible(modal.el), 1000); await u.waitUntil(() => u.isVisible(modal.el), 1000);
const msg = 'My custom status'; const msg = 'My custom status';

View File

@ -20,6 +20,7 @@ const tpl_message = (o) => html`
?has_mentions=${o.has_mentions} ?has_mentions=${o.has_mentions}
?is_delayed=${o.is_delayed} ?is_delayed=${o.is_delayed}
?is_encrypted=${o.is_encrypted} ?is_encrypted=${o.is_encrypted}
?is_first_unread=${o.is_first_unread}
?is_me_message=${o.is_me_message} ?is_me_message=${o.is_me_message}
?is_only_emojis=${o.is_only_emojis} ?is_only_emojis=${o.is_only_emojis}
?is_retracted=${o.is_retracted} ?is_retracted=${o.is_retracted}
@ -67,6 +68,18 @@ function getDayIndicator (model) {
} }
} }
function getHats (model) {
if (model.get('type') === 'groupchat') {
if (api.settings.get('muc_hats_from_vcard')) {
const role = model.vcard ? model.vcard.get('role') : null;
return role ? role.split(',') : [];
} else {
return model.occupant?.get('hats') || [];
}
}
return [];
}
class MessageHistory extends CustomElement { class MessageHistory extends CustomElement {
@ -91,30 +104,18 @@ class MessageHistory extends CustomElement {
} }
const day = getDayIndicator(model); const day = getDayIndicator(model);
const templates = day ? [day] : []; const templates = day ? [day] : [];
const is_retracted = model.get('retracted') || model.get('moderated') === 'retracted';
const is_groupchat = model.get('type') === 'groupchat'; 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 chatbox = this.chatview.model;
const has_mentions = is_groupchat && model.get('sender') === 'them' && chatbox.isUserMentioned(model);
const message = tpl_message( const message = tpl_message(
Object.assign(model.toJSON(), { Object.assign(model.toJSON(), {
'chatview': this.chatview, 'chatview': this.chatview,
'has_mentions': is_groupchat && model.get('sender') === 'them' && chatbox.isUserMentioned(model),
'hats': getHats(model),
'is_first_unread': chatbox.get('first_unread_id') === model.get('id'),
'is_me_message': model.isMeCommand(), 'is_me_message': model.isMeCommand(),
'is_retracted': model.get('retracted') || model.get('moderated') === 'retracted',
'occupant': model.occupant, 'occupant': model.occupant,
'username': model.getDisplayName(), 'username': model.getDisplayName(),
has_mentions,
hats,
is_retracted,
model, model,
})); }));
return [...templates, message]; return [...templates, message];

View File

@ -17,7 +17,8 @@ const i18n_edit_message = __('Edit this message');
const i18n_edited = __('This message has been edited'); const i18n_edited = __('This message has been edited');
const i18n_show = __('Show more'); const i18n_show = __('Show more');
const i18n_show_less = __('Show less'); const i18n_show_less = __('Show less');
const i18n_uploading = __('Uploading file:') const i18n_uploading = __('Uploading file:');
const i18n_new_messages = __('New messages');
class Message extends CustomElement { class Message extends CustomElement {
@ -30,19 +31,19 @@ class Message extends CustomElement {
editable: { type: Boolean }, editable: { type: Boolean },
error: { type: String }, error: { type: String },
error_text: { type: String }, error_text: { type: String },
first_unread: { type: Boolean },
from: { type: String }, from: { type: String },
has_mentions: { type: Boolean }, has_mentions: { type: Boolean },
hats: { type: Array }, hats: { type: Array },
edited: { type: String },
is_delayed: { type: Boolean }, is_delayed: { type: Boolean },
is_encrypted: { type: Boolean }, is_encrypted: { type: Boolean },
is_first_unread: { type: Boolean },
is_me_message: { type: Boolean }, is_me_message: { type: Boolean },
is_only_emojis: { type: Boolean }, is_only_emojis: { type: Boolean },
is_retracted: { type: Boolean }, is_retracted: { type: Boolean },
is_spoiler: { type: Boolean }, is_spoiler: { type: Boolean },
is_spoiler_visible: { type: Boolean }, is_spoiler_visible: { type: Boolean },
message_type: { type: String }, message_type: { type: String },
edited: { type: String },
model: { type: Object }, model: { type: Object },
moderated_by: { type: String }, moderated_by: { type: String },
moderation_reason: { type: String }, moderation_reason: { type: String },
@ -125,6 +126,7 @@ class Message extends CustomElement {
renderChatMessage () { renderChatMessage () {
const is_groupchat_message = (this.message_type === 'groupchat'); const is_groupchat_message = (this.message_type === 'groupchat');
return html` return html`
${ this.is_first_unread ? html`<div class="message date-separator"><hr class="separator"><span class="separator-text">${ i18n_new_messages }</span></div>` : '' }
<div class="message chat-msg ${this.message_type} ${this.getExtraMessageClasses()} <div class="message chat-msg ${this.message_type} ${this.getExtraMessageClasses()}
${ this.is_me_message ? 'chat-msg--action' : '' } ${ this.is_me_message ? 'chat-msg--action' : '' }
${this.isFollowup() ? 'chat-msg--followup' : ''}" ${this.isFollowup() ? 'chat-msg--followup' : ''}"
@ -132,7 +134,6 @@ class Message extends CustomElement {
${ renderAvatar(this) } ${ renderAvatar(this) }
<div class="chat-msg__content chat-msg__content--${this.sender} ${this.is_me_message ? 'chat-msg__content--action' : ''}"> <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"> <span class="chat-msg__heading">
${ (this.is_me_message) ? html` ${ (this.is_me_message) ? html`
<time timestamp="${this.time}" class="chat-msg__time">${this.pretty_time}</time> <time timestamp="${this.time}" class="chat-msg__time">${this.pretty_time}</time>

View File

@ -1145,26 +1145,14 @@ converse.plugins.add('converse-chat', {
return; return;
} }
if (utils.isNewMessage(message) && this.isHidden()) { if (utils.isNewMessage(message) && this.isHidden()) {
this.setFirstUnreadMsgId(message); const settings = {
this.save({'num_unread': this.get('num_unread') + 1}); 'num_unread': this.get('num_unread') + 1
_converse.incrementMsgCounter(); };
} if (this.get('num_unread') === 0) {
}, settings['first_unread_id'] = message.get('id');
/**
* Sets the msgid of the first unread realtime message in a ChatBox.
* @param {_converse.Message} message
*/
setFirstUnreadMsgId (message) {
if (this.get('num_unread') == 0) {
const first_unread_id = this.get('first_unread_id');
if (first_unread_id) {
const msg = this.messages.get(first_unread_id);
if (msg) msg.save("first_unread", false);
} }
message.save("first_unread", true); this.save(settings);
this.save({'first_unread_id': message.get('id')}); _converse.incrementMsgCounter();
} }
}, },

View File

@ -2408,8 +2408,12 @@ converse.plugins.add('converse-muc', {
const body = message.get('message'); const body = message.get('message');
if (!body) { return; } if (!body) { return; }
if (u.isNewMessage(message) && this.isHidden()) { if (u.isNewMessage(message) && this.isHidden()) {
this.setFirstUnreadMsgId(message); const settings = {
const settings = {'num_unread_general': this.get('num_unread_general') + 1}; 'num_unread_general': this.get('num_unread_general') + 1
};
if (this.get('num_unread') === 0) {
settings['first_unread_id'] = message.get('id');
}
if (this.isUserMentioned(message)) { if (this.isUserMentioned(message)) {
settings.num_unread = this.get('num_unread') + 1; settings.num_unread = this.get('num_unread') + 1;
_converse.incrementMsgCounter(); _converse.incrementMsgCounter();