more infinite scroll functionality. preparing to use recoil infinite scroll element
This commit is contained in:
parent
0b4c5356a9
commit
75d585880f
@ -0,0 +1,36 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
|
||||
import { LoadableValueScrolling } from "../require/loadables";
|
||||
import { useScrollableCallables } from "../require/react-helper";
|
||||
import Retry from './retry';
|
||||
|
||||
export interface InfiniteScrollRecoilProps<T, E> {
|
||||
scrollable: LoadableValueScrolling<T, E>;
|
||||
initialErrorMessage: string; // For if there was a problem loading the initial messages
|
||||
aboveErrorMessage: string; // For if there was a problem loading messages above the list
|
||||
belowErrorMessage: string; // For if there was a problem loading messages below the list
|
||||
children: ReactNode;
|
||||
}
|
||||
function InfiniteScrollRecoil<T>(props: InfiniteScrollRecoilProps<T[], T>) {
|
||||
const { scrollable, initialErrorMessage, aboveErrorMessage, belowErrorMessage, children } = props;
|
||||
|
||||
const {
|
||||
fetchAboveCallable,
|
||||
fetchBelowCallable,
|
||||
onScrollCallable
|
||||
} = useScrollableCallables(scrollable, 600); // Activate fetch above/below when 600 client px from the top/bottom
|
||||
|
||||
return (
|
||||
<div className="infinite-scroll-scroll-base" onScroll={onScrollCallable}>
|
||||
<div className="infinite-scroll-elements">
|
||||
<Retry error={scrollable?.above?.error} text={aboveErrorMessage} retryFunc={fetchAboveCallable} />
|
||||
{children}
|
||||
<Retry error={scrollable.error} text={initialErrorMessage} retryFunc={scrollable.retry ?? (async () => {/* do nothing if we are unloaded */})} />
|
||||
<Retry error={scrollable?.below?.error} text={belowErrorMessage} retryFunc={fetchBelowCallable} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default InfiniteScrollRecoil;
|
||||
|
@ -61,3 +61,4 @@ const InfiniteScroll: FC<InfiniteScrollProps> = (props: InfiniteScrollProps) =>
|
||||
};
|
||||
|
||||
export default InfiniteScroll;
|
||||
|
||||
|
@ -8,6 +8,7 @@ import { useAsyncSubmitButton } from '../require/react-helper';
|
||||
import Button from './button';
|
||||
|
||||
export interface RetryProps {
|
||||
// TODO: Replace this will the boolean 'hasError'
|
||||
error: unknown | null,
|
||||
text: string,
|
||||
retryFunc: () => Promise<void>
|
||||
|
@ -61,6 +61,10 @@ export function createFetchValueScrollingFunc<T, E>(
|
||||
const selfState = await getPromise(node);
|
||||
if (isPended(selfState)) return; // Don't send another request if we're already loading
|
||||
|
||||
// Cancel any active requests to the top/bottom
|
||||
if (selfState.above) selfState.above.cancel();
|
||||
if (selfState.below) selfState.below.cancel();
|
||||
|
||||
setSelf(DEF_PENDED_SCROLLING_VALUE);
|
||||
try {
|
||||
const result = await fetchFunc(guild, count);
|
||||
@ -93,8 +97,9 @@ export function createFetchValueScrollingReferenceFunc<T>(
|
||||
} {
|
||||
const { node, setSelf, getPromise } = atomEffectParam;
|
||||
// TODO: Improve cancellation behavior. The way it is now, we have to wait for promises to resolve before we can
|
||||
// fetch below. On giga-slow internet, this may stink if you fetch messages above, cancel the messages below, and then try to scroll back down to the fetchBottomFunc
|
||||
// fetch above/below again. On giga-slow internet, this may stink if you fetch messages above, cancel the messages below, and then try to scroll back down to the fetchBottomFunc
|
||||
// (you'd have to wait for the bottom request to finish before it sends the next fetch)
|
||||
// This also could show up if you change channels while a fetch is in progress (since it auto-cancels)
|
||||
let canceled = false;
|
||||
const cancel = () => { canceled = true; };
|
||||
const fetchValueReferenceFunc = async (reference: T) => {
|
||||
@ -371,8 +376,8 @@ export function multipleScrollingGuildSubscriptionEffect<
|
||||
guildId,
|
||||
fetchCount,
|
||||
fetchFuncs.fetchBottomFunc,
|
||||
(result: T[]) => createLoadedScrollingEnd(result.length >= fetchCount, fetchValueAboveReferenceFunc, cancelAbove),
|
||||
(_result: T[]) => createLoadedScrollingEnd(false, fetchValueBelowReferenceFunc, cancelBelow)
|
||||
(result: T[]) => createLoadedScrollingEnd(result.length >= fetchCount, fetchValueAboveReferenceFunc, cancelAbove), // above end
|
||||
(_result: T[]) => createLoadedScrollingEnd(false, fetchValueBelowReferenceFunc, cancelBelow) // below end
|
||||
);
|
||||
// Fetch Above a Reference
|
||||
const {
|
||||
@ -382,12 +387,13 @@ export function multipleScrollingGuildSubscriptionEffect<
|
||||
atomEffectParam,
|
||||
guildId,
|
||||
(selfState: LoadedValueScrolling<T[], T>) => selfState.above,
|
||||
(selfState: LoadedValueScrolling<T[], T>, end: LoadableScrollingEnd<T>) => ({ ...selfState, above: end }), // for "pending, etc"
|
||||
(selfState: LoadedValueScrolling<T[], T>, end: LoadableScrollingEnd<T>) => ({ ...selfState, above: end }), // for "cancelled, pending, etc"
|
||||
(selfState: LoadedValueScrolling<T[], T>, end: LoadedScrollingEnd<T>, result: T[]) => {
|
||||
let nextValue = result.concat(selfState.value).sort(sortFunc);
|
||||
let sliced = false;
|
||||
if (nextValue.length > maxElements) {
|
||||
nextValue = nextValue.slice(undefined, maxElements);
|
||||
cancelBelow();
|
||||
sliced = true;
|
||||
}
|
||||
const loadedValue: LoadedValueScrolling<T[], T> = {
|
||||
@ -418,6 +424,7 @@ export function multipleScrollingGuildSubscriptionEffect<
|
||||
let sliced = false;
|
||||
if (nextValue.length > maxElements) {
|
||||
nextValue = nextValue.slice(nextValue.length - maxElements, undefined);
|
||||
cancelAbove();
|
||||
sliced = true;
|
||||
}
|
||||
const loadedValue: LoadedValueScrolling<T[], T> = {
|
||||
@ -505,6 +512,7 @@ export function applyListScrollingFuncIfLoaded<T extends { id: string }, A>(
|
||||
// This would be very convenient to have. Albeit, new messages *should* be coming to the bottom anyway.
|
||||
// also, deleted/updated/inserted between messages should (hopefully) not happen very much
|
||||
nextValue = nextValue.slice(nextValue.length - maxElements, undefined);
|
||||
selfState.above.cancel(); // Cancel any pending request since we are removing its reference
|
||||
sliced = true;
|
||||
}
|
||||
const loadedValue: LoadedValueScrolling<T[], T> = {
|
||||
|
@ -65,19 +65,19 @@ export function isLoaded<T>(loadableValue: LoadableValue<T>): loadableValue is L
|
||||
return loadableValue.value !== undefined;
|
||||
}
|
||||
|
||||
export interface UnloadedScrollingEnd {
|
||||
export interface CancelledScrollingEnd<T> {
|
||||
hasMore: undefined | boolean; // Could be set to a boolean if we delete from opposite end while adding new elements
|
||||
hasError: undefined;
|
||||
error: undefined;
|
||||
retry: undefined;
|
||||
cancel: undefined;
|
||||
fetch: (reference: T) => Promise<void>;
|
||||
cancel: () => void;
|
||||
loading: false;
|
||||
}
|
||||
export interface LoadingScrollingEnd<T> {
|
||||
hasMore: undefined | boolean;
|
||||
hasError: undefined;
|
||||
error: undefined;
|
||||
retry: (reference: T) => Promise<void>;
|
||||
fetch: (reference: T) => Promise<void>;
|
||||
cancel: () => void;
|
||||
loading: true;
|
||||
}
|
||||
@ -85,7 +85,7 @@ export interface LoadedScrollingEnd<T> {
|
||||
hasMore: boolean;
|
||||
hasError: false;
|
||||
error: undefined;
|
||||
retry: (reference: T) => Promise<void>;
|
||||
fetch: (reference: T) => Promise<void>;
|
||||
cancel: () => void;
|
||||
loading: false;
|
||||
}
|
||||
@ -93,54 +93,50 @@ export interface FailedScrollingEnd<T> {
|
||||
hasMore: undefined | boolean;
|
||||
hasError: true;
|
||||
error: unknown;
|
||||
retry: (reference: T) => Promise<void>;
|
||||
fetch: (reference: T) => Promise<void>;
|
||||
cancel: () => void;
|
||||
loading: false;
|
||||
}
|
||||
export type LoadableScrollingEnd<T> = UnloadedScrollingEnd | LoadingScrollingEnd<T> | LoadedScrollingEnd<T> | FailedScrollingEnd<T>;
|
||||
export const DEF_UNLOADED_SCROLL_END: UnloadedScrollingEnd = { hasMore: undefined, hasError: undefined, error: undefined, retry: undefined, cancel: undefined, loading: false };
|
||||
export function createLoadingScrollingEnd<T>(retry: (reference: T) => Promise<void>, cancel: () => void): LoadingScrollingEnd<T> {
|
||||
export type LoadableScrollingEnd<T> = CancelledScrollingEnd<T> | LoadingScrollingEnd<T> | LoadedScrollingEnd<T> | FailedScrollingEnd<T>;
|
||||
export function createCancelledScrollingEnd<T>(loadingScrollingEnd: LoadingScrollingEnd<T>): CancelledScrollingEnd<T> {
|
||||
return { ...loadingScrollingEnd, loading: false };
|
||||
}
|
||||
export function createLoadingScrollingEnd<T>(fetch: (reference: T) => Promise<void>, cancel: () => void): LoadingScrollingEnd<T> {
|
||||
return {
|
||||
hasMore: undefined,
|
||||
hasError: undefined,
|
||||
error: undefined,
|
||||
retry,
|
||||
fetch,
|
||||
cancel,
|
||||
loading: true
|
||||
};
|
||||
}
|
||||
export function createLoadedScrollingEnd<T>(hasMore: boolean, retry: (reference: T) => Promise<void>, cancel: () => void): LoadedScrollingEnd<T> {
|
||||
export function createLoadedScrollingEnd<T>(hasMore: boolean, fetch: (reference: T) => Promise<void>, cancel: () => void): LoadedScrollingEnd<T> {
|
||||
return {
|
||||
hasMore,
|
||||
hasError: false,
|
||||
error: undefined,
|
||||
retry,
|
||||
fetch,
|
||||
cancel,
|
||||
loading: false
|
||||
};
|
||||
}
|
||||
export function createFailedScrollingEnd<T>(error: unknown, retry: (reference: T) => Promise<void>, cancel: () => void): FailedScrollingEnd<T> {
|
||||
export function createFailedScrollingEnd<T>(error: unknown, fetch: (reference: T) => Promise<void>, cancel: () => void): FailedScrollingEnd<T> {
|
||||
return {
|
||||
hasMore: undefined,
|
||||
hasError: true,
|
||||
error,
|
||||
retry,
|
||||
fetch,
|
||||
cancel,
|
||||
loading: false
|
||||
};
|
||||
}
|
||||
export function isEndUnload<T>(loadableScrollingEnd: LoadableScrollingEnd<T>): loadableScrollingEnd is UnloadedScrollingEnd {
|
||||
return loadableScrollingEnd.hasError === undefined && loadableScrollingEnd.loading === false;
|
||||
}
|
||||
export function isEndPended<T>(loadableScrollingEnd: LoadableScrollingEnd<T>): loadableScrollingEnd is LoadingScrollingEnd<T> {
|
||||
return loadableScrollingEnd.loading === true;
|
||||
}
|
||||
export function isEndFailed<T>(loadableScrollingEnd: LoadableScrollingEnd<T>): loadableScrollingEnd is FailedScrollingEnd<T> {
|
||||
return loadableScrollingEnd.hasError === true;
|
||||
}
|
||||
export function isEndLoaded<T>(loadableScrollingEnd: LoadableScrollingEnd<T>): loadableScrollingEnd is LoadedScrollingEnd<T> {
|
||||
return loadableScrollingEnd.hasError === false;
|
||||
}
|
||||
|
||||
export type UnloadedValueScrolling = UnloadedValue & {
|
||||
above: undefined;
|
||||
|
@ -13,6 +13,7 @@ import electron from 'electron';
|
||||
import ElementsUtil, { IAlignment } from './elements-util';
|
||||
import React from 'react';
|
||||
import FileDropTarget from '../components/file-drop-target';
|
||||
import { isLoaded, LoadableValueScrolling } from './loadables';
|
||||
|
||||
/** Returns a ref that is true if the component is mounted and false otherwise. Very useful for async stuff */
|
||||
export function useIsMountedRef() {
|
||||
@ -320,6 +321,76 @@ export function useColumnReverseInfiniteScroll(
|
||||
|
||||
return [ onScrollCallable, loadAbove, loadBelow ];
|
||||
}
|
||||
// Returns functions that can be used to fetch the elements above the top element / below the bottom element
|
||||
export function useScrollableCallables<T>(scrollable: LoadableValueScrolling<T[], T>, threshold: number): {
|
||||
fetchAboveCallable: () => Promise<void>, // these fetch functions are returned for use in retry buttons
|
||||
fetchBelowCallable: () => Promise<void>,
|
||||
onScrollCallable: (event: UIEvent<HTMLElement>) => void
|
||||
} {
|
||||
const isMounted = useIsMountedRef();
|
||||
const [ loadingAbove, setLoadingAbove ] = useState<boolean>(false);
|
||||
const [ loadingBelow, setLoadingBelow ] = useState<boolean>(false);
|
||||
|
||||
const fetchAboveCallable = useCallback(async () => {
|
||||
if (loadingAbove) return;
|
||||
setLoadingAbove(true);
|
||||
if (!isLoaded(scrollable)) return;
|
||||
if (scrollable.above.hasMore !== true) return; // Don't load unless we know there could be more
|
||||
if (scrollable.value.length === 0) return; // There's no references available. In this case, hasMore should already have been false/undefined anyway
|
||||
const topReference = scrollable.value[0] as T;
|
||||
try {
|
||||
await scrollable.above.fetch(topReference);
|
||||
} finally {
|
||||
if (isMounted.current) {
|
||||
setLoadingAbove(false);
|
||||
}
|
||||
}
|
||||
}, [ scrollable ]);
|
||||
const fetchBelowCallable = useCallback(async () => {
|
||||
if (loadingBelow) return;
|
||||
setLoadingBelow(true);
|
||||
if (!isLoaded(scrollable)) return;
|
||||
if (scrollable.below.hasMore !== true) return; // Don't load unless we know there could be more
|
||||
if (scrollable.value.length === 0) return; // There's no references available. In this case, hasMore should already have been false/undefined anyway
|
||||
const bottomReference = scrollable.value[scrollable.value.length - 1] as T;
|
||||
try {
|
||||
await scrollable.below.fetch(bottomReference);
|
||||
} finally {
|
||||
if (isMounted.current) {
|
||||
setLoadingBelow(false);
|
||||
}
|
||||
}
|
||||
}, [ scrollable ]);
|
||||
|
||||
const onScrollCallable = useCallback(async (event: UIEvent<HTMLElement>) => {
|
||||
const scrollTop = event.currentTarget.scrollTop;
|
||||
const scrollHeight = event.currentTarget.scrollHeight;
|
||||
const clientHeight = event.currentTarget.clientHeight;
|
||||
|
||||
// WARNING
|
||||
// There's likely an inconsistency between browsers on this so have fun when you're working
|
||||
// on the cross-platform implementation
|
||||
// scrollTop apparantly is negative for column-reverse divs (this actually kindof makes sense
|
||||
// if you flip your head upside down)
|
||||
// I expect this was a change with some version of chromium.
|
||||
// MDN documentation issue: https://github.com/mdn/content/issues/10968
|
||||
|
||||
const distToTop = -(clientHeight - scrollHeight - scrollTop); // keep in mind scrollTop is negative >:]
|
||||
const distToBottom = -scrollTop;
|
||||
|
||||
//LOG.debug(`scroll callable update. to top: ${distToTop}, to bottom: ${distToBottom}`)
|
||||
|
||||
if (distToTop < threshold) {
|
||||
await fetchAboveCallable();
|
||||
}
|
||||
|
||||
if (distToBottom < threshold) {
|
||||
await fetchBelowCallable();
|
||||
}
|
||||
}, [ fetchAboveCallable, fetchBelowCallable, threshold ]);
|
||||
|
||||
return { fetchAboveCallable, fetchBelowCallable, onScrollCallable };
|
||||
}
|
||||
|
||||
// Makes sure to also allow you to fly-out a click starting inside of the ref'd element but was dragged outside
|
||||
/** Calls the close action when you hit escape or click outside of the ref element */
|
||||
|
Loading…
Reference in New Issue
Block a user