import { type JSX, useId, useMemo } from 'react'; import type { HasObjectId } from '@growi/core'; import { useTranslation } from 'next-i18next'; import InAppNotificationElm from '~/client/components/InAppNotification/InAppNotificationElm'; import InfiniteScroll from '~/client/components/InfiniteScroll'; import { NewsItem } from '~/features/news/client/components/NewsItem'; import { useSWRINFxNews } from '~/features/news/client/hooks/use-news'; import type { INewsItemWithReadStatus } from '~/features/news/interfaces/news-item'; import type { IInAppNotification } from '~/interfaces/in-app-notification'; import { InAppNotificationStatuses } from '~/interfaces/in-app-notification'; import { useSWRINFxInAppNotifications } from '~/stores/in-app-notification'; import type { FilterType } from './InAppNotification'; const NEWS_PER_PAGE = 10; type InAppNotificationFormsProps = { isUnopendNotificationsVisible: boolean; onChangeUnopendNotificationsVisible: () => void; activeFilter: FilterType; onChangeFilter: (filter: FilterType) => void; }; export const InAppNotificationForms = ( props: InAppNotificationFormsProps, ): JSX.Element => { const { isUnopendNotificationsVisible, onChangeUnopendNotificationsVisible, activeFilter, onChangeFilter, } = props; const { t } = useTranslation('commons'); const toggleId = useId(); return (
{/* Filter tabs */}
{/* Unread-only toggle */}
); }; type InAppNotificationContentProps = { isUnopendNotificationsVisible: boolean; activeFilter: FilterType; }; type MergedItem = | { type: 'news'; item: INewsItemWithReadStatus; sortKey: Date } | { type: 'notification'; item: IInAppNotification & HasObjectId; sortKey: Date; }; export const InAppNotificationContent = ( props: InAppNotificationContentProps, ): JSX.Element => { const { isUnopendNotificationsVisible, activeFilter } = props; const { t } = useTranslation('commons'); const notificationStatus = isUnopendNotificationsVisible ? InAppNotificationStatuses.STATUS_UNOPENED : undefined; // Always call both hooks (React rules of hooks) const newsResponse = useSWRINFxNews( NEWS_PER_PAGE, { onlyUnread: isUnopendNotificationsVisible }, { keepPreviousData: true }, ); const notificationResponse = useSWRINFxInAppNotifications( NEWS_PER_PAGE, { status: notificationStatus }, { keepPreviousData: true }, ); const allNewsItems: INewsItemWithReadStatus[] = useMemo(() => { if (!newsResponse.data) return []; return newsResponse.data.flatMap((page) => page.docs); }, [newsResponse.data]); const allNotificationItems: (IInAppNotification & HasObjectId)[] = useMemo(() => { if (!notificationResponse.data) return []; return notificationResponse.data.flatMap( (page) => page.docs, ) as (IInAppNotification & HasObjectId)[]; }, [notificationResponse.data]); // Determine if each stream has exhausted its pages const newsExhausted = useMemo( () => newsResponse.data != null && newsResponse.data.length > 0 && !newsResponse.data[newsResponse.data.length - 1].hasNextPage, [newsResponse.data], ); const notifExhausted = useMemo( () => notificationResponse.data != null && notificationResponse.data.length > 0 && !notificationResponse.data[notificationResponse.data.length - 1] .hasNextPage, [notificationResponse.data], ); // Synthetic SWRInfiniteResponse for InfiniteScroll in 'all' mode const allModeSWRResponse = useMemo( () => ({ data: newsResponse.data, error: newsResponse.error ?? notificationResponse.error, isValidating: newsResponse.isValidating || notificationResponse.isValidating, isLoading: newsResponse.isLoading || notificationResponse.isLoading, mutate: newsResponse.mutate, setSize: (updater: number | ((size: number) => number)) => { const promises: Promise[] = []; if (!newsExhausted) { promises.push( newsResponse.setSize( typeof updater === 'function' ? updater(newsResponse.size) : updater, ), ); } if (!notifExhausted) { promises.push( notificationResponse.setSize( typeof updater === 'function' ? updater(notificationResponse.size) : updater, ), ); } return Promise.all(promises) as unknown as Promise< (typeof newsResponse.data)[] >; }, size: Math.max(newsResponse.size, notificationResponse.size), }), [newsResponse, notificationResponse, newsExhausted, notifExhausted], ); // Merged and sorted items for 'all' filter const mergedItems: MergedItem[] = useMemo(() => { const newsEntries: MergedItem[] = allNewsItems.map((item) => ({ type: 'news', item, sortKey: item.publishedAt instanceof Date ? item.publishedAt : new Date(item.publishedAt), })); const notifEntries: MergedItem[] = allNotificationItems.map((item) => ({ type: 'notification', item, sortKey: item.createdAt instanceof Date ? item.createdAt : new Date(item.createdAt), })); return [...newsEntries, ...notifEntries].sort( (a, b) => b.sortKey.getTime() - a.sortKey.getTime(), ); }, [allNewsItems, allNotificationItems]); const handleReadMutate = () => { newsResponse.mutate(); }; if (activeFilter === 'news') { if (allNewsItems.length === 0 && !newsResponse.isValidating) { return <>{t('in_app_notification.no_news')}; } return (
{allNewsItems.map((item) => ( ))}
); } if (activeFilter === 'notifications') { if ( allNotificationItems.length === 0 && !notificationResponse.isValidating ) { return <>{t('in_app_notification.no_notification')}; } return (
{allNotificationItems.map((notification) => ( ))}
); } // 'all' filter: merged view if ( mergedItems.length === 0 && !newsResponse.isValidating && !notificationResponse.isValidating ) { return <>{t('in_app_notification.no_notification')}; } return (
[0]['swrInifiniteResponse'] } isReachingEnd={newsExhausted && notifExhausted} >
{mergedItems.map((entry) => { if (entry.type === 'news') { return ( ); } return ( ); })}
); };