diff --git a/src/app.tsx b/src/app.tsx index de21d1c5..5163a65a 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -2,6 +2,8 @@ import { useEffect } from 'react'; import { Outlet, Route, Routes } from 'react-router-dom'; import useTheme from './hooks/use-theme'; import styles from './app.module.css'; +import All from './views/all'; +import Inbox from './views/inbox'; import Home from './views/home'; import PendingPost from './views/pending-post'; import Post from './views/post'; @@ -16,7 +18,6 @@ import ChallengeModal from './components/challenge-modal'; import Header from './components/header'; import StickyHeader from './components/sticky-header'; import TopBar from './components/topbar'; -import All from './views/all'; function App() { const [theme] = useTheme(); @@ -63,8 +64,8 @@ function App() { } /> } /> } /> - } /> - } /> + } /> + } /> } /> @@ -76,6 +77,7 @@ function App() { } /> } /> } /> + } /> diff --git a/src/components/account-bar/account-bar.tsx b/src/components/account-bar/account-bar.tsx index 38ef5b72..7892f407 100644 --- a/src/components/account-bar/account-bar.tsx +++ b/src/components/account-bar/account-bar.tsx @@ -29,8 +29,8 @@ const AccountBar = () => { const accountDropdownClass = isAccountDropdownOpen ? styles.visible : styles.hidden; const accountSelectButtonRef = useRef(null); - const [isMailUnread, setIsMailUnread] = useState(false); - const mailClass = isMailUnread ? styles.mailIconUnread : styles.mailIconRead; + const unreadNotificationCount = account?.unreadNotificationCount ? ` (${account.unreadNotificationCount})` : ''; + const mailClass = unreadNotificationCount ? styles.mailIconUnread : styles.mailIconRead; let submitLink = '/submit'; if (isSubplebbit) { @@ -100,16 +100,9 @@ const AccountBar = () => { | - { - e.preventDefault(); - setIsMailUnread(!isMailUnread); - }} - > + - {isMailUnread && 1} + {unreadNotificationCount && {unreadNotificationCount}} | diff --git a/src/components/header/header.tsx b/src/components/header/header.tsx index eff11b67..6d7c83bf 100644 --- a/src/components/header/header.tsx +++ b/src/components/header/header.tsx @@ -9,6 +9,7 @@ import { isAllView, isAuthorView, isHomeView, + isInboxView, isPostView, isSettingsView, isSubplebbitView, @@ -88,16 +89,13 @@ const SortItems = () => { const sortLink = isSubplebbit ? `/p/${params.subplebbitAddress}/${choice}` : isAll ? `p/all/${choice}` : choice; return ( -
  • - handleSelect(choice)} - > - {sortLabels[index]} - -
  • - )}); +
  • + handleSelect(choice)}> + {sortLabels[index]} + +
  • + ); + }); }; const AuthorHeaderTabs = () => { @@ -202,6 +200,7 @@ const Header = () => { const isAll = isAllView(location.pathname); const isAuthor = isAuthorView(location.pathname); const isHome = isHomeView(location.pathname, params); + const isInbox = isInboxView(location.pathname); const isPost = isPostView(location.pathname, params); const isProfile = isProfileView(location.pathname); const isSettings = isSettingsView(location.pathname); @@ -209,7 +208,7 @@ const Header = () => { const isSubmit = isSubmitView(location.pathname); const isSubplebbitSubmit = isSubplebbitSubmitView(location.pathname, params); - const hasFewTabs = isPost || isSubmit || isSubplebbitSubmit || isSettings; + const hasFewTabs = isPost || isSubmit || isSubplebbitSubmit || isSettings || isInbox; const hasStickyHeader = isHome || (isSubplebbit && !isSubplebbitSubmit && !isPost && !isAbout) || (isProfile && !isAbout) || isAll || (isAuthor && !isAbout); const logoSrc = isSubplebbit ? suggested?.avatarUrl : isProfile ? imageUrl : '/assets/logo/seedit.png'; const logoIsAvatar = (isSubplebbit && suggested?.avatarUrl) || (isProfile && imageUrl); diff --git a/src/components/sidebar/sidebar.tsx b/src/components/sidebar/sidebar.tsx index c0fb809d..93abedaa 100644 --- a/src/components/sidebar/sidebar.tsx +++ b/src/components/sidebar/sidebar.tsx @@ -88,7 +88,7 @@ const Sidebar = ({ address, cid, createdAt, description, downvoteCount = 0, role const isPost = isPostView(location.pathname, params); const subplebbitCreator = findSubplebbitCreator(roles); const creatorAddress = subplebbitCreator === 'anonymous' ? 'anonymous' : `${getShortAddress(subplebbitCreator)}`; - const submitRoute = (isHome || isAll) ? '/submit' : `/p/${address}/submit`; + const submitRoute = isHome || isAll ? '/submit' : `/p/${address}/submit`; return (
    diff --git a/src/components/topbar/topbar.tsx b/src/components/topbar/topbar.tsx index a1e32197..b7c54bd4 100644 --- a/src/components/topbar/topbar.tsx +++ b/src/components/topbar/topbar.tsx @@ -46,7 +46,11 @@ const TopBar = () => { const [selectedSortType, setSelectedSortType] = useState(params.sortType || '/hot'); const getTimeFilterLink = (choice: string) => { - return isSubplebbit ? `/p/${params.subplebbitAddress}/${selectedSortType}/${choice}` : isAll ? `p/all/${selectedSortType}/${choice}` : `/${selectedSortType}/${choice}`; + return isSubplebbit + ? `/p/${params.subplebbitAddress}/${selectedSortType}/${choice}` + : isAll + ? `p/all/${selectedSortType}/${choice}` + : `/${selectedSortType}/${choice}`; }; const getSelectedSortLabel = () => { diff --git a/src/lib/utils/view-utils.ts b/src/lib/utils/view-utils.ts index da2d3a6a..37f181b9 100644 --- a/src/lib/utils/view-utils.ts +++ b/src/lib/utils/view-utils.ts @@ -42,6 +42,10 @@ export const isHomeView = (pathname: string, params: ParamsType): boolean => { return pathname === '/' || (sortTypes.includes(pathname) || (timeFilterNames.includes(params.timeFilterName as TimeFilterKey) && !pathname.startsWith('/p/all'))); }; +export const isInboxView = (pathname: string): boolean => { + return pathname.startsWith('/inbox'); +} + export const isPendingView = (pathname: string, params: ParamsType): boolean => { return pathname === `/profile/${params.accountCommentIndex}`; }; diff --git a/src/views/author/author.tsx b/src/views/author/author.tsx index 154fb011..23d1845d 100644 --- a/src/views/author/author.tsx +++ b/src/views/author/author.tsx @@ -1,21 +1,21 @@ -import { useEffect, useRef } from "react"; -import { useNavigate, useParams } from "react-router-dom"; -import { useAuthorComments } from "@plebbit/plebbit-react-hooks" -import { StateSnapshot, Virtuoso, VirtuosoHandle } from "react-virtuoso"; -import styles from "./author.module.css"; -import AuthorSidebar from "../../components/author-sidebar"; -import Post from "../../components/post"; +import { useEffect, useRef } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import { useAuthorComments } from '@plebbit/plebbit-react-hooks'; +import { StateSnapshot, Virtuoso, VirtuosoHandle } from 'react-virtuoso'; +import styles from './author.module.css'; +import AuthorSidebar from '../../components/author-sidebar'; +import Post from '../../components/post'; const lastVirtuosoStates: { [key: string]: StateSnapshot } = {}; -const Loading = () => 'loading...' +const Loading = () => 'loading...'; const Author = () => { const { authorAddress, commentCid, sortType } = useParams(); const navigate = useNavigate(); - const {authorComments, lastCommentCid, hasMore, loadMore} = useAuthorComments({commentCid, authorAddress}); + const { authorComments, lastCommentCid, hasMore, loadMore } = useAuthorComments({ commentCid, authorAddress }); - const Footer = hasMore ? Loading : undefined + const Footer = hasMore ? Loading : undefined; const virtuosoRef = useRef(null); @@ -37,9 +37,9 @@ const Author = () => { // always redirect to latest author cid useEffect(() => { if (authorAddress && lastCommentCid && commentCid && lastCommentCid !== commentCid) { - navigate(`/u/${authorAddress}/c/${lastCommentCid}`, {replace: true}) + navigate(`/u/${authorAddress}/c/${lastCommentCid}`, { replace: true }); } - }, [authorAddress, lastCommentCid, commentCid, navigate]) + }, [authorAddress, lastCommentCid, commentCid, navigate]); return (
    @@ -51,9 +51,9 @@ const Author = () => { increaseViewportBy={{ bottom: 1200, top: 600 }} totalCount={authorComments?.length || 0} data={authorComments} - itemContent={(index, post) => post ? : null} + itemContent={(index, post) => (post ? : null)} useWindowScroll={true} - components={{Footer}} + components={{ Footer }} endReached={loadMore} ref={virtuosoRef} restoreStateFrom={lastVirtuosoState} diff --git a/src/views/inbox/inbox.module.css b/src/views/inbox/inbox.module.css new file mode 100644 index 00000000..e69de29b diff --git a/src/views/inbox/inbox.tsx b/src/views/inbox/inbox.tsx new file mode 100644 index 00000000..5a8b0450 --- /dev/null +++ b/src/views/inbox/inbox.tsx @@ -0,0 +1,58 @@ +import { useEffect, useRef } from 'react'; +import { StateSnapshot, Virtuoso, VirtuosoHandle } from 'react-virtuoso'; +import { useAccount, useNotifications } from '@plebbit/plebbit-react-hooks'; +import styles from './inbox.module.css'; +import Post from '../../components/post'; + +const lastVirtuosoStates: { [key: string]: StateSnapshot } = {}; + +const Inbox = () => { + const account = useAccount(); + const { unreadNotificationCount } = account || {}; + const { notifications, markAsRead } = useNotifications(); + + const virtuosoRef = useRef(null); + + useEffect(() => { + const setLastVirtuosoState = () => + virtuosoRef.current?.getState((snapshot) => { + // TODO: not sure if checking for empty snapshot.ranges works for all scenarios + if (snapshot?.ranges?.length) { + lastVirtuosoStates[unreadNotificationCount] = snapshot; + } + }); + window.addEventListener('scroll', setLastVirtuosoState); + // clean listener on unmount + return () => window.removeEventListener('scroll', setLastVirtuosoState); + }, [unreadNotificationCount]); + + const lastVirtuosoState = lastVirtuosoStates?.[unreadNotificationCount]; + + if (account && !notifications.length) { + return 'empty'; + } + + return ( +
    + + ( +
    + +
    + )} + useWindowScroll={true} + ref={virtuosoRef} + restoreStateFrom={lastVirtuosoState} + initialScrollTop={lastVirtuosoState?.scrollTop} + /> +
    + ); +}; + +export default Inbox; diff --git a/src/views/inbox/index.ts b/src/views/inbox/index.ts new file mode 100644 index 00000000..a5910b45 --- /dev/null +++ b/src/views/inbox/index.ts @@ -0,0 +1 @@ +export {default} from './inbox'; \ No newline at end of file