diff --git a/packages/web/components/elements/LabelChip.tsx b/packages/web/components/elements/LabelChip.tsx
index 73c249664..9aa959897 100644
--- a/packages/web/components/elements/LabelChip.tsx
+++ b/packages/web/components/elements/LabelChip.tsx
@@ -1,20 +1,13 @@
import { SpanBox, HStack } from './LayoutPrimitives'
import { Circle, X } from 'phosphor-react'
-import { isDarkTheme } from '../../lib/themeUpdater'
type LabelChipProps = {
text: string
color: string // expected to be a RGB hex color string
isSelected?: boolean
- useAppAppearance?: boolean
}
export function LabelChip(props: LabelChipProps): JSX.Element {
- const isDark = isDarkTheme()
-
- const selectedBorder = isDark ? '#FFEA9F' : 'black'
- const unSelectedBorder = isDark ? '#2A2A2A' : '#F5F5F5'
-
return (
diff --git a/packages/web/components/elements/OutlinedLabelChip.tsx b/packages/web/components/elements/OutlinedLabelChip.tsx
new file mode 100644
index 000000000..5991fd378
--- /dev/null
+++ b/packages/web/components/elements/OutlinedLabelChip.tsx
@@ -0,0 +1,35 @@
+import { SpanBox, HStack } from './LayoutPrimitives'
+import { Circle, X } from 'phosphor-react'
+
+type LabelChipProps = {
+ text: string
+ color: string // expected to be a RGB hex color string
+ isSelected?: boolean
+}
+
+export function OutlinedLabelChip(props: LabelChipProps): JSX.Element {
+ return (
+
+
+ {props.text}
+
+ )
+}
diff --git a/packages/web/components/elements/StyledText.tsx b/packages/web/components/elements/StyledText.tsx
index c9460941e..50ad458fa 100644
--- a/packages/web/components/elements/StyledText.tsx
+++ b/packages/web/components/elements/StyledText.tsx
@@ -37,6 +37,23 @@ const textVariants = {
my: '6px',
color: '$grayText',
},
+ settingsSection: {
+ fontWeight: '600',
+ fontSize: '17px',
+ fontFamily: '$inter',
+ color: '$grayText',
+ m: '0px',
+ my: '15px',
+ marginBlockStart: '0px',
+ marginBlockEnd: '0px',
+ },
+ settingsItem: {
+ fontSize: '13px',
+ fontFamily: '$display',
+ color: '$grayText',
+ marginBlockStart: '0px',
+ marginBlockEnd: '0px',
+ },
userNote: {
fontSize: '16px',
paddingTop: '0px',
diff --git a/packages/web/components/elements/icons/DragIcon.tsx b/packages/web/components/elements/icons/DragIcon.tsx
new file mode 100644
index 000000000..f40c0316b
--- /dev/null
+++ b/packages/web/components/elements/icons/DragIcon.tsx
@@ -0,0 +1,73 @@
+/* eslint-disable functional/no-class */
+/* eslint-disable functional/no-this-expression */
+import { IconProps } from './IconProps'
+
+import React from 'react'
+
+export class DragIcon extends React.Component {
+ render() {
+ const size = (this.props.size || 26).toString()
+ const color = (this.props.color || '#2A2A2A').toString()
+
+ return (
+
+ )
+ }
+}
diff --git a/packages/web/components/elements/icons/FollowingIcon.tsx b/packages/web/components/elements/icons/FollowingIcon.tsx
index ef882dce2..e9fca6c04 100644
--- a/packages/web/components/elements/icons/FollowingIcon.tsx
+++ b/packages/web/components/elements/icons/FollowingIcon.tsx
@@ -22,20 +22,20 @@ export class FollowingIcon extends React.Component {
d="M12.5 4.5L4.5 8.5L12.5 12.5L20.5 8.5L12.5 4.5Z"
fill={color}
stroke={color}
- stroke-linecap="round"
- stroke-linejoin="round"
+ strokeLinecap="round"
+ strokeLinejoin="round"
/>
diff --git a/packages/web/components/elements/icons/HighlightsIcon.tsx b/packages/web/components/elements/icons/HighlightsIcon.tsx
index ef882dce2..10684b273 100644
--- a/packages/web/components/elements/icons/HighlightsIcon.tsx
+++ b/packages/web/components/elements/icons/HighlightsIcon.tsx
@@ -4,38 +4,48 @@ import { IconProps } from './IconProps'
import React from 'react'
-export class FollowingIcon extends React.Component {
+export class HighlightsIcon extends React.Component {
render() {
const size = (this.props.size || 26).toString()
const color = (this.props.color || '#2A2A2A').toString()
return (
diff --git a/packages/web/components/elements/icons/HomeIcon.tsx b/packages/web/components/elements/icons/HomeIcon.tsx
new file mode 100644
index 000000000..0f9e7ce0d
--- /dev/null
+++ b/packages/web/components/elements/icons/HomeIcon.tsx
@@ -0,0 +1,29 @@
+/* eslint-disable functional/no-class */
+/* eslint-disable functional/no-this-expression */
+import { IconProps } from './IconProps'
+
+import React from 'react'
+
+export class HomeIcon extends React.Component {
+ render() {
+ const size = (this.props.size || 26).toString()
+ const color = (this.props.color || '#2A2A2A').toString()
+
+ return (
+
+ )
+ }
+}
diff --git a/packages/web/components/elements/icons/NewsletterFlairIcon.tsx b/packages/web/components/elements/icons/NewsletterFlairIcon.tsx
index 47cef57f8..d6d197a11 100644
--- a/packages/web/components/elements/icons/NewsletterFlairIcon.tsx
+++ b/packages/web/components/elements/icons/NewsletterFlairIcon.tsx
@@ -6,9 +6,6 @@ import React from 'react'
export class NewsletterFlairIcon extends React.Component {
render() {
- const size = (this.props.size || 26).toString()
- const color = (this.props.color || '#2A2A2A').toString()
-
return (
diff --git a/packages/web/components/templates/article/HighlightsLayer.tsx b/packages/web/components/templates/article/HighlightsLayer.tsx
index d5bb8b0bb..dd1ed5bf1 100644
--- a/packages/web/components/templates/article/HighlightsLayer.tsx
+++ b/packages/web/components/templates/article/HighlightsLayer.tsx
@@ -12,6 +12,7 @@ import type { Highlight } from '../../../lib/networking/fragments/highlightFragm
import {
getHighlightElements,
highlightIdAttribute,
+ highlightLabelIdAttribute,
highlightNoteIdAttribute,
SelectionAttributes,
} from '../../../lib/highlights/highlightHelpers'
@@ -79,15 +80,13 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
const focusedHighlightMousePos = useRef({ pageX: 0, pageY: 0 })
const [currentHighlightIdx, setCurrentHighlightIdx] = useState(0)
- const [focusedHighlight, setFocusedHighlight] = useState<
- Highlight | undefined
- >(undefined)
+ const [focusedHighlight, setFocusedHighlight] =
+ useState(undefined)
const [selectionData, setSelectionData] = useSelection(highlightLocations)
- const [labelsTarget, setLabelsTarget] = useState(
- undefined
- )
+ const [labelsTarget, setLabelsTarget] =
+ useState(undefined)
const [
confirmDeleteHighlightWithNoteId,
@@ -388,6 +387,13 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
highlight: highlight,
highlightModalAction: 'addComment',
})
+ } else if ((target as Element).hasAttribute(highlightLabelIdAttribute)) {
+ const id = (target as HTMLSpanElement).getAttribute(
+ highlightLabelIdAttribute
+ )
+ const highlight = highlights.find(($0) => $0.id === id)
+ setFocusedHighlight(highlight)
+ setLabelsTarget(highlight)
} else {
window?.webkit?.messageHandlers.viewerAction?.postMessage({
actionID: 'pageTapped',
@@ -395,7 +401,7 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
setFocusedHighlight(undefined)
}
},
- [openNoteModal, highlights]
+ [openNoteModal, highlights, setLabelsTarget]
)
const handleDoubleClick = useCallback(
diff --git a/packages/web/components/templates/homeFeed/HighlightsLayout.tsx b/packages/web/components/templates/homeFeed/HighlightsLayout.tsx
index b28a172ca..1b66e1f91 100644
--- a/packages/web/components/templates/homeFeed/HighlightsLayout.tsx
+++ b/packages/web/components/templates/homeFeed/HighlightsLayout.tsx
@@ -20,23 +20,24 @@ import {
import { LibraryHighlightGridCard } from '../../patterns/LibraryCards/LibraryHighlightGridCard'
import { NotebookContent } from '../article/Notebook'
import { EmptyHighlights } from './EmptyHighlights'
-import { DEFAULT_HEADER_HEIGHT } from './HeaderSpacer'
import { highlightsAsMarkdown } from './HighlightItem'
+import { MenuHeaderButton } from './LibraryHeader'
type HighlightItemsLayoutProps = {
items: LibraryItem[]
viewer: UserBasicData | undefined
+ showFilterMenu: boolean
+ setShowFilterMenu: (show: boolean) => void
+
gridContainerRef: React.RefObject
}
export function HighlightItemsLayout(
props: HighlightItemsLayoutProps
): JSX.Element {
- // const headerHeight = useGetHeaderHeight()
- const [currentItem, setCurrentItem] = useState(
- undefined
- )
+ const [currentItem, setCurrentItem] =
+ useState(undefined)
const listReducer = (
state: LibraryItem[],
@@ -106,10 +107,7 @@ export function HighlightItemsLayout(
@@ -122,15 +120,12 @@ export function HighlightItemsLayout(
+ distribution="start"
+ >
+
+
+
+
{
// We set a relatively high delay for the refresh at the end, as it's likely there's an issue
// in processing. We give it the best attempt to be able to resolve, but if it doesn't we set
// the state as Failed. On refresh it will try again if the backend sends "PROCESSING"
-const TIMEOUT_DELAYS = [1000, 2000, 2500, 3500, 5000, 10000, 60000]
+const TIMEOUT_DELAYS = [2000, 3500, 5000]
export function HomeFeedContainer(): JSX.Element {
const { viewerData } = useGetViewerQuery()
@@ -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)
@@ -206,8 +209,13 @@ export function HomeFeedContainer(): JSX.Element {
let startIdx = 0
const seeIfUpdated = async () => {
- if (startIdx > TIMEOUT_DELAYS.length) {
+ if (startIdx >= TIMEOUT_DELAYS.length) {
item.node.state = State.FAILED
+ const updatedArticle = { ...item }
+ updatedArticle.node = { ...item.node }
+ updatedArticle.isLoading = false
+ console.log(`Updating Metadata of ${item.node.slug}.`)
+ performActionOnItem('update-item', updatedArticle)
return
}
@@ -931,6 +939,10 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element {
key: 'libraryLayout',
initialValue: 'LIST_LAYOUT',
})
+ const [navMenuStyle] = usePersistedState<'legacy' | 'shortcuts'>({
+ key: 'library-nav-menu-style',
+ initialValue: 'legacy',
+ })
const updateLayout = useCallback(
async (newLayout: LayoutType) => {
@@ -967,21 +979,35 @@ 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' && (
)}
diff --git a/packages/web/components/templates/homeFeed/LibraryHeader.tsx b/packages/web/components/templates/homeFeed/LibraryHeader.tsx
index fc402b755..96aae0ed1 100644
--- a/packages/web/components/templates/homeFeed/LibraryHeader.tsx
+++ b/packages/web/components/templates/homeFeed/LibraryHeader.tsx
@@ -48,7 +48,8 @@ export const headerControlWidths = (
return {
width: '95%',
'@mdDown': {
- width: multiSelectMode !== 'off' ? '100%' : '95%',
+ padding: '15px',
+ width: '100%',
},
'@media (min-width: 930px)': {
width: '620px',
diff --git a/packages/web/components/templates/navMenu/LibraryLegacyMenu.tsx b/packages/web/components/templates/navMenu/LibraryLegacyMenu.tsx
new file mode 100644
index 000000000..f9c34c9c2
--- /dev/null
+++ b/packages/web/components/templates/navMenu/LibraryLegacyMenu.tsx
@@ -0,0 +1,637 @@
+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, X } from 'phosphor-react'
+import {
+ Subscription,
+ SubscriptionType,
+ useGetSubscriptionsQuery,
+} from '../../../lib/networking/queries/useGetSubscriptionsQuery'
+import { useGetLabelsQuery } from '../../../lib/networking/queries/useGetLabelsQuery'
+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 { useGetSavedSearchQuery } from '../../../lib/networking/queries/useGetSavedSearchQuery'
+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'
+
+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 LibraryLegacyMenu(props: LibraryFilterMenuProps): JSX.Element {
+ const [labels, setLabels] = usePersistedState
@@ -139,6 +181,277 @@ export function LibraryFilterMenu(props: LibraryFilterMenuProps): JSX.Element {
)
}
+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: [],
+ })
+
+ // const shortcuts: Shortcut[] = [
+ // {
+ // id: '12asdfasdf',
+ // name: 'Omnivore Blog',
+ // icon: 'https://substackcdn.com/image/fetch/w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F052c15c4-ecfd-4d32-87db-13bcac9afad5_512x512.png',
+ // filter: 'subscription:"Money Talk"',
+ // type: 'feed',
+ // },
+ // {
+ // id: 'sdfsdfgdsfg',
+ // name: 'Follow the Money | Arne & Harr',
+ // filter: 'subscription:"Money Talk"',
+ // type: 'feed',
+ // },
+ // {
+ // id: 'sdfasdfasdfsdfsdfsgasdfg',
+ // name: 'Andrew Kenneson from Center for the Study of Partisanship and Ideology',
+ // // icon: 'https://substackcdn.com/image/fetch/w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F052c15c4-ecfd-4d32-87db-13bcac9afad5_512x512.png',
+ // filter: 'in:all label:"Hockey"',
+ // type: 'newsletter',
+ // },
+ // {
+ // id: 'sdfasdfasdfsdfsdfsgasdfg',
+ // name: 'Robert的博客',
+ // // icon: 'https://substackcdn.com/image/fetch/w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F052c15c4-ecfd-4d32-87db-13bcac9afad5_512x512.png',
+ // filter: 'in:all label:"Hockey"',
+ // type: 'feed',
+ // },
+ // {
+ // id: 'sdfasdfasdfasdfasf',
+ // name: 'Oldest First',
+ // // icon: 'https://substackcdn.com/image/fetch/w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F052c15c4-ecfd-4d32-87db-13bcac9afad5_512x512.png',
+ // filter: 'in:all label:"Hockey"',
+ // type: 'search',
+ // },
+ // {
+ // id: 'sdfasdfasdfgasdfg',
+ // name: 'Hockey',
+ // // icon: 'https://substackcdn.com/image/fetch/w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F052c15c4-ecfd-4d32-87db-13bcac9afad5_512x512.png',
+ // filter: 'in:all label:"Hockey"',
+ // type: 'label',
+ // label: {
+ // id: 'sdfsdfsdf',
+ // name: 'Hockey',
+ // color: '#E98B8B',
+ // createdAt: new Date(),
+ // },
+ // },
+ // ]
+ //
+
+ 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 {
@@ -175,7 +488,7 @@ function SavedSearches(
return (
@@ -353,9 +666,8 @@ function MenuPanel(props: MenuPanelProps): JSX.Element {
void
+ setShowFilterMenu: (show: boolean) => void
+}
+
+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
diff --git a/packages/web/components/templates/navMenu/SettingsMenu.tsx b/packages/web/components/templates/navMenu/SettingsMenu.tsx
index c674d7282..c9b0bac7a 100644
--- a/packages/web/components/templates/navMenu/SettingsMenu.tsx
+++ b/packages/web/components/templates/navMenu/SettingsMenu.tsx
@@ -12,8 +12,9 @@ import { NavMenuFooter } from './Footer'
const HorizontalDivider = styled(SpanBox, {
width: '100%',
height: '1px',
- my: '25px',
- background: `${theme.colors.grayLine.toString()}`,
+ my: '5px',
+ opacity: '0.1',
+ background: `${theme.colors.thTextSubtle3.toString()}`,
})
const StyledLink = styled(SpanBox, {
@@ -87,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/components/templates/reader/ReaderHeader.tsx b/packages/web/components/templates/reader/ReaderHeader.tsx
index 992152222..9ee83e7cf 100644
--- a/packages/web/components/templates/reader/ReaderHeader.tsx
+++ b/packages/web/components/templates/reader/ReaderHeader.tsx
@@ -2,12 +2,38 @@ import { HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
import { Button } from '../../elements/Button'
import { PrimaryDropdown } from '../PrimaryDropdown'
import { LogoBox } from '../../elements/LogoBox'
-import { ReactNode } from 'react'
+import { ReactNode, useEffect, useState } from 'react'
import { DEFAULT_HEADER_HEIGHT } from '../homeFeed/HeaderSpacer'
import { theme } from '../../tokens/stitches.config'
import { ReaderSettingsIcon } from '../../elements/icons/ReaderSettingsIcon'
import { CircleUtilityMenuIcon } from '../../elements/icons/CircleUtilityMenuIcon'
+function useScrollDirection() {
+ const [scrollDirection, setScrollDirection] = useState('up')
+
+ useEffect(() => {
+ let lastScrollY = window.pageYOffset
+
+ const updateScrollDirection = () => {
+ const scrollY = window.pageYOffset
+ const direction = scrollY > lastScrollY ? 'down' : 'up'
+ if (
+ direction !== scrollDirection &&
+ (scrollY - lastScrollY > 10 || scrollY - lastScrollY < -10)
+ ) {
+ setScrollDirection(direction)
+ }
+ lastScrollY = scrollY > 0 ? scrollY : 0
+ }
+ window.addEventListener('scroll', updateScrollDirection) // add event listener
+ return () => {
+ window.removeEventListener('scroll', updateScrollDirection) // clean up
+ }
+ }, [scrollDirection])
+
+ return scrollDirection
+}
+
type ReaderHeaderProps = {
alwaysDisplayToolbar: boolean
hideDisplaySettings: boolean
@@ -16,6 +42,8 @@ type ReaderHeaderProps = {
}
export function ReaderHeader(props: ReaderHeaderProps): JSX.Element {
+ const scrollDirection = useScrollDirection()
+
return (
<>
0,
+ false,
input.color
)
diff --git a/packages/web/lib/highlights/highlightGenerator.ts b/packages/web/lib/highlights/highlightGenerator.ts
index 19d1131bb..a74b33185 100644
--- a/packages/web/lib/highlights/highlightGenerator.ts
+++ b/packages/web/lib/highlights/highlightGenerator.ts
@@ -4,7 +4,9 @@ import type { Highlight } from '../networking/fragments/highlightFragment'
import { interpolationSearch } from './interpolationSearch'
import {
highlightIdAttribute,
+ highlightLabelIdAttribute,
highlightNoteIdAttribute,
+ labelsImage,
noteImage,
} from './highlightHelpers'
@@ -80,10 +82,18 @@ function nodeAttributesFromHighlight(
const patch = highlight.patch
const id = highlight.id
const withNote = !!highlight.annotation
+ const withLabels = (highlight.labels?.length ?? 0) > 0
const tooltip = undefined
const customColor = highlight.color
- return makeHighlightNodeAttributes(patch, id, withNote, customColor, tooltip)
+ return makeHighlightNodeAttributes(
+ patch,
+ id,
+ withNote,
+ withLabels,
+ customColor,
+ tooltip
+ )
}
/**
@@ -100,6 +110,7 @@ export function makeHighlightNodeAttributes(
patch: string,
id: string,
withNote: boolean,
+ withLabels: boolean,
customColor?: string,
tooltip?: string
): HighlightNodeAttributes {
@@ -183,12 +194,10 @@ export function makeHighlightNodeAttributes(
startingTextNodeIndex++
}
if (withNote && lastElement) {
- lastElement.classList.add('last_element')
-
const svg = noteImage(customColor)
svg.setAttribute(highlightNoteIdAttribute, id)
- const ctr = document.createElement('div')
+ const ctr = document.createElement('span')
ctr.className = 'highlight_note_button'
ctr.appendChild(svg)
ctr.setAttribute(highlightNoteIdAttribute, id)
@@ -197,6 +206,19 @@ export function makeHighlightNodeAttributes(
lastElement.appendChild(ctr)
}
+ if (withLabels && lastElement) {
+ const svg = labelsImage(customColor)
+ svg.setAttribute(highlightLabelIdAttribute, id)
+
+ const ctr = document.createElement('span')
+ ctr.className = 'highlight_label_button'
+ ctr.appendChild(svg)
+ ctr.setAttribute(highlightLabelIdAttribute, id)
+ ctr.setAttribute('width', '14px')
+ ctr.setAttribute('height', '14px')
+
+ lastElement.appendChild(ctr)
+ }
return {
prefix,
@@ -209,10 +231,10 @@ export function makeHighlightNodeAttributes(
/**
* Given a text selection by user, annotate the article around the selection and
- * produce a {@link https://github.com/google/diff-match-patch | diff patch}
- *
+ * produce a {@link https://github.com/google/diff-match-patch | diff patch}
+ *
* The diff patch is used for identifying the selection/highlight location
- *
+ *
* @param range text selection range
* @returns diff patch
*/
@@ -304,11 +326,11 @@ const getArticleTextNodes = (
/**
* Return the offsets to the selection/highlight
- *
+ *
* @param patch {@link generateDiffPatch|diff patch} identifying a selection/highlight location
- * @returns
+ * @returns
* - highlightTextStart - The start of highlight, offset from the start of article by characters
- * - highlightTextEnd - The end of highlight (non-inclusive), offset from the start of article by characters
+ * - highlightTextEnd - The end of highlight (non-inclusive), offset from the start of article by characters
* - matchingHighlightContent - the matched highlight
*/
const selectionOffsetsFromPatch = (
diff --git a/packages/web/lib/highlights/highlightHelpers.ts b/packages/web/lib/highlights/highlightHelpers.ts
index f000ca207..adf39259d 100644
--- a/packages/web/lib/highlights/highlightHelpers.ts
+++ b/packages/web/lib/highlights/highlightHelpers.ts
@@ -1,4 +1,4 @@
-import { highlightColor, highlightColorVar } from '../themeUpdater'
+import { highlightColorVar } from '../themeUpdater'
export type SelectionAttributes = {
selection: Selection
@@ -14,6 +14,7 @@ export type SelectionAttributes = {
export const highlightIdAttribute = 'omnivore-highlight-id'
export const highlightNoteIdAttribute = 'omnivore-highlight-note-id'
+export const highlightLabelIdAttribute = 'omnivore-highlight-label-id'
export function getHighlightElements(highlightId: string): Element[] {
return Array.from(
@@ -46,3 +47,23 @@ export function noteImage(color: string | undefined): SVGSVGElement {
svg.appendChild(path)
return svg
}
+
+export const labelsImage = (color: string | undefined): SVGSVGElement => {
+ const svgURI = 'http://www.w3.org/2000/svg'
+ const svg = document.createElementNS(svgURI, 'svg')
+ svg.setAttribute('viewBox', '0 0 14 14')
+ svg.setAttribute('width', '14')
+ svg.setAttribute('height', '14')
+ svg.setAttribute('fill', 'none')
+
+ const path = document.createElementNS(svgURI, 'path')
+ path.setAttribute(
+ 'd',
+ 'M1.75 3.5V6.517C1.75007 6.82639 1.87303 7.12309 2.09183 7.34183L6.58933 11.8393C6.85297 12.1029 7.21052 12.251 7.58333 12.251C7.95615 12.251 8.3137 12.1029 8.57733 11.8393L11.8393 8.57733C12.1029 8.3137 12.251 7.95615 12.251 7.58333C12.251 7.21052 12.1029 6.85297 11.8393 6.58933L7.34183 2.09183C7.12309 1.87303 6.82639 1.75007 6.517 1.75H3.5C3.03587 1.75 2.59075 1.93437 2.26256 2.26256C1.93437 2.59075 1.75 3.03587 1.75 3.5Z'
+ )
+ path.setAttribute('stroke', `rgba(${highlightColorVar(color)}, 0.8)`)
+ path.setAttribute('stroke-width', '1.8')
+ path.setAttribute('stroke-linejoin', 'round')
+ svg.appendChild(path)
+ return svg
+}
diff --git a/packages/web/lib/networking/queries/useGetSubscriptionsQuery.tsx b/packages/web/lib/networking/queries/useGetSubscriptionsQuery.tsx
index 50efdebf8..764b732d9 100644
--- a/packages/web/lib/networking/queries/useGetSubscriptionsQuery.tsx
+++ b/packages/web/lib/networking/queries/useGetSubscriptionsQuery.tsx
@@ -16,6 +16,7 @@ export type Subscription = {
newsletterEmail?: string
url?: string
+ icon?: string
description?: string
status: SubscriptionStatus
@@ -57,6 +58,7 @@ export function useGetSubscriptionsQuery(
type
newsletterEmail
url
+ icon
description
status
unsubscribeMailTo
diff --git a/packages/web/pages/settings/shortcuts.tsx b/packages/web/pages/settings/shortcuts.tsx
new file mode 100644
index 000000000..1ee7d39f0
--- /dev/null
+++ b/packages/web/pages/settings/shortcuts.tsx
@@ -0,0 +1,582 @@
+import {
+ ReactNode,
+ useCallback,
+ useEffect,
+ useMemo,
+ useReducer,
+ useState,
+} from 'react'
+import { applyStoredTheme } from '../../lib/themeUpdater'
+
+import { useGetLabelsQuery } from '../../lib/networking/queries/useGetLabelsQuery'
+import { useGetSavedSearchQuery } from '../../lib/networking/queries/useGetSavedSearchQuery'
+import { SettingsLayout } from '../../components/templates/SettingsLayout'
+import { Toaster } from 'react-hot-toast'
+import {
+ Box,
+ VStack,
+ HStack,
+ SpanBox,
+ Separator,
+} from '../../components/elements/LayoutPrimitives'
+import { LabelChip } from '../../components/elements/LabelChip'
+import { StyledText } from '../../components/elements/StyledText'
+import {
+ Subscription,
+ SubscriptionType,
+ useGetSubscriptionsQuery,
+} from '../../lib/networking/queries/useGetSubscriptionsQuery'
+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'
+import { Button } from '../../components/elements/Button'
+import { styled } from '@stitches/react'
+import { SavedSearch } from '../../lib/networking/fragments/savedSearchFragment'
+
+type ListAction = 'RESET' | 'ADD_ITEM' | 'REMOVE_ITEM'
+
+const SHORTCUTS_KEY = 'library-shortcuts'
+
+export default function Shortcuts(): JSX.Element {
+ applyStoredTheme()
+ const [navMenuStyle, setNavMenuStyle] = usePersistedState<
+ 'legacy' | 'shortcuts'
+ >({
+ key: 'library-nav-menu-style',
+ initialValue: 'legacy',
+ })
+
+ const listReducer = (
+ state: { state: string; items: Shortcut[] },
+ action: {
+ type: ListAction
+ item?: Shortcut
+ }
+ ) => {
+ switch (action.type) {
+ case 'RESET': {
+ const itemStr = window['localStorage'].getItem(SHORTCUTS_KEY)
+ if (itemStr) {
+ try {
+ const parsed = JSON.parse(itemStr)
+ if (Array.isArray(parsed)) {
+ return { state: 'CURRENT', items: parsed as Shortcut[] }
+ }
+ } catch (err) {
+ console.log('error: ', err)
+ }
+ }
+ return { state: 'CURRENT', items: [] }
+ }
+ case 'ADD_ITEM': {
+ const item = action.item
+ if (!item) {
+ return state
+ }
+ const existing = state.items.find(
+ (existing) => existing.type == item.type && existing.id == item.id
+ )
+ if (existing) {
+ return state
+ }
+ state.items.push(item)
+ return { state: 'CURRENT', items: [...state.items] }
+ }
+ case 'REMOVE_ITEM': {
+ const item = action.item
+ if (!item) {
+ return state
+ }
+ const updated = state.items.filter((existing) => existing.id != item.id)
+ return { state: 'CURRENT', items: [...updated] }
+ }
+ default:
+ throw new Error('unknown action')
+ }
+ }
+
+ const [shortcuts, dispatchList] = useReducer(listReducer, {
+ state: 'INITIAL',
+ items: [],
+ })
+
+ useEffect(() => {
+ try {
+ if (shortcuts.state == 'CURRENT') {
+ window['localStorage'].setItem(
+ SHORTCUTS_KEY,
+ JSON.stringify(shortcuts.items)
+ )
+ }
+ } catch (error) {
+ console.log('error": ', error)
+ }
+ }, [shortcuts])
+
+ useEffect(() => {
+ dispatchList({ type: 'RESET' })
+ }, [])
+
+ return (
+
+
+
+
+
+
+ Shortcuts
+
+
+ Use shortcuts to access your most important reads quickly
+
+
+ {
+ setNavMenuStyle(
+ navMenuStyle == 'shortcuts' ? 'legacy' : 'shortcuts'
+ )
+ event.preventDefault()
+ }}
+ >
+ {navMenuStyle === 'shortcuts' ? (
+
+ ) : (
+
+ )}
+
+ Enable shortcuts
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export const SectionSeparator = styled(Separator, {
+ height: '1px',
+ my: '30px',
+ backgroundColor: '$grayBorder',
+})
+
+type ListProps = {
+ shortcuts: Shortcut[]
+ dispatchList: (arg: { type: ListAction; item?: Shortcut | undefined }) => void
+}
+
+const AvailableItems = (props: ListProps): JSX.Element => {
+ const { labels } = useGetLabelsQuery()
+ const { savedSearches } = useGetSavedSearchQuery()
+ const { subscriptions } = useGetSubscriptionsQuery()
+
+ const sortedLabels = useMemo(() => {
+ if (!labels) {
+ return []
+ }
+ return labels.sort((a, b) =>
+ a.name.toLocaleLowerCase().localeCompare(b.name.toLocaleLowerCase())
+ )
+ }, [labels])
+
+ const sortedSubscriptions = useMemo(() => {
+ if (!subscriptions) {
+ return []
+ }
+ return subscriptions.sort((a, b) =>
+ a.name.toLocaleLowerCase().localeCompare(b.name.toLocaleLowerCase())
+ )
+ }, [subscriptions])
+
+ const sortedsavedSearches = useMemo(() => {
+ if (!savedSearches) {
+ return []
+ }
+ return savedSearches.sort((a, b) =>
+ a.name.toLocaleLowerCase().localeCompare(b.name.toLocaleLowerCase())
+ )
+ }, [savedSearches])
+
+ const searchSelected = useCallback(
+ (search: SavedSearch) => {
+ return !!props.shortcuts.find((shortcut) => shortcut.id == search.id)
+ },
+ [props]
+ )
+
+ const labelSelected = useCallback(
+ (label: Label) => {
+ return !!props.shortcuts.find((shortcut) => shortcut.id == label.id)
+ },
+ [props]
+ )
+
+ const subscriptionSelected = useCallback(
+ (subscription: Subscription) => {
+ return !!props.shortcuts.find(
+ (shortcut) => shortcut.id == subscription.id
+ )
+ },
+ [props]
+ )
+
+ return (
+
+ Saved Searches
+ {sortedsavedSearches?.map((search) => {
+ return (
+
+ )
+ })}
+
+
+ Labels
+ {sortedLabels.map((label) => {
+ return (
+
+ )
+ })}
+
+
+ Subscriptions
+ {sortedSubscriptions.map((subscription) => {
+ return (
+
+ )
+ })}
+
+ )
+}
+
+type AvailableItemButtonProps = {
+ shortcut: Shortcut
+ isSelected: boolean
+ listAction: (arg: { type: ListAction; item?: Shortcut | undefined }) => void
+}
+
+const AvailableItemButton = (props: AvailableItemButtonProps): JSX.Element => {
+ const shortcutId = `checkbox-search-${props.shortcut.id}`
+ return (
+
+ {props.shortcut.name}
+
+ )
+}
+
+export type Shortcut = {
+ type: 'search' | 'label' | 'newsletter' | 'feed'
+
+ id: string
+ name: string
+ filter: string
+
+ icon?: string
+ label?: Label
+}
+
+const SelectedItems = (props: ListProps): JSX.Element => {
+ return (
+
+ Your shortcuts
+ {props.shortcuts.map((shortcut) => {
+ return (
+
+
+ {shortcut.name}
+
+
+
+
+ )
+ })}
+
+ )
+}
+
+type CheckboxButtonProps = {
+ itemKey: string
+ title: string
+ isSelected: boolean
+ item: Shortcut
+
+ listAction: (arg: { type: ListAction; item?: Shortcut | undefined }) => void
+ children: ReactNode
+}
+
+function CheckboxButton(props: CheckboxButtonProps): JSX.Element {
+ const handleChange = useCallback(
+ (selected: boolean) => {
+ if (!selected) {
+ props.listAction({
+ type: 'REMOVE_ITEM',
+ item: props.item,
+ })
+ } else {
+ props.listAction({
+ type: 'ADD_ITEM',
+ item: props.item,
+ })
+ }
+ },
+ [props]
+ )
+ return (
+ {
+ handleChange(!props.isSelected)
+ event.preventDefault()
+ }}
+ >
+ {props.isSelected ? (
+
+ ) : (
+
+ )}
+ {props.children}
+
+ )
+}
diff --git a/packages/web/styles/articleInnerStyling.css b/packages/web/styles/articleInnerStyling.css
index faf39b314..c38f290a7 100644
--- a/packages/web/styles/articleInnerStyling.css
+++ b/packages/web/styles/articleInnerStyling.css
@@ -44,23 +44,25 @@
font-family: var(--text-font-family);
}
-.article-inner-css .highlight_with_note .highlight_note_button {
+.article-inner-css .highlight_note_button {
display: unset !important;
margin: 0px !important;
max-width: unset !important;
height: unset !important;
padding: 0px 8px;
cursor: pointer;
+ margin-left: 5px !important;
+}
+
+.article-inner-css .highlight_label_button {
+ display: unset !important;
+ margin: 0px !important;
+ max-width: unset !important;
+ height: unset !important;
+ padding: 0px 8px;
+ cursor: pointer;
+ margin-left: 4px !important;
}
-/*
-on smaller screens we display the note icon
-@media (max-width: 768px) {
- .highlight_with_note.last_element:after {
- content: url(/static/icons/highlight-note-icon.svg);
- padding: 0 3px;
- cursor: pointer;
- }
-} */
.article-inner-css h1,
.article-inner-css h2,