Set 'scrolled' flag on model.ui

This prevents it from being persisted across page loads and makes more
sense logically.

Also move markScrolled to utils and MUC unread messages indicator to bottom panel.
This commit is contained in:
JC Brand 2021-06-17 10:43:08 +02:00
parent 5ea9564cc3
commit b6f2662ad7
15 changed files with 87 additions and 74 deletions

View File

@ -33,8 +33,8 @@ const ChatBox = ModelWithContact.extend({
'message_type': 'chat',
'nickname': undefined,
'num_unread': 0,
'time_sent': (new Date(0)).toISOString(),
'time_opened': this.get('time_opened') || (new Date()).getTime(),
'time_sent': (new Date(0)).toISOString(),
'type': _converse.PRIVATE_CHAT_TYPE,
'url': ''
}
@ -65,7 +65,7 @@ const ChatBox = ModelWithContact.extend({
this.presence.on('change:show', item => this.onPresenceChanged(item));
}
this.on('change:chat_state', this.sendChatState, this);
this.on('change:scrolled', this.onScrolledChanged, this);
this.ui.on('change:scrolled', this.onScrolledChanged, this);
await this.fetchMessages();
/**
@ -249,7 +249,7 @@ const ChatBox = ModelWithContact.extend({
onMessageAdded (message) {
if (api.settings.get('prune_messages_above') &&
(api.settings.get('pruning_behavior') === 'scrolled' || !this.get('scrolled')) &&
(api.settings.get('pruning_behavior') === 'scrolled' || !this.ui.get('scrolled')) &&
!u.isEmptyMessage(message)
) {
debouncedPruneHistory(this);
@ -331,7 +331,7 @@ const ChatBox = ModelWithContact.extend({
},
onScrolledChanged () {
if (!this.get('scrolled')) {
if (!this.ui.get('scrolled')) {
this.clearUnreadMsgCounter();
this.pruneHistoryWhenScrolledDown();
}
@ -1072,8 +1072,8 @@ const ChatBox = ModelWithContact.extend({
// gets scrolled down. We always want to scroll down
// when the user writes a message as opposed to when a
// message is received.
this.set('scrolled', false);
} else if (this.isHidden() || this.get('scrolled')) {
this.ui.set('scrolled', false);
} else if (this.isHidden() || this.ui.get('scrolled')) {
const settings = {
'num_unread': this.get('num_unread') + 1
};
@ -1095,7 +1095,7 @@ const ChatBox = ModelWithContact.extend({
},
isScrolledUp () {
return this.get('scrolled');
return this.ui.get('scrolled');
}
});

View File

@ -97,8 +97,8 @@ const ChatRoomMixin = {
this.on('change:chat_state', this.sendChatState, this);
this.on('change:hidden', this.onHiddenChange, this);
this.on('change:scrolled', this.onScrolledChanged, this);
this.on('destroy', this.removeHandlers, this);
this.ui.on('change:scrolled', this.onScrolledChanged, this);
await this.restoreSession();
this.session.on('change:connection_status', this.onConnectionStatusChanged, this);
@ -2585,8 +2585,8 @@ const ChatRoomMixin = {
// 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.isHidden() || this.get('scrolled')) {
this.ui.set('scrolled', false);
} else if (this.isHidden() || this.ui.get('scrolled')) {
const settings = {
'num_unread_general': this.get('num_unread_general') + 1
};

View File

@ -12,7 +12,7 @@ describe("A Groupchat Message", function () {
const muc_jid = 'lounge@montague.lit';
const model = await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
expect(model.get('scrolled')).toBeFalsy();
expect(model.ui.get('scrolled')).toBeFalsy();
model.sendMessage('1st message');
model.sendMessage('2nd message');
@ -25,7 +25,7 @@ describe("A Groupchat Message", function () {
await u.waitUntil(() => model.messages.length === 4);
await u.waitUntil(() => model.messages.length === 3, 550);
model.set('scrolled', true);
model.ui.set('scrolled', true);
model.sendMessage('5th message');
model.sendMessage('6th message');
await u.waitUntil(() => model.messages.length === 5);
@ -33,7 +33,7 @@ describe("A Groupchat Message", function () {
// Wait long enough to be sure the debounced pruneHistory method didn't fire.
await new Promise(resolve => setTimeout(resolve, 550));
expect(model.messages.length).toBe(5);
model.set('scrolled', false);
model.ui.set('scrolled', false);
await u.waitUntil(() => model.messages.length === 3, 550);
// Test incoming messages

View File

@ -41,7 +41,7 @@ export default class ChatBottomPanel extends ElementView {
viewUnreadMessages (ev) {
ev?.preventDefault?.();
this.model.save({ 'scrolled': false });
this.model.ui.set({ 'scrolled': false });
}
emitFocused (ev) {

View File

@ -12,7 +12,7 @@ export default (o) => {
const show_spoiler_button = api.settings.get('visible_toolbar_buttons').spoiler;
const show_toolbar = api.settings.get('show_toolbar');
return html`
${ o.model.get('scrolled') && o.model.get('num_unread') ?
${ o.model.ui.get('scrolled') && o.model.get('num_unread') ?
html`<div class="new-msgs-indicator" @click=${ev => o.viewUnreadMessages(ev)}>▼ ${ unread_msgs } ▼</div>` : '' }
${api.settings.get('show_toolbar') ? html`
<converse-chat-toolbar

View File

@ -964,7 +964,7 @@ describe("Chatboxes", function () {
const view = await mock.openChatBoxFor(_converse, sender_jid)
const sent_stanzas = [];
spyOn(_converse.connection, 'send').and.callFake(s => sent_stanzas.push(s?.nodeTree ?? s));
view.model.save('scrolled', true);
view.model.ui.set('scrolled', true);
await _converse.handleMessageStanza(msg);
await u.waitUntil(() => view.model.messages.length);
expect(view.model.get('num_unread')).toBe(1);
@ -1026,7 +1026,7 @@ describe("Chatboxes", function () {
const sent_stanzas = [];
spyOn(_converse.connection, 'send').and.callFake(s => sent_stanzas.push(s?.nodeTree ?? s));
const chatbox = _converse.chatboxes.get(sender_jid);
chatbox.save('scrolled', true);
chatbox.ui.set('scrolled', true);
_converse.windowState = 'hidden';
const msg = msgFactory();
_converse.handleMessageStanza(msg);
@ -1075,7 +1075,7 @@ describe("Chatboxes", function () {
const sent_stanzas = [];
spyOn(_converse.connection, 'send').and.callFake(s => sent_stanzas.push(s?.nodeTree ?? s));
const chatbox = _converse.chatboxes.get(sender_jid);
chatbox.save('scrolled', true);
chatbox.ui.set('scrolled', true);
_converse.windowState = 'hidden';
const msg = msgFactory();
_converse.handleMessageStanza(msg);
@ -1105,7 +1105,7 @@ describe("Chatboxes", function () {
await u.waitUntil(() => rosterview.querySelectorAll('.roster-group').length, 500);
await mock.openChatBoxFor(_converse, sender_jid);
const chatbox = _converse.chatboxes.get(sender_jid);
chatbox.save('scrolled', true);
chatbox.ui.set('scrolled', true);
msg = mock.createChatMessage(_converse, sender_jid, 'This message will be unread');
await _converse.handleMessageStanza(msg);
await u.waitUntil(() => chatbox.messages.length);
@ -1186,7 +1186,7 @@ describe("Chatboxes", function () {
const msgFactory = () => mock.createChatMessage(_converse, sender_jid, 'This message will be received as unread, but eventually will be read');
const selector = `a.open-chat:contains("${chatbox.get('nickname')}") .msgs-indicator`;
const select_msgs_indicator = () => sizzle(selector, rosterview).pop();
chatbox.save('scrolled', true);
chatbox.ui.set('scrolled', true);
_converse.handleMessageStanza(msgFactory());
const view = _converse.chatboxviews.get(sender_jid);
await u.waitUntil(() => view.model.messages.length);
@ -1211,7 +1211,7 @@ describe("Chatboxes", function () {
const msgFactory = () => mock.createChatMessage(_converse, sender_jid, msg);
const selector = 'a.open-chat:contains("' + chatbox.get('nickname') + '") .msgs-indicator';
const select_msgs_indicator = () => sizzle(selector, rosterview).pop();
chatbox.save('scrolled', true);
chatbox.ui.set('scrolled', true);
_converse.handleMessageStanza(msgFactory());
await u.waitUntil(() => view.model.messages.length);
expect(select_msgs_indicator().textContent).toBe('1');

View File

@ -1197,7 +1197,7 @@ describe("A Chat Message", function () {
// Create enough messages so that there's a scrollbar.
const promises = [];
view.querySelector('.chat-content').scrollTop = 0;
view.model.set('scrolled', true);
view.model.ui.set('scrolled', true);
for (let i=0; i<20; i++) {
_converse.handleMessageStanza($msg({
@ -1213,7 +1213,7 @@ describe("A Chat Message", function () {
const indicator_el = await u.waitUntil(() => view.querySelector('.new-msgs-indicator'));
expect(view.model.get('scrolled')).toBe(true);
expect(view.model.ui.get('scrolled')).toBe(true);
expect(view.querySelector('.chat-content').scrollTop).toBe(0);
indicator_el.click();
await u.waitUntil(() => !view.querySelector('.new-msgs-indicator'));

View File

@ -190,7 +190,7 @@ describe("A Minimized ChatBoxView's Unread Message Count", function () {
const minimized_chats = document.querySelector("converse-minimized-chats")
const selectUnreadMsgCount = () => minimized_chats.querySelector('#toggle-minimized-chats .unread-message-count');
const chatbox = _converse.chatboxes.get(sender_jid);
chatbox.save('scrolled', true);
chatbox.ui.set('scrolled', true);
_converse.handleMessageStanza(msgFactory());
await u.waitUntil(() => chatbox.messages.length);
const view = _converse.chatboxviews.get(sender_jid);

View File

@ -9,13 +9,14 @@ export default class MUCMessageForm extends MessageForm {
toHTML () {
return tpl_muc_message_form(
Object.assign(this.model.toJSON(), {
'onDrop': ev => this.onDrop(ev),
'hint_value': this.querySelector('.spoiler-hint')?.value,
'message_value': this.querySelector('.chat-textarea')?.value,
'onChange': ev => this.model.set({'draft': ev.target.value}),
'onDrop': ev => this.onDrop(ev),
'onKeyDown': ev => this.onKeyDown(ev),
'onKeyUp': ev => this.onKeyUp(ev),
'onPaste': ev => this.onPaste(ev),
'scrolled': this.model.ui.get('scrolled'),
'viewUnreadMessages': ev => this.viewUnreadMessages(ev)
}));
}

View File

@ -5,13 +5,10 @@ import { resetElementHeight } from 'plugins/chatview/utils.js';
export default (o) => {
const unread_msgs = __('You have unread messages');
const label_message = o.composing_spoiler ? __('Hidden message') : __('Message');
const label_spoiler_hint = __('Optional hint');
const show_send_button = api.settings.get('show_send_button');
return html`
${ (o.scrolled && o.num_unread) ? html`<div class="new-msgs-indicator" @click=${ev => o.viewUnreadMessages(ev)}>▼ ${ unread_msgs } ▼</div>` : '' }
<form class="setNicknameButtonForm hidden">
<input type="submit" class="btn btn-primary" name="join" value="Join"/>
</form>

View File

@ -7,6 +7,7 @@ import { html } from "lit";
const tpl_can_edit = (o) => {
const unread_msgs = __('You have unread messages');
const message_limit = api.settings.get('message_limit');
const show_call_button = api.settings.get('visible_toolbar_buttons').call;
const show_emoji_button = api.settings.get('visible_toolbar_buttons').emoji;
@ -14,6 +15,8 @@ const tpl_can_edit = (o) => {
const show_spoiler_button = api.settings.get('visible_toolbar_buttons').spoiler;
const show_toolbar = api.settings.get('show_toolbar');
return html`
${ (o.model.ui.get('scrolled') && o.model.get('num_unread')) ?
html`<div class="new-msgs-indicator" @click=${ev => o.viewUnreadMessages(ev)}>▼ ${ unread_msgs } ▼</div>` : '' }
${show_toolbar ? html`
<converse-chat-toolbar
class="chat-toolbar no-text-select"
@ -38,7 +41,7 @@ export default (o) => {
const i18n_not_allowed = __("You're not allowed to send messages in this room");
if (conn_status === converse.ROOMSTATUS.ENTERED) {
return html`
${ o.model.get('scrolled') && o.model.get('num_unread_general') ?
${ o.model.ui.get('scrolled') && o.model.get('num_unread_general') ?
html`<div class="new-msgs-indicator" @click=${ev => o.viewUnreadMessages(ev)}>▼ ${ unread_msgs } ▼</div>` : '' }
${(o.can_edit) ? tpl_can_edit(o) : html`<span class="muc-bottom-panel muc-bottom-panel--muted">${i18n_not_allowed}</span>`}`;
} else if (conn_status == converse.ROOMSTATUS.NICKNAME_REQUIRED) {

View File

@ -277,13 +277,13 @@ describe("Groupchats", function () {
<delay xmlns="urn:xmpp:delay" stamp="2020-07-14T17:46:47Z" from="juliet@shakespeare.lit"/>
</message>`);
view.model.save('scrolled', true); // hack
view.model.ui.set('scrolled', true); // hack
_converse.connection._dataRecv(mock.createRequest(message));
await u.waitUntil(() => view.model.messages.length);
const chat_new_msgs_indicator = await u.waitUntil(() => view.querySelector('.new-msgs-indicator'));
chat_new_msgs_indicator.click();
expect(view.model.get('scrolled')).toBeFalsy();
expect(view.model.ui.get('scrolled')).toBeFalsy();
await u.waitUntil(() => !u.isVisible(chat_new_msgs_indicator));
done();
}));

View File

@ -98,8 +98,8 @@ export default class BaseChatView extends CustomElement {
scrollDown (ev) {
ev?.preventDefault?.();
ev?.stopPropagation?.();
if (this.model.get('scrolled')) {
u.safeSave(this.model, { 'scrolled': false });
if (this.model.ui.get('scrolled')) {
this.model.ui.set({ 'scrolled': false });
}
onScrolledDown(this.model);
}

View File

@ -1,10 +1,8 @@
import './message-history';
import debounce from 'lodash/debounce';
import { CustomElement } from 'shared/components/element.js';
import { _converse, api } from '@converse/headless/core';
import { html } from 'lit';
import { onScrolledDown } from './utils.js';
import { safeSave } from '@converse/headless/utils/core.js';
import { markScrolled } from './utils.js';
import './styles/chat-content.scss';
@ -19,11 +17,17 @@ export default class ChatContent extends CustomElement {
connectedCallback () {
super.connectedCallback();
this.markScrolled = debounce(this._markScrolled, 50);
this.initialize();
}
disconnectedCallback () {
super.disconnectedCallback();
this.removeEventListener('scroll', markScrolled);
}
initialize () {
this.model = _converse.chatboxes.get(this.jid);
this.listenTo(this.model, 'change:hidden_occupants', this.requestUpdate);
this.listenTo(this.model, 'change:scrolled', this.scrollDown);
this.listenTo(this.model.messages, 'add', this.requestUpdate);
this.listenTo(this.model.messages, 'change', this.requestUpdate);
this.listenTo(this.model.messages, 'remove', this.requestUpdate);
@ -31,11 +35,12 @@ export default class ChatContent extends CustomElement {
this.listenTo(this.model.messages, 'reset', this.requestUpdate);
this.listenTo(this.model.notifications, 'change', this.requestUpdate);
this.listenTo(this.model.ui, 'change', this.requestUpdate);
this.listenTo(this.model.ui, 'change:scrolled', this.scrollDown);
if (this.model.occupants) {
this.listenTo(this.model.occupants, 'change', this.requestUpdate);
}
this.addEventListener('scroll', () => this.markScrolled());
this.addEventListener('scroll', markScrolled);
}
render () {
@ -51,41 +56,8 @@ export default class ChatContent extends CustomElement {
`;
}
/**
* Called when the chat content is scrolled up or down.
* We want to record when the user has scrolled away from
* the bottom, so that we don't automatically scroll away
* from what the user is reading when new messages are received.
*
* Don't call this method directly, instead, call `markScrolled`,
* which debounces this method by 100ms.
* @private
*/
_markScrolled () {
let scrolled = true;
const is_at_bottom = this.scrollTop === 0;
const is_at_top =
Math.ceil(this.clientHeight-this.scrollTop) >= (this.scrollHeight-Math.ceil(this.scrollHeight/20));
if (is_at_bottom) {
scrolled = false;
onScrolledDown(this.model);
} else if (is_at_top) {
/**
* Triggered once the chat's message area has been scrolled to the top
* @event _converse#chatBoxScrolledUp
* @property { _converse.ChatBoxView | _converse.ChatRoomView } view
* @example _converse.api.listen.on('chatBoxScrolledUp', obj => { ... });
*/
api.trigger('chatBoxScrolledUp', this);
}
if (this.model.get('scolled') !== scrolled) {
safeSave(this.model, { scrolled });
}
}
scrollDown () {
if (this.model.get('scrolled')) {
if (this.model.ui.get('scrolled')) {
return;
}
if (this.scrollTo) {

View File

@ -1,9 +1,10 @@
import debounce from 'lodash/debounce';
import tpl_new_day from "./templates/new-day.js";
import { _converse, api, converse } from '@converse/headless/core';
const { dayjs } = converse.env;
export function onScrolledDown (model) {
function onScrolledDown (model) {
if (!model.isHidden()) {
if (api.settings.get('allow_url_history_change')) {
// Clear location hash if set to one of the messages in our history
@ -13,6 +14,45 @@ export function onScrolledDown (model) {
}
}
/**
* Called when the chat content is scrolled up or down.
* We want to record when the user has scrolled away from
* the bottom, so that we don't automatically scroll away
* from what the user is reading when new messages are received.
*
* Don't call this method directly, instead, call `markScrolled`,
* which debounces this method.
*/
function _markScrolled (ev) {
const el = ev.target;
if (el.nodeName.toLowerCase() !== 'converse-chat-content') {
return;
}
let scrolled = true;
const is_at_bottom = Math.floor(el.scrollTop) === 0;
const is_at_top =
Math.ceil(el.clientHeight-el.scrollTop) >= (el.scrollHeight-Math.ceil(el.scrollHeight/20));
if (is_at_bottom) {
scrolled = false;
onScrolledDown(el.model);
} else if (is_at_top) {
/**
* Triggered once the chat's message area has been scrolled to the top
* @event _converse#chatBoxScrolledUp
* @property { _converse.ChatBoxView | _converse.ChatRoomView } view
* @example _converse.api.listen.on('chatBoxScrolledUp', obj => { ... });
*/
api.trigger('chatBoxScrolledUp', el);
}
if (el.model.get('scolled') !== scrolled) {
el.model.ui.set({ scrolled });
}
}
export const markScrolled = debounce((ev) => _markScrolled(ev), 50);
/**
* Given a message object, returns a TemplateResult indicating a new day if
* the passed in message is more than a day later than its predecessor.