diff --git a/src/components/common/messaging/Message.tsx b/src/components/common/messaging/Message.tsx index 28ee957c8f9c8cebf13931adcd6451ebbe4e2303..92d6c3622b7c2880ba87dd89a535652c3b108deb 100644 --- a/src/components/common/messaging/Message.tsx +++ b/src/components/common/messaging/Message.tsx @@ -12,6 +12,7 @@ import MessageBase, { MessageContent, MessageDetail, MessageInfo } from "./Messa import Overline from "../../ui/Overline"; import { useContext } from "preact/hooks"; import { AppContext } from "../../../context/revoltjs/RevoltClient"; +import { memo } from "preact/compat"; interface Props { attachContext?: boolean @@ -22,7 +23,7 @@ interface Props { 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: of dependencies. We only need to update on u/avatar. const user = useUser(message.author); @@ -58,3 +59,5 @@ export default function Message({ attachContext, message, contrast, content: rep </MessageBase> ) } + +export default memo(Message); diff --git a/src/lib/defer.ts b/src/lib/defer.ts index 2ad2d46cba58ba9e179009410ada6c057062c339..79de63ae0c714660094570d7ac890b45c2cf8115 100644 --- a/src/lib/defer.ts +++ b/src/lib/defer.ts @@ -1,3 +1 @@ -export function defer(cb: () => void) { - setTimeout(cb, 0); -} +export const defer = (cb: () => void) => setTimeout(cb, 0); diff --git a/src/pages/channels/messaging/MessageArea.tsx b/src/pages/channels/messaging/MessageArea.tsx index a178fab821519e614f6f19a289961181c052bf46..11cc849bc80747f54d7b94fa7e4f4ae72776b0ac 100644 --- a/src/pages/channels/messaging/MessageArea.tsx +++ b/src/pages/channels/messaging/MessageArea.tsx @@ -11,6 +11,7 @@ import { SingletonMessageRenderer } from "../../../lib/renderer/Singleton"; import { IntermediateContext } from "../../../context/intermediate/Intermediate"; import { ClientStatus, StatusContext } from "../../../context/revoltjs/RevoltClient"; import { useContext, useEffect, useLayoutEffect, useRef, useState } from "preact/hooks"; +import { defer } from "../../../lib/defer"; const Area = styled.div` height: 100%; @@ -47,21 +48,61 @@ export function MessageArea({ id }: Props) { // ? Current channel state. const [state, setState] = useState<RenderState>({ type: "LOADING" }); - // ? Hook-based scrolling mechanism. - const [scrollState, setSS] = useState<ScrollState>({ - type: "Free" - }); + // ? useRef to avoid re-renders + const scrollState = useRef<ScrollState>({ type: "Free" }); const setScrollState = (v: ScrollState) => { if (v.type === 'StayAtBottom') { - if (scrollState.type === 'Bottom' || atBottom()) { - setSS({ type: 'ScrollToBottom', smooth: v.smooth }); + if (scrollState.current.type === 'Bottom' || atBottom()) { + scrollState.current = { type: 'ScrollToBottom', smooth: v.smooth }; } else { - setSS({ type: 'Free' }); + scrollState.current = { type: 'Free' }; } } 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. @@ -113,19 +154,20 @@ export function MessageArea({ id }: Props) { // ? Scroll to the bottom before the browser paints. useLayoutEffect(() => { - if (scrollState.type === "ScrollToBottom") { + // ! FIXME: NO REACTIVITY + if (scrollState.current.type === "ScrollToBottom") { setScrollState({ type: "Bottom", scrollingUntil: + new Date() + 150 }); animateScroll.scrollToBottom({ 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( Math.max( 101, ref.current.scrollTop + - (ref.current.scrollHeight - scrollState.previousHeight) + (ref.current.scrollHeight - scrollState.current.previousHeight) ), { container: ref.current, @@ -134,8 +176,8 @@ export function MessageArea({ id }: Props) { ); setScrollState({ type: "Free" }); - } else if (scrollState.type === "ScrollTop") { - animateScroll.scrollTo(scrollState.y, { + } else if (scrollState.current.type === "ScrollTop") { + animateScroll.scrollTo(scrollState.current.y, { container: ref.current, duration: 0 }); @@ -148,10 +190,10 @@ export function MessageArea({ id }: Props) { // ? Also handle StayAtBottom useEffect(() => { async function onScroll() { - if (scrollState.type === "Free" && atBottom()) { + if (scrollState.current.type === "Free" && atBottom()) { setScrollState({ type: "Bottom" }); - } else if (scrollState.type === "Bottom" && !atBottom()) { - if (scrollState.scrollingUntil && scrollState.scrollingUntil > + new Date()) return; + } else if (scrollState.current.type === "Bottom" && !atBottom()) { + if (scrollState.current.scrollingUntil && scrollState.current.scrollingUntil > + new Date()) return; setScrollState({ type: "Free" }); } } @@ -178,7 +220,7 @@ export function MessageArea({ id }: Props) { // ? Scroll down whenever the message area resizes. function stbOnResize() { - if (!atBottom() && scrollState.type === "Bottom") { + if (!atBottom() && scrollState.current.type === "Bottom") { animateScroll.scrollToBottom({ container: ref.current, duration: 0 diff --git a/src/pages/channels/messaging/MessageRenderer.tsx b/src/pages/channels/messaging/MessageRenderer.tsx index 2c665827055234fc1277f7da7ba6a2d3be3022ad..fba6b281b59f34a0d2b1e65b163393afa3331b07 100644 --- a/src/pages/channels/messaging/MessageRenderer.tsx +++ b/src/pages/channels/messaging/MessageRenderer.tsx @@ -1,4 +1,5 @@ import { decodeTime } from "ulid"; +import { memo } from "preact/compat"; import MessageEditor from "./MessageEditor"; import { Children } from "../../../types/Preact"; import ConversationStart from "./ConversationStart"; @@ -156,8 +157,8 @@ function MessageRenderer({ id, state, queue }: Props) { return <>{ render }</>; } -export default connectState<Omit<Props, 'queue'>>(MessageRenderer, state => { +export default memo(connectState<Omit<Props, 'queue'>>(MessageRenderer, state => { return { queue: state.queue }; -}); +}));