mirror of
https://github.com/revoltchat/revite.git
synced 2024-12-24 22:52:09 -05:00
Optimise re-renders when scrolling / updating messages.
This commit is contained in:
parent
11c524d6a9
commit
0ce77951cb
4 changed files with 68 additions and 24 deletions
|
@ -12,6 +12,7 @@ import MessageBase, { MessageContent, MessageDetail, MessageInfo } from "./Messa
|
||||||
import Overline from "../../ui/Overline";
|
import Overline from "../../ui/Overline";
|
||||||
import { useContext } from "preact/hooks";
|
import { useContext } from "preact/hooks";
|
||||||
import { AppContext } from "../../../context/revoltjs/RevoltClient";
|
import { AppContext } from "../../../context/revoltjs/RevoltClient";
|
||||||
|
import { memo } from "preact/compat";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
attachContext?: boolean
|
attachContext?: boolean
|
||||||
|
@ -22,7 +23,7 @@ interface Props {
|
||||||
head?: boolean
|
head?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Message({ attachContext, message, contrast, content: replacement, head: preferHead, queued }: Props) {
|
function Message({ attachContext, message, contrast, content: replacement, head: preferHead, queued }: Props) {
|
||||||
// TODO: Can improve re-renders here by providing a list
|
// TODO: Can improve re-renders here by providing a list
|
||||||
// TODO: of dependencies. We only need to update on u/avatar.
|
// TODO: of dependencies. We only need to update on u/avatar.
|
||||||
const user = useUser(message.author);
|
const user = useUser(message.author);
|
||||||
|
@ -58,3 +59,5 @@ export default function Message({ attachContext, message, contrast, content: rep
|
||||||
</MessageBase>
|
</MessageBase>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default memo(Message);
|
||||||
|
|
|
@ -1,3 +1 @@
|
||||||
export function defer(cb: () => void) {
|
export const defer = (cb: () => void) => setTimeout(cb, 0);
|
||||||
setTimeout(cb, 0);
|
|
||||||
}
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { SingletonMessageRenderer } from "../../../lib/renderer/Singleton";
|
||||||
import { IntermediateContext } from "../../../context/intermediate/Intermediate";
|
import { IntermediateContext } from "../../../context/intermediate/Intermediate";
|
||||||
import { ClientStatus, StatusContext } from "../../../context/revoltjs/RevoltClient";
|
import { ClientStatus, StatusContext } from "../../../context/revoltjs/RevoltClient";
|
||||||
import { useContext, useEffect, useLayoutEffect, useRef, useState } from "preact/hooks";
|
import { useContext, useEffect, useLayoutEffect, useRef, useState } from "preact/hooks";
|
||||||
|
import { defer } from "../../../lib/defer";
|
||||||
|
|
||||||
const Area = styled.div`
|
const Area = styled.div`
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -47,21 +48,61 @@ export function MessageArea({ id }: Props) {
|
||||||
// ? Current channel state.
|
// ? Current channel state.
|
||||||
const [state, setState] = useState<RenderState>({ type: "LOADING" });
|
const [state, setState] = useState<RenderState>({ type: "LOADING" });
|
||||||
|
|
||||||
// ? Hook-based scrolling mechanism.
|
// ? useRef to avoid re-renders
|
||||||
const [scrollState, setSS] = useState<ScrollState>({
|
const scrollState = useRef<ScrollState>({ type: "Free" });
|
||||||
type: "Free"
|
|
||||||
});
|
|
||||||
|
|
||||||
const setScrollState = (v: ScrollState) => {
|
const setScrollState = (v: ScrollState) => {
|
||||||
if (v.type === 'StayAtBottom') {
|
if (v.type === 'StayAtBottom') {
|
||||||
if (scrollState.type === 'Bottom' || atBottom()) {
|
if (scrollState.current.type === 'Bottom' || atBottom()) {
|
||||||
setSS({ type: 'ScrollToBottom', smooth: v.smooth });
|
scrollState.current = { type: 'ScrollToBottom', smooth: v.smooth };
|
||||||
} else {
|
} else {
|
||||||
setSS({ type: 'Free' });
|
scrollState.current = { type: 'Free' };
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setSS(v);
|
scrollState.current = v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defer(() => {
|
||||||
|
if (scrollState.current.type === "ScrollToBottom") {
|
||||||
|
setScrollState({ type: "Bottom", scrollingUntil: + new Date() + 150 });
|
||||||
|
|
||||||
|
animateScroll.scrollToBottom({
|
||||||
|
container: ref.current,
|
||||||
|
duration: scrollState.current.smooth ? 150 : 0
|
||||||
|
});
|
||||||
|
} else if (scrollState.current.type === "OffsetTop") {
|
||||||
|
animateScroll.scrollTo(
|
||||||
|
Math.max(
|
||||||
|
101,
|
||||||
|
ref.current.scrollTop +
|
||||||
|
(ref.current.scrollHeight - scrollState.current.previousHeight)
|
||||||
|
),
|
||||||
|
{
|
||||||
|
container: ref.current,
|
||||||
|
duration: 0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
setScrollState({ type: "Free" });
|
||||||
|
} else if (scrollState.current.type === "ScrollTop") {
|
||||||
|
animateScroll.scrollTo(scrollState.current.y, {
|
||||||
|
container: ref.current,
|
||||||
|
duration: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
setScrollState({ type: "Free" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/*if (v.type === 'StayAtBottom') {
|
||||||
|
if (scrollState.current.type === 'Bottom' || atBottom()) {
|
||||||
|
scrollState.current = { type: 'ScrollToBottom', smooth: v.smooth };
|
||||||
|
} else {
|
||||||
|
scrollState.current = { type: 'Free' };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
scrollState.current = v;
|
||||||
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
// ? Determine if we are at the bottom of the scroll container.
|
// ? Determine if we are at the bottom of the scroll container.
|
||||||
|
@ -113,19 +154,20 @@ export function MessageArea({ id }: Props) {
|
||||||
|
|
||||||
// ? Scroll to the bottom before the browser paints.
|
// ? Scroll to the bottom before the browser paints.
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (scrollState.type === "ScrollToBottom") {
|
// ! FIXME: NO REACTIVITY
|
||||||
|
if (scrollState.current.type === "ScrollToBottom") {
|
||||||
setScrollState({ type: "Bottom", scrollingUntil: + new Date() + 150 });
|
setScrollState({ type: "Bottom", scrollingUntil: + new Date() + 150 });
|
||||||
|
|
||||||
animateScroll.scrollToBottom({
|
animateScroll.scrollToBottom({
|
||||||
container: ref.current,
|
container: ref.current,
|
||||||
duration: scrollState.smooth ? 150 : 0
|
duration: scrollState.current.smooth ? 150 : 0
|
||||||
});
|
});
|
||||||
} else if (scrollState.type === "OffsetTop") {
|
} else if (scrollState.current.type === "OffsetTop") {
|
||||||
animateScroll.scrollTo(
|
animateScroll.scrollTo(
|
||||||
Math.max(
|
Math.max(
|
||||||
101,
|
101,
|
||||||
ref.current.scrollTop +
|
ref.current.scrollTop +
|
||||||
(ref.current.scrollHeight - scrollState.previousHeight)
|
(ref.current.scrollHeight - scrollState.current.previousHeight)
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
container: ref.current,
|
container: ref.current,
|
||||||
|
@ -134,8 +176,8 @@ export function MessageArea({ id }: Props) {
|
||||||
);
|
);
|
||||||
|
|
||||||
setScrollState({ type: "Free" });
|
setScrollState({ type: "Free" });
|
||||||
} else if (scrollState.type === "ScrollTop") {
|
} else if (scrollState.current.type === "ScrollTop") {
|
||||||
animateScroll.scrollTo(scrollState.y, {
|
animateScroll.scrollTo(scrollState.current.y, {
|
||||||
container: ref.current,
|
container: ref.current,
|
||||||
duration: 0
|
duration: 0
|
||||||
});
|
});
|
||||||
|
@ -148,10 +190,10 @@ export function MessageArea({ id }: Props) {
|
||||||
// ? Also handle StayAtBottom
|
// ? Also handle StayAtBottom
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function onScroll() {
|
async function onScroll() {
|
||||||
if (scrollState.type === "Free" && atBottom()) {
|
if (scrollState.current.type === "Free" && atBottom()) {
|
||||||
setScrollState({ type: "Bottom" });
|
setScrollState({ type: "Bottom" });
|
||||||
} else if (scrollState.type === "Bottom" && !atBottom()) {
|
} else if (scrollState.current.type === "Bottom" && !atBottom()) {
|
||||||
if (scrollState.scrollingUntil && scrollState.scrollingUntil > + new Date()) return;
|
if (scrollState.current.scrollingUntil && scrollState.current.scrollingUntil > + new Date()) return;
|
||||||
setScrollState({ type: "Free" });
|
setScrollState({ type: "Free" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -178,7 +220,7 @@ export function MessageArea({ id }: Props) {
|
||||||
|
|
||||||
// ? Scroll down whenever the message area resizes.
|
// ? Scroll down whenever the message area resizes.
|
||||||
function stbOnResize() {
|
function stbOnResize() {
|
||||||
if (!atBottom() && scrollState.type === "Bottom") {
|
if (!atBottom() && scrollState.current.type === "Bottom") {
|
||||||
animateScroll.scrollToBottom({
|
animateScroll.scrollToBottom({
|
||||||
container: ref.current,
|
container: ref.current,
|
||||||
duration: 0
|
duration: 0
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { decodeTime } from "ulid";
|
import { decodeTime } from "ulid";
|
||||||
|
import { memo } from "preact/compat";
|
||||||
import MessageEditor from "./MessageEditor";
|
import MessageEditor from "./MessageEditor";
|
||||||
import { Children } from "../../../types/Preact";
|
import { Children } from "../../../types/Preact";
|
||||||
import ConversationStart from "./ConversationStart";
|
import ConversationStart from "./ConversationStart";
|
||||||
|
@ -156,8 +157,8 @@ function MessageRenderer({ id, state, queue }: Props) {
|
||||||
return <>{ render }</>;
|
return <>{ render }</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connectState<Omit<Props, 'queue'>>(MessageRenderer, state => {
|
export default memo(connectState<Omit<Props, 'queue'>>(MessageRenderer, state => {
|
||||||
return {
|
return {
|
||||||
queue: state.queue
|
queue: state.queue
|
||||||
};
|
};
|
||||||
});
|
}));
|
||||||
|
|
Loading…
Reference in a new issue