Declarative scrolling and rendering new messages indicator
- Increment `num_unread` when new messages appear while scrolled up - Set scrolling state in model code (as opposed to view)
This commit is contained in:
parent
ec93e2fff3
commit
fe3e63d8c5
@ -1036,7 +1036,13 @@ const ChatBox = ModelWithContact.extend({
|
||||
return
|
||||
}
|
||||
if (u.isNewMessage(message)) {
|
||||
if (this.isHidden()) {
|
||||
if (message.get('sender') === 'me') {
|
||||
// We remove the "scrolled" flag so that the chat area
|
||||
// gets scrolled down. We always want to scroll down
|
||||
// when the user writes a message as opposed to when a
|
||||
// message is received.
|
||||
this.model.set('scrolled', false);
|
||||
} else if (this.isHidden() || this.get('scrolled')) {
|
||||
const settings = {
|
||||
'num_unread': this.get('num_unread') + 1
|
||||
};
|
||||
|
@ -2,7 +2,7 @@ import tpl_chatbox_message_form from './templates/chatbox_message_form.js';
|
||||
import tpl_toolbar from './templates/toolbar.js';
|
||||
import { ElementView } from '@converse/skeletor/src/element.js';
|
||||
import { __ } from 'i18n';
|
||||
import { _converse, api, converse } from "@converse/headless/core";
|
||||
import { _converse, api, converse } from '@converse/headless/core';
|
||||
import { html, render } from 'lit';
|
||||
import { clearMessages, parseMessageForCommands } from './utils.js';
|
||||
|
||||
@ -11,20 +11,24 @@ import './styles/chat-bottom-panel.scss';
|
||||
const { u } = converse.env;
|
||||
|
||||
export default class ChatBottomPanel extends ElementView {
|
||||
|
||||
events = {
|
||||
'click .send-button': 'onFormSubmitted',
|
||||
'click .toggle-clear': 'clearMessages',
|
||||
}
|
||||
'click .toggle-clear': 'clearMessages'
|
||||
};
|
||||
|
||||
async connectedCallback () {
|
||||
super.connectedCallback();
|
||||
this.model = _converse.chatboxes.get(this.getAttribute('jid'));
|
||||
this.listenTo(this.model, 'change:composing_spoiler', this.renderMessageForm);
|
||||
this.listenTo(this.model, 'change', o => this.onModelChanged(o.changed));
|
||||
await this.model.initialized;
|
||||
this.listenTo(this.model.messages, 'change:correcting', this.onMessageCorrecting);
|
||||
this.render();
|
||||
api.listen.on('chatBoxScrolledDown', () => this.hideNewMessagesIndicator());
|
||||
}
|
||||
|
||||
onModelChanged (changed) {
|
||||
if ('composing_spoiler' in changed || 'num_unread' in changed || 'scrolled' in changed) {
|
||||
this.renderMessageForm();
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
@ -36,7 +40,8 @@ export default class ChatBottomPanel extends ElementView {
|
||||
if (!api.settings.get('show_toolbar')) {
|
||||
return this;
|
||||
}
|
||||
const options = Object.assign({
|
||||
const options = Object.assign(
|
||||
{
|
||||
'model': this.model,
|
||||
'chatview': _converse.chatboxviews.get(this.getAttribute('jid'))
|
||||
},
|
||||
@ -62,17 +67,12 @@ export default class ChatBottomPanel extends ElementView {
|
||||
'onDrop': ev => this.onDrop(ev),
|
||||
'hint_value': this.querySelector('.spoiler-hint')?.value,
|
||||
'inputChanged': ev => this.inputChanged(ev),
|
||||
'label_message': this.model.get('composing_spoiler') ? __('Hidden message') : __('Message'),
|
||||
'label_spoiler_hint': __('Optional hint'),
|
||||
'message_value': this.querySelector('.chat-textarea')?.value,
|
||||
'onChange': ev => this.updateCharCounter(ev.target.value),
|
||||
'onKeyDown': ev => this.onKeyDown(ev),
|
||||
'onKeyUp': ev => this.onKeyUp(ev),
|
||||
'onPaste': ev => this.onPaste(ev),
|
||||
'show_send_button': api.settings.get('show_send_button'),
|
||||
'show_toolbar': api.settings.get('show_toolbar'),
|
||||
'unread_msgs': __('You have unread messages'),
|
||||
'viewUnreadMessages': ev => this.viewUnreadMessages(ev),
|
||||
'viewUnreadMessages': ev => this.viewUnreadMessages(ev)
|
||||
})
|
||||
),
|
||||
form_container
|
||||
@ -85,11 +85,6 @@ export default class ChatBottomPanel extends ElementView {
|
||||
viewUnreadMessages (ev) {
|
||||
ev?.preventDefault?.();
|
||||
this.model.save({ 'scrolled': false, 'scrollTop': null });
|
||||
_converse.chatboxviews.get(this.getAttribute('jid'))?.scrollDown();
|
||||
}
|
||||
|
||||
hideNewMessagesIndicator () {
|
||||
this.querySelector('.new-msgs-indicator')?.classList.add('hidden');
|
||||
}
|
||||
|
||||
onMessageCorrecting (message) {
|
||||
@ -244,7 +239,7 @@ export default class ChatBottomPanel extends ElementView {
|
||||
}
|
||||
const ev = document.createEvent('HTMLEvents');
|
||||
ev.initEvent('change', false, true);
|
||||
textarea.dispatchEvent(ev)
|
||||
textarea.dispatchEvent(ev);
|
||||
u.placeCaretAtEnd(textarea);
|
||||
}
|
||||
|
||||
|
@ -1,31 +1,39 @@
|
||||
import { __ } from 'i18n';
|
||||
import { api } from "@converse/headless/core";
|
||||
import { html } from "lit";
|
||||
|
||||
|
||||
export default (o) => html`
|
||||
<div class="new-msgs-indicator hidden" @click=${ev => o.viewUnreadMessages(ev)}>▼ ${ o.unread_msgs } ▼</div>
|
||||
<form class="setNicknameButtonForm hidden">
|
||||
<input type="submit" class="btn btn-primary" name="join" value="Join"/>
|
||||
</form>
|
||||
<form class="sendXMPPMessage">
|
||||
<span class="chat-toolbar no-text-select"></span>
|
||||
<input type="text" placeholder="${o.label_spoiler_hint || ''}" value="${o.hint_value || ''}" class="${o.composing_spoiler ? '' : 'hidden'} spoiler-hint"/>
|
||||
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');
|
||||
|
||||
<div class="suggestion-box">
|
||||
<ul class="suggestion-box__results suggestion-box__results--above" hidden=""></ul>
|
||||
<textarea
|
||||
autofocus
|
||||
type="text"
|
||||
@drop=${o.onDrop}
|
||||
@input=${o.inputChanged}
|
||||
@keydown=${o.onKeyDown}
|
||||
@keyup=${o.onKeyUp}
|
||||
@paste=${o.onPaste}
|
||||
@change=${o.onChange}
|
||||
class="chat-textarea suggestion-box__input
|
||||
${ o.show_send_button ? 'chat-textarea-send-button' : '' }
|
||||
${ o.composing_spoile ? 'spoiler' : '' }"
|
||||
placeholder="${o.label_message}">${ o.message_value || '' }</textarea>
|
||||
<span class="suggestion-box__additions visually-hidden" role="status" aria-live="assertive" aria-relevant="additions"></span>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
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>
|
||||
<form class="sendXMPPMessage">
|
||||
<span class="chat-toolbar no-text-select"></span>
|
||||
<input type="text" placeholder="${label_spoiler_hint || ''}" value="${o.hint_value || ''}" class="${o.composing_spoiler ? '' : 'hidden'} spoiler-hint"/>
|
||||
|
||||
<div class="suggestion-box">
|
||||
<ul class="suggestion-box__results suggestion-box__results--above" hidden=""></ul>
|
||||
<textarea
|
||||
autofocus
|
||||
type="text"
|
||||
@drop=${o.onDrop}
|
||||
@input=${o.inputChanged}
|
||||
@keydown=${o.onKeyDown}
|
||||
@keyup=${o.onKeyUp}
|
||||
@paste=${o.onPaste}
|
||||
@change=${o.onChange}
|
||||
class="chat-textarea suggestion-box__input
|
||||
${ show_send_button ? 'chat-textarea-send-button' : '' }
|
||||
${ o.composing_spoiler ? 'spoiler' : '' }"
|
||||
placeholder="${label_message}">${ o.message_value || '' }</textarea>
|
||||
<span class="suggestion-box__additions visually-hidden" role="status" aria-live="assertive" aria-relevant="additions"></span>
|
||||
</div>
|
||||
</form>`;
|
||||
}
|
||||
|
@ -32,7 +32,6 @@ export default class ChatView extends BaseChatView {
|
||||
this.render();
|
||||
|
||||
// Need to be registered after render has been called.
|
||||
this.listenTo(this.model.messages, 'add', this.onMessageAdded);
|
||||
this.listenTo(this.model, 'change:show_help_messages', this.renderHelpMessages);
|
||||
|
||||
await this.model.messages.fetched;
|
||||
|
@ -78,7 +78,6 @@ export default class MUCBottomPanel extends BottomPanel {
|
||||
ev?.preventDefault?.();
|
||||
ev?.stopPropagation?.();
|
||||
this.model.save({ 'hidden_occupants': true });
|
||||
_converse.chatboxviews.get(this.getAttribute('jid'))?.scrollDown();
|
||||
}
|
||||
|
||||
onKeyDown (ev) {
|
||||
|
@ -29,7 +29,6 @@ export default class MUCView extends BaseChatView {
|
||||
await this.render();
|
||||
|
||||
// Need to be registered after render has been called.
|
||||
this.listenTo(this.model.messages, 'add', this.onMessageAdded);
|
||||
this.listenTo(this.model.occupants, 'change:show', this.showJoinOrLeaveNotification);
|
||||
|
||||
this.updateAfterTransition();
|
||||
|
@ -39,8 +39,6 @@ export default class MUCSidebar extends CustomElement {
|
||||
ev?.preventDefault?.();
|
||||
ev?.stopPropagation?.();
|
||||
u.safeSave(this.model, { 'hidden_occupants': true });
|
||||
// FIXME: do this declaratively
|
||||
_converse.chatboxviews.get(this.jid)?.scrollDown();
|
||||
}
|
||||
|
||||
onOccupantClicked (ev) {
|
||||
|
@ -1934,7 +1934,6 @@ describe("Groupchats", function () {
|
||||
const message = 'This message is received while the chat area is scrolled up';
|
||||
await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
|
||||
const view = _converse.chatboxviews.get('lounge@montague.lit');
|
||||
spyOn(view, 'scrollDown').and.callThrough();
|
||||
// Create enough messages so that there's a scrollbar.
|
||||
const promises = [];
|
||||
for (let i=0; i<20; i++) {
|
||||
|
@ -9,7 +9,6 @@ export default class BaseChatView extends ElementView {
|
||||
|
||||
initDebounced () {
|
||||
this.markScrolled = debounce(this._markScrolled, 100);
|
||||
this.debouncedScrollDown = debounce(this.scrollDown, 100);
|
||||
}
|
||||
|
||||
disconnectedCallback () {
|
||||
@ -18,13 +17,6 @@ export default class BaseChatView extends ElementView {
|
||||
_converse.chatboxviews.remove(jid, this);
|
||||
}
|
||||
|
||||
hideNewMessagesIndicator () {
|
||||
const new_msgs_indicator = this.querySelector('.new-msgs-indicator');
|
||||
if (new_msgs_indicator !== null) {
|
||||
new_msgs_indicator.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
maybeFocus () {
|
||||
api.settings.get('auto_focus') && this.focus();
|
||||
}
|
||||
@ -77,20 +69,6 @@ export default class BaseChatView extends ElementView {
|
||||
api.trigger('chatBoxFocused', this, ev);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to the previously saved scrollTop position, or scroll
|
||||
* down if it wasn't set.
|
||||
*/
|
||||
maintainScrollTop () {
|
||||
const pos = this.model.get('scrollTop');
|
||||
if (pos) {
|
||||
const msgs_container = this.querySelector('.chat-content__messages');
|
||||
msgs_container.scrollTop = pos;
|
||||
} else {
|
||||
this.scrollDown();
|
||||
}
|
||||
}
|
||||
|
||||
onStatusMessageChanged (item) {
|
||||
this.renderHeading();
|
||||
/**
|
||||
@ -107,24 +85,6 @@ export default class BaseChatView extends ElementView {
|
||||
});
|
||||
}
|
||||
|
||||
showNewMessagesIndicator () {
|
||||
u.showElement(this.querySelector('.new-msgs-indicator'));
|
||||
}
|
||||
|
||||
onMessageAdded (message) {
|
||||
if (u.isNewMessage(message)) {
|
||||
if (message.get('sender') === 'me') {
|
||||
// We remove the "scrolled" flag so that the chat area
|
||||
// gets scrolled down. We always want to scroll down
|
||||
// when the user writes a message as opposed to when a
|
||||
// message is received.
|
||||
this.model.set('scrolled', false);
|
||||
} else if (this.model.get('scrolled', true)) {
|
||||
this.showNewMessagesIndicator();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getBottomPanel () {
|
||||
if (this.model.get('type') === _converse.CHATROOMS_TYPE) {
|
||||
return this.querySelector('converse-muc-bottom-panel');
|
||||
@ -183,12 +143,10 @@ export default class BaseChatView extends ElementView {
|
||||
'scrollTop': null
|
||||
});
|
||||
}
|
||||
this.querySelector('.chat-content__messages')?.scrollDown();
|
||||
this.onScrolledDown();
|
||||
}
|
||||
|
||||
onScrolledDown () {
|
||||
this.hideNewMessagesIndicator();
|
||||
if (!this.model.isHidden()) {
|
||||
this.model.clearUnreadMsgCounter();
|
||||
if (api.settings.get('allow_url_history_change')) {
|
||||
@ -197,14 +155,6 @@ export default class BaseChatView extends ElementView {
|
||||
hash && this.model.messages.get(hash.slice(1)) && _converse.router.history.navigate();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Triggered once the chat's message area has been scrolled down to the bottom.
|
||||
* @event _converse#chatBoxScrolledDown
|
||||
* @type {object}
|
||||
* @property { _converse.ChatBox | _converse.ChatRoom } chatbox - The chat model
|
||||
* @example _converse.api.listen.on('chatBoxScrolledDown', obj => { ... });
|
||||
*/
|
||||
api.trigger('chatBoxScrolledDown', { 'chatbox': this.model }); // TODO: clean up
|
||||
}
|
||||
|
||||
onWindowStateChanged (data) {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import "./message-history";
|
||||
import debounce from 'lodash-es/debounce';
|
||||
import './message-history';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { CustomElement } from 'shared/components/element.js';
|
||||
import { _converse, api } from "@converse/headless/core";
|
||||
import { _converse, api } from '@converse/headless/core';
|
||||
import { html } from 'lit';
|
||||
|
||||
export default class ChatContent extends CustomElement {
|
||||
@ -14,14 +14,19 @@ export default class ChatContent extends CustomElement {
|
||||
|
||||
connectedCallback () {
|
||||
super.connectedCallback();
|
||||
this.debouncedScrolldown = debounce(this.scrollDown, 100);
|
||||
this.debouncedMaintainScroll = debounce(this.maintainScrollPosition, 100);
|
||||
|
||||
this.model = _converse.chatboxes.get(this.jid);
|
||||
this.listenTo(this.model, 'change:hidden_occupants', this.requestUpdate);
|
||||
this.listenTo(this.model, 'change:scrolled', this.requestUpdate);
|
||||
this.listenTo(this.model.messages, 'add', this.requestUpdate);
|
||||
this.listenTo(this.model.messages, 'change', this.requestUpdate);
|
||||
this.listenTo(this.model.messages, 'remove', this.requestUpdate);
|
||||
this.listenTo(this.model.messages, 'rendered', this.requestUpdate);
|
||||
this.listenTo(this.model.messages, 'reset', this.requestUpdate);
|
||||
this.listenTo(this.model.notifications, 'change', this.requestUpdate);
|
||||
this.listenTo(this.model.ui, 'change', this.requestUpdate);
|
||||
|
||||
if (this.model.occupants) {
|
||||
this.listenTo(this.model.occupants, 'change', this.requestUpdate);
|
||||
}
|
||||
@ -31,7 +36,7 @@ export default class ChatContent extends CustomElement {
|
||||
// didn't initiate the scrolling.
|
||||
this.was_scrolled_up = this.model.get('scrolled');
|
||||
this.addEventListener('imageLoaded', () => {
|
||||
!this.was_scrolled_up && this.scrollDown();
|
||||
this.debouncedMaintainScroll(this.was_scrolled_up);
|
||||
});
|
||||
}
|
||||
|
||||
@ -47,7 +52,19 @@ export default class ChatContent extends CustomElement {
|
||||
}
|
||||
|
||||
updated () {
|
||||
!this.model.get('scrolled') && this.debouncedScrolldown();
|
||||
this.was_scrolled_up = this.model.get('scrolled');
|
||||
this.debouncedMaintainScroll();
|
||||
}
|
||||
|
||||
maintainScrollPosition () {
|
||||
if (this.was_scrolled_up) {
|
||||
const pos = this.model.get('scrollTop');
|
||||
if (pos) {
|
||||
this.scrollTop = pos;
|
||||
}
|
||||
} else {
|
||||
this.scrollDown();
|
||||
}
|
||||
}
|
||||
|
||||
scrollDown () {
|
||||
@ -57,6 +74,14 @@ export default class ChatContent extends CustomElement {
|
||||
} else {
|
||||
this.scrollTop = this.scrollHeight;
|
||||
}
|
||||
/**
|
||||
* Triggered once the converse-chat-content element has been scrolled down to the bottom.
|
||||
* @event _converse#chatBoxScrolledDown
|
||||
* @type {object}
|
||||
* @property { _converse.ChatBox | _converse.ChatRoom } chatbox - The chat model
|
||||
* @example _converse.api.listen.on('chatBoxScrolledDown', obj => { ... });
|
||||
*/
|
||||
api.trigger('chatBoxScrolledDown', { 'chatbox': this.model });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { CustomElement } from 'shared/components/element.js';
|
||||
import { _converse, api } from "@converse/headless/core";
|
||||
import { api } from "@converse/headless/core";
|
||||
import tpl_unfurl from './templates/unfurl.js';
|
||||
|
||||
import './styles/unfurl.scss';
|
||||
@ -29,7 +29,7 @@ export default class MessageUnfurl extends CustomElement {
|
||||
}
|
||||
|
||||
onImageLoad () {
|
||||
_converse.chatboxviews.get(this.getAttribute('jid'))?.scrollDown();
|
||||
this.dispatchEvent(new CustomEvent('imageLoaded', { detail: this, 'bubbles': true }));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -28,7 +28,7 @@ class RichTextRenderer {
|
||||
class RichTextDirective extends Directive {
|
||||
render (text, offset, mentions, options, callback) { // eslint-disable-line class-methods-use-this
|
||||
const renderer = new RichTextRenderer(text, offset, mentions, options);
|
||||
const result =renderer.render();
|
||||
const result = renderer.render();
|
||||
callback?.();
|
||||
return result;
|
||||
}
|
||||
|
@ -30,7 +30,7 @@
|
||||
modtools_disable_assign: ['owner', 'moderator', 'participant', 'visitor'],
|
||||
modtools_disable_query: ['moderator', 'participant', 'visitor'],
|
||||
enable_smacks: true,
|
||||
connection_options: { 'worker': '/dist/shared-connection-worker.js' },
|
||||
// connection_options: { 'worker': '/dist/shared-connection-worker.js' },
|
||||
persistent_store: 'IndexedDB',
|
||||
message_archiving: 'always',
|
||||
muc_domain: 'conference.chat.example.org',
|
||||
|
Loading…
Reference in New Issue
Block a user