import { ReactNode, useEffect, useMemo, useRef, useState } from 'react' import { StyledText } from '../../elements/StyledText' import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives' import { Button } from '../../elements/Button' import { Circle, DotsThree, MagnifyingGlass, X } from '@phosphor-icons/react' import { Subscription, SubscriptionType, } from '../../../lib/networking/queries/useGetSubscriptionsQuery' import { Label } from '../../../lib/networking/fragments/labelFragment' import { theme } from '../../tokens/stitches.config' import { useRegisterActions } from 'kbar' import { LogoBox } from '../../elements/LogoBox' import { usePersistedState } from '../../../lib/hooks/usePersistedState' import { SavedSearch } from '../../../lib/networking/fragments/savedSearchFragment' import { ToggleCaretDownIcon } from '../../elements/icons/ToggleCaretDownIcon' import Link from 'next/link' import { ToggleCaretRightIcon } from '../../elements/icons/ToggleCaretRightIcon' import { NavMenuFooter } from './Footer' import { FollowingIcon } from '../../elements/icons/FollowingIcon' import { HomeIcon } from '../../elements/icons/HomeIcon' import { LibraryIcon } from '../../elements/icons/LibraryIcon' import { HighlightsIcon } from '../../elements/icons/HighlightsIcon' import { CoverImage } from '../../elements/CoverImage' import { OutlinedLabelChip } from '../../elements/OutlinedLabelChip' import { NewsletterIcon } from '../../elements/icons/NewsletterIcon' import { Dropdown, DropdownOption } from '../../elements/DropdownElements' import { useRouter } from 'next/router' import { DiscoverIcon } from '../../elements/icons/DiscoverIcon' import { escapeQuotes } from '../../../utils/helper' import { Shortcut } from '../../../lib/networking/shortcuts/useShortcuts' export const LIBRARY_LEFT_MENU_WIDTH = '275px' type LibraryFilterMenuProps = { setShowAddLinkModal: (show: boolean) => void searchTerm: string | undefined applySearchQuery: (searchTerm: string) => void showFilterMenu: boolean setShowFilterMenu: (show: boolean) => void } export function LibraryFilterMenu(props: LibraryFilterMenuProps): JSX.Element { return ( <> {/* This spacer pushes library content to the right of the fixed left side menu. */} ) } const LibraryNav = (props: LibraryFilterMenuProps): JSX.Element => { return ( } /> } /> } /> } /> } /> ) } const Shortcuts = (props: LibraryFilterMenuProps): JSX.Element => { const router = useRouter() const [shortcuts] = usePersistedState({ key: 'library-shortcuts', isSessionStorage: false, initialValue: [], }) return ( SHORTCUTS } css={{ ml: 'auto' }} > { router.push(`/settings/shortcuts`) }} title="Edit shortcuts" /> {shortcuts.map((shortcut) => { const selected = props.searchTerm === shortcut.filter return ( { props.applySearchQuery(shortcut.filter) props.setShowFilterMenu(false) e.preventDefault() }} > {(shortcut.type == 'feed' || shortcut.type == 'newsletter') && ( )} {shortcut.type == 'search' && ( )} {shortcut.type == 'label' && } ) })} ) } type ShortcutItemProps = { shortcut: Shortcut } const FeedOrNewsletterShortcut = (props: ShortcutItemProps): JSX.Element => { return ( {props.shortcut.icon ? ( ) : props.shortcut.type == 'newsletter' ? ( ) : ( )} {props.shortcut.name} ) } const SearchShortcut = (props: ShortcutItemProps): JSX.Element => { return ( {props.shortcut.name} ) } const LabelShortcut = (props: ShortcutItemProps): JSX.Element => { return ( ) } function SavedSearches( props: LibraryFilterMenuProps & { savedSearches: SavedSearch[] | undefined } ): JSX.Element { const sortedSearches = useMemo(() => { return props.savedSearches ?.filter((it) => it.visible) ?.sort( (left: SavedSearch, right: SavedSearch) => left.position - right.position ) }, [props.savedSearches]) useRegisterActions( (sortedSearches ?? []).map((item, idx) => { const key = String(idx + 1) return { id: `saved_search_${key}`, name: item.name, shortcut: [key], section: 'Saved Searches', keywords: '?' + item.name, perform: () => { props.applySearchQuery(item.filter) }, } }), [props.savedSearches] ) const [collapsed, setCollapsed] = usePersistedState({ key: `--saved-searches-collapsed`, initialValue: false, }) return ( {!collapsed && sortedSearches && sortedSearches?.map((item) => ( ))} {!collapsed && sortedSearches !== undefined && ( )} ) } function Subscriptions( props: LibraryFilterMenuProps & { subscriptions: Subscription[] | undefined } ): JSX.Element { const [collapsed, setCollapsed] = usePersistedState({ key: `--subscriptions-collapsed`, initialValue: false, }) const sortedSubscriptions = useMemo(() => { if (!props.subscriptions) { return [] } return props.subscriptions .filter((s) => s.status == 'ACTIVE') .sort((a, b) => a.name.localeCompare(b.name)) }, [props.subscriptions]) useRegisterActions( (sortedSubscriptions ?? []).map((subscription, idx) => { const key = String(idx + 1) const name = subscription.name return { id: `subscription_${key}`, section: 'Subscriptions', name: name, keywords: '*' + name, perform: () => { props.applySearchQuery(`subscription:\"${escapeQuotes(name)}\"`) }, } }), [sortedSubscriptions] ) return ( {!collapsed ? ( <> {(sortedSubscriptions ?? []).map((item) => { switch (item.type) { case SubscriptionType.NEWSLETTER: return ( ) case SubscriptionType.RSS: return ( ) } })} ) : ( )} ) } function Labels( props: LibraryFilterMenuProps & { labels: Label[] } ): JSX.Element { const [collapsed, setCollapsed] = usePersistedState({ key: `--labels-collapsed`, initialValue: false, }) const sortedLabels = useMemo(() => { return props.labels.sort((left: Label, right: Label) => left.name.localeCompare(right.name) ) }, [props.labels]) return ( {!collapsed && ( <> {sortedLabels.map((item) => { return })} )} ) } type MenuPanelProps = { title: string children: ReactNode editFunc?: () => void editTitle?: string hideBottomBorder?: boolean collapsed: boolean setCollapsed: (collapsed: boolean) => void } function MenuPanel(props: MenuPanelProps): JSX.Element { return ( {props.title} {props.children} ) } type NavButtonProps = { text: string icon: ReactNode filterTerm: string searchTerm: string | undefined applySearchQuery: (searchTerm: string) => void setShowFilterMenu: (show: boolean) => void } type NavButtonRedirectProps = { text: string icon: ReactNode redirectLocation: string } function NavRedirectButton(props: NavButtonRedirectProps): JSX.Element { const [selected, setSelected] = useState(false) const router = useRouter() useEffect(() => { setSelected(window.location.pathname.includes(props.redirectLocation)) }, []) return ( { router.push(props.redirectLocation) e.preventDefault() }} > {props.icon} {props.text} ) } function NavButton(props: NavButtonProps): JSX.Element { const isInboxFilter = (filter: string) => { return filter === '' || filter === 'in:inbox' } const selected = useMemo(() => { if (isInboxFilter(props.filterTerm) && !props.searchTerm) { return true } return props.searchTerm === props.filterTerm }, [props.searchTerm, props.filterTerm]) return ( { props.applySearchQuery(props.filterTerm) props.setShowFilterMenu(false) e.preventDefault() }} > {props.icon} {props.text} ) } type FilterButtonProps = { text: string filterTerm: string searchTerm: string | undefined applySearchQuery: (searchTerm: string) => void setShowFilterMenu: (show: boolean) => void } function FilterButton(props: FilterButtonProps): JSX.Element { const isInboxFilter = (filter: string) => { return filter === '' || filter === 'in:inbox' } const selected = useMemo(() => { if (isInboxFilter(props.filterTerm) && !props.searchTerm) { return true } return props.searchTerm === props.filterTerm }, [props.searchTerm, props.filterTerm]) return ( { props.applySearchQuery(props.filterTerm) props.setShowFilterMenu(false) e.preventDefault() }} > {props.text} ) } type LabelButtonProps = { label: Label searchTerm: string | undefined applySearchQuery: (searchTerm: string) => void } function LabelButton(props: LabelButtonProps): JSX.Element { const labelId = `checkbox-label-${props.label.id}` const checkboxRef = useRef(null) const state = useMemo(() => { const term = props.searchTerm ?? '' if (term.indexOf(`label:\"${escapeQuotes(props.label.name)}\"`) >= 0) { return 'on' } return 'off' }, [props.searchTerm, props.label]) return ( { const escapedLabelName = escapeQuotes(props.label.name) if (e.target.checked) { props.applySearchQuery( `${props.searchTerm ?? ''} label:\"${escapedLabelName}\"` ) } else { const query = props.searchTerm?.replace( `label:\"${escapedLabelName}\"`, '' ) ?? '' props.applySearchQuery(query) } }} /> ) } type EditButtonProps = { title: string destination: string } function EditButton(props: EditButtonProps): JSX.Element { return ( {props.title} ) }