teableio/teable

View on GitHub
apps/nextjs-app/src/features/app/components/notifications/NotificationsManage.tsx

Summary

Maintainability
A
25 mins
Test Coverage
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { NotificationStatesEnum } from '@teable/core';
import { Bell, CheckCircle2 as Read, RefreshCcw } from '@teable/icons';
import {
  getNotificationList,
  getNotificationUnreadCount,
  notificationReadAll,
} from '@teable/openapi';
import { useNotification } from '@teable/sdk';
import { ReactQueryKeys } from '@teable/sdk/config/react-query-keys';
import { Button, Popover, PopoverContent, PopoverTrigger } from '@teable/ui-lib';
import { cn } from '@teable/ui-lib/shadcn';
import { useTranslation } from 'next-i18next';
import React, { useEffect, useState } from 'react';
import { NotificationList } from './NotificationList';

export const NotificationsManage: React.FC = () => {
  const queryClient = useQueryClient();
  const notification = useNotification();
  const { t } = useTranslation('common');

  const [isOpen, setOpen] = useState(false);
  const [unreadCount, setUnreadCount] = useState<number>(0);

  const [newUnreadCount, setNewUnreadCount] = useState<number | undefined>(undefined);

  const [notifyStatus, setNotifyStatus] = useState(NotificationStatesEnum.Unread);

  const { data: queryUnreadCount = 0 } = useQuery({
    queryKey: ReactQueryKeys.notifyUnreadCount(),
    queryFn: () => getNotificationUnreadCount().then(({ data }) => data.unreadCount),
  });

  useEffect(() => {
    if (notification?.unreadCount == null) return;

    setNewUnreadCount(notification.unreadCount);
  }, [notification?.unreadCount]);

  useEffect(() => {
    setUnreadCount(newUnreadCount ?? queryUnreadCount);
  }, [newUnreadCount, queryUnreadCount]);

  const {
    data: notifyPage,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
  } = useInfiniteQuery({
    queryKey: ReactQueryKeys.notifyList({ status: notifyStatus }),
    queryFn: ({ pageParam }) =>
      getNotificationList({ notifyStates: notifyStatus, cursor: pageParam }).then(
        ({ data }) => data
      ),
    getNextPageParam: (lastPage) => lastPage.nextCursor,
    enabled: isOpen,
  });

  const { mutateAsync: markAllAsReadMutator } = useMutation({
    mutationFn: notificationReadAll,
    onSuccess: () => {
      refresh();
    },
  });

  const refresh = () => {
    setNewUnreadCount(undefined);
    queryClient.invalidateQueries(ReactQueryKeys.notifyUnreadCount());
    queryClient.resetQueries(ReactQueryKeys.notifyList({ status: notifyStatus }), { exact: true });
  };

  const renderNewButton = () => {
    if (!newUnreadCount) return;

    const num = newUnreadCount - queryUnreadCount;

    if (num < 1) return;
    return (
      <div>
        <Button
          variant="outline"
          size="xs"
          onClick={() => {
            refresh();
          }}
        >
          <RefreshCcw />
          <p>{t('notification.new', { count: num })}</p>
        </Button>
      </div>
    );
  };

  return (
    <Popover onOpenChange={setOpen}>
      <PopoverTrigger asChild>
        <Button
          variant="ghost"
          size={'xs'}
          className="relative"
          onClick={() => {
            setNotifyStatus(NotificationStatesEnum.Unread);
            refresh();
          }}
        >
          <Bell className="size-5 shrink-0" />
          {unreadCount > 0 ? (
            <span className="absolute right-2.5 top-1 inline-flex -translate-y-1/2 translate-x-1/2 items-center justify-center rounded-full bg-red-400 p-1 text-[8px] leading-none text-white">
              {unreadCount}
            </span>
          ) : (
            ''
          )}
        </Button>
      </PopoverTrigger>
      <PopoverContent side="left" align="end" className="min-w-[500px] p-0">
        <div className="w-full">
          <NotificationList
            className="relative mb-2 mt-3 max-h-[78vh] overflow-auto"
            notifyStatus={notifyStatus}
            data={notifyPage?.pages}
            hasNextPage={hasNextPage}
            isFetchingNextPage={isFetchingNextPage}
            onShowMoreClick={() => fetchNextPage()}
          />
          {notifyStatus === NotificationStatesEnum.Unread ? (
            <div className="mb-2 mt-4 flex justify-end">
              <Button
                variant="ghost"
                size="xs"
                className="mr-2"
                disabled={unreadCount < 1}
                onClick={() => {
                  markAllAsReadMutator();
                }}
              >
                <Read />
                {t('notification.markAllAsRead')}
              </Button>
            </div>
          ) : (
            ''
          )}
          <div className="flex items-center justify-between border-t border-solid p-4">
            <div className="text-sm font-normal">{t('notification.title')}</div>
            {renderNewButton()}
            <div>
              <Button
                variant="ghost"
                size="xs"
                className={cn('ml-2', {
                  'bg-secondary': notifyStatus === NotificationStatesEnum.Unread,
                })}
                onClick={() => setNotifyStatus(NotificationStatesEnum.Unread)}
              >
                {t('notification.title')}
              </Button>
              <Button
                variant="ghost"
                size="xs"
                className={cn('ml-2', {
                  'bg-secondary': notifyStatus === NotificationStatesEnum.Read,
                })}
                onClick={() => setNotifyStatus(NotificationStatesEnum.Read)}
              >
                {t('notification.read')}
              </Button>
            </div>
          </div>
        </div>
      </PopoverContent>
    </Popover>
  );
};