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 (
);
})}
);
};