import classNames from 'clsx'; import { List as ImmutableList, Map as ImmutableMap } from 'immutable'; import debounce from 'lodash/debounce'; import React, { useCallback, useEffect, useRef } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { createSelector } from 'reselect'; import { expandNotifications, scrollTopNotifications, dequeueNotifications, } from 'soapbox/actions/notifications'; import { getSettings } from 'soapbox/actions/settings'; import PullToRefresh from 'soapbox/components/pull-to-refresh'; import ScrollTopButton from 'soapbox/components/scroll-top-button'; import ScrollableList from 'soapbox/components/scrollable_list'; import { Column } from 'soapbox/components/ui'; import PlaceholderNotification from 'soapbox/features/placeholder/components/placeholder_notification'; import { useAppDispatch, useAppSelector, useSettings } from 'soapbox/hooks'; import FilterBar from './components/filter_bar'; import Notification from './components/notification'; import type { VirtuosoHandle } from 'react-virtuoso'; import type { RootState } from 'soapbox/store'; import type { Notification as NotificationEntity } from 'soapbox/types/entities'; const messages = defineMessages({ title: { id: 'column.notifications', defaultMessage: 'Notifications' }, queue: { id: 'notifications.queue_label', defaultMessage: 'Click to see {count} new {count, plural, one {notification} other {notifications}}' }, }); const getNotifications = createSelector([ state => getSettings(state).getIn(['notifications', 'quickFilter', 'show']), state => getSettings(state).getIn(['notifications', 'quickFilter', 'active']), state => ImmutableList((getSettings(state).getIn(['notifications', 'shows']) as ImmutableMap).filter(item => !item).keys()), (state: RootState) => state.notifications.items.toList(), ], (showFilterBar, allowedType, excludedTypes, notifications: ImmutableList) => { if (!showFilterBar || allowedType === 'all') { // used if user changed the notification settings after loading the notifications from the server // otherwise a list of notifications will come pre-filtered from the backend // we need to turn it off for FilterBar in order not to block ourselves from seeing a specific category return notifications.filterNot(item => item !== null && excludedTypes.includes(item.get('type'))); } return notifications.filter(item => item !== null && allowedType === item.get('type')); }); const Notifications = () => { const dispatch = useAppDispatch(); const intl = useIntl(); const settings = useSettings(); const showFilterBar = settings.getIn(['notifications', 'quickFilter', 'show']); const activeFilter = settings.getIn(['notifications', 'quickFilter', 'active']); const notifications = useAppSelector(state => getNotifications(state)); const isLoading = useAppSelector(state => state.notifications.isLoading); // const isUnread = useAppSelector(state => state.notifications.unread > 0); const hasMore = useAppSelector(state => state.notifications.hasMore); const totalQueuedNotificationsCount = useAppSelector(state => state.notifications.totalQueuedNotificationsCount || 0); const node = useRef(null); const column = useRef(null); const scrollableContentRef = useRef | null>(null); // const handleLoadGap = (maxId) => { // dispatch(expandNotifications({ maxId })); // }; const handleLoadOlder = useCallback(debounce(() => { const last = notifications.last(); dispatch(expandNotifications({ maxId: last && last.get('id') })); }, 300, { leading: true }), [notifications]); const handleScrollToTop = useCallback(debounce(() => { dispatch(scrollTopNotifications(true)); }, 100), []); const handleScroll = useCallback(debounce(() => { dispatch(scrollTopNotifications(false)); }, 100), []); const handleMoveUp = (id: string) => { const elementIndex = notifications.findIndex(item => item !== null && item.get('id') === id) - 1; _selectChild(elementIndex); }; const handleMoveDown = (id: string) => { const elementIndex = notifications.findIndex(item => item !== null && item.get('id') === id) + 1; _selectChild(elementIndex); }; const _selectChild = (index: number) => { node.current?.scrollIntoView({ index, behavior: 'smooth', done: () => { const container = column.current; const element = container?.querySelector(`[data-index="${index}"] .focusable`); if (element) { (element as HTMLDivElement).focus(); } }, }); }; const handleDequeueNotifications = () => { dispatch(dequeueNotifications()); }; const handleRefresh = () => { return dispatch(expandNotifications()); }; useEffect(() => { handleDequeueNotifications(); dispatch(scrollTopNotifications(true)); return () => { handleLoadOlder.cancel(); handleScrollToTop.cancel(); handleScroll.cancel(); dispatch(scrollTopNotifications(false)); }; }, []); const emptyMessage = activeFilter === 'all' ? : ; let scrollableContent: ImmutableList | null = null; const filterBarContainer = showFilterBar ? () : null; if (isLoading && scrollableContentRef.current) { scrollableContent = scrollableContentRef.current; } else if (notifications.size > 0 || hasMore) { scrollableContent = notifications.map((item) => ( )); } else { scrollableContent = null; } scrollableContentRef.current = scrollableContent; const scrollContainer = ( 0, 'space-y-2': notifications.size === 0, })} > {scrollableContent as ImmutableList} ); return ( {filterBarContainer} {scrollContainer} ); }; export default Notifications;