Commit dd91d3cc authored by JC Brand's avatar JC Brand

Use flexbox to keep the chat scrolled down

By using `display: flex` and `flex-direction: column-reverse`, the chat
now automatically scrolls down when loaded, without requiring any
extra JavaScript.

We still need to scroll down with JavaScript when sending a message.

By using `column-reverse`, the messages container now works in reverse.
So the newest message is the first element in the container and the
oldest message is the last. This is the reverse of before.

Due to this, this change will likely break some plugins.
parent 21b0f246
......@@ -19,6 +19,7 @@
"window": true
},
"rules": {
"lodash/prefer-lodash-chain": "off",
"lodash/prefer-lodash-method": [2, {
"ignoreMethods": [
"assign", "every", "keys", "find", "endsWith", "startsWith", "filter",
......
......@@ -56,6 +56,8 @@
- Removed events `statusChanged` and `statusMessageChanged`. Instead, you can
listen on the `change:status` or `change:status\_message` events on
`_converse.xmppstatus`.
- Use flexbox instead of JavaScript to keep chat scrolled down. Due to this
change, messages are now inserted into the DOM in reverse order than before.
### API changes
......
......@@ -2408,11 +2408,19 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/backbone/-/backbone-1.4.0.tgz",
"integrity": "sha512-RLmDrRXkVdouTg38jcgHhyQ/2zjg7a8E6sz2zxfz21Hh17xDJYUHBZimVIt5fUyS8vbfpeSmTL3gUjTEvUV3qQ==",
"dev": true,
"requires": {
"underscore": ">=1.8.3"
}
},
"backbone.browserStorage": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/backbone.browserStorage/-/backbone.browserStorage-0.0.5.tgz",
"integrity": "sha512-Cf8B90EIWyHMm/ReS5yFmFMOXPVNda6QcTFcdyp1RW/1zM3LZF2Nf4U601/seIaEu/X8cRVEKqTINpPKql3sxA==",
"requires": {
"backbone": "~1.x.x",
"underscore": ">=1.4.0"
}
},
"backbone.nativeview": {
"version": "github:conversejs/Backbone.NativeView#5997c8197ca594e6b8469447f28310c78bd1d95e",
"from": "github:conversejs/Backbone.NativeView#5997c8197ca594e6b8469447f28310c78bd1d95e",
......@@ -7096,8 +7104,7 @@
"lodash": {
"version": "4.17.11",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==",
"dev": true
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="
},
"lodash-template-webpack-loader": {
"version": "github:jcbrand/lodash-template-webpack-loader#258c095ab22130dfde454fa59ee0986f302bb733",
......@@ -12004,6 +12011,14 @@
"find-up": "^2.1.0"
}
},
"pluggable.js": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/pluggable.js/-/pluggable.js-2.0.1.tgz",
"integrity": "sha512-SBt6v6Tbp20Jf8hU0cpcc/+HBHGMY8/Q+yA6Ih0tBQE8tfdZ6U4PRG0iNvUUjLx/hVyOP53n0UfGBymlfaaXCg==",
"requires": {
"lodash": "^4.17.11"
}
},
"po2json": {
"version": "0.4.5",
"resolved": "https://registry.npmjs.org/po2json/-/po2json-0.4.5.tgz",
......@@ -13733,6 +13748,10 @@
"through": "^2.3.4"
}
},
"strophe.js": {
"version": "github:strophe/strophejs#31f31b52fd37a92eebee7b47d668a7d7dc40df3b",
"from": "github:strophe/strophejs#31f31b52fd37a92eebee7b47d668a7d7dc40df3b"
},
"style-loader": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/style-loader/-/style-loader-0.23.1.tgz",
......@@ -14163,6 +14182,11 @@
"integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
"dev": true
},
"twemoji": {
"version": "11.3.0",
"resolved": "https://registry.npmjs.org/twemoji/-/twemoji-11.3.0.tgz",
"integrity": "sha512-xN/vlR6+gDmfjt6LInAqwGAv3Agwrmzx5TD1jEFwKS19IOGDrX0/3OB8GP1wUYPVIdkaer5hw6qd+52jzvz0Lg=="
},
"type-check": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
......@@ -14250,8 +14274,7 @@
"underscore": {
"version": "1.8.3",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz",
"integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=",
"dev": true
"integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI="
},
"underscore-contrib": {
"version": "0.3.0",
......
......@@ -207,6 +207,9 @@
margin-bottom: 0.25em;
}
.chat-content {
display: flex;
flex-direction: column-reverse;
padding: 1em;
height: 100%;
font-size: var(--message-font-size);
color: var(--text-color);
......
......@@ -33,7 +33,6 @@
color: var(--separator-text-color);
display: inline-block;
line-height: 2em;
padding: 0 1em;
position: relative;
z-index: 5;
}
......@@ -44,7 +43,7 @@
font-size: var(--message-font-size);
line-height: var(--line-height-small);
font-size: 90%;
padding: 0.17rem 1rem;
padding: 0.17rem 0;
&.badge {
color: var(--chat-head-text-color);
......@@ -77,11 +76,10 @@
}
&.chat-msg {
display: inline-flex;
display: flex;
width: 100%;
flex-direction: row;
overflow: auto; // Ensures that content stays inside
padding: 0.125rem 1rem;
padding: 0.125rem 0;
&.onload {
animation: colorchange-chatmessage 1s;
......@@ -152,7 +150,6 @@
display: flex;
flex-direction: row;
justify-content: space-between;
width: 100%;
}
.chat-msg__message {
......@@ -269,7 +266,7 @@
}
&.chat-msg--action {
.chat-msg__content {
flex-wrap: nowrap;
flex-wrap: wrap;
flex-direction: row;
justify-content: flex-start;
}
......
......@@ -44,7 +44,7 @@
}).c('body').t('hello world').tree();
await _converse.chatboxes.onMessage(msg);
await test_utils.waitUntil(() => view.content.querySelectorAll('.chat-msg').length);
expect(view.content.lastElementChild.textContent.trim().indexOf('hello world')).not.toBe(-1);
expect(view.content.firstElementChild.textContent.trim().indexOf('hello world')).not.toBe(-1);
done();
}));
......@@ -78,22 +78,22 @@
message = '/me is as well';
await test_utils.sendMessage(view, message);
expect(view.el.querySelectorAll('.chat-msg--action').length).toBe(2);
await test_utils.waitUntil(() => sizzle('.chat-msg__author:last', view.el).pop().textContent.trim() === '**Romeo Montague');
const last_el = sizzle('.chat-msg__text:last', view.el).pop();
await test_utils.waitUntil(() => sizzle('.chat-msg__author:first', view.el).pop().textContent.trim() === '**Romeo Montague');
const last_el = sizzle('.chat-msg__text:first', view.el).pop();
expect(last_el.textContent).toBe('is as well');
expect(u.hasClass('chat-msg--followup', last_el)).toBe(false);
// Check that /me messages after a normal message don't
// get the 'chat-msg--followup' class.
message = 'This a normal message';
await test_utils.sendMessage(view, message);
let message_el = view.el.querySelector('.message:last-child');
let message_el = view.el.querySelector('.message:first-child');
expect(u.hasClass('chat-msg--followup', message_el)).toBeFalsy();
message = '/me wrote a 3rd person message';
await test_utils.sendMessage(view, message);
message_el = view.el.querySelector('.message:last-child');
message_el = view.el.querySelector('.message:first-child');
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(u.isVisible(sizzle('.chat-msg__author:last', view.el).pop())).toBeTruthy();
expect(sizzle('.chat-msg__text:first', view.el).pop().textContent).toBe('wrote a 3rd person message');
expect(u.isVisible(sizzle('.chat-msg__author:first', view.el).pop())).toBeTruthy();
expect(u.hasClass('chat-msg--followup', message_el)).toBeFalsy();
done();
}));
......@@ -267,7 +267,7 @@
const jid = el.textContent.replace(/ /g,'.').toLowerCase() + '@montague.lit';
spyOn(_converse.api, "trigger");
el.click();
await test_utils.waitUntil(() => _converse.api.trigger.calls.count(), 500);
await test_utils.waitUntil(() => _converse.api.trigger.calls.count(), 1000);
expect(_converse.chatboxes.length).toEqual(2);
expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxFocused', jasmine.any(Object));
done();
......@@ -361,7 +361,6 @@
spyOn(_converse.api, "trigger");
// We need to rebind all events otherwise our spy won't be called
chatview.delegateEvents();
chatview.el.querySelector('.toggle-chatbox-button').click();
expect(chatview.minimize).toHaveBeenCalled();
......@@ -377,7 +376,6 @@
expect(trimmedview.restore).toHaveBeenCalled();
expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxMaximized', jasmine.any(Object));
await test_utils.waitUntil(() => u.isVisible(chatview.el.querySelector('.chat-body')), 500);
const toggle_el = sizzle('.toggle-chatbox-button', chatview.el).pop();
expect(u.hasClass('fa-minus', toggle_el)).toBeTruthy();
expect(u.hasClass('fa-plus', toggle_el)).toBeFalsy();
......
This diff is collapsed.
This diff is collapsed.
......@@ -208,7 +208,7 @@
_converse.connection._dataRecv(test_utils.createRequest(stanza));
await new Promise((resolve, reject) => view.once('messageInserted', resolve));
expect(view.model.messages.length).toBe(2);
expect(view.el.querySelectorAll('.chat-msg__body')[1].textContent.trim())
expect(view.el.querySelectorAll('.chat-msg__body')[0].textContent.trim())
.toBe('This is an encrypted message from the contact');
// #1193 Check for a received message without <body> tag
......@@ -228,7 +228,7 @@
await new Promise((resolve, reject) => view.once('messageInserted', resolve));
await test_utils.waitUntil(() => view.model.messages.length > 1);
expect(view.model.messages.length).toBe(3);
expect(view.el.querySelectorAll('.chat-msg__body')[2].textContent.trim())
expect(view.el.querySelectorAll('.chat-msg__body')[0].textContent.trim())
.toBe('Another received encrypted message without fallback');
done();
}));
......
This diff is collapsed.
......@@ -470,7 +470,6 @@ converse.plugins.add('converse-muc-views', {
this.initDebounced();
this.model.messages.on('add', this.onMessageAdded, this);
this.model.messages.on('rendered', this.scrollDown, this);
this.model.messages.on('reset', () => {
this.content.innerHTML = '';
this.removeAll();
......@@ -680,7 +679,6 @@ converse.plugins.add('converse-muc-views', {
this.model.clearUnreadMsgCounter();
this.model.save();
}
this.scrollDown();
this.renderEmojiPicker();
},
......@@ -706,7 +704,6 @@ converse.plugins.add('converse-muc-views', {
} else if (conn_status === converse.ROOMSTATUS.ENTERED) {
this.hideSpinner();
this.setChatState(_converse.ACTIVE);
this.scrollDown();
this.focus();
} else if (conn_status === converse.ROOMSTATUS.DISCONNECTED) {
this.showDisconnectMessage();
......@@ -762,7 +759,6 @@ converse.plugins.add('converse-muc-views', {
ev.stopPropagation();
}
this.model.save({'hidden_occupants': true});
this.scrollDown();
},
toggleOccupants (ev) {
......@@ -774,7 +770,6 @@ converse.plugins.add('converse-muc-views', {
ev.stopPropagation();
}
this.model.save({'hidden_occupants': !this.model.get('hidden_occupants')});
this.scrollDown();
},
onOccupantClicked (ev) {
......@@ -1297,18 +1292,23 @@ converse.plugins.add('converse-muc-views', {
}
},
/**
* Working backwards, get the first join/leave notification
* from the same user, on the same day and BEFORE any chat
* messages were received.
* @private
* @method _converse.ChatRoomView#getPreviousJoinOrLeaveNotification
* @param {HTMLElement} el
* @param {string} nick
*/
getPreviousJoinOrLeaveNotification (el, nick) {
/* Working backwards, get the first join/leave notification
* from the same user, on the same day and BEFORE any chat
* messages were received.
*/
while (!_.isNil(el)) {
const data = _.get(el, 'dataset', {});
if (!_.includes(_.get(el, 'classList', []), 'chat-info')) {
return;
}
if (!dayjs(el.getAttribute('data-isodate')).isSame(new Date(), "day")) {
el = el.previousElementSibling;
el = el.nextElementSibling;
continue;
}
if (data.join === nick ||
......@@ -1317,7 +1317,7 @@ converse.plugins.add('converse-muc-views', {
data.joinleave === nick) {
return el;
}
el = el.previousElementSibling;
el = el.nextElementSibling;
}
},
......@@ -1328,7 +1328,7 @@ converse.plugins.add('converse-muc-views', {
}
const nick = occupant.get('nick'),
stat = _converse.muc_show_join_leave_status ? occupant.get('status') : null,
prev_info_el = this.getPreviousJoinOrLeaveNotification(this.content.lastElementChild, nick),
prev_info_el = this.getPreviousJoinOrLeaveNotification(this.content.firstElementChild, nick),
data = _.get(prev_info_el, 'dataset', {});
if (data.leave === nick) {
......@@ -1346,8 +1346,8 @@ converse.plugins.add('converse-muc-views', {
'message': message
};
this.content.removeChild(prev_info_el);
this.content.insertAdjacentHTML('beforeend', tpl_info(data));
const el = this.content.lastElementChild;
this.content.insertAdjacentHTML('afterBegin', tpl_info(data));
const el = this.content.firstElementChild;
setTimeout(() => u.addClass('fade-out', el), 5000);
setTimeout(() => el.parentElement && el.parentElement.removeChild(el), 5500);
} else {
......@@ -1366,13 +1366,12 @@ converse.plugins.add('converse-muc-views', {
};
if (prev_info_el) {
this.content.removeChild(prev_info_el);
this.content.insertAdjacentHTML('beforeend', tpl_info(data));
this.content.insertAdjacentHTML('afterBegin', tpl_info(data));
} else {
this.content.insertAdjacentHTML('beforeend', tpl_info(data));
this.insertDayIndicator(this.content.lastElementChild);
this.content.insertAdjacentHTML('afterBegin', tpl_info(data));
this.insertDayIndicator(this.content.firstElementChild);
}
}
this.scrollDown();
},
showLeaveNotification (occupant) {
......@@ -1383,7 +1382,7 @@ converse.plugins.add('converse-muc-views', {
}
const nick = occupant.get('nick'),
stat = _converse.muc_show_join_leave_status ? occupant.get('status') : null,
prev_info_el = this.getPreviousJoinOrLeaveNotification(this.content.lastElementChild, nick),
prev_info_el = this.getPreviousJoinOrLeaveNotification(this.content.firstElementChild, nick),
dataset = _.get(prev_info_el, 'dataset', {});
if (dataset.join === nick) {
......@@ -1401,8 +1400,8 @@ converse.plugins.add('converse-muc-views', {
'message': message
};
this.content.removeChild(prev_info_el);
this.content.insertAdjacentHTML('beforeend', tpl_info(data));
const el = this.content.lastElementChild;
this.content.insertAdjacentHTML('afterBegin', tpl_info(data));
const el = this.content.firstElementChild;
setTimeout(() => u.addClass('fade-out', el), 5000);
setTimeout(() => el.parentElement && el.parentElement.removeChild(el), 5500);
} else {
......@@ -1421,13 +1420,12 @@ converse.plugins.add('converse-muc-views', {
}
if (prev_info_el) {
this.content.removeChild(prev_info_el);
this.content.insertAdjacentHTML('beforeend', tpl_info(data));
this.content.insertAdjacentHTML('afterBegin', tpl_info(data));
} else {
this.content.insertAdjacentHTML('beforeend', tpl_info(data));
this.insertDayIndicator(this.content.lastElementChild);
this.content.insertAdjacentHTML('afterBegin', tpl_info(data));
this.insertDayIndicator(this.content.firstElementChild);
}
}
this.scrollDown();
},
renderAfterTransition () {
......@@ -1442,7 +1440,6 @@ converse.plugins.add('converse-muc-views', {
} else {
u.showElement(this.el.querySelector('.chat-area'));
u.showElement(this.el.querySelector('.occupants'));
this.scrollDown();
}
},
......@@ -1492,7 +1489,6 @@ converse.plugins.add('converse-muc-views', {
'render_message': true
}));
}
this.scrollDown();
}
});
......
......@@ -258,7 +258,7 @@ u.getLastChildElement = function (el, selector='*') {
}
u.hasClass = function (className, el) {
return _.includes(el.classList, className);
return Array.from(el.classList).includes(className);
};
u.addClass = function (className, el) {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment