import './notifications.css'; import { useIdle } from '@uidotdev/usehooks'; import { memo } from 'preact/compat'; import { useCallback, useEffect, useRef, useState } from 'preact/hooks'; import { useSearchParams } from 'react-router-dom'; import { useSnapshot } from 'valtio'; import AccountBlock from '../components/account-block'; import FollowRequestButtons from '../components/follow-request-buttons'; import Icon from '../components/icon'; import Link from '../components/link'; import Loader from '../components/loader'; import NavMenu from '../components/nav-menu'; import Notification from '../components/notification'; import { api } from '../utils/api'; import enhanceContent from '../utils/enhance-content'; import groupNotifications from '../utils/group-notifications'; import handleContentLinks from '../utils/handle-content-links'; import niceDateTime from '../utils/nice-date-time'; import { getRegistration } from '../utils/push-notifications'; import shortenNumber from '../utils/shorten-number'; import states, { saveStatus } from '../utils/states'; import { getCurrentInstance } from '../utils/store-utils'; import useScroll from '../utils/useScroll'; import useTitle from '../utils/useTitle'; const LIMIT = 30; // 30 is the maximum limit :( const emptySearchParams = new URLSearchParams(); function Notifications({ columnMode }) { useTitle('Notifications', '/notifications'); const { masto, instance } = api(); const snapStates = useSnapshot(states); const [uiState, setUIState] = useState('default'); const [searchParams] = columnMode ? [emptySearchParams] : useSearchParams(); const notificationID = searchParams.get('id'); const notificationAccessToken = searchParams.get('access_token'); const [showMore, setShowMore] = useState(false); const [onlyMentions, setOnlyMentions] = useState(false); const scrollableRef = useRef(); const { nearReachEnd, scrollDirection, reachStart, nearReachStart } = useScroll({ scrollableRef, }); const hiddenUI = scrollDirection === 'end' && !nearReachStart; const [followRequests, setFollowRequests] = useState([]); const [announcements, setAnnouncements] = useState([]); console.debug('RENDER Notifications'); const notificationsIterator = useRef(); async function fetchNotifications(firstLoad) { if (firstLoad || !notificationsIterator.current) { // Reset iterator notificationsIterator.current = masto.v1.notifications.list({ limit: LIMIT, excludeTypes: ['follow_request'], }); } const allNotifications = await notificationsIterator.current.next(); const notifications = allNotifications.value; if (notifications?.length) { notifications.forEach((notification) => { saveStatus(notification.status, instance, { skipThreading: true, }); }); const groupedNotifications = groupNotifications(notifications); if (firstLoad) { states.notificationsLast = notifications[0]; states.notifications = groupedNotifications; } else { states.notifications.push(...groupedNotifications); } } states.notificationsShowNew = false; states.notificationsLastFetchTime = Date.now(); return allNotifications; } function fetchFollowRequests() { // Note: no pagination here yet because this better be on a separate page. Should be rare use-case??? return masto.v1.followRequests.list({ limit: 80, }); } const loadFollowRequests = () => { setUIState('loading'); (async () => { try { const requests = await fetchFollowRequests(); setFollowRequests(requests); setUIState('default'); } catch (e) { setUIState('error'); } })(); }; function fetchAnnouncements() { return masto.v1.announcements.list(); } const loadNotifications = (firstLoad) => { setUIState('loading'); (async () => { try { const fetchNotificationsPromise = fetchNotifications(firstLoad); const fetchFollowRequestsPromise = fetchFollowRequests(); const fetchAnnouncementsPromise = fetchAnnouncements(); if (firstLoad) { const announcements = await fetchAnnouncementsPromise; announcements.sort((a, b) => { // Sort by updatedAt first, then createdAt const aDate = new Date(a.updatedAt || a.createdAt); const bDate = new Date(b.updatedAt || b.createdAt); return bDate - aDate; }); setAnnouncements(announcements); const requests = await fetchFollowRequestsPromise; setFollowRequests(requests); } const { done } = await fetchNotificationsPromise; setShowMore(!done); setUIState('default'); } catch (e) { setUIState('error'); } })(); }; useEffect(() => { loadNotifications(true); }, []); useEffect(() => { if (reachStart) { loadNotifications(true); } }, [reachStart]); useEffect(() => { if (nearReachEnd && showMore) { loadNotifications(); } }, [nearReachEnd, showMore]); const isHovering = useRef(false); const idle = useIdle(5000); console.debug('🧘♀️ IDLE', idle); const loadUpdates = useCallback(() => { console.log('✨ Load updates', { autoRefresh: snapStates.settings.autoRefresh, scrollTop: scrollableRef.current?.scrollTop === 0, isHovering: isHovering.current, inBackground: inBackground(), notificationsShowNew: snapStates.notificationsShowNew, uiState, }); if ( snapStates.settings.autoRefresh && scrollableRef.current?.scrollTop === 0 && (!isHovering.current || idle) && !inBackground() && snapStates.notificationsShowNew && uiState !== 'loading' ) { loadNotifications(true); } }, [ idle, snapStates.notificationsShowNew, snapStates.settings.autoRefresh, uiState, ]); useEffect(loadUpdates, [snapStates.notificationsShowNew]); const todayDate = new Date(); const yesterdayDate = new Date(todayDate - 24 * 60 * 60 * 1000); let currentDay = new Date(); const showTodayEmpty = !snapStates.notifications.some( (notification) => new Date(notification.createdAt).toDateString() === todayDate.toDateString(), ); const announcementsListRef = useRef(); useEffect(() => { if (notificationID) { states.routeNotification = { id: notificationID, accessToken: atob(notificationAccessToken), }; } }, [notificationID, notificationAccessToken]); useEffect(() => { if (uiState === 'default') { (async () => { const registration = await getRegistration(); if (registration) { const notifications = await registration.getNotifications(); console.log('🔔 Push notifications', notifications); // Close all notifications? // notifications.forEach((notification) => { // notification.close(); // }); } })(); } }, [uiState]); return (
{uiState === 'default' ? "You're all caught up." : <>…>}
)} {snapStates.notifications.length ? ( <> {snapStates.notifications.map((notification) => { if (onlyMentions && notification.type !== 'mention') { return null; } const notificationDay = new Date(notification.createdAt); const differentDay = notificationDay.toDateString() !== currentDay.toDateString(); if (differentDay) { currentDay = notificationDay; } // if notificationDay is yesterday, show "Yesterday" // if notificationDay is before yesterday, show date const heading = notificationDay.toDateString() === yesterdayDate.toDateString() ? 'Yesterday' : niceDateTime(currentDay, { hideTime: true, }); return ( <> {differentDay &&███████████ ████
Unable to load notifications
{updatedAt && updatedAtText !== publishedDateText && ( <> {' '} •{' '} Updated{' '} > )}