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
class="chat-messages-scroll chat-messages-container"
{{on "scroll" this.computeScrollState passive=true}}
{{chat/on-throttled-scroll this.resetIdle (hash delay=500)}}
{{chat/on-throttled-scroll this.computeArrow (hash delay=150)}}
{{chat/on-scroll this.resetIdle (hash delay=500)}}
{{chat/on-scroll this.computeArrow (hash delay=150)}}
>
<div class="chat-message-actions-desktop-anchor"></div>
<div
class="chat-messages-container"
{{chat/did-mutate-childlist this.computeDatesSeparators}}
>
<div class="chat-messages-container" {{chat/on-resize this.didResizePane}}>
{{#if this.loadedOnce}}
{{#each @channel.messages key="id" as |message|}}
<ChatMessage
@ -67,7 +64,6 @@
@resendStagedMessage={{this.resendStagedMessage}}
@messageDidEnterViewport={{this.messageDidEnterViewport}}
@messageDidLeaveViewport={{this.messageDidLeaveViewport}}
@forceRendering={{this.forceRendering}}
/>
{{/each}}
{{else}}
@ -75,6 +71,7 @@
{{/if}}
</div>
{{! at bottom even if shown at top due to column-reverse }}
{{#if (and this.loadedOnce (not @channel.messagesManager.canLoadMorePast))}}
<div class="all-loaded-message">
{{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 Component from "@glimmer/component";
import { bind, debounce } from "discourse-common/utils/decorators";
import discourseDebounce from "discourse-common/lib/debounce";
import EmberObject, { action } from "@ember/object";
import { ajax } from "discourse/lib/ajax";
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 { inject as service } from "@ember/service";
import { Promise } from "rsvp";
@ -64,7 +63,6 @@ export default class ChatLivePane extends Component {
setupListeners(element) {
this._scrollerEl = element.querySelector(".chat-messages-scroll");
window.addEventListener("resize", this.onResizeHandler);
document.addEventListener("scroll", this._forceBodyScroll, {
passive: true,
});
@ -76,14 +74,19 @@ export default class ChatLivePane extends Component {
@action
teardownListeners() {
window.removeEventListener("resize", this.onResizeHandler);
cancel(this.resizeHandler);
document.removeEventListener("scroll", this._forceBodyScroll);
removeOnPresenceChange(this.onPresenceChangeCallback);
this._unsubscribeToUpdates(this._loadedChannelId);
this.requestedTargetMessageId = null;
}
@action
didResizePane() {
this.fillPaneAttempt();
this.computeDatesSeparators();
this.forceRendering();
}
@action
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
onPresenceChangeCallback(present) {
if (present) {
@ -286,7 +278,6 @@ export default class ChatLivePane extends Component {
.finally(() => {
this[loadingMoreKey] = false;
this.fillPaneAttempt();
this.computeDatesSeparators();
});
}
@ -1238,7 +1229,6 @@ export default class ChatLivePane extends Component {
}
if (this.capabilities.isIOS) {
this._scrollerEl.style.transform = "translateZ(0)";
this._scrollerEl.style.overflow = "hidden";
}
@ -1251,8 +1241,6 @@ export default class ChatLivePane extends Component {
}
this._scrollerEl.style.overflow = "auto";
this._scrollerEl.style.transform = "unset";
this.computeDatesSeparators();
}, 50);
}
});
@ -1307,25 +1295,48 @@ export default class ChatLivePane extends Component {
@action
computeDatesSeparators() {
throttle(this, this._computeDatesSeparators, 50, false);
}
_computeDatesSeparators() {
schedule("afterRender", () => {
const dates = [
...this._scrollerEl.querySelectorAll(".chat-message-separator-date"),
].reverse();
const scrollHeight = this._scrollerEl.scrollHeight;
const height = this._scrollerEl.querySelector(
".chat-messages-container"
).clientHeight;
dates
.map((date, index) => {
const item = { bottom: "0px", date };
const item = { bottom: 0, date };
const line = date.nextElementSibling;
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;
})
// group all writes at the end
.forEach((item) => {
item.date.style.bottom = item.bottom;
item.date.style.top = item.top;
item.date.style.bottom = item.bottom + "px";
item.date.style.height = item.height + "px";
});
});
}

View File

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

View File

@ -517,8 +517,6 @@ export default class ChatMessage extends Component {
this.currentUser.id
);
this.args.forceRendering?.();
return ajax(
`/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 { bind } from "discourse-common/utils/decorators";
export default class ChatOnThrottledScroll extends Modifier {
export default class ChatOnScroll extends Modifier {
constructor(owner, args) {
super(owner, args);
registerDestructor(this, (instance) => instance.cleanup());

View File

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