Просмотр исходного кода

feat: implement SWRInfinite

https://youtrack.weseek.co.jp/issue/GW-7757
- Add Infinite scroll Component
- Implement InfiniteScroll component to RecentChanges
- update useSWRInifinitexRecentlyUpdated function
- Change useSWRInifinitexRecentlyUpdated return type
I Komang Mudana 4 лет назад
Родитель
Сommit
596078e5a1

+ 59 - 0
packages/app/src/components/Sidebar/InfiniteScroll.tsx

@@ -0,0 +1,59 @@
+import React, { Ref, useEffect, useState } from 'react';
+import type { SWRInfiniteResponse } from 'swr/infinite';
+
+type Props<T> = {
+  swr: SWRInfiniteResponse<T>
+  children: React.ReactChild | ((item: T) => React.ReactNode)
+  loadingIndicator?: React.ReactNode
+  endingIndicator?: React.ReactNode
+  isReachingEnd: boolean | ((swr: SWRInfiniteResponse<T>) => boolean)
+  offset?: number
+}
+
+const useIntersection = <T extends HTMLElement>(): [boolean, Ref<T>] => {
+  const [intersecting, setIntersecting] = useState<boolean>(false);
+  const [element, setElement] = useState<HTMLElement>();
+  useEffect(() => {
+    if (!element) return;
+    const observer = new IntersectionObserver((entries) => {
+      setIntersecting(entries[0]?.isIntersecting);
+    });
+    observer.observe(element);
+    return () => observer.unobserve(element);
+  }, [element]);
+  return [intersecting, el => el && setElement(el)];
+};
+
+const InfiniteScroll = <T, >(props: Props<T>): React.ReactElement<Props<T>> => {
+  const {
+    swr,
+    swr: { setSize, data, isValidating },
+    children,
+    loadingIndicator,
+    endingIndicator,
+    isReachingEnd,
+    offset = 0,
+  } = props;
+
+  const [intersecting, ref] = useIntersection<HTMLDivElement>();
+
+  const ending = typeof isReachingEnd === 'function' ? isReachingEnd(swr) : isReachingEnd;
+
+  useEffect(() => {
+    if (intersecting && !isValidating && !ending) {
+      setSize(size => size + 1);
+    }
+  }, [intersecting, isValidating, setSize, ending]);
+
+  return (
+    <>
+      {typeof children === 'function' ? data?.map(item => children(item)) : children}
+      <div style={{ position: 'relative' }}>
+        <div ref={ref} style={{ position: 'absolute', top: offset }}></div>
+        {ending ? endingIndicator : loadingIndicator}
+      </div>
+    </>
+  );
+};
+
+export default InfiniteScroll;

+ 29 - 5
packages/app/src/components/Sidebar/RecentChanges.tsx

@@ -14,6 +14,7 @@ import { useSWRxRecentlyUpdated, useSWRInifinitexRecentlyUpdated } from '~/store
 import loggerFactory from '~/utils/logger';
 
 import LinkedPagePath from '~/models/linked-page-path';
+import InfiniteScroll from './InfiniteScroll';
 
 
 import FormattedDistanceDate from '../FormattedDistanceDate';
@@ -124,7 +125,7 @@ SmallPageItem.propTypes = {
 const RecentChanges = (): JSX.Element => {
 
   const { t } = useTranslation();
-  const { data: pages, mutate } = useSWRInifinitexRecentlyUpdated();
+  const swr = useSWRInifinitexRecentlyUpdated();
   const [isRecentChangesSidebarSmall, setIsRecentChangesSidebarSmall] = useState(false);
 
   const retrieveSizePreferenceFromLocalStorage = useCallback(() => {
@@ -147,7 +148,7 @@ const RecentChanges = (): JSX.Element => {
     <>
       <div className="grw-sidebar-content-header p-3 d-flex">
         <h3 className="mb-0  text-nowrap">{t('Recent Changes')}</h3>
-        <button type="button" className="btn btn-sm ml-auto grw-btn-reload" onClick={() => mutate()}>
+        <button type="button" className="btn btn-sm ml-auto grw-btn-reload" onClick={() => swr.mutate()}>
           <i className="icon icon-reload"></i>
         </button>
         <div className="d-flex align-items-center">
@@ -166,9 +167,32 @@ const RecentChanges = (): JSX.Element => {
       </div>
       <div className="grw-recent-changes p-3">
         <ul className="list-group list-group-flush">
-          {(pages ? pages[0] : []).map(page => (isRecentChangesSidebarSmall
-            ? <SmallPageItem key={page._id} page={page} />
-            : <LargePageItem key={page._id} page={page} />))}
+          {/* <InfiniteScroll
+            swr={swr}
+            isReachingEnd={swr => swr.data?.[0]?.items.length === 0 || (swr.data?.[swr.data?.length - 1]?.items.length ?? 0) < 20
+            }
+          >
+            {(response) => {
+              response?.items.map((item) => {
+                isRecentChangesSidebarSmall
+                  ? <SmallPageItem key={page._id} page={page} />
+                  : <LargePageItem key={page._id} page={page} />;
+              });
+            }}
+          </InfiniteScroll> */}
+          <InfiniteScroll
+            swr={swr}
+            // TODO detect reaching ends
+            isReachingEnd={swr => swr.data?.[0]?.items.length === 0 || 20 * swr.size > 177
+            }
+          >
+            {response => response?.items.map(page => (
+              isRecentChangesSidebarSmall
+                ? <SmallPageItem key={page._id} page={page} />
+                : <LargePageItem key={page._id} page={page} />
+            ))
+            }
+          </InfiniteScroll>
         </ul>
       </div>
     </>

+ 9 - 3
packages/app/src/stores/page.tsx

@@ -34,10 +34,16 @@ export const useSWRxRecentlyUpdated = (): SWRResponse<(IPageHasId)[], Error> =>
     endpoint => apiv3Get<{ pages:(IPageHasId)[] }>(endpoint).then(response => response.data?.pages),
   );
 };
-export const useSWRInifinitexRecentlyUpdated = () : SWRInfiniteResponse<(IPageHasId)[], Error> => {
+export const useSWRInifinitexRecentlyUpdated = () : SWRInfiniteResponse<IPagingResult<IPageHasId>, Error> => {
   return useSWRInfinite(
-    (offset: number) => `/pages/recent?offset=${offset + 1}&limit=20`,
-    (endpoint: string) => apiv3Get<{ pages:(IPageHasId)[], offset: number, limit: number }>(endpoint).then(response => response.data?.pages),
+    (offset: number) => `/pages/recent?offset=${offset}`,
+    (endpoint: string) => apiv3Get<{ pages:IPageHasId[], totalCount: number, limit: number }>(endpoint).then((response) => {
+      return {
+        items: response.data.pages,
+        totalCount: response.data.totalCount,
+        limit: response.data.limit,
+      };
+    }),
   );
 };
 // eslint-disable-next-line @typescript-eslint/no-unused-vars