jump to bottom works!
This commit is contained in:
parent
29b7c47a7e
commit
f5d0240772
@ -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)' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
@ -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
11
src/.prettierrc.yml
Normal 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"
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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 () => {
|
||||
|
@ -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 };
|
||||
}
|
||||
|
@ -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)) {
|
||||
|
Loading…
Reference in New Issue
Block a user