From dae0687fd26cd1dcb75f5ac12c0da00a28d4bdb3 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Tue, 20 Feb 2024 13:20:08 +0800 Subject: [PATCH] Allow toggling legacy menu, better header for the left menu popover on mobile --- .../templates/homeFeed/HomeFeedContainer.tsx | 47 +- .../templates/navMenu/LibraryLegacyMenu.tsx | 610 ++++++++++++++++++ .../templates/navMenu/LibraryMenu.tsx | 39 +- .../templates/navMenu/SettingsMenu.tsx | 1 + packages/web/pages/settings/shortcuts.tsx | 58 ++ 5 files changed, 731 insertions(+), 24 deletions(-) create mode 100644 packages/web/components/templates/navMenu/LibraryLegacyMenu.tsx diff --git a/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx b/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx index 86d08c070..6ef10c1c1 100644 --- a/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx +++ b/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx @@ -37,6 +37,7 @@ import { EditLibraryItemModal } from './EditItemModals' import { EmptyLibrary } from './EmptyLibrary' import { HighlightItemsLayout } from './HighlightsLayout' import { LibraryFilterMenu } from '../navMenu/LibraryMenu' +import { LibraryLegacyMenu } from '../navMenu/LibraryLegacyMenu' import { LibraryHeader, MultiSelectMode } from './LibraryHeader' import { UploadModal } from '../UploadModal' import { BulkAction } from '../../../lib/networking/mutations/bulkActionMutation' @@ -85,11 +86,13 @@ export function HomeFeedContainer(): JSX.Element { const gridContainerRef = useRef(null) - const [labelsTarget, setLabelsTarget] = - useState(undefined) + const [labelsTarget, setLabelsTarget] = useState( + undefined + ) - const [notebookTarget, setNotebookTarget] = - useState(undefined) + const [notebookTarget, setNotebookTarget] = useState( + undefined + ) const [showAddLinkModal, setShowAddLinkModal] = useState(false) const [showEditTitleModal, setShowEditTitleModal] = useState(false) @@ -931,6 +934,10 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element { key: 'libraryLayout', initialValue: 'LIST_LAYOUT', }) + const [navMenuStyle] = usePersistedState<'legacy' | 'shortcuts'>({ + key: 'library-nav-menu-style', + initialValue: 'shortcuts', + }) const updateLayout = useCallback( async (newLayout: LayoutType) => { @@ -967,16 +974,28 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element { )} - { - props.applySearchQuery(searchQuery) - }} - showFilterMenu={showFilterMenu} - setShowFilterMenu={setShowFilterMenu} - /> - + {navMenuStyle == 'shortcuts' && ( + { + props.applySearchQuery(searchQuery) + }} + showFilterMenu={showFilterMenu} + setShowFilterMenu={setShowFilterMenu} + /> + )} + {navMenuStyle == 'legacy' && ( + { + props.applySearchQuery(searchQuery) + }} + showFilterMenu={showFilterMenu} + setShowFilterMenu={setShowFilterMenu} + /> + )} {!props.isValidating && props.mode == 'highlights' && ( void + + searchTerm: string | undefined + applySearchQuery: (searchTerm: string) => void + + showFilterMenu: boolean + setShowFilterMenu: (show: boolean) => void +} + +export function LibraryLegacyMenu(props: LibraryFilterMenuProps): JSX.Element { + const [labels, setLabels] = usePersistedState({ + key: 'menu-labels', + isSessionStorage: false, + initialValue: [], + }) + const [savedSearches, setSavedSearches] = usePersistedState({ + key: 'menu-searches', + isSessionStorage: false, + initialValue: [], + }) + const [subscriptions, setSubscriptions] = usePersistedState({ + key: 'menu-subscriptions', + isSessionStorage: false, + initialValue: [], + }) + const labelsResponse = useGetLabelsQuery() + const searchesResponse = useGetSavedSearchQuery() + const subscriptionsResponse = useGetSubscriptionsQuery() + + useEffect(() => { + if ( + !labelsResponse.error && + !labelsResponse.isLoading && + labelsResponse.labels + ) { + setLabels(labelsResponse.labels) + } + }, [setLabels, labelsResponse]) + + useEffect(() => { + if ( + !subscriptionsResponse.error && + !subscriptionsResponse.isLoading && + subscriptionsResponse.subscriptions + ) { + setSubscriptions(subscriptionsResponse.subscriptions) + } + }, [setSubscriptions, subscriptionsResponse]) + + useEffect(() => { + if ( + !searchesResponse.error && + !searchesResponse.isLoading && + searchesResponse.savedSearches + ) { + setSavedSearches(searchesResponse.savedSearches) + } + }, [setSavedSearches, searchesResponse]) + + return ( + <> + + + + + + + + + + + {/* This spacer pushes library content to the right of + the fixed left side menu. */} + + + ) +} + +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:\"${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 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:\"${props.label.name}\"`) >= 0) { + return 'on' + } + return 'off' + }, [props.searchTerm, props.label]) + + return ( + + + + { + if (e.target.checked) { + props.applySearchQuery( + `${props.searchTerm ?? ''} label:\"${props.label.name}\"` + ) + } else { + const query = + props.searchTerm?.replace( + `label:\"${props.label.name}\"`, + '' + ) ?? '' + props.applySearchQuery(query) + } + }} + /> + + + ) +} + +type EditButtonProps = { + title: string + destination: string +} + +function EditButton(props: EditButtonProps): JSX.Element { + return ( + + + {props.title} + + + ) +} diff --git a/packages/web/components/templates/navMenu/LibraryMenu.tsx b/packages/web/components/templates/navMenu/LibraryMenu.tsx index 6bff3edde..6f52fae54 100644 --- a/packages/web/components/templates/navMenu/LibraryMenu.tsx +++ b/packages/web/components/templates/navMenu/LibraryMenu.tsx @@ -2,14 +2,7 @@ import { ReactNode, useEffect, useMemo, useRef } from 'react' import { StyledText } from '../../elements/StyledText' import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives' import { Button } from '../../elements/Button' -import { - Circle, - DotsThree, - Gear, - GearSix, - MagnifyingGlass, - UserGear, -} from 'phosphor-react' +import { Circle, DotsThree, MagnifyingGlass, X } from 'phosphor-react' import { Subscription, SubscriptionType, @@ -123,7 +116,7 @@ export function LibraryFilterMenu(props: LibraryFilterMenuProps): JSX.Element { width: '100%', transition: 'visibility 0s, top 150ms', }, - zIndex: 3, + zIndex: 10, }} > - + + + + + diff --git a/packages/web/components/templates/navMenu/SettingsMenu.tsx b/packages/web/components/templates/navMenu/SettingsMenu.tsx index 620fd1857..c9b0bac7a 100644 --- a/packages/web/components/templates/navMenu/SettingsMenu.tsx +++ b/packages/web/components/templates/navMenu/SettingsMenu.tsx @@ -88,6 +88,7 @@ export function SettingsMenu(): JSX.Element { { name: 'Feeds', destination: '/settings/feeds' }, { name: 'Subscriptions', destination: '/settings/subscriptions' }, { name: 'Labels', destination: '/settings/labels' }, + { name: 'Shortcuts', destination: '/settings/shortcuts' }, { name: 'Saved Searches', destination: '/settings/saved-searches' }, { name: 'Pinned Searches', destination: '/settings/pinned-searches' }, ] diff --git a/packages/web/pages/settings/shortcuts.tsx b/packages/web/pages/settings/shortcuts.tsx index 28158fa8b..5caf39fe5 100644 --- a/packages/web/pages/settings/shortcuts.tsx +++ b/packages/web/pages/settings/shortcuts.tsx @@ -18,9 +18,17 @@ import { useGetSubscriptionsQuery } from '../../lib/networking/queries/useGetSub import { DragIcon } from '../../components/elements/icons/DragIcon' import { CoverImage } from '../../components/elements/CoverImage' import { Label } from '../../lib/networking/fragments/labelFragment' +import { usePersistedState } from '../../lib/hooks/usePersistedState' +import { CheckSquare, Square } from 'phosphor-react' export default function Shortcuts(): JSX.Element { applyStoredTheme() + const [navMenuStyle, setNavMenuStyle] = usePersistedState< + 'legacy' | 'shortcuts' + >({ + key: 'library-nav-menu-style', + initialValue: 'shortcuts', + }) return ( @@ -44,6 +52,56 @@ export default function Shortcuts(): JSX.Element { maxWidth: '880px', }} > + + + Shortcuts + + + Use shortcuts to access your most important reads quickly + + + { + // setHidePinnedSearches(!hidePinnedSearches) + setNavMenuStyle( + navMenuStyle == 'shortcuts' ? 'legacy' : 'shortcuts' + ) + event.preventDefault() + }} + > + {navMenuStyle === 'shortcuts' ? ( + + ) : ( + + )} + + Enable shortcuts + +