diff --git a/figma/server-settings.fig b/archive/figma/server-settings.fig similarity index 100% rename from figma/server-settings.fig rename to archive/figma/server-settings.fig diff --git a/src/.eslintrc.js b/src/.eslintrc.js index 663ae87..921c7de 100644 --- a/src/.eslintrc.js +++ b/src/.eslintrc.js @@ -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)' }, + ], }, }; diff --git a/src/.prettierrc.json b/src/.prettierrc.json deleted file mode 100644 index d111229..0000000 --- a/src/.prettierrc.json +++ /dev/null @@ -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" -} diff --git a/src/.prettierrc.yml b/src/.prettierrc.yml new file mode 100644 index 0000000..275e923 --- /dev/null +++ b/src/.prettierrc.yml @@ -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" diff --git a/src/client/webapp/elements/components/infinite-scroll.tsx b/src/client/webapp/elements/components/infinite-scroll.tsx index 2d84a94..a7a2a51 100644 --- a/src/client/webapp/elements/components/infinite-scroll.tsx +++ b/src/client/webapp/elements/components/infinite-scroll.tsx @@ -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 { @@ -10,52 +10,71 @@ export interface InfiniteScrollRecoilProps { 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(props: InfiniteScrollRecoilProps) { - 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 ( -
-
-
You are viewing older messages
-
{ console.log('jump to bottom'); }}> - Jump to Bottom -
-
-
- ); - }, [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 ( +
+
+
You are viewing older messages
+
+ Jump to Bottom +
+
+
+ ); + }, [scrollable, jumpToBottomCallable]); return ( -
-
-
- - {children} - - { - /* do nothing if we are unloaded */ - }) - } - /> -
-
- {jumpToBottom} -
+
+
+
+ + {children} + + + { + /* do nothing if we are unloaded */ + }) + } + /> +
+
+ {jumpToBottomElement} +
); } diff --git a/src/client/webapp/elements/require/atoms-funcs.ts b/src/client/webapp/elements/require/atoms-funcs.ts index 067ceef..92290ee 100644 --- a/src/client/webapp/elements/require/atoms-funcs.ts +++ b/src/client/webapp/elements/require/atoms-funcs.ts @@ -72,6 +72,7 @@ export function createFetchValueScrollingFunc( fetchFunc: (guild: CombinedGuild, count: number) => Promise>, createAboveEndFunc: (result: Defined) => LoadableScrollingEnd, createBelowEndFunc: (result: Defined) => LoadableScrollingEnd, + createBottomEndFunc: (result: Defined) => LoadableScrollingEnd, ): FetchValueScrollingFunc { const { node, setSelf, getPromise } = atomEffectParam; const fetchValueReferenceFunc = async () => { @@ -94,6 +95,7 @@ export function createFetchValueScrollingFunc( fetchValueReferenceFunc, createAboveEndFunc(result), createBelowEndFunc(result), + createBottomEndFunc(result), ), ); } catch (e: unknown) { @@ -104,24 +106,24 @@ export function createFetchValueScrollingFunc( return fetchValueReferenceFunc; } -export type FetchValueScrollingReferenceFunc = (reference: T) => Promise; -export function createFetchValueScrollingReferenceFunc( +export type FetchValueScrollingReferenceFunc = (reference: ET) => Promise; +export function createFetchValueScrollingReferenceFunc( atomEffectParam: AtomEffectParam>, guildId: number, - getFunc: (selfState: LoadedValueScrolling) => LoadableScrollingEnd, + getFunc: (selfState: LoadedValueScrolling) => LoadableScrollingEnd, applyEndToSelf: ( selfState: LoadedValueScrolling, - end: LoadableScrollingEnd, + end: LoadableScrollingEnd, ) => LoadedValueScrolling, applyResultToSelf: ( selfState: LoadedValueScrolling, - end: LoadedScrollingEnd, + end: LoadedScrollingEnd, result: T[], ) => LoadedValueScrolling, count: number, - fetchReferenceFunc: (guild: CombinedGuild, reference: T, count: number) => Promise, + fetchReferenceFunc: (guild: CombinedGuild, reference: ET, count: number) => Promise, ): { - fetchValueReferenceFunc: FetchValueScrollingReferenceFunc; + fetchValueReferenceFunc: FetchValueScrollingReferenceFunc; cancel: () => void; } { const { node, setSelf, getPromise } = atomEffectParam; @@ -133,7 +135,7 @@ export function createFetchValueScrollingReferenceFunc( 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( if (isEndPended(selfEnd)) return; // don't send a request if we're already loading canceled = false; - selfEnd = createLoadingScrollingEnd(fetchValueReferenceFunc, cancel); + selfEnd = createLoadingScrollingEnd(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( @@ -474,6 +478,7 @@ export function multipleScrollingGuildSubscriptionEffect< fetchCount, fetchFuncs.fetchAboveFunc, ); + // fetch Below a Reference const { fetchValueReferenceFunc: fetchValueBelowReferenceFunc, cancel: cancelBelow } = createFetchValueScrollingReferenceFunc( @@ -507,6 +512,39 @@ export function multipleScrollingGuildSubscriptionEffect< fetchFuncs.fetchBelowFunc, ); + // fetch the bottom + const { fetchValueReferenceFunc: fetchValueBottomReferenceFunc, cancel: cancelBottom } = + createFetchValueScrollingReferenceFunc( + atomEffectParam, + guildId, + (selfState: LoadedValueScrolling) => selfState.bottom, + (selfState: LoadedValueScrolling, end: LoadableScrollingEnd) => ({ + ...selfState, + bottom: end, + }), + (selfState: LoadedValueScrolling, end: LoadedScrollingEnd, result: T[]) => { + // make sure to cancel the ends in case they are pending + cancelAbove(); + cancelBelow(); + const loadedValue: LoadedValueScrolling = { + ...selfState, + value: result, // simply overwrite + above: { + ...selfState.above, + hasMore: true, + } as LoadableScrollingEnd, + below: { + ...selfState.below, + hasMore: false, + } as LoadableScrollingEnd, + 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 () => { diff --git a/src/client/webapp/elements/require/loadables.ts b/src/client/webapp/elements/require/loadables.ts index cb4394e..5be3914 100644 --- a/src/client/webapp/elements/require/loadables.ts +++ b/src/client/webapp/elements/require/loadables.ts @@ -172,42 +172,50 @@ export function isEndFailed( export type UnloadedValueScrolling = UnloadedValue & { above: undefined; below: undefined; + bottom: undefined; }; export type LoadingValueScrolling = LoadingValue & { above: undefined; below: undefined; + bottom: undefined; }; export type LoadedValueScrolling = LoadedValue & { above: LoadableScrollingEnd; below: LoadableScrollingEnd; + bottom: LoadableScrollingEnd; // void since no reference needed to fetch the bottom }; export type FailedValueScrolling = FailedValue & { above: undefined; below: undefined; + bottom: undefined; }; export type LoadableValueScrolling = | UnloadedValueScrolling | LoadingValueScrolling | LoadedValueScrolling | 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( value: Defined, retry: () => Promise, above: LoadableScrollingEnd, below: LoadableScrollingEnd, + bottom: LoadableScrollingEnd, ): LoadedValueScrolling { - return { ...createLoadedValue(value, retry), above, below }; + return { ...createLoadedValue(value, retry), above, below, bottom }; } export function createFailedValueScrolling(error: unknown, retry: () => Promise): FailedValueScrolling { - return { ...createFailedValue(error, retry), above: undefined, below: undefined }; + return { ...createFailedValue(error, retry), above: undefined, below: undefined, bottom: undefined }; } diff --git a/src/client/webapp/elements/require/react-helper.tsx b/src/client/webapp/elements/require/react-helper.tsx index 335522f..5c940f6 100644 --- a/src/client/webapp/elements/require/react-helper.tsx +++ b/src/client/webapp/elements/require/react-helper.tsx @@ -142,6 +142,7 @@ export function useAsyncVoidCallback( const [callable] = useAsyncCallback(async (isMounted: MutableRefObject) => { 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( 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( scrollable: LoadableValueScrolling, threshold: number, ): { - fetchAboveCallable: () => Promise; // these fetch functions are returned for use in retry buttons + fetchAboveCallable: () => Promise; // these fetch functions are returned for use in retry/reset buttons fetchBelowCallable: () => Promise; + fetchBottomCallable: () => Promise; onScrollCallable: (event: UIEvent) => void; } { const isMounted = useIsMountedRef(); const [loadingAbove, setLoadingAbove] = useState(false); const [loadingBelow, setLoadingBelow] = useState(false); + const [loadingBottom, setLoadingBottom] = useState(false); const fetchAboveCallable = useCallback(async () => { if (loadingAbove) return; @@ -326,6 +331,19 @@ export function useScrollableCallables( } }, [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) => { const scrollTop = event.currentTarget.scrollTop; @@ -356,7 +374,7 @@ export function useScrollableCallables( [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; mouseDownTarget: Node | null; mouseUpTarget: Node | null }> >(new Map()); - const [hadMouseDown, setHadMouseDown] = useState(false); + const [hadMouseDown, setHadMouseDown] = useState(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)) {