import './notifications-menu.css'; import { ControlledMenu } from '@szhsin/react-menu'; import { memo } from 'preact/compat'; import { useEffect, useRef, useState } from 'preact/hooks'; import { useSnapshot } from 'valtio'; import Columns from '../components/columns'; import Icon from '../components/icon'; import Link from '../components/link'; import Loader from '../components/loader'; import Notification from '../components/notification'; import { api } from '../utils/api'; import db from '../utils/db'; import groupNotifications from '../utils/group-notifications'; import openCompose from '../utils/open-compose'; import states, { saveStatus } from '../utils/states'; import { getCurrentAccountNS } from '../utils/store-utils'; import Following from './following'; function Home() { const snapStates = useSnapshot(states); useEffect(() => { (async () => { const keys = await db.drafts.keys(); if (keys.length) { const ns = getCurrentAccountNS(); const ownKeys = keys.filter((key) => key.startsWith(ns)); if (ownKeys.length) { states.showDrafts = true; } } })(); }, []); return ( <> {(snapStates.settings.shortcutsColumnsMode || snapStates.settings.shortcutsViewMode === 'multi-column') && !!snapStates.shortcuts?.length ? ( <Columns /> ) : ( <Following title="Home" path="/" id="home" headerStart={false} headerEnd={<NotificationsLink />} /> )} <button // hidden={scrollDirection === 'end' && !nearReachStart} type="button" id="compose-button" onClick={(e) => { if (e.shiftKey) { const newWin = openCompose(); if (!newWin) { alert('Looks like your browser is blocking popups.'); states.showCompose = true; } } else { states.showCompose = true; } }} > <Icon icon="quill" size="xl" alt="Compose" /> </button> </> ); } function NotificationsLink() { const snapStates = useSnapshot(states); const notificationLinkRef = useRef(); const [menuState, setMenuState] = useState(undefined); return ( <> <Link ref={notificationLinkRef} to="/notifications" class={`button plain notifications-button ${ snapStates.notificationsShowNew ? 'has-badge' : '' } ${menuState}`} onClick={(e) => { e.stopPropagation(); if (window.matchMedia('(min-width: calc(40em))').matches) { e.preventDefault(); setMenuState((state) => (!state ? 'open' : undefined)); } }} > <Icon icon="notification" size="l" alt="Notifications" /> </Link> <NotificationsMenu state={menuState} anchorRef={notificationLinkRef} onClose={() => setMenuState(undefined)} /> </> ); } const NOTIFICATIONS_LIMIT = 30; const NOTIFICATIONS_DISPLAY_LIMIT = 5; function NotificationsMenu({ anchorRef, state, onClose }) { const { masto, instance } = api(); const snapStates = useSnapshot(states); const [uiState, setUIState] = useState('default'); const notificationsIterator = masto.v1.notifications.list({ limit: NOTIFICATIONS_LIMIT, }); async function fetchNotifications() { const allNotifications = await notificationsIterator.next(); const notifications = allNotifications.value; if (notifications?.length) { notifications.forEach((notification) => { saveStatus(notification.status, instance, { skipThreading: true, }); }); const groupedNotifications = groupNotifications(notifications); states.notificationsLast = notifications[0]; states.notifications = groupedNotifications; } states.notificationsShowNew = false; states.notificationsLastFetchTime = Date.now(); return allNotifications; } function loadNotifications() { setUIState('loading'); (async () => { try { await fetchNotifications(); setUIState('default'); } catch (e) { setUIState('error'); } })(); } useEffect(() => { if (state === 'open') loadNotifications(); }, [state]); return ( <ControlledMenu menuClassName="notifications-menu" state={state} anchorRef={anchorRef} onClose={onClose} portal={{ target: document.body, }} overflow="auto" viewScroll="close" position="anchor" align="center" boundingBoxPadding="8 8 8 8" > <header> <h2>Notifications</h2> </header> {snapStates.notifications.length ? ( <> {snapStates.notifications .slice(0, NOTIFICATIONS_DISPLAY_LIMIT) .map((notification) => ( <Notification key={notification.id} instance={instance} notification={notification} /> ))} </> ) : uiState === 'loading' ? ( <div class="ui-state"> <Loader abrupt /> </div> ) : ( uiState === 'error' && ( <div class="ui-state"> <p>Unable to fetch notifications.</p> <p> <button type="button" onClick={loadNotifications}> Try again </button> </p> </div> ) )} <footer> <Link to="/mentions" class="button plain"> <Icon icon="at" /> <span>Mentions</span> </Link> <Link to="/notifications" class="button plain2"> <b>See all</b> <Icon icon="arrow-right" /> </Link> </footer> </ControlledMenu> ); } export default memo(Home);