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:
parent
0a06974a8a
commit
92797109ba
|
@ -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"}}
|
||||||
|
|
|
@ -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";
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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}`,
|
||||||
{
|
{
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
|
@ -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}}
|
|
||||||
/>
|
/>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue