DEV: uses container resize event instead of mutation (#20757)

This commit takes advantage of the `ResizeObserver` to know when dates should be re-computed, it works like this:

```
scrollable-div
--  child-enclosing-div with resize observer
---- message 1
---- message 2
---- message x
```

It also switches to bottom/height for date separators sizing, instead of bottom/top, it prevents a bug where setting the top of the first item (at the top) would cause scrollbar to move to top.

<!-- NOTE: All pull requests should have tests (rspec in Ruby, qunit in JavaScript). If your code does not include test coverage, please include an explanation of why it was omitted. -->
This commit is contained in:
Joffrey JAFFEUX 2023-03-21 11:30:32 +01:00 committed by GitHub
parent 0a06974a8a
commit 92797109ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 71 additions and 63 deletions

View File

@ -41,14 +41,11 @@
<div <div
class="chat-messages-scroll chat-messages-container" class="chat-messages-scroll chat-messages-container"
{{on "scroll" this.computeScrollState passive=true}} {{on "scroll" this.computeScrollState passive=true}}
{{chat/on-throttled-scroll this.resetIdle (hash delay=500)}} {{chat/on-scroll this.resetIdle (hash delay=500)}}
{{chat/on-throttled-scroll this.computeArrow (hash delay=150)}} {{chat/on-scroll this.computeArrow (hash delay=150)}}
> >
<div class="chat-message-actions-desktop-anchor"></div> <div class="chat-message-actions-desktop-anchor"></div>
<div <div class="chat-messages-container" {{chat/on-resize this.didResizePane}}>
class="chat-messages-container"
{{chat/did-mutate-childlist this.computeDatesSeparators}}
>
{{#if this.loadedOnce}} {{#if this.loadedOnce}}
{{#each @channel.messages key="id" as |message|}} {{#each @channel.messages key="id" as |message|}}
<ChatMessage <ChatMessage
@ -67,7 +64,6 @@
@resendStagedMessage={{this.resendStagedMessage}} @resendStagedMessage={{this.resendStagedMessage}}
@messageDidEnterViewport={{this.messageDidEnterViewport}} @messageDidEnterViewport={{this.messageDidEnterViewport}}
@messageDidLeaveViewport={{this.messageDidLeaveViewport}} @messageDidLeaveViewport={{this.messageDidLeaveViewport}}
@forceRendering={{this.forceRendering}}
/> />
{{/each}} {{/each}}
{{else}} {{else}}
@ -75,6 +71,7 @@
{{/if}} {{/if}}
</div> </div>
{{! at bottom even if shown at top due to column-reverse }}
{{#if (and this.loadedOnce (not @channel.messagesManager.canLoadMorePast))}} {{#if (and this.loadedOnce (not @channel.messagesManager.canLoadMorePast))}}
<div class="all-loaded-message"> <div class="all-loaded-message">
{{i18n "chat.all_loaded"}} {{i18n "chat.all_loaded"}}

View File

@ -4,11 +4,10 @@ import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
import ChatMessageDraft from "discourse/plugins/chat/discourse/models/chat-message-draft"; import ChatMessageDraft from "discourse/plugins/chat/discourse/models/chat-message-draft";
import Component from "@glimmer/component"; import Component from "@glimmer/component";
import { bind, debounce } from "discourse-common/utils/decorators"; import { bind, debounce } from "discourse-common/utils/decorators";
import discourseDebounce from "discourse-common/lib/debounce";
import EmberObject, { action } from "@ember/object"; import EmberObject, { action } from "@ember/object";
import { ajax } from "discourse/lib/ajax"; import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error"; import { popupAjaxError } from "discourse/lib/ajax-error";
import { cancel, schedule } from "@ember/runloop"; import { cancel, schedule, throttle } from "@ember/runloop";
import discourseLater from "discourse-common/lib/later"; import discourseLater from "discourse-common/lib/later";
import { inject as service } from "@ember/service"; import { inject as service } from "@ember/service";
import { Promise } from "rsvp"; import { Promise } from "rsvp";
@ -64,7 +63,6 @@ export default class ChatLivePane extends Component {
setupListeners(element) { setupListeners(element) {
this._scrollerEl = element.querySelector(".chat-messages-scroll"); this._scrollerEl = element.querySelector(".chat-messages-scroll");
window.addEventListener("resize", this.onResizeHandler);
document.addEventListener("scroll", this._forceBodyScroll, { document.addEventListener("scroll", this._forceBodyScroll, {
passive: true, passive: true,
}); });
@ -76,14 +74,19 @@ export default class ChatLivePane extends Component {
@action @action
teardownListeners() { teardownListeners() {
window.removeEventListener("resize", this.onResizeHandler);
cancel(this.resizeHandler);
document.removeEventListener("scroll", this._forceBodyScroll); document.removeEventListener("scroll", this._forceBodyScroll);
removeOnPresenceChange(this.onPresenceChangeCallback); removeOnPresenceChange(this.onPresenceChangeCallback);
this._unsubscribeToUpdates(this._loadedChannelId); this._unsubscribeToUpdates(this._loadedChannelId);
this.requestedTargetMessageId = null; this.requestedTargetMessageId = null;
} }
@action
didResizePane() {
this.fillPaneAttempt();
this.computeDatesSeparators();
this.forceRendering();
}
@action @action
resetIdle() { resetIdle() {
resetIdle(); resetIdle();
@ -126,17 +129,6 @@ export default class ChatLivePane extends Component {
} }
} }
@bind
onResizeHandler() {
cancel(this.resizeHandler);
this.resizeHandler = discourseDebounce(
this,
this.fillPaneAttempt,
this.details,
250
);
}
@bind @bind
onPresenceChangeCallback(present) { onPresenceChangeCallback(present) {
if (present) { if (present) {
@ -286,7 +278,6 @@ export default class ChatLivePane extends Component {
.finally(() => { .finally(() => {
this[loadingMoreKey] = false; this[loadingMoreKey] = false;
this.fillPaneAttempt(); this.fillPaneAttempt();
this.computeDatesSeparators();
}); });
} }
@ -1238,7 +1229,6 @@ export default class ChatLivePane extends Component {
} }
if (this.capabilities.isIOS) { if (this.capabilities.isIOS) {
this._scrollerEl.style.transform = "translateZ(0)";
this._scrollerEl.style.overflow = "hidden"; this._scrollerEl.style.overflow = "hidden";
} }
@ -1251,8 +1241,6 @@ export default class ChatLivePane extends Component {
} }
this._scrollerEl.style.overflow = "auto"; this._scrollerEl.style.overflow = "auto";
this._scrollerEl.style.transform = "unset";
this.computeDatesSeparators();
}, 50); }, 50);
} }
}); });
@ -1307,25 +1295,48 @@ export default class ChatLivePane extends Component {
@action @action
computeDatesSeparators() { computeDatesSeparators() {
throttle(this, this._computeDatesSeparators, 50, false);
}
_computeDatesSeparators() {
schedule("afterRender", () => { schedule("afterRender", () => {
const dates = [ const dates = [
...this._scrollerEl.querySelectorAll(".chat-message-separator-date"), ...this._scrollerEl.querySelectorAll(".chat-message-separator-date"),
].reverse(); ].reverse();
const scrollHeight = this._scrollerEl.scrollHeight; const height = this._scrollerEl.querySelector(
".chat-messages-container"
).clientHeight;
dates dates
.map((date, index) => { .map((date, index) => {
const item = { bottom: "0px", date }; const item = { bottom: 0, date };
const line = date.nextElementSibling;
if (index > 0) { if (index > 0) {
item.bottom = scrollHeight - dates[index - 1].offsetTop + "px"; const prevDate = dates[index - 1];
const prevLine = prevDate.nextElementSibling;
item.bottom = height - prevLine.offsetTop;
} }
item.top = date.nextElementSibling.offsetTop + "px";
if (dates.length === 1) {
item.height = height;
} else {
if (index === 0) {
item.height = height - line.offsetTop;
} else {
const prevDate = dates[index - 1];
const prevLine = prevDate.nextElementSibling;
item.height =
height - line.offsetTop - (height - prevLine.offsetTop);
}
}
return item; return item;
}) })
// group all writes at the end // group all writes at the end
.forEach((item) => { .forEach((item) => {
item.date.style.bottom = item.bottom; item.date.style.bottom = item.bottom + "px";
item.date.style.top = item.top; item.date.style.height = item.height + "px";
}); });
}); });
} }

View File

@ -120,7 +120,6 @@
@cooked={{@message.cooked}} @cooked={{@message.cooked}}
@uploads={{@message.uploads}} @uploads={{@message.uploads}}
@edited={{@message.edited}} @edited={{@message.edited}}
@onToggleCollapse={{fn @forceRendering (noop)}}
> >
{{#if @message.reactions.length}} {{#if @message.reactions.length}}
<div class="chat-message-reaction-list"> <div class="chat-message-reaction-list">

View File

@ -517,8 +517,6 @@ export default class ChatMessage extends Component {
this.currentUser.id this.currentUser.id
); );
this.args.forceRendering?.();
return ajax( return ajax(
`/chat/${this.args.message.channelId}/react/${this.args.message.id}`, `/chat/${this.args.message.channelId}/react/${this.args.message.id}`,
{ {

View File

@ -1,24 +0,0 @@
import Modifier from "ember-modifier";
import { registerDestructor } from "@ember/destroyable";
export default class ChatDidMutateChildlist extends Modifier {
constructor(owner, args) {
super(owner, args);
registerDestructor(this, (instance) => instance.cleanup());
}
modify(element, [callback]) {
this.mutationObserver = new MutationObserver(() => {
callback();
});
this.mutationObserver.observe(element, {
childList: true,
subtree: true,
});
}
cleanup() {
this.mutationObserver?.disconnect();
}
}

View File

@ -0,0 +1,29 @@
import Modifier from "ember-modifier";
import { registerDestructor } from "@ember/destroyable";
import { cancel, throttle } from "@ember/runloop";
export default class ChatOnResize extends Modifier {
constructor(owner, args) {
super(owner, args);
registerDestructor(this, (instance) => instance.cleanup());
}
modify(element, [fn, options = {}]) {
this.resizeObserver = new ResizeObserver((entries) => {
this.throttleHandler = throttle(
this,
fn,
entries,
options.delay ?? 0,
options.immediate ?? false
);
});
this.resizeObserver.observe(element);
}
cleanup() {
cancel(this.throttleHandler);
this.resizeObserver?.disconnect();
}
}

View File

@ -3,7 +3,7 @@ import { registerDestructor } from "@ember/destroyable";
import { cancel, throttle } from "@ember/runloop"; import { cancel, throttle } from "@ember/runloop";
import { bind } from "discourse-common/utils/decorators"; import { bind } from "discourse-common/utils/decorators";
export default class ChatOnThrottledScroll extends Modifier { export default class ChatOnScroll extends Modifier {
constructor(owner, args) { constructor(owner, args) {
super(owner, args); super(owner, args);
registerDestructor(this, (instance) => instance.cleanup()); registerDestructor(this, (instance) => instance.cleanup());

View File

@ -57,7 +57,6 @@ module("Discourse Chat | Component | chat-message", function (hooks) {
onHoverMessage: () => {}, onHoverMessage: () => {},
messageDidEnterViewport: () => {}, messageDidEnterViewport: () => {},
messageDidLeaveViewport: () => {}, messageDidLeaveViewport: () => {},
forceRendering: () => {},
}; };
} }
@ -76,7 +75,6 @@ module("Discourse Chat | Component | chat-message", function (hooks) {
@onHoverMessage={{this.onHoverMessage}} @onHoverMessage={{this.onHoverMessage}}
@messageDidEnterViewport={{this.messageDidEnterViewport}} @messageDidEnterViewport={{this.messageDidEnterViewport}}
@messageDidLeaveViewport={{this.messageDidLeaveViewport}} @messageDidLeaveViewport={{this.messageDidLeaveViewport}}
@forceRendering={{this.forceRendering}}
/> />
`; `;