'use client' import { useEffect, useRef } from 'react' import useSWRInfinite from 'swr/infinite' import { debounce } from 'lodash-es' import { useTranslation } from 'react-i18next' import { useSearchParams } from 'next/navigation' import AppCard from './AppCard' import NewAppCard from './NewAppCard' import type { AppListResponse } from '@/models/app' import { fetchAppList } from '@/service/apps' import { useSelector } from '@/context/app-context' import { NEED_REFRESH_APP_LIST_KEY, SPARK_FREE_QUOTA_PENDING } from '@/config' const getKey = (pageIndex: number, previousPageData: AppListResponse) => { if (!pageIndex || previousPageData.has_more) return { url: 'apps', params: { page: pageIndex + 1, limit: 30 } } return null } const Apps = () => { const { t } = useTranslation() const { data, isLoading, setSize, mutate } = useSWRInfinite(getKey, fetchAppList, { revalidateFirstPage: false }) const loadingStateRef = useRef(false) const pageContainerRef = useSelector(state => state.pageContainerRef) const anchorRef = useRef<HTMLAnchorElement>(null) const searchParams = useSearchParams() useEffect(() => { document.title = `${t('app.title')} - Dify` if (localStorage.getItem(NEED_REFRESH_APP_LIST_KEY) === '1') { localStorage.removeItem(NEED_REFRESH_APP_LIST_KEY) mutate() } if ( localStorage.getItem(SPARK_FREE_QUOTA_PENDING) !== '1' && searchParams.get('type') === 'provider_apply_callback' && searchParams.get('provider') === 'spark' && searchParams.get('result') === 'success' ) localStorage.setItem(SPARK_FREE_QUOTA_PENDING, '1') }, []) useEffect(() => { loadingStateRef.current = isLoading }, [isLoading]) useEffect(() => { const onScroll = debounce(() => { if (!loadingStateRef.current) { const { scrollTop, clientHeight } = pageContainerRef.current! const anchorOffset = anchorRef.current!.offsetTop if (anchorOffset - scrollTop - clientHeight < 100) setSize(size => size + 1) } }, 50) pageContainerRef.current?.addEventListener('scroll', onScroll) return () => pageContainerRef.current?.removeEventListener('scroll', onScroll) }, []) return ( <nav className='grid content-start grid-cols-1 gap-4 px-12 pt-8 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 grow shrink-0'> {data?.map(({ data: apps }) => apps.map(app => ( <AppCard key={app.id} app={app} onDelete={mutate} /> )))} <NewAppCard ref={anchorRef} onSuccess={mutate} /> </nav> ) } export default Apps