jump to bottom works!

This commit is contained in:
Michael Peters 2023-01-10 23:34:46 -08:00
parent 29b7c47a7e
commit f5d0240772
8 changed files with 155 additions and 71 deletions

View File

@ -14,7 +14,7 @@ module.exports = {
// syntax
'no-unused-vars': 'off',
'arrow-body-style': ['warn', 'as-needed'],
'capitalized-comments': ['warn', 'never', { ignorePattern: 'TODO|WARNING|NOTE' }], // make these statements stand out
'capitalized-comments': ['warn', 'never', { ignorePattern: 'TODO|WARNING|NOTE|LOG' }], // make these statements stand out
// 'curly': [ 'warn', 'multi-line', 'consistent' ],
'dot-notation': 'warn',
eqeqeq: 'warn',
@ -154,6 +154,9 @@ module.exports = {
// 'react/jsx-tag-spacing': [ 'warn', { closingSlash: 'never', beforeSelfClosing: 'always', afterOpening: 'never', beforeClosing: 'never' } ],
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
'react-hooks/exhaustive-deps': [
'warn',
{ additionalHooks: '(useOneTimeAsyncAction|useAsyncCallback|useAsyncVoidCallback|useAsyncSubmitButton)' },
],
},
};

View File

@ -1,13 +0,0 @@
{
"printWidth": 120,
"tabWidth": 4,
"useTabs": true,
"semi": true,
"singleQuote": true,
"quoteProps": "as-needed",
"jsxSingleQuote": false,
"trailingComma": "all",
"bracketSpacing": true,
"bracketSameLine": false,
"arrowParens": "avoid"
}

11
src/.prettierrc.yml Normal file
View File

@ -0,0 +1,11 @@
printWidth: 120
tabWidth: 4
useTabs: true
semi: true
singleQuote: true
quoteProps: "as-needed"
jsxSingleQuote: false
trailingComma: "all"
bracketSpacing: true
bracketSameLine: false
arrowParens: "avoid"

View File

@ -1,7 +1,7 @@
import React, { useMemo, MutableRefObject, ReactNode } from 'react';
import { LoadableValueScrolling } from '../require/loadables';
import { useScrollableCallables } from '../require/react-helper';
import { useAsyncVoidCallback, useScrollableCallables } from '../require/react-helper';
import Retry from './retry';
export interface InfiniteScrollRecoilProps<T, E> {
@ -10,52 +10,71 @@ export interface InfiniteScrollRecoilProps<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
bottomErrorMessage: string; // for if there was a problem loading messages below the list
children: ReactNode;
}
function InfiniteScrollRecoil<T>(props: InfiniteScrollRecoilProps<T[], T>) {
const { infiniteScrollRef, scrollable, initialErrorMessage, aboveErrorMessage, belowErrorMessage, children } =
props;
const {
infiniteScrollRef,
scrollable,
initialErrorMessage,
aboveErrorMessage,
belowErrorMessage,
bottomErrorMessage,
children,
} = props;
const { fetchAboveCallable, fetchBelowCallable, onScrollCallable } = useScrollableCallables(scrollable, 600); // activate fetch above/below when 600 client px from the top/bottom
const { fetchAboveCallable, fetchBelowCallable, fetchBottomCallable, onScrollCallable } = useScrollableCallables(
scrollable,
600,
); // activate fetch above/below when 600 client px from the top/bottom
const jumpToBottom = useMemo(() => {
if (!scrollable.below?.hasMore) {
return null;
}
return (
<div className="jump-to-bottom-wrapper">
<div className="jump-to-bottom">
<div className="text">You are viewing older messages</div>
<div className="jump" onClick={async () => { console.log('jump to bottom'); }}>
Jump to Bottom
</div>
</div>
</div>
);
}, [scrollable]);
const [jumpToBottomCallable] = useAsyncVoidCallback(async () => {
await fetchBottomCallable();
if (infiniteScrollRef.current) {
// NOTE: 0 is actually the bottom due to column-reverse
infiniteScrollRef.current.scrollTop = 0;
}
}, [fetchBottomCallable, infiniteScrollRef]);
const jumpToBottomElement = useMemo(() => {
if (!scrollable.below?.hasMore) {
return null;
}
return (
<div className="jump-to-bottom-wrapper">
<div className="jump-to-bottom">
<div className="text">You are viewing older messages</div>
<div className="jump" onClick={jumpToBottomCallable}>
Jump to Bottom
</div>
</div>
</div>
);
}, [scrollable, jumpToBottomCallable]);
return (
<div>
<div ref={infiniteScrollRef} 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.below?.error} text={belowErrorMessage} retryFunc={fetchBelowCallable} />
<Retry
error={scrollable.error}
text={initialErrorMessage}
// TODO: Allow null instead of noop func to prevent re-renders
retryFunc={
scrollable.retry ??
(async () => {
/* do nothing if we are unloaded */
})
}
/>
</div>
</div>
{jumpToBottom}
</div>
<div>
<div ref={infiniteScrollRef} 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.below?.error} text={belowErrorMessage} retryFunc={fetchBelowCallable} />
<Retry error={scrollable.bottom?.error} text={bottomErrorMessage} retryFunc={fetchBottomCallable} />
<Retry
error={scrollable.error}
text={initialErrorMessage}
// TODO: Allow null instead of noop func to prevent re-renders
retryFunc={
scrollable.retry ??
(async () => {
/* do nothing if we are unloaded */
})
}
/>
</div>
</div>
{jumpToBottomElement}
</div>
);
}

View File

@ -72,6 +72,7 @@ export function createFetchValueScrollingFunc<T, E>(
fetchFunc: (guild: CombinedGuild, count: number) => Promise<Defined<T>>,
createAboveEndFunc: (result: Defined<T>) => LoadableScrollingEnd<E>,
createBelowEndFunc: (result: Defined<T>) => LoadableScrollingEnd<E>,
createBottomEndFunc: (result: Defined<T>) => LoadableScrollingEnd<void>,
): FetchValueScrollingFunc {
const { node, setSelf, getPromise } = atomEffectParam;
const fetchValueReferenceFunc = async () => {
@ -94,6 +95,7 @@ export function createFetchValueScrollingFunc<T, E>(
fetchValueReferenceFunc,
createAboveEndFunc(result),
createBelowEndFunc(result),
createBottomEndFunc(result),
),
);
} catch (e: unknown) {
@ -104,24 +106,24 @@ export function createFetchValueScrollingFunc<T, E>(
return fetchValueReferenceFunc;
}
export type FetchValueScrollingReferenceFunc<T> = (reference: T) => Promise<void>;
export function createFetchValueScrollingReferenceFunc<T>(
export type FetchValueScrollingReferenceFunc<ET> = (reference: ET) => Promise<void>;
export function createFetchValueScrollingReferenceFunc<T, ET extends T | void = T>(
atomEffectParam: AtomEffectParam<LoadableValueScrolling<T[], T>>,
guildId: number,
getFunc: (selfState: LoadedValueScrolling<T[], T>) => LoadableScrollingEnd<T>,
getFunc: (selfState: LoadedValueScrolling<T[], T>) => LoadableScrollingEnd<ET>,
applyEndToSelf: (
selfState: LoadedValueScrolling<T[], T>,
end: LoadableScrollingEnd<T>,
end: LoadableScrollingEnd<ET>,
) => LoadedValueScrolling<T[], T>,
applyResultToSelf: (
selfState: LoadedValueScrolling<T[], T>,
end: LoadedScrollingEnd<T>,
end: LoadedScrollingEnd<ET>,
result: T[],
) => LoadedValueScrolling<T[], T>,
count: number,
fetchReferenceFunc: (guild: CombinedGuild, reference: T, count: number) => Promise<T[]>,
fetchReferenceFunc: (guild: CombinedGuild, reference: ET, count: number) => Promise<T[]>,
): {
fetchValueReferenceFunc: FetchValueScrollingReferenceFunc<T>;
fetchValueReferenceFunc: FetchValueScrollingReferenceFunc<ET>;
cancel: () => void;
} {
const { node, setSelf, getPromise } = atomEffectParam;
@ -133,7 +135,7 @@ export function createFetchValueScrollingReferenceFunc<T>(
const cancel = () => {
canceled = true;
};
const fetchValueReferenceFunc = async (reference: T) => {
const fetchValueReferenceFunc = async (reference: ET) => {
const guild = await getPromise(guildState(guildId));
if (guild === null) return; // NOTE: This would only happen if this atom is created before its corresponding guild exists in the guildsManager
@ -144,7 +146,7 @@ export function createFetchValueScrollingReferenceFunc<T>(
if (isEndPended(selfEnd)) return; // don't send a request if we're already loading
canceled = false;
selfEnd = createLoadingScrollingEnd(fetchValueReferenceFunc, cancel);
selfEnd = createLoadingScrollingEnd<ET>(fetchValueReferenceFunc, cancel);
setSelf(applyEndToSelf(selfState, selfEnd));
try {
const result = await fetchReferenceFunc(guild, reference, count);
@ -441,7 +443,9 @@ export function multipleScrollingGuildSubscriptionEffect<
(result: T[]) =>
createLoadedScrollingEnd(result.length >= fetchCount, fetchValueAboveReferenceFunc, cancelAbove), // above end
(_result: T[]) => createLoadedScrollingEnd(false, fetchValueBelowReferenceFunc, cancelBelow), // below end
(_result: T[]) => createLoadedScrollingEnd(false, fetchValueBottomReferenceFunc, cancelBottom), // bottom end
);
// fetch Above a Reference
const { fetchValueReferenceFunc: fetchValueAboveReferenceFunc, cancel: cancelAbove } =
createFetchValueScrollingReferenceFunc<T>(
@ -474,6 +478,7 @@ export function multipleScrollingGuildSubscriptionEffect<
fetchCount,
fetchFuncs.fetchAboveFunc,
);
// fetch Below a Reference
const { fetchValueReferenceFunc: fetchValueBelowReferenceFunc, cancel: cancelBelow } =
createFetchValueScrollingReferenceFunc<T>(
@ -507,6 +512,39 @@ export function multipleScrollingGuildSubscriptionEffect<
fetchFuncs.fetchBelowFunc,
);
// fetch the bottom
const { fetchValueReferenceFunc: fetchValueBottomReferenceFunc, cancel: cancelBottom } =
createFetchValueScrollingReferenceFunc<T, void>(
atomEffectParam,
guildId,
(selfState: LoadedValueScrolling<T[], T>) => selfState.bottom,
(selfState: LoadedValueScrolling<T[], T>, end: LoadableScrollingEnd<void>) => ({
...selfState,
bottom: end,
}),
(selfState: LoadedValueScrolling<T[], T>, end: LoadedScrollingEnd<void>, result: T[]) => {
// make sure to cancel the ends in case they are pending
cancelAbove();
cancelBelow();
const loadedValue: LoadedValueScrolling<T[], T> = {
...selfState,
value: result, // simply overwrite
above: {
...selfState.above,
hasMore: true,
} as LoadableScrollingEnd<T>,
below: {
...selfState.below,
hasMore: false,
} as LoadableScrollingEnd<T>,
bottom: end,
};
return loadedValue;
},
fetchCount,
(guild: CombinedGuild, _reference: void, count: number) => fetchFuncs.fetchBottomFunc(guild, count),
);
// fetch bottom value on first get
if (trigger === 'get') {
(async () => {

View File

@ -172,42 +172,50 @@ export function isEndFailed<T>(
export type UnloadedValueScrolling = UnloadedValue & {
above: undefined;
below: undefined;
bottom: undefined;
};
export type LoadingValueScrolling = LoadingValue & {
above: undefined;
below: undefined;
bottom: undefined;
};
export type LoadedValueScrolling<T, E> = LoadedValue<T> & {
above: LoadableScrollingEnd<E>;
below: LoadableScrollingEnd<E>;
bottom: LoadableScrollingEnd<void>; // void since no reference needed to fetch the bottom
};
export type FailedValueScrolling = FailedValue & {
above: undefined;
below: undefined;
bottom: undefined;
};
export type LoadableValueScrolling<T, E> =
| UnloadedValueScrolling
| LoadingValueScrolling
| LoadedValueScrolling<T, E>
| FailedValueScrolling;
export const DEF_UNLOADED_SCROLLING_VALUE: UnloadedValueScrolling = {
...DEF_UNLOADED_VALUE,
above: undefined,
below: undefined,
bottom: undefined,
};
export const DEF_PENDED_SCROLLING_VALUE: LoadingValueScrolling = {
...DEF_PENDED_VALUE,
above: undefined,
below: undefined,
bottom: undefined,
};
export function createLoadedValueScrolling<T, E>(
value: Defined<T>,
retry: () => Promise<void>,
above: LoadableScrollingEnd<E>,
below: LoadableScrollingEnd<E>,
bottom: LoadableScrollingEnd<void>,
): LoadedValueScrolling<T, E> {
return { ...createLoadedValue(value, retry), above, below };
return { ...createLoadedValue(value, retry), above, below, bottom };
}
export function createFailedValueScrolling(error: unknown, retry: () => Promise<void>): FailedValueScrolling {
return { ...createFailedValue(error, retry), above: undefined, below: undefined };
return { ...createFailedValue(error, retry), above: undefined, below: undefined, bottom: undefined };
}

View File

@ -142,6 +142,7 @@ export function useAsyncVoidCallback(
const [callable] = useAsyncCallback(async (isMounted: MutableRefObject<boolean>) => {
await actionFunc(isMounted);
return { errorMessage: null, result: null };
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps);
return [callable];
}
@ -240,6 +241,7 @@ export function useDownloadButton(
setText(textMapping.success);
},
// TODO: look into this warning
[downloadName, fetchBuff, ...fetchBuffDeps, filePath, fileBuffer],
);
@ -268,6 +270,7 @@ export function useAsyncSubmitButton<ResultType>(
if (actionReturnValue.errorMessage === null) setComplete(true);
return actionReturnValue;
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[...deps, actionFunc],
);
@ -287,13 +290,15 @@ export function useScrollableCallables<T>(
scrollable: LoadableValueScrolling<T[], T>,
threshold: number,
): {
fetchAboveCallable: () => Promise<void>; // these fetch functions are returned for use in retry buttons
fetchAboveCallable: () => Promise<void>; // these fetch functions are returned for use in retry/reset buttons
fetchBelowCallable: () => Promise<void>;
fetchBottomCallable: () => Promise<void>;
onScrollCallable: (event: UIEvent<HTMLElement>) => void;
} {
const isMounted = useIsMountedRef();
const [loadingAbove, setLoadingAbove] = useState<boolean>(false);
const [loadingBelow, setLoadingBelow] = useState<boolean>(false);
const [loadingBottom, setLoadingBottom] = useState<boolean>(false);
const fetchAboveCallable = useCallback(async () => {
if (loadingAbove) return;
@ -326,6 +331,19 @@ export function useScrollableCallables<T>(
}
}, [isMounted, loadingBelow, scrollable]);
const fetchBottomCallable = useCallback(async () => {
if (loadingBottom) return;
if (!isLoaded(scrollable)) return;
setLoadingBottom(true);
try {
await scrollable.bottom.fetch();
} finally {
if (isMounted.current) {
setLoadingBottom(false);
}
}
}, [isMounted, loadingBottom, scrollable]);
const onScrollCallable = useCallback(
async (event: UIEvent<HTMLElement>) => {
const scrollTop = event.currentTarget.scrollTop;
@ -356,7 +374,7 @@ export function useScrollableCallables<T>(
[scrollable, fetchAboveCallable, fetchBelowCallable, threshold],
);
return { fetchAboveCallable, fetchBelowCallable, onScrollCallable };
return { fetchAboveCallable, fetchBelowCallable, fetchBottomCallable, onScrollCallable };
}
// makes sure to also allow you to fly-out a click starting inside of the ref'd element but was dragged outside
@ -369,7 +387,7 @@ export function useActionWhenEscapeOrClickedOrContextOutsideEffect(
const data = useRef<
Map<number, { ref: RefObject<HTMLElement>; mouseDownTarget: Node | null; mouseUpTarget: Node | null }>
>(new Map());
const [hadMouseDown, setHadMouseDown] = useState<boolean>(false);
const [hadMouseDown, setHadMouseDown] = useState<boolean>(false);
useEffect(() => {
data.current.clear();
@ -381,7 +399,7 @@ export function useActionWhenEscapeOrClickedOrContextOutsideEffect(
const handleClickOutside = useCallback(
(event: MouseEvent) => {
if (!hadMouseDown) return; // the click event can fire during the mouse up event that spawns the element that uses this hook
if (!hadMouseDown) return; // the click event can fire during the mouse up event that spawns the element that uses this hook
for (const [_idx, { ref, mouseDownTarget, mouseUpTarget }] of data.current) {
if (!ref.current) return;
@ -398,7 +416,7 @@ export function useActionWhenEscapeOrClickedOrContextOutsideEffect(
);
const handleMouseDown = useCallback((event: MouseEvent) => {
setHadMouseDown(true);
setHadMouseDown(true);
for (const [_idx, dataElement] of data.current) {
if (!dataElement.ref.current) return;
if (dataElement.ref.current.contains(event.target as Node)) {