diff --git a/src/client/webapp/elements/components/infinite-scroll.tsx b/src/client/webapp/elements/components/infinite-scroll.tsx index 87fda83..7461fa2 100644 --- a/src/client/webapp/elements/components/infinite-scroll.tsx +++ b/src/client/webapp/elements/components/infinite-scroll.tsx @@ -1,13 +1,13 @@ -import React, { Dispatch, FC, ReactNode, SetStateAction, useEffect } from 'react'; +import React, { Dispatch, FC, ReactNode, SetStateAction } from 'react'; import { useColumnReverseInfiniteScroll } from '../require/react-helper'; import Retry from './retry'; export interface InfiniteScrollProps { fetchRetryCallable: () => Promise; - fetchAboveCallable: () => Promise<{ hasMoreAbove: boolean, removedFromBottom: boolean }>; - fetchBelowCallable: () => Promise<{ hasMoreBelow: boolean, removedFromTop: boolean }>; + fetchAboveCallable: () => Promise; + fetchBelowCallable: () => Promise; setScrollRatio: Dispatch>; - fetchResult: { hasMoreAbove: boolean, hasMoreBelow: boolean } | null; + ends: { hasMoreAbove: boolean, hasMoreBelow: boolean } | null; fetchError: unknown | null; fetchAboveError: unknown | null; fetchBelowError: unknown | null; @@ -24,7 +24,7 @@ const InfiniteScroll: FC = (props: InfiniteScrollProps) => fetchAboveCallable, fetchBelowCallable, setScrollRatio, - fetchResult, + ends, fetchError, fetchAboveError, fetchBelowError, @@ -36,22 +36,16 @@ const InfiniteScroll: FC = (props: InfiniteScrollProps) => const [ updateScrollCallable, - onLoadCallable, fetchAboveRetry, fetchBelowRetry ] = useColumnReverseInfiniteScroll( 600, + ends, fetchAboveCallable, fetchBelowCallable, setScrollRatio ); - useEffect(() => { - if (fetchResult) { - onLoadCallable(fetchResult) - } - }, [ fetchResult, onLoadCallable ]); - return (
diff --git a/src/client/webapp/elements/lists/message-list.tsx b/src/client/webapp/elements/lists/message-list.tsx index bb6d02c..49a5ceb 100644 --- a/src/client/webapp/elements/lists/message-list.tsx +++ b/src/client/webapp/elements/lists/message-list.tsx @@ -20,7 +20,7 @@ const MessageList: FC = (props: MessageListProps) => { fetchAboveCallable, fetchBelowCallable, setScrollRatio, - fetchResult, + ends, messagesResult, messagesFetchError, messagesFetchAboveError, @@ -46,7 +46,7 @@ const MessageList: FC = (props: MessageListProps) => { fetchAboveCallable={fetchAboveCallable} fetchBelowCallable={fetchBelowCallable} setScrollRatio={setScrollRatio} - fetchResult={fetchResult} + ends={ends} fetchError={messagesFetchError} fetchAboveError={messagesFetchAboveError} fetchBelowError={messagesFetchBelowError} diff --git a/src/client/webapp/elements/require/guild-subscriptions.ts b/src/client/webapp/elements/require/guild-subscriptions.ts index 3a8a046..3b45f88 100644 --- a/src/client/webapp/elements/require/guild-subscriptions.ts +++ b/src/client/webapp/elements/require/guild-subscriptions.ts @@ -344,10 +344,10 @@ function useMultipleGuildSubscriptionScrolling< fetchBelowFunc: ((reference: T) => Promise), ): [ fetchRetryCallable: () => Promise, - fetchAboveCallable: () => Promise<{ hasMoreAbove: boolean, removedFromBottom: boolean }>, - fetchBelowCallable: () => Promise<{ hasMoreBelow: boolean, removedFromTop: boolean }>, + fetchAboveCallable: () => Promise, + fetchBelowCallable: () => Promise, setScrollRatio: Dispatch>, - fetchResult: { hasMoreAbove: boolean, hasMoreBelow: boolean } | null, + ends: { hasMoreAbove: boolean, hasMoreBelow: boolean } | null, lastResult: SubscriptionResult | null, fetchError: unknown | null, fetchAboveError: unknown | null, @@ -370,7 +370,7 @@ function useMultipleGuildSubscriptionScrolling< const [ fetchAboveError, setFetchAboveError ] = useState(null); const [ fetchBelowError, setFetchBelowError ] = useState(null); - const [ fetchResult, setFetchResult ] = useState<{ hasMoreAbove: boolean, hasMoreBelow: boolean } | null>(null); + const [ ends, setEnds ] = useState<{ hasMoreAbove: boolean, hasMoreBelow: boolean } | null>(null); // Percentage of scroll from top. i.e. 300px from top of 1000px scroll = 0.3 scroll ratio const [ scrollRatio, setScrollRatio ] = useState(0.5); @@ -394,17 +394,17 @@ function useMultipleGuildSubscriptionScrolling< return elements.slice(fromTop, elements.length - fromBottom); } - const fetchAboveCallable = useCallback(async (): Promise<{ hasMoreAbove: boolean, removedFromBottom: boolean }> => { - if (!isMounted.current) return { hasMoreAbove: false, removedFromBottom: false }; - if (!lastResult || !lastResult.value || lastResult.value.length === 0) return { hasMoreAbove: false, removedFromBottom: false }; - if (guild !== lastResult.guild) return { hasMoreAbove: false, removedFromBottom: false }; + const fetchAboveCallable = useCallback(async (): Promise => { + if (!isMounted.current) return; + if (!lastResult || !lastResult.value || lastResult.value.length === 0) return; + if (guild !== lastResult.guild) return; try { const reference = lastResult.value[0] as T; const aboveElements = await fetchAboveFunc(reference); const lastResultAfterAwait = getStateAfterAwait(setLastResult); - if (!isMounted.current) return { hasMoreAbove: false, removedFromBottom: false }; - if (!lastResultAfterAwait || guild !== lastResultAfterAwait.guild) return { hasMoreAbove: false, removedFromBottom: false }; + if (!isMounted.current) return; + if (!lastResultAfterAwait || guild !== lastResultAfterAwait.guild) return; setFetchAboveError(null); if (aboveElements) { const hasMoreAbove = aboveElements.length >= maxFetchElements; @@ -418,30 +418,30 @@ function useMultipleGuildSubscriptionScrolling< } return { value: newValue, guild: lastResult.guild }; }); - return { hasMoreAbove, removedFromBottom }; + setEnds(prev => (prev && { hasMoreBelow: removedFromBottom || prev.hasMoreBelow, hasMoreAbove: prev.hasMoreAbove })); } else { - return { hasMoreAbove: false, removedFromBottom: false }; + setEnds(prev => (prev && { hasMoreBelow: prev.hasMoreBelow, hasMoreAbove: false })); } } catch (e: unknown) { LOG.error('error fetching above for subscription', e); const lastResultAfterAwait = getStateAfterAwait(setLastResult); - if (!isMounted.current) return { hasMoreAbove: false, removedFromBottom: false }; - if (!lastResultAfterAwait || guild !== lastResultAfterAwait.guild) return { hasMoreAbove: false, removedFromBottom: false }; + if (!isMounted.current) return; + if (!lastResultAfterAwait || guild !== lastResultAfterAwait.guild) return; setFetchAboveError(e); - return { hasMoreAbove: true, removedFromBottom: false }; + setEnds(prev => (prev && { hasMoreBelow: prev.hasMoreBelow, hasMoreAbove: true })) } }, [ guild, lastResult, fetchAboveFunc, maxFetchElements ]); - const fetchBelowCallable = useCallback(async (): Promise<{ hasMoreBelow: boolean, removedFromTop: boolean }> => { - if (!isMounted.current) return { hasMoreBelow: false, removedFromTop: false }; - if (!lastResult || !lastResult.value || lastResult.value.length === 0) return { hasMoreBelow: false, removedFromTop: false }; - if (guild !== lastResult.guild) return { hasMoreBelow: false, removedFromTop: false }; + const fetchBelowCallable = useCallback(async (): Promise => { + if (!isMounted.current) return; + if (!lastResult || !lastResult.value || lastResult.value.length === 0) return; + if (guild !== lastResult.guild) return; try { const reference = lastResult.value[lastResult.value.length - 1] as T; const belowElements = await fetchBelowFunc(reference); const lastResultAfterAwait = getStateAfterAwait(setLastResult); - if (!isMounted.current) return { hasMoreBelow: false, removedFromTop: false }; - if (!lastResultAfterAwait || guild !== lastResultAfterAwait.guild) return { hasMoreBelow: false, removedFromTop: false }; + if (!isMounted.current) return; + if (!lastResultAfterAwait || guild !== lastResultAfterAwait.guild) return; setFetchBelowError(null); if (belowElements) { const hasMoreBelow = belowElements.length >= maxFetchElements; @@ -454,18 +454,18 @@ function useMultipleGuildSubscriptionScrolling< removedFromTop = true; } return { value: newValue, guild: lastResult.guild }; - }) - return { hasMoreBelow, removedFromTop }; + }); + setEnds(prev => (prev && { hasMoreBelow, hasMoreAbove: removedFromTop || prev.hasMoreAbove })); // :) } else { - return { hasMoreBelow: false, removedFromTop: false }; + setEnds(prev => (prev && { hasMoreBelow: false, hasMoreAbove: prev.hasMoreAbove })) } } catch (e: unknown) { LOG.error('error fetching below for subscription', e); const lastResultAfterAwait = getStateAfterAwait(setLastResult); - if (!isMounted.current) return { hasMoreBelow: false, removedFromTop: false }; - if (!lastResultAfterAwait || guild !== lastResultAfterAwait.guild) return { hasMoreBelow: false, removedFromTop: false }; + if (!isMounted.current) return; + if (!lastResultAfterAwait || guild !== lastResultAfterAwait.guild) return; setFetchBelowError(e); - return { hasMoreBelow: true, removedFromTop: false }; + setEnds(prev => (prev && { hasMoreBelow: true, hasMoreAbove: prev.hasMoreAbove })); } }, [ lastResult, fetchBelowFunc, maxFetchElements ]); @@ -476,7 +476,7 @@ function useMultipleGuildSubscriptionScrolling< fetchValue = fetchValue.slice(Math.max(fetchValue.length - maxElements)).sort(sortFunc); } //LOG.debug('Got items: ', { fetchValueLength: fetchValue?.length ?? '' }) - setFetchResult({ hasMoreAbove, hasMoreBelow: false }); + setEnds({ hasMoreAbove, hasMoreBelow: false }); setLastResult({ value: fetchValue, guild: fetchValueGuild }); setFetchError(null); }, [ sortFunc, maxFetchElements, maxElements ]); @@ -494,6 +494,7 @@ function useMultipleGuildSubscriptionScrolling< const newElements = newEventArgsMap(...args); setLastResult((lastResult) => { if (lastResult === null) return null; + if (ends?.hasMoreBelow) return lastResult; // Don't add to bottom if we are not at the bottom let newValue = (lastResult.value ?? []).concat(newElements).sort(sortFunc); if (newValue.length > maxElements) { // Remove in a way that tries to keep the scrollbar position consistent @@ -566,7 +567,7 @@ function useMultipleGuildSubscriptionScrolling< fetchAboveCallable, fetchBelowCallable, setScrollRatio, - fetchResult, + ends, lastResult, fetchError, fetchAboveError, diff --git a/src/client/webapp/elements/require/react-helper.tsx b/src/client/webapp/elements/require/react-helper.tsx index f81f9e1..3d9bc94 100644 --- a/src/client/webapp/elements/require/react-helper.tsx +++ b/src/client/webapp/elements/require/react-helper.tsx @@ -252,12 +252,12 @@ export function useAsyncSubmitButton( export function useColumnReverseInfiniteScroll( threshold: number, - loadMoreAbove: () => Promise<{ hasMoreAbove: boolean, removedFromBottom: boolean }>, - loadMoreBelow: () => Promise<{ hasMoreBelow: boolean, removedFromTop: boolean }>, + ends: { hasMoreBelow: boolean, hasMoreAbove: boolean } | null, + loadMoreAbove: () => Promise, + loadMoreBelow: () => Promise, setScrollRatio: Dispatch> ): [ updateCallable: (event: UIEvent) => void, - onLoadCallable: (params: { hasMoreAbove: boolean, hasMoreBelow: boolean }) => void, loadAboveRetry: () => Promise, loadBelowRetry: () => Promise ] { @@ -266,28 +266,23 @@ export function useColumnReverseInfiniteScroll( const [ loadingAbove, setLoadingAbove ] = useState(false); const [ loadingBelow, setLoadingBelow ] = useState(false); - const [ hasMoreAbove, setHasMoreAbove ] = useState(false); - const [ hasMoreBelow, setHasMoreBelow ] = useState(false); - const loadAbove = useCallback(async () => { - if (loadingAbove || !hasMoreAbove) return; + if (loadingAbove) return; + if (!ends || !ends.hasMoreAbove) return; setLoadingAbove(true); - const loadResult = await loadMoreAbove(); + await loadMoreAbove(); if (!isMounted.current) return; - setHasMoreAbove(loadResult.hasMoreAbove); - setHasMoreBelow(oldHasMoreBelow => oldHasMoreBelow || loadResult.removedFromBottom); setLoadingAbove(false); - }, [ loadingAbove, hasMoreAbove, loadMoreAbove ]); + }, [ loadingAbove, loadMoreAbove, ends ]); const loadBelow = useCallback(async () => { - if (loadingBelow || !hasMoreBelow) return; + if (loadingBelow) return; + if (!ends || !ends.hasMoreBelow) return; setLoadingBelow(true); - const loadResult = await loadMoreBelow(); + await loadMoreBelow(); if (!isMounted.current) return; - setHasMoreBelow(loadResult.hasMoreBelow); - setHasMoreAbove(oldHasMoreAbove => oldHasMoreAbove || loadResult.removedFromTop); setLoadingBelow(false); - }, [ loadingBelow, hasMoreBelow, loadMoreBelow ]); + }, [ loadingBelow, loadMoreBelow, ends ]); const onScrollCallable = useCallback(async (event: UIEvent) => { const scrollTop = event.currentTarget.scrollTop; @@ -318,12 +313,7 @@ export function useColumnReverseInfiniteScroll( } }, [ setScrollRatio, loadAbove, loadBelow, threshold ]); - const onLoadCallable = useCallback((params: { hasMoreAbove: boolean, hasMoreBelow: boolean }) => { - setHasMoreAbove(params.hasMoreAbove); - setHasMoreBelow(params.hasMoreBelow); - }, []); - - return [ onScrollCallable, onLoadCallable, loadAbove, loadBelow ]; + return [ onScrollCallable, loadAbove, loadBelow ]; } // Makes sure to also allow you to fly-out a click starting inside of the ref'd element but was dragged outside @@ -485,7 +475,7 @@ export function useContextClickContextMenu( return createContextMenu(alignment, relativeToPos, close); }, [ alignment, relativeToPos ]); - const [ contextMenu, toggle, close, open ] = useContextMenu(createContextMenuWithRelativeToPos, createContextMenuDeps); + const [ contextMenu, _toggle, _close, open ] = useContextMenu(createContextMenuWithRelativeToPos, createContextMenuDeps); const onContextMenu = useCallback((event: React.MouseEvent) => { setRelativeToPos({ x: event.clientX, y: event.clientY });