diff --git a/packages/web/components/nav-containers/HighlightsContainer.tsx b/packages/web/components/nav-containers/HighlightsContainer.tsx
new file mode 100644
index 000000000..ef1358249
--- /dev/null
+++ b/packages/web/components/nav-containers/HighlightsContainer.tsx
@@ -0,0 +1,319 @@
+import { NavigationLayout } from '../templates/NavigationLayout'
+import { Box, HStack, VStack } from '../elements/LayoutPrimitives'
+import { useFetchMore } from '../../lib/hooks/useFetchMoreScroll'
+import { useCallback, useMemo, useState } from 'react'
+import { useGetHighlights } from '../../lib/networking/queries/useGetHighlights'
+import { Highlight } from '../../lib/networking/fragments/highlightFragment'
+import { NextRouter, useRouter } from 'next/router'
+import {
+ UserBasicData,
+ useGetViewerQuery,
+} from '../../lib/networking/queries/useGetViewerQuery'
+import { SetHighlightLabelsModalPresenter } from '../templates/article/SetLabelsModalPresenter'
+import { TrashIcon } from '../elements/icons/TrashIcon'
+import { showErrorToast, showSuccessToast } from '../../lib/toastHelpers'
+import { ConfirmationModal } from '../patterns/ConfirmationModal'
+import { deleteHighlightMutation } from '../../lib/networking/mutations/deleteHighlightMutation'
+import { LabelChip } from '../elements/LabelChip'
+import ReactMarkdown from 'react-markdown'
+import remarkGfm from 'remark-gfm'
+import { timeAgo } from '../patterns/LibraryCards/LibraryCardStyles'
+import { HighlightHoverActions } from '../patterns/HighlightHoverActions'
+import {
+ autoUpdate,
+ offset,
+ size,
+ useFloating,
+ useHover,
+ useInteractions,
+} from '@floating-ui/react'
+import { highlightColor } from '../../lib/themeUpdater'
+
+import { HighlightViewNote } from '../patterns/HighlightNotes'
+import { theme } from '../tokens/stitches.config'
+
+const PAGE_SIZE = 10
+
+export function HighlightsContainer(): JSX.Element {
+ const router = useRouter()
+ const viewer = useGetViewerQuery()
+ const [showFilterMenu, setShowFilterMenu] = useState(false)
+ const [_, setShowAddLinkModal] = useState(false)
+
+ const { isLoading, setSize, size, data, mutate } = useGetHighlights({
+ first: PAGE_SIZE,
+ })
+
+ const hasMore = useMemo(() => {
+ if (!data) {
+ return false
+ }
+ return data[data.length - 1].highlights.pageInfo.hasNextPage
+ }, [data])
+
+ const handleFetchMore = useCallback(() => {
+ if (isLoading || !hasMore) {
+ return
+ }
+ setSize(size + 1)
+ }, [isLoading, hasMore, setSize, size])
+
+ useFetchMore(handleFetchMore)
+
+ const highlights = useMemo(() => {
+ if (!data) {
+ return []
+ }
+ return data.flatMap((res) => res.highlights.edges.map((edge) => edge.node))
+ }, [data])
+
+ return (
+
+ {highlights.map((highlight) => {
+ return (
+ viewer.viewerData?.me && (
+
+ )
+ )
+ })}
+
+ )
+}
+
+type HighlightCardProps = {
+ highlight: Highlight
+ viewer: UserBasicData
+ router: NextRouter
+ mutate: () => void
+}
+
+type HighlightAnnotationProps = {
+ highlight: Highlight
+}
+
+function HighlightAnnotation({
+ highlight,
+}: HighlightAnnotationProps): JSX.Element {
+ const [noteMode, setNoteMode] = useState<'edit' | 'preview'>('preview')
+ const [annotation, setAnnotation] = useState(highlight.annotation)
+
+ return (
+ {
+ setAnnotation(highlight.annotation)
+ }}
+ />
+ )
+}
+
+function HighlightCard(props: HighlightCardProps): JSX.Element {
+ const [isOpen, setIsOpen] = useState(false)
+ const [showConfirmDeleteHighlightId, setShowConfirmDeleteHighlightId] =
+ useState(undefined)
+ const [labelsTarget, setLabelsTarget] = useState(
+ undefined
+ )
+
+ const viewInReader = useCallback(
+ (highlightId: string) => {
+ const router = props.router
+ const viewer = props.viewer
+ const item = props.highlight.libraryItem
+
+ if (!router || !router.isReady || !viewer || !item) {
+ showErrorToast('Error navigating to highlight')
+ return
+ }
+
+ router.push(
+ {
+ pathname: '/[username]/[slug]',
+ query: {
+ username: viewer.profile.username,
+ slug: item.slug,
+ },
+ hash: highlightId,
+ },
+ `${viewer.profile.username}/${item.slug}#${highlightId}`,
+ {
+ scroll: false,
+ }
+ )
+ },
+ [props.highlight.libraryItem, props.viewer, props.router]
+ )
+
+ const { refs, floatingStyles, context } = useFloating({
+ open: isOpen,
+ onOpenChange: setIsOpen,
+ middleware: [
+ offset({
+ mainAxis: -25,
+ }),
+ size(),
+ ],
+ placement: 'top-end',
+ whileElementsMounted: autoUpdate,
+ })
+
+ const hover = useHover(context)
+
+ const { getReferenceProps, getFloatingProps } = useInteractions([hover])
+
+ return (
+
+
+
+
+
+
+ {timeAgo(props.highlight.updatedAt)}
+
+ {props.highlight.quote && (
+
+ {props.highlight.quote}
+
+ )}
+
+ {props.highlight.labels && (
+
+ {props.highlight.labels.map((label) => {
+ return (
+
+ )
+ })}
+
+ )}
+
+ {props.highlight.libraryItem?.title}
+
+
+ {props.highlight.libraryItem?.author}
+
+ {showConfirmDeleteHighlightId && (
+ {
+ ;(async () => {
+ const highlightId = showConfirmDeleteHighlightId
+ const success = await deleteHighlightMutation(
+ props.highlight.libraryItem?.id || '',
+ showConfirmDeleteHighlightId
+ )
+ props.mutate()
+ if (success) {
+ showSuccessToast('Highlight deleted.', {
+ position: 'bottom-right',
+ })
+ const event = new CustomEvent('deleteHighlightbyId', {
+ detail: highlightId,
+ })
+ document.dispatchEvent(event)
+ } else {
+ showErrorToast('Error deleting highlight', {
+ position: 'bottom-right',
+ })
+ }
+ })()
+ setShowConfirmDeleteHighlightId(undefined)
+ }}
+ onOpenChange={() => setShowConfirmDeleteHighlightId(undefined)}
+ icon={
+
+ }
+ />
+ )}
+ {labelsTarget && (
+ {
+ // Don't actually need to do something here
+ console.log('update highlight: ', highlight)
+ }}
+ onOpenChange={() => {
+ props.mutate()
+ setLabelsTarget(undefined)
+ }}
+ />
+ )}
+
+ )
+}
diff --git a/packages/web/components/nav-containers/HomeContainer.tsx b/packages/web/components/nav-containers/HomeContainer.tsx
new file mode 100644
index 000000000..59ff4e84a
--- /dev/null
+++ b/packages/web/components/nav-containers/HomeContainer.tsx
@@ -0,0 +1,920 @@
+import * as HoverCard from '@radix-ui/react-hover-card'
+import { styled } from '@stitches/react'
+import { useRouter } from 'next/router'
+import { useCallback, useEffect, useMemo, useReducer, useState } from 'react'
+import { Button } from '../elements/Button'
+import { AddToLibraryActionIcon } from '../elements/icons/home/AddToLibraryActionIcon'
+import { ArchiveActionIcon } from '../elements/icons/home/ArchiveActionIcon'
+import { CommentActionIcon } from '../elements/icons/home/CommentActionIcon'
+import { RemoveActionIcon } from '../elements/icons/home/RemoveActionIcon'
+import { ShareActionIcon } from '../elements/icons/home/ShareActionIcon'
+import Pagination from '../elements/Pagination'
+import { timeAgo } from '../patterns/LibraryCards/LibraryCardStyles'
+import { theme } from '../tokens/stitches.config'
+import { useApplyLocalTheme } from '../../lib/hooks/useApplyLocalTheme'
+import { useGetHiddenHomeSection } from '../../lib/networking/queries/useGetHiddenHomeSection'
+import {
+ HomeItem,
+ HomeItemSource,
+ HomeItemSourceType,
+ HomeSection,
+ useGetHomeItems,
+} from '../../lib/networking/queries/useGetHome'
+import {
+ SubscriptionType,
+ useGetSubscriptionsQuery,
+} from '../../lib/networking/queries/useGetSubscriptionsQuery'
+import { Box, HStack, SpanBox, VStack } from '../elements/LayoutPrimitives'
+import { Toaster } from 'react-hot-toast'
+import { useGetViewerQuery } from '../../lib/networking/queries/useGetViewerQuery'
+import useLibraryItemActions from '../../lib/hooks/useLibraryItemActions'
+
+export function HomeContainer(): JSX.Element {
+ const router = useRouter()
+ const homeData = useGetHomeItems()
+ const { viewerData } = useGetViewerQuery()
+
+ useApplyLocalTheme()
+
+ const viewerUsername = useMemo(() => {
+ return viewerData?.me?.profile.username
+ }, [viewerData])
+
+ useEffect(() => {
+ window.localStorage.setItem('nav-return', router.asPath)
+ }, [router.asPath])
+
+ return (
+
+
+
+ {homeData.sections?.map((homeSection, idx) => {
+ if (homeSection.items.length < 1) {
+ console.log('empty home section: ', homeSection)
+ return
+ }
+ switch (homeSection.layout) {
+ case 'just_added':
+ return (
+
+ )
+ case 'top_picks':
+ return (
+
+ )
+ case 'quick_links':
+ return (
+
+ )
+ case 'hidden':
+ return (
+
+ )
+ default:
+ console.log('unknown home section: ', homeSection)
+ return
+ }
+ })}
+
+
+ )
+}
+
+type HomeSectionProps = {
+ homeSection: HomeSection
+ viewerUsername: string | undefined
+}
+
+const JustAddedHomeSection = (props: HomeSectionProps): JSX.Element => {
+ const router = useRouter()
+ return (
+
+
+
+ {props.homeSection.title}
+
+
+
+
+
+
+ {props.homeSection.items.map((homeItem) => {
+ return
+ })}
+
+
+ )
+}
+
+const TopPicksHomeSection = (props: HomeSectionProps): JSX.Element => {
+ const listReducer = (
+ state: HomeItem[],
+ action: {
+ type: string
+ itemId?: string
+ items?: HomeItem[]
+ }
+ ) => {
+ console.log('handling action: ', action)
+ switch (action.type) {
+ case 'RESET':
+ return action.items ?? []
+ case 'REMOVE_ITEM':
+ return state.filter((item) => item.id !== action.itemId)
+ default:
+ throw new Error()
+ }
+ }
+
+ const [items, dispatchList] = useReducer(listReducer, [])
+
+ function handleDelete(item: HomeItem) {
+ dispatchList({
+ type: 'REMOVE_ITEM',
+ itemId: item.id,
+ })
+ }
+
+ useEffect(() => {
+ dispatchList({
+ type: 'RESET',
+ items: props.homeSection.items,
+ })
+ }, [props])
+
+ return (
+
+ {items.length > 0 && (
+
+ {props.homeSection.title}
+
+ )}
+
+ (
+
+ )}
+ />
+
+ )
+}
+
+const QuickLinksHomeSection = (props: HomeSectionProps): JSX.Element => {
+ return (
+
+
+ {props.homeSection.title}
+
+
+ (
+
+ )}
+ />
+
+ )
+}
+
+const HiddenHomeSection = (props: HomeSectionProps): JSX.Element => {
+ const [isHidden, setIsHidden] = useState(true)
+ return (
+
+ setIsHidden(!isHidden)}
+ >
+
+ {props.homeSection.title}
+
+
+ {isHidden ? 'Show' : 'Hide'}
+
+
+
+ {isHidden ? <>> : }
+
+ )
+}
+
+const HiddenHomeSectionView = (): JSX.Element => {
+ const hiddenSectionData = useGetHiddenHomeSection()
+
+ if (hiddenSectionData.error) {
+ return Error loading hidden section
+ }
+
+ if (hiddenSectionData.isValidating) {
+ return Loading...
+ }
+
+ if (!hiddenSectionData.section) {
+ return No hidden section data
+ }
+
+ return (
+
+ {hiddenSectionData.section.items.map((homeItem) => {
+ return
+ })}
+
+ )
+}
+
+const CoverImage = styled('img', {
+ objectFit: 'cover',
+})
+
+type HomeItemViewProps = {
+ homeItem: HomeItem
+ viewerUsername?: string | undefined
+}
+
+const TimeAgo = (props: HomeItemViewProps): JSX.Element => {
+ return (
+
+ {timeAgo(props.homeItem.date)}
+
+ )
+}
+
+const Title = (props: HomeItemViewProps): JSX.Element => {
+ return (
+
+ {props.homeItem.title}
+
+ )
+}
+
+const TitleSmall = (props: HomeItemViewProps): JSX.Element => {
+ return (
+
+ {props.homeItem.title}
+
+ )
+}
+
+type PreviewContentProps = {
+ previewContent?: string
+ maxLines?: string
+}
+
+const PreviewContent = (props: PreviewContentProps): JSX.Element => {
+ return (
+
+ {props.previewContent ?? ''}
+
+ )
+}
+
+const JustAddedItemView = (props: HomeItemViewProps): JSX.Element => {
+ const router = useRouter()
+
+ return (
+ {
+ const path = `/${props.viewerUsername ?? 'me'}/${props.homeItem.slug}`
+ if (event.metaKey || event.ctrlKey) {
+ window.open(path, '_blank')
+ } else {
+ router.push(path)
+ }
+ }}
+ >
+
+
+
+
+
+
+
+
+
+ )
+}
+
+type TopPicksItemViewProps = {
+ dispatchList: (args: { type: string; itemId?: string }) => void
+}
+
+const TopPicksItemView = (
+ props: HomeItemViewProps & TopPicksItemViewProps
+): JSX.Element => {
+ const router = useRouter()
+ const { archiveItem, deleteItem, moveItem } = useLibraryItemActions()
+
+ return (
+ {
+ const path = `/${props.viewerUsername ?? 'me'}/${props.homeItem.slug}`
+ if (event.metaKey || event.ctrlKey) {
+ window.open(path, '_blank')
+ } else {
+ router.push(path)
+ }
+ }}
+ alignment="start"
+ >
+
+
+
+
+
+
+
+
+ {props.homeItem.thumbnail && (
+
+ )}
+
+
+
+
+
+ {props.homeItem.canSave && (
+
+ )}
+ {props.homeItem.canArchive && (
+
+ )}
+ {props.homeItem.canDelete && (
+
+ )}
+ {props.homeItem.canShare && (
+
+ )}
+
+
+
+ )
+}
+
+const QuickLinkHomeItemView = (props: HomeItemViewProps): JSX.Element => {
+ const router = useRouter()
+
+ return (
+ {
+ const path = `/${props.viewerUsername ?? 'me'}/${props.homeItem.slug}`
+ if (event.metaKey || event.ctrlKey) {
+ window.open(path, '_blank')
+ } else {
+ router.push(path)
+ }
+ }}
+ >
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+const SiteIconSmall = styled('img', {
+ width: '16px',
+ height: '16px',
+ borderRadius: '100px',
+})
+
+const SiteIconLarge = styled('img', {
+ width: '25px',
+ height: '25px',
+ borderRadius: '100px',
+})
+
+type SourceInfoProps = {
+ subtle?: boolean
+}
+
+const SourceInfo = (props: HomeItemViewProps & SourceInfoProps) => (
+
+
+
+ {props.homeItem.source.icon && (
+
+ )}
+
+ {props.homeItem.source.name}
+
+
+
+
+
+
+
+
+
+
+)
+
+type SourceHoverContentProps = {
+ source: HomeItemSource
+}
+
+const SubscriptionSourceHoverContent = (
+ props: SourceHoverContentProps
+): JSX.Element => {
+ const mapSourceType = (
+ sourceType: HomeItemSourceType
+ ): SubscriptionType | undefined => {
+ switch (sourceType) {
+ case 'RSS':
+ case 'NEWSLETTER':
+ return sourceType as SubscriptionType
+ default:
+ return undefined
+ }
+ }
+ const { subscriptions, isValidating } = useGetSubscriptionsQuery(
+ mapSourceType(props.source.type)
+ )
+ const subscription = useMemo(() => {
+ if (props.source.id && subscriptions) {
+ return subscriptions.find((sub) => sub.id == props.source.id)
+ }
+ return undefined
+ }, [subscriptions])
+
+ return (
+
+
+ {props.source.icon && }
+
+ {props.source.name}
+
+
+ {subscription && subscription.status == 'ACTIVE' && (
+
+ )}
+
+
+
+ {subscription ? <>{subscription.description}> : <>>}
+
+
+ )
+}
diff --git a/packages/web/components/tokens/stitches.config.ts b/packages/web/components/tokens/stitches.config.ts
index 4351694c9..99dd8d755 100644
--- a/packages/web/components/tokens/stitches.config.ts
+++ b/packages/web/components/tokens/stitches.config.ts
@@ -416,7 +416,7 @@ const blackThemeSpec = {
const apolloThemeSpec = {
colors: {
- readerBg: '#6A6968',
+ readerBg: '#474747',
readerFont: '#F3F3F3',
readerMargin: '#474747',
readerFontHighContrast: 'white',
diff --git a/packages/web/lib/hooks/useLibraryItemActions.tsx b/packages/web/lib/hooks/useLibraryItemActions.tsx
new file mode 100644
index 000000000..d2b4d28ab
--- /dev/null
+++ b/packages/web/lib/hooks/useLibraryItemActions.tsx
@@ -0,0 +1,69 @@
+import { useState, useEffect, useCallback } from 'react'
+import { setLinkArchivedMutation } from '../networking/mutations/setLinkArchivedMutation'
+import {
+ showErrorToast,
+ showSuccessToast,
+ showSuccessToastWithUndo,
+} from '../toastHelpers'
+import { deleteLinkMutation } from '../networking/mutations/deleteLinkMutation'
+import { updatePageMutation } from '../networking/mutations/updatePageMutation'
+import { State } from '../networking/fragments/articleFragment'
+
+export default function useLibraryItemActions() {
+ const archiveItem = useCallback(async (itemId: string) => {
+ const result = await setLinkArchivedMutation({
+ linkId: itemId,
+ archived: true,
+ })
+
+ if (result) {
+ showSuccessToast('Link archived', { position: 'bottom-right' })
+ } else {
+ showErrorToast('Error archiving link', { position: 'bottom-right' })
+ }
+
+ return !!result
+ }, [])
+
+ const deleteItem = useCallback(async (itemId: string, undo: () => void) => {
+ const result = await deleteLinkMutation(itemId)
+
+ if (result) {
+ showSuccessToastWithUndo('Item removed', async () => {
+ const result = await updatePageMutation({
+ pageId: itemId,
+ state: State.SUCCEEDED,
+ })
+
+ undo()
+
+ if (result) {
+ showSuccessToast('Item recovered')
+ } else {
+ showErrorToast('Error recovering, check your deleted items')
+ }
+ })
+ } else {
+ showErrorToast('Error removing item', { position: 'bottom-right' })
+ }
+
+ return !!result
+ }, [])
+
+ const moveItem = useCallback(async (itemId: string) => {
+ const result = await setLinkArchivedMutation({
+ linkId: itemId,
+ archived: true,
+ })
+
+ if (result) {
+ showSuccessToast('Link archived', { position: 'bottom-right' })
+ } else {
+ showErrorToast('Error archiving link', { position: 'bottom-right' })
+ }
+
+ return !!result
+ }, [])
+
+ return { archiveItem, deleteItem, moveItem }
+}
diff --git a/packages/web/lib/networking/mutations/moveToLibraryMutation.ts b/packages/web/lib/networking/mutations/moveToLibraryMutation.ts
new file mode 100644
index 000000000..205312dd6
--- /dev/null
+++ b/packages/web/lib/networking/mutations/moveToLibraryMutation.ts
@@ -0,0 +1,41 @@
+import { gql } from 'graphql-request'
+import { gqlFetcher } from '../networkHelpers'
+
+type MoveToFolderResponseData = {
+ success?: boolean
+ errorCodes?: string[]
+}
+
+type MoveToFolderResponse = {
+ moveToFolder?: MoveToFolderResponseData
+}
+
+export async function moveToFolderMutation(
+ itemId: string,
+ folder: string
+): Promise {
+ const mutation = gql`
+ mutation MoveToFolder($id: ID!, $folder: String!) {
+ moveToFolder(id: $id, folder: $folder) {
+ ... on MoveToFolderSuccess {
+ success
+ }
+ ... on MoveToFolderError {
+ errorCodes
+ }
+ }
+ }
+ `
+
+ try {
+ const response = await gqlFetcher(mutation, { id: itemId, folder })
+ const data = response as MoveToFolderResponse | undefined
+ if (data?.moveToFolder?.errorCodes) {
+ return false
+ }
+ return data?.moveToFolder?.success ?? false
+ } catch (error) {
+ console.log('MoveToFolder error', error)
+ return false
+ }
+}