diff --git a/src/client/webapp/elements/lists/message-list.tsx b/src/client/webapp/elements/lists/message-list.tsx index 49a5ceb..4c7a809 100644 --- a/src/client/webapp/elements/lists/message-list.tsx +++ b/src/client/webapp/elements/lists/message-list.tsx @@ -20,7 +20,6 @@ const MessageList: FC = (props: MessageListProps) => { fetchAboveCallable, fetchBelowCallable, setScrollRatio, - ends, messagesResult, messagesFetchError, messagesFetchAboveError, @@ -29,10 +28,10 @@ const MessageList: FC = (props: MessageListProps) => { const messageElements = useMemo(() => { const result = []; - if (messagesResult && messagesResult.value) { - for (let i = 0; i < messagesResult.value.length; ++i) { - const prevMessage = messagesResult.value[i - 1] ?? null; - const message = messagesResult.value[i] as Message; + if (messagesResult) { + for (let i = 0; i < messagesResult.value.elements.length; ++i) { + const prevMessage = messagesResult.value.elements[i - 1] ?? null; + const message = messagesResult.value.elements[i] as Message; result.push(); } } @@ -46,7 +45,7 @@ const MessageList: FC = (props: MessageListProps) => { fetchAboveCallable={fetchAboveCallable} fetchBelowCallable={fetchBelowCallable} setScrollRatio={setScrollRatio} - ends={ends} + ends={messagesResult?.value.ends ?? null} 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 3b45f88..d93f97f 100644 --- a/src/client/webapp/elements/require/guild-subscriptions.ts +++ b/src/client/webapp/elements/require/guild-subscriptions.ts @@ -59,6 +59,14 @@ export interface SubscriptionResult { guild: CombinedGuild; } +export interface ScrollingSubscriptionResult { + value: { + ends: { hasMoreAbove: boolean, hasMoreBelow: boolean }; + elements: T[]; + }, + guild: CombinedGuild; +} + export function isNonNullAndHasValue(subscriptionResult: SubscriptionResult | null): subscriptionResult is SubscriptionResult { return !!(subscriptionResult !== null && subscriptionResult.value !== null); } @@ -347,8 +355,7 @@ function useMultipleGuildSubscriptionScrolling< fetchAboveCallable: () => Promise, fetchBelowCallable: () => Promise, setScrollRatio: Dispatch>, - ends: { hasMoreAbove: boolean, hasMoreBelow: boolean } | null, - lastResult: SubscriptionResult | null, + lastResult: ScrollingSubscriptionResult | null, fetchError: unknown | null, fetchAboveError: unknown | null, fetchBelowError: unknown | null, @@ -364,14 +371,12 @@ function useMultipleGuildSubscriptionScrolling< const isMounted = useIsMountedRef(); // TODO: lastResult.value should really be only T[] instead of | null since we set it to [] anyway in the onUpdate, etc functions - const [ lastResult, setLastResult ] = useState<{ value: T[], guild: CombinedGuild } | null>(null); + const [ lastResult, setLastResult ] = useState | null>(null); const [ fetchError, setFetchError ] = useState(null); const [ fetchAboveError, setFetchAboveError ] = useState(null); const [ fetchBelowError, setFetchBelowError ] = useState(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); @@ -389,18 +394,18 @@ function useMultipleGuildSubscriptionScrolling< } }, [ scrollRatio ]); - function removeByCounts(elements: T[], counts: { fromTop: number, fromBottom: number }): T[] { + function removeByCounts(elements: T[], counts: { fromTop: number, fromBottom: number }): T[] { const { fromTop, fromBottom } = counts; return elements.slice(fromTop, elements.length - fromBottom); } const fetchAboveCallable = useCallback(async (): Promise => { if (!isMounted.current) return; - if (!lastResult || !lastResult.value || lastResult.value.length === 0) return; + if (!lastResult || lastResult.value.elements.length === 0) return; if (guild !== lastResult.guild) return; try { - const reference = lastResult.value[0] as T; + const reference = lastResult.value.elements[0] as T; const aboveElements = await fetchAboveFunc(reference); const lastResultAfterAwait = getStateAfterAwait(setLastResult); if (!isMounted.current) return; @@ -411,16 +416,30 @@ function useMultipleGuildSubscriptionScrolling< let removedFromBottom = false; setLastResult((lastResult) => { if (!lastResult) return null; - let newValue = aboveElements.concat(lastResult.value ?? []).sort(sortFunc); - if (newValue.length > maxElements) { - newValue = newValue.slice(0, maxElements); + let newElements = aboveElements.concat(lastResult.value.elements ?? []).sort(sortFunc); + if (newElements.length > maxElements) { + newElements = newElements.slice(0, maxElements); removedFromBottom = true; } - return { value: newValue, guild: lastResult.guild }; + return { + value: { + elements: newElements, + ends: { hasMoreBelow: removedFromBottom || lastResult.value.ends.hasMoreBelow, hasMoreAbove } + }, + guild: lastResult.guild + }; }); - setEnds(prev => (prev && { hasMoreBelow: removedFromBottom || prev.hasMoreBelow, hasMoreAbove: prev.hasMoreAbove })); } else { - setEnds(prev => (prev && { hasMoreBelow: prev.hasMoreBelow, hasMoreAbove: false })); + setLastResult((lastResult) => { + if (!lastResult) return null; + return { + value: { + elements: lastResult.value.elements, + ends: { hasMoreBelow: lastResult.value.ends.hasMoreBelow, hasMoreAbove: false } + }, + guild: lastResult.guild + }; + }) } } catch (e: unknown) { LOG.error('error fetching above for subscription', e); @@ -428,16 +447,25 @@ function useMultipleGuildSubscriptionScrolling< if (!isMounted.current) return; if (!lastResultAfterAwait || guild !== lastResultAfterAwait.guild) return; setFetchAboveError(e); - setEnds(prev => (prev && { hasMoreBelow: prev.hasMoreBelow, hasMoreAbove: true })) + setLastResult((lastResult) => { + if (!lastResult) return null; + return { + value: { + elements: lastResult.value.elements, + ends: { hasMoreBelow: lastResult.value.ends.hasMoreBelow, hasMoreAbove: true }, + }, + guild: lastResult.guild + }; + }); } }, [ guild, lastResult, fetchAboveFunc, maxFetchElements ]); const fetchBelowCallable = useCallback(async (): Promise => { if (!isMounted.current) return; - if (!lastResult || !lastResult.value || lastResult.value.length === 0) return; + if (!lastResult || !lastResult.value || lastResult.value.elements.length === 0) return; if (guild !== lastResult.guild) return; try { - const reference = lastResult.value[lastResult.value.length - 1] as T; + const reference = lastResult.value.elements[lastResult.value.elements.length - 1] as T; const belowElements = await fetchBelowFunc(reference); const lastResultAfterAwait = getStateAfterAwait(setLastResult); if (!isMounted.current) return; @@ -448,16 +476,30 @@ function useMultipleGuildSubscriptionScrolling< let removedFromTop = false; setLastResult((lastResult) => { if (!lastResult) return null; - let newValue = (lastResult.value ?? []).concat(belowElements).sort(sortFunc); - if (newValue.length > maxElements) { - newValue = newValue.slice(Math.max(newValue.length - maxElements, 0)); + let newElements = (lastResult.value.elements ?? []).concat(belowElements).sort(sortFunc); + if (newElements.length > maxElements) { + newElements = newElements.slice(Math.max(newElements.length - maxElements, 0)); removedFromTop = true; } - return { value: newValue, guild: lastResult.guild }; + return { + value: { + elements: newElements, + ends: { hasMoreBelow, hasMoreAbove: removedFromTop || lastResult.value.ends.hasMoreAbove } + }, + guild: lastResult.guild + }; }); - setEnds(prev => (prev && { hasMoreBelow, hasMoreAbove: removedFromTop || prev.hasMoreAbove })); // :) } else { - setEnds(prev => (prev && { hasMoreBelow: false, hasMoreAbove: prev.hasMoreAbove })) + setLastResult((lastResult) => { + if (!lastResult) return null; + return { + value: { + elements: lastResult.value.elements, + ends: { hasMoreBelow: false, hasMoreAbove: lastResult.value.ends.hasMoreAbove } + }, + guild: lastResult.guild + }; + }); } } catch (e: unknown) { LOG.error('error fetching below for subscription', e); @@ -465,7 +507,16 @@ function useMultipleGuildSubscriptionScrolling< if (!isMounted.current) return; if (!lastResultAfterAwait || guild !== lastResultAfterAwait.guild) return; setFetchBelowError(e); - setEnds(prev => (prev && { hasMoreBelow: true, hasMoreAbove: prev.hasMoreAbove })); + setLastResult((lastResult) => { + if (!lastResult) return null; + return { + value: { + elements: lastResult.value.elements, + ends: { hasMoreBelow: true, hasMoreAbove: lastResult.value.ends.hasMoreAbove } + }, + guild: lastResult.guild + }; + }); } }, [ lastResult, fetchBelowFunc, maxFetchElements ]); @@ -476,8 +527,13 @@ function useMultipleGuildSubscriptionScrolling< fetchValue = fetchValue.slice(Math.max(fetchValue.length - maxElements)).sort(sortFunc); } //LOG.debug('Got items: ', { fetchValueLength: fetchValue?.length ?? '' }) - setEnds({ hasMoreAbove, hasMoreBelow: false }); - setLastResult({ value: fetchValue, guild: fetchValueGuild }); + setLastResult({ + value: { + elements: fetchValue, + ends: { hasMoreAbove, hasMoreBelow: false } + }, + guild: fetchValueGuild + }); setFetchError(null); }, [ sortFunc, maxFetchElements, maxElements ]); @@ -494,13 +550,25 @@ 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) { + if (lastResult.value.ends.hasMoreBelow) return lastResult; // Don't add to bottom if we are not at the bottom + let newResultElements = (lastResult.value.elements ?? []).concat(newElements).sort(sortFunc); + let newEnds = lastResult.value.ends; + if (newResultElements.length > maxElements) { // Remove in a way that tries to keep the scrollbar position consistent - newValue = removeByCounts(newValue, getRemoveCounts(newValue.length - maxElements)); + const removeCounts = getRemoveCounts(newResultElements.length - maxElements); + newResultElements = removeByCounts(newResultElements, removeCounts); + newEnds = { + hasMoreBelow: removeCounts.fromBottom > 0 || lastResult.value.ends.hasMoreBelow, + hasMoreAbove: removeCounts.fromTop > 0 || lastResult.value.ends.hasMoreAbove + }; } - return { value: newValue, guild: guild }; + return { + value: { + elements: newResultElements, + ends: newEnds + }, + guild: guild + }; }); }, [ guild, newEventArgsMap ]) as (Connectable & Conflictable)[NE]; const boundUpdateFunc = useCallback((...args: Arguments): void => { @@ -509,7 +577,15 @@ function useMultipleGuildSubscriptionScrolling< const updatedElements = updatedEventArgsMap(...args); setLastResult((lastResult) => { if (lastResult === null) return null; - return { value: (lastResult.value ?? []).map(element => updatedElements.find(updatedElement => updatedElement.id === element.id) ?? element).sort(sortFunc), guild: guild }; + return { + value: { + elements: (lastResult.value.elements ?? []) + .map(element => updatedElements.find(updatedElement => updatedElement.id === element.id) ?? element) + .sort(sortFunc), + ends: lastResult.value.ends + }, + guild: guild + }; }); }, [ guild, updatedEventArgsMap ]) as (Connectable & Conflictable)[UE]; const boundRemovedFunc = useCallback((...args: Arguments): void => { @@ -519,7 +595,14 @@ function useMultipleGuildSubscriptionScrolling< setLastResult((lastResult) => { if (lastResult === null) return null; const deletedIds = new Set(removedElements.map(removedElement => removedElement.id)); - return { value: (lastResult.value ?? []).filter(element => !deletedIds.has(element.id)), guild: guild }; + return { + value: { + elements: (lastResult.value.elements ?? []) + .filter(element => !deletedIds.has(element.id)), + ends: lastResult.value.ends + }, + guild: guild + }; }); }, [ guild, removedEventArgsMap ]) as (Connectable & Conflictable)[RE]; const boundConflictFunc = useCallback((...args: Arguments): void => { @@ -529,15 +612,27 @@ function useMultipleGuildSubscriptionScrolling< setLastResult((lastResult) => { if (lastResult === null) return null; const deletedIds = new Set(changes.deleted.map(deletedElement => deletedElement.id)); - let newValue = (lastResult.value ?? []) + let newResultElements = (lastResult.value.elements ?? []) .concat(changes.added) .map(element => changes.updated.find(change => change.newDataPoint.id === element.id)?.newDataPoint ?? element) .filter(element => !deletedIds.has(element.id)) .sort(sortFunc); - if (newValue.length > maxElements) { - newValue = removeByCounts(newValue, getRemoveCounts(newValue.length - maxElements)); + let newEnds = lastResult.value.ends; + if (newResultElements.length > maxElements) { + const removeCounts = getRemoveCounts(newResultElements.length - maxElements); + newResultElements = removeByCounts(newResultElements, removeCounts); + newEnds = { + hasMoreBelow: removeCounts.fromBottom > 0 || lastResult.value.ends.hasMoreBelow, + hasMoreAbove: removeCounts.fromTop > 0 || lastResult.value.ends.hasMoreAbove + }; } - return { value: newValue, guild: guild }; + return { + value: { + elements: newResultElements, + ends: newEnds + }, + guild: guild + }; }); }, [ guild, conflictEventArgsMap ]) as (Connectable & Conflictable)[CE]; @@ -567,7 +662,6 @@ function useMultipleGuildSubscriptionScrolling< fetchAboveCallable, fetchBelowCallable, setScrollRatio, - ends, lastResult, fetchError, fetchAboveError,