From 770dbbd68a4fe278970859864ba667f21692088d Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 7 Apr 2022 15:54:07 -0700 Subject: [PATCH 001/125] Add margins on wider screens, keyboard commands to adjust --- .../components/templates/PrimaryLayout.tsx | 9 ++++++- .../templates/article/ArticleContainer.tsx | 24 +++++++++++++++---- .../keyboardShortcuts/navigationShortcuts.ts | 14 +++++++++++ .../web/pages/[username]/[slug]/index.tsx | 19 ++++++++++++++- 4 files changed, 60 insertions(+), 6 deletions(-) diff --git a/packages/web/components/templates/PrimaryLayout.tsx b/packages/web/components/templates/PrimaryLayout.tsx index 17757cacd..b3b1d1db6 100644 --- a/packages/web/components/templates/PrimaryLayout.tsx +++ b/packages/web/components/templates/PrimaryLayout.tsx @@ -62,7 +62,14 @@ export function PrimaryLayout(props: PrimaryLayoutProps): JSX.Element { {props.pageMetaDataProps ? ( ) : null} - + l.id) || [] ) const updateFontSize = async (newFontSize: number) => { - setFontSize(newFontSize) - await userPersonalizationMutation({ fontSize: newFontSize }) + if (fontSize !== newFontSize) { + setFontSize(newFontSize) + await userPersonalizationMutation({ fontSize: newFontSize }) + } + } + + const updateMargin = async (newMargin: number) => { + if (margin !== newMargin) { + setMargin(newMargin) + await userPersonalizationMutation({ margin: newMargin }) + } } useEffect(() => { updateFontSize(props.fontSize ?? 20) }, [props.fontSize]) + useEffect(() => { + updateMargin(props.margin ?? 140) + }, [props.margin]) + // Listen for font size and color mode change events sent from host apps (ios, macos...) useEffect(() => { const increaseFontSize = async () => { @@ -101,6 +115,7 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element { css={{ padding: '16px', maxWidth: '94%', + background: props.isAppleAppEmbed ? 'unset' : theme.colors.grayBg.toString(), '--text-font-family': styles.fontFamily, '--text-font-size': `${styles.fontSize}px`, '--line-height': `150%`, @@ -112,12 +127,13 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element { '--font-color-transparent': styles.readerFontColorTransparent, '--table-header-color': styles.readerTableHeaderColor, '--headers-color': styles.readerHeadersColor, + margin: `30px 0px`, '@sm': { '--blockquote-padding': '1em 2em', '--blockquote-icon-font-size': '1.7rem', '--figure-margin': '2.6875rem auto', '--hr-margin': '2em', - margin: `30px ${styles.margin / 2}px`, + margin: `30px 0px`, }, '@md': { maxWidth: '92%', @@ -125,7 +141,7 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element { '@lg': { margin: `30px 0`, width: 'auto', - maxWidth: 1024 - styles.margin, + maxWidth: 1024 - (styles.margin), }, }} > diff --git a/packages/web/lib/keyboardShortcuts/navigationShortcuts.ts b/packages/web/lib/keyboardShortcuts/navigationShortcuts.ts index e516aa2fe..2ce3f7ab8 100644 --- a/packages/web/lib/keyboardShortcuts/navigationShortcuts.ts +++ b/packages/web/lib/keyboardShortcuts/navigationShortcuts.ts @@ -207,6 +207,8 @@ type ArticleKeyboardAction = | 'openOriginalArticle' | 'incrementFontSize' | 'decrementFontSize' + | 'incrementMarginWidth' + | 'decrementMarginWidth' | 'editLabels' export function articleKeyboardCommands( @@ -238,6 +240,18 @@ export function articleKeyboardCommands( shortcutKeyDescription: '-', callback: () => actionHandler('decrementFontSize'), }, + { + shortcutKeys: [']'], + actionDescription: 'Increase margin width', + shortcutKeyDescription: ']', + callback: () => actionHandler('incrementMarginWidth'), + }, + { + shortcutKeys: ['['], + actionDescription: 'Decrease margin width', + shortcutKeyDescription: '[', + callback: () => actionHandler('decrementMarginWidth'), + }, { shortcutKeys: ['l'], actionDescription: 'Edit labels', diff --git a/packages/web/pages/[username]/[slug]/index.tsx b/packages/web/pages/[username]/[slug]/index.tsx index a341789af..4bbf2cc2e 100644 --- a/packages/web/pages/[username]/[slug]/index.tsx +++ b/packages/web/pages/[username]/[slug]/index.tsx @@ -23,6 +23,7 @@ import Script from 'next/script' import { EditLabelsModal } from '../../../components/templates/article/EditLabelsModal' import { Label } from '../../../lib/networking/fragments/labelFragment' import { isVipUser } from '../../../lib/featureFlag' +import { theme } from '../../../components/tokens/stitches.config' const PdfArticleContainerNoSSR = dynamic( () => import('./../../../components/templates/article/PdfArticleContainer'), @@ -45,12 +46,16 @@ export default function Home(): JSX.Element { const { preferencesData } = useGetUserPreferences() const article = articleData?.article.article const [fontSize, setFontSize] = useState(preferencesData?.fontSize ?? 20) + const [marginWidth, setMarginWidth] = useState(preferencesData?.fontSize ?? 20) useKeyboardShortcuts(navigationCommands(router)) const updateFontSize = async (newFontSize: number) => { setFontSize(newFontSize) - await userPersonalizationMutation({ fontSize: newFontSize }) + } + + const updateMarginWidth = async (newMargin: number) => { + setMarginWidth(newMargin) } useKeyboardShortcuts( @@ -68,6 +73,12 @@ export default function Home(): JSX.Element { case 'decrementFontSize': await updateFontSize(Math.max(fontSize - 2, 10)) break + case 'incrementMarginWidth': + updateMarginWidth(Math.min(marginWidth + 50, 560)) + break + case 'decrementMarginWidth': + updateMarginWidth(Math.max(marginWidth - 50, 0)) + break case 'editLabels': if (viewerData?.me && isVipUser(viewerData?.me)) { setShowLabelsModal(true) @@ -108,6 +119,11 @@ export default function Home(): JSX.Element { distribution="center" ref={scrollRef} className="disable-webkit-callout" + css={{ + '@smDown': { + background: theme.colors.grayBg.toString(), + } + }} > Date: Thu, 7 Apr 2022 20:23:27 -0700 Subject: [PATCH 002/125] WIP: reader control breakpoints This implements the left side controls on desktop and in the header on smaller screens. partial work, committing as-is to test on device --- .../elements/images/OmnivoreNameLogo.tsx | 3 +- .../web/components/patterns/PrimaryHeader.tsx | 47 +++++++++++++- .../templates/article/ArticleContainer.tsx | 10 +-- .../web/components/tokens/stitches.config.ts | 1 + .../web/pages/[username]/[slug]/index.tsx | 63 ++++++++++++++++++- 5 files changed, 112 insertions(+), 12 deletions(-) diff --git a/packages/web/components/elements/images/OmnivoreNameLogo.tsx b/packages/web/components/elements/images/OmnivoreNameLogo.tsx index 482845d0d..0f3e25328 100644 --- a/packages/web/components/elements/images/OmnivoreNameLogo.tsx +++ b/packages/web/components/elements/images/OmnivoreNameLogo.tsx @@ -2,6 +2,7 @@ import { config } from '../../tokens/stitches.config' import Image from 'next/image' import { StyledText } from '../../elements/StyledText' import Link from 'next/link' +import { SpanBox } from '../LayoutPrimitives' export function OmnivoreNameLogoImage(): JSX.Element { return ( @@ -51,7 +52,7 @@ export function OmnivoreNameLogo(props: OmnivoreNameLogoProps): JSX.Element { - Omnivore + {/* Omnivore */} ) diff --git a/packages/web/components/patterns/PrimaryHeader.tsx b/packages/web/components/patterns/PrimaryHeader.tsx index a74c39999..32e347c37 100644 --- a/packages/web/components/patterns/PrimaryHeader.tsx +++ b/packages/web/components/patterns/PrimaryHeader.tsx @@ -17,7 +17,7 @@ import { UserBasicData } from '../../lib/networking/queries/useGetViewerQuery' import { setupAnalytics } from '../../lib/analytics' import { Button } from '../elements/Button' import Link from 'next/link' -import { ArrowSquareOut } from 'phosphor-react' +import { ArchiveBox, ArrowSquareOut, DotsThree, HighlighterCircle, TagSimple, TextAa } from 'phosphor-react' type HeaderProps = { user?: UserBasicData @@ -183,12 +183,53 @@ function NavHeader(props: NavHeaderProps): JSX.Element { + + + + + + + + + + + + + + + {props.username ? ( - + {/* - + */} l.id) || [] ) @@ -99,7 +100,7 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element { }, [props.article]) const styles = { - margin: props.margin ?? 140, + margin: props.margin ?? 360, fontSize, fontFamily: props.fontFamily ?? 'inter', readerFontColor: theme.colors.readerFont.toString(), @@ -114,7 +115,7 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element { id="article-container" css={{ padding: '16px', - maxWidth: '94%', + maxWidth: '100%', background: props.isAppleAppEmbed ? 'unset' : theme.colors.grayBg.toString(), '--text-font-family': styles.fontFamily, '--text-font-size': `${styles.fontSize}px`, @@ -127,7 +128,6 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element { '--font-color-transparent': styles.readerFontColorTransparent, '--table-header-color': styles.readerTableHeaderColor, '--headers-color': styles.readerHeadersColor, - margin: `30px 0px`, '@sm': { '--blockquote-padding': '1em 2em', '--blockquote-icon-font-size': '1.7rem', @@ -136,7 +136,7 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element { margin: `30px 0px`, }, '@md': { - maxWidth: '92%', + maxWidth: 1024 - (styles.margin), }, '@lg': { margin: `30px 0`, diff --git a/packages/web/components/tokens/stitches.config.ts b/packages/web/components/tokens/stitches.config.ts index 7e9500fcf..de59a709b 100644 --- a/packages/web/components/tokens/stitches.config.ts +++ b/packages/web/components/tokens/stitches.config.ts @@ -156,6 +156,7 @@ export const { styled, css, theme, getCssText, globalCss, keyframes, config } = xsmDown: '(max-width: 375px)', smDown: '(max-width: 575px)', mdDown: '(max-width: 768px)', + lgDown: '(max-width: 992px)', sm: '(min-width: 576px)', md: '(min-width: 768px)', lg: '(min-width: 992px)', diff --git a/packages/web/pages/[username]/[slug]/index.tsx b/packages/web/pages/[username]/[slug]/index.tsx index 4bbf2cc2e..697c3f9c3 100644 --- a/packages/web/pages/[username]/[slug]/index.tsx +++ b/packages/web/pages/[username]/[slug]/index.tsx @@ -23,7 +23,18 @@ import Script from 'next/script' import { EditLabelsModal } from '../../../components/templates/article/EditLabelsModal' import { Label } from '../../../lib/networking/fragments/labelFragment' import { isVipUser } from '../../../lib/featureFlag' -import { theme } from '../../../components/tokens/stitches.config' +import { styled, theme } from '../../../components/tokens/stitches.config' +import { Button } from '../../../components/elements/Button' +import { ArchiveBox, DotsThree, HighlighterCircle, TagSimple, TextAa } from 'phosphor-react' +import { Separator } from '@radix-ui/react-separator' + +const MenuSeparator = styled(Separator, { + width: '100%', + margin: 0, + backgroundColor: 'red', + borderBottom: `1px solid ${theme.colors.grayLine.toString()}`, + my: '8px', +}) const PdfArticleContainerNoSSR = dynamic( () => import('./../../../components/templates/article/PdfArticleContainer'), @@ -46,15 +57,17 @@ export default function Home(): JSX.Element { const { preferencesData } = useGetUserPreferences() const article = articleData?.article.article const [fontSize, setFontSize] = useState(preferencesData?.fontSize ?? 20) - const [marginWidth, setMarginWidth] = useState(preferencesData?.fontSize ?? 20) + const [marginWidth, setMarginWidth] = useState(preferencesData?.margin ?? 360) useKeyboardShortcuts(navigationCommands(router)) const updateFontSize = async (newFontSize: number) => { setFontSize(newFontSize) + await userPersonalizationMutation({ fontSize: newFontSize }) } const updateMarginWidth = async (newMargin: number) => { + console.log('margin', newMargin) setMarginWidth(newMargin) } @@ -77,7 +90,7 @@ export default function Home(): JSX.Element { updateMarginWidth(Math.min(marginWidth + 50, 560)) break case 'decrementMarginWidth': - updateMarginWidth(Math.max(marginWidth - 50, 0)) + updateMarginWidth(Math.max(marginWidth - 50, 200)) break case 'editLabels': if (viewerData?.me && isVipUser(viewerData?.me)) { @@ -108,6 +121,50 @@ export default function Home(): JSX.Element { /> + + + + + + + + + + + + + + + + {article.contentReader == 'PDF' ? ( Date: Thu, 7 Apr 2022 20:50:59 -0700 Subject: [PATCH 003/125] Simplify --- packages/web/components/patterns/PrimaryHeader.tsx | 10 +++++----- packages/web/pages/[username]/[slug]/index.tsx | 14 +++++++------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/web/components/patterns/PrimaryHeader.tsx b/packages/web/components/patterns/PrimaryHeader.tsx index 32e347c37..2dde10616 100644 --- a/packages/web/components/patterns/PrimaryHeader.tsx +++ b/packages/web/components/patterns/PrimaryHeader.tsx @@ -202,22 +202,22 @@ function NavHeader(props: NavHeaderProps): JSX.Element { m: '0px', }} > - - - - - diff --git a/packages/web/pages/[username]/[slug]/index.tsx b/packages/web/pages/[username]/[slug]/index.tsx index 697c3f9c3..0a0711f70 100644 --- a/packages/web/pages/[username]/[slug]/index.tsx +++ b/packages/web/pages/[username]/[slug]/index.tsx @@ -141,25 +141,25 @@ export default function Home(): JSX.Element { m: '0px', }} > - - - - + - - + - From 602dbb29be362f0af2e8a55b3cac61be52220639 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 7 Apr 2022 21:11:40 -0700 Subject: [PATCH 004/125] Play with transparent header in desktop --- packages/web/components/patterns/PrimaryHeader.tsx | 7 ++++--- packages/web/components/templates/PrimaryLayout.tsx | 12 ++++++++++-- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/web/components/patterns/PrimaryHeader.tsx b/packages/web/components/patterns/PrimaryHeader.tsx index 2dde10616..9e40bf6f7 100644 --- a/packages/web/components/patterns/PrimaryHeader.tsx +++ b/packages/web/components/patterns/PrimaryHeader.tsx @@ -159,16 +159,17 @@ function NavHeader(props: NavHeaderProps): JSX.Element { zIndex: 5, width: '100%', boxShadow: props.isDisplayingShadow ? '$panelShadow' : 'unset', - bg: '$grayBase', p: '0px $3 0px $3', height: '68px', position: 'fixed', - minHeight: '68px', + bg: 'transparent', '@smDown': { height: '48px', - minHeight: '48px', p: '0px 18px 0px 16px', }, + '@lgDown': { + bg: '$grayBase', + }, }} > diff --git a/packages/web/components/templates/PrimaryLayout.tsx b/packages/web/components/templates/PrimaryLayout.tsx index b3b1d1db6..3c7764c90 100644 --- a/packages/web/components/templates/PrimaryLayout.tsx +++ b/packages/web/components/templates/PrimaryLayout.tsx @@ -84,8 +84,8 @@ export function PrimaryLayout(props: PrimaryLayoutProps): JSX.Element { + {props.children} {showLogoutConfirmation ? ( Date: Fri, 8 Apr 2022 09:56:29 -0700 Subject: [PATCH 005/125] Encapsulate the article actions menu, standardize header height at 48px --- .../templates/article/ArticleActionsMenu.tsx | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 packages/web/components/templates/article/ArticleActionsMenu.tsx diff --git a/packages/web/components/templates/article/ArticleActionsMenu.tsx b/packages/web/components/templates/article/ArticleActionsMenu.tsx new file mode 100644 index 000000000..0e30ec958 --- /dev/null +++ b/packages/web/components/templates/article/ArticleActionsMenu.tsx @@ -0,0 +1,69 @@ +import { Separator } from "@radix-ui/react-separator" +import { ArchiveBox, DotsThree, HighlighterCircle, TagSimple, TextAa } from "phosphor-react" +import { Button } from "../../elements/Button" +import { Box } from "../../elements/LayoutPrimitives" +import { styled, theme } from "../../tokens/stitches.config" + +export type ArticleActionsMenuLayout = 'horizontal' | 'vertical' + +type ArticleActionsMenuProps = { + // pageTestId: string + // hideHeader?: boolean + // pageMetaDataProps?: PageMetaDataProps + // scrollElementRef?: MutableRefObject + // displayFontStepper?: boolean + layout: ArticleActionsMenuLayout +} + + + +export function MenuSeparator(props: ArticleActionsMenuProps) { + const LineSeparator = styled(Separator, { + width: '100%', + margin: 0, + backgroundColor: 'red', + borderBottom: `1px solid ${theme.colors.grayLine.toString()}`, + my: '8px', + }) + return (props.layout == 'vertical' ? : <>) +} + +export function ArticleActionsMenu(props: ArticleActionsMenuProps): JSX.Element { + return ( + + + + + + + + + + + + + + ) +} \ No newline at end of file From 19e3af66d5a06861ea426b7a4ae4a98462a0ae18 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Fri, 8 Apr 2022 10:01:14 -0700 Subject: [PATCH 006/125] Encapsulate the article actions menu, standardize header height at 48px --- packages/web/components/elements/Button.tsx | 8 ++++ .../web/components/patterns/PrimaryHeader.tsx | 46 ++----------------- .../components/templates/PrimaryLayout.tsx | 5 +- .../components/templates/SettingsLayout.tsx | 3 +- .../web/pages/[username]/[slug]/index.tsx | 36 ++------------- 5 files changed, 17 insertions(+), 81 deletions(-) diff --git a/packages/web/components/elements/Button.tsx b/packages/web/components/elements/Button.tsx index b3ccce06d..ccd3b0131 100644 --- a/packages/web/components/elements/Button.tsx +++ b/packages/web/components/elements/Button.tsx @@ -153,6 +153,14 @@ export const Button = styled('button', { opacity: 0.8, }, }, + articleActionIcon: { + bg: 'transparent', + border: 'none', + cursor: 'pointer', + '&:hover': { + opacity: 0.8, + }, + }, ghost: { color: 'transparent', border: 'none', diff --git a/packages/web/components/patterns/PrimaryHeader.tsx b/packages/web/components/patterns/PrimaryHeader.tsx index 9e40bf6f7..2d62089c7 100644 --- a/packages/web/components/patterns/PrimaryHeader.tsx +++ b/packages/web/components/patterns/PrimaryHeader.tsx @@ -18,6 +18,7 @@ import { setupAnalytics } from '../../lib/analytics' import { Button } from '../elements/Button' import Link from 'next/link' import { ArchiveBox, ArrowSquareOut, DotsThree, HighlighterCircle, TagSimple, TextAa } from 'phosphor-react' +import { ArticleActionsMenu } from '../templates/article/ArticleActionsMenu' type HeaderProps = { user?: UserBasicData @@ -160,11 +161,10 @@ function NavHeader(props: NavHeaderProps): JSX.Element { width: '100%', boxShadow: props.isDisplayingShadow ? '$panelShadow' : 'unset', p: '0px $3 0px $3', - height: '68px', + height: '48px', position: 'fixed', bg: 'transparent', '@smDown': { - height: '48px', p: '0px 18px 0px 16px', }, '@lgDown': { @@ -195,33 +195,8 @@ function NavHeader(props: NavHeaderProps): JSX.Element { mr: '16px', }} > - - + - - - - - - - @@ -230,21 +205,6 @@ function NavHeader(props: NavHeaderProps): JSX.Element { alignment="center" css={{ display: 'flex', alignItems: 'center' }} > - {/* - - - - */} {props.children} diff --git a/packages/web/components/templates/SettingsLayout.tsx b/packages/web/components/templates/SettingsLayout.tsx index 7f8f09f26..c2df14f0d 100644 --- a/packages/web/components/templates/SettingsLayout.tsx +++ b/packages/web/components/templates/SettingsLayout.tsx @@ -54,8 +54,7 @@ export function SettingsLayout(props: SettingsLayoutProps): JSX.Element { /> - - - - - - - - - - - - - + - {article.contentReader == 'PDF' ? ( Date: Fri, 8 Apr 2022 10:36:45 -0700 Subject: [PATCH 007/125] Use a themed font colour for the article actions buttons --- .../templates/article/ArticleActionsMenu.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/web/components/templates/article/ArticleActionsMenu.tsx b/packages/web/components/templates/article/ArticleActionsMenu.tsx index 0e30ec958..4e0eb283c 100644 --- a/packages/web/components/templates/article/ArticleActionsMenu.tsx +++ b/packages/web/components/templates/article/ArticleActionsMenu.tsx @@ -44,25 +44,25 @@ export function ArticleActionsMenu(props: ArticleActionsMenuProps): JSX.Element }} > ) From 32b08d11124e7db5f9f4ea94a758d348eba28edc Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Fri, 8 Apr 2022 10:42:20 -0700 Subject: [PATCH 008/125] Disable dots menu Since there are no extra actions yet --- .../templates/article/ArticleActionsMenu.tsx | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/packages/web/components/templates/article/ArticleActionsMenu.tsx b/packages/web/components/templates/article/ArticleActionsMenu.tsx index 4e0eb283c..8be346b8c 100644 --- a/packages/web/components/templates/article/ArticleActionsMenu.tsx +++ b/packages/web/components/templates/article/ArticleActionsMenu.tsx @@ -7,16 +7,9 @@ import { styled, theme } from "../../tokens/stitches.config" export type ArticleActionsMenuLayout = 'horizontal' | 'vertical' type ArticleActionsMenuProps = { - // pageTestId: string - // hideHeader?: boolean - // pageMetaDataProps?: PageMetaDataProps - // scrollElementRef?: MutableRefObject - // displayFontStepper?: boolean layout: ArticleActionsMenuLayout } - - export function MenuSeparator(props: ArticleActionsMenuProps) { const LineSeparator = styled(Separator, { width: '100%', @@ -33,11 +26,12 @@ export function ArticleActionsMenu(props: ArticleActionsMenuProps): JSX.Element + @@ -59,11 +54,11 @@ export function ArticleActionsMenu(props: ArticleActionsMenuProps): JSX.Element - + {/* + */} ) } \ No newline at end of file From 78129a4a85ccc15cc603ab4017f4ba03819ebcfe Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Fri, 8 Apr 2022 15:26:18 -0700 Subject: [PATCH 009/125] WIP: article actoins and reader settings --- .../components/elements/DropdownElements.tsx | 8 ++ .../components/elements/TickedRangeSlider.tsx | 39 ++++++ .../web/components/elements/images/AIcon.tsx | 14 +++ .../templates/article/ArticleActionsMenu.tsx | 52 ++++++-- .../templates/article/EditLabelsModal.tsx | 1 - .../templates/article/ReaderSettingsModal.tsx | 90 ++++++++++++++ .../web/pages/[username]/[slug]/index.tsx | 113 +++++++++++------- packages/web/styles/globals.css | 36 ++++++ 8 files changed, 299 insertions(+), 54 deletions(-) create mode 100644 packages/web/components/elements/TickedRangeSlider.tsx create mode 100644 packages/web/components/elements/images/AIcon.tsx create mode 100644 packages/web/components/templates/article/ReaderSettingsModal.tsx diff --git a/packages/web/components/elements/DropdownElements.tsx b/packages/web/components/elements/DropdownElements.tsx index 818898f06..d5f3b78c6 100644 --- a/packages/web/components/elements/DropdownElements.tsx +++ b/packages/web/components/elements/DropdownElements.tsx @@ -65,6 +65,7 @@ const StyledLabel = styled(Label, { }) export type DropdownAlignment = 'start' | 'end' | 'center' +export type DropdownSide = 'top' | 'right' | 'bottom' | 'left' type DropdownProps = { labelText?: string @@ -73,6 +74,8 @@ type DropdownProps = { children: React.ReactNode styledArrow?: boolean align?: DropdownAlignment + side?: DropdownSide + sideOffset?: number disabled?: boolean css?: CSS } @@ -108,8 +111,11 @@ export function Dropdown({ labelText, showArrow = true, disabled = false, + side = 'bottom', + sideOffset = 0, css }: DropdownProps): JSX.Element { + console.log('side', side) return ( {triggerElement} @@ -119,6 +125,8 @@ export function Dropdown({ // remove focus from dropdown ;(document.activeElement as HTMLElement).blur() }} + side={side} + sideOffset={sideOffset} align={align ? align : 'center'} > {labelText && {labelText}} diff --git a/packages/web/components/elements/TickedRangeSlider.tsx b/packages/web/components/elements/TickedRangeSlider.tsx new file mode 100644 index 000000000..c112d8d73 --- /dev/null +++ b/packages/web/components/elements/TickedRangeSlider.tsx @@ -0,0 +1,39 @@ + + +import { Box, HStack } from './LayoutPrimitives' +import { styled, theme } from '../tokens/stitches.config' + +type TickedRangeSliderProps = { + ticks?: number, + value: number, + onChange: (value: number) => void, + min?: number, + max?: number, + step?: number, +} + +const Tick = styled(Box, { + background: theme.colors.grayBorderHover, + width: 2, + height: 8, +}) + +export function TickedRangeSlider({ + ticks = 8, + min = 10, + max = 28, + step = 1, + value, + onChange, + } : TickedRangeSliderProps +): JSX.Element { + + return ( + + onChange(e.target.value as any)} value={value} type="range" min={min} max={max} step={step} className='slider'/> + + {[...Array(ticks)].map((val, idx) => )} + + + ) +} \ No newline at end of file diff --git a/packages/web/components/elements/images/AIcon.tsx b/packages/web/components/elements/images/AIcon.tsx new file mode 100644 index 000000000..f7972a2f0 --- /dev/null +++ b/packages/web/components/elements/images/AIcon.tsx @@ -0,0 +1,14 @@ +type AIconProps = { + size: number + color: string + style?: React.CSSProperties +} + +export function AIcon(props: AIconProps): JSX.Element { + return ( + + + + + ) +} \ No newline at end of file diff --git a/packages/web/components/templates/article/ArticleActionsMenu.tsx b/packages/web/components/templates/article/ArticleActionsMenu.tsx index 8be346b8c..0dab2698f 100644 --- a/packages/web/components/templates/article/ArticleActionsMenu.tsx +++ b/packages/web/components/templates/article/ArticleActionsMenu.tsx @@ -1,20 +1,26 @@ import { Separator } from "@radix-ui/react-separator" import { ArchiveBox, DotsThree, HighlighterCircle, TagSimple, TextAa } from "phosphor-react" import { Button } from "../../elements/Button" -import { Box } from "../../elements/LayoutPrimitives" +import { Dropdown } from "../../elements/DropdownElements" +import { Box, SpanBox } from "../../elements/LayoutPrimitives" import { styled, theme } from "../../tokens/stitches.config" +import { ReaderSettings } from "./ReaderSettingsModal" export type ArticleActionsMenuLayout = 'horizontal' | 'vertical' type ArticleActionsMenuProps = { layout: ArticleActionsMenuLayout + articleActionHandler: (action: string, arg?: number) => void } -export function MenuSeparator(props: ArticleActionsMenuProps) { +type MenuSeparatorProps = { + layout: ArticleActionsMenuLayout +} + +export function MenuSeparator(props: MenuSeparatorProps) { const LineSeparator = styled(Separator, { width: '100%', margin: 0, - backgroundColor: 'red', borderBottom: `1px solid ${theme.colors.grayLine.toString()}`, my: '8px', }) @@ -23,12 +29,12 @@ export function MenuSeparator(props: ArticleActionsMenuProps) { export function ArticleActionsMenu(props: ArticleActionsMenuProps): JSX.Element { return ( + <> - + + + + } + css={{ m: '0px', p: '0px', outlineStyle: 'solid', outlineWidth: '1px', outlineColor: theme.colors.grayLine.toString() }} + > + + + - + + + + } + css={{ background: 'red' }} + > + {/* */} + - - {/* @@ -60,5 +85,6 @@ export function ArticleActionsMenu(props: ArticleActionsMenuProps): JSX.Element */} + ) } \ No newline at end of file diff --git a/packages/web/components/templates/article/EditLabelsModal.tsx b/packages/web/components/templates/article/EditLabelsModal.tsx index 6b3e043d5..d06cc6e04 100644 --- a/packages/web/components/templates/article/EditLabelsModal.tsx +++ b/packages/web/components/templates/article/EditLabelsModal.tsx @@ -15,7 +15,6 @@ import { ArticleAttributes } from '../../../lib/networking/queries/useGetArticle import { LabelChip } from '../../elements/LabelChip' type EditLabelsModalProps = { - labels: Label[] article: ArticleAttributes onOpenChange: (open: boolean) => void setLabels: (labels: Label[]) => void diff --git a/packages/web/components/templates/article/ReaderSettingsModal.tsx b/packages/web/components/templates/article/ReaderSettingsModal.tsx new file mode 100644 index 000000000..b51669c2c --- /dev/null +++ b/packages/web/components/templates/article/ReaderSettingsModal.tsx @@ -0,0 +1,90 @@ +import { + ModalRoot, + ModalOverlay, + ModalContent, +} from '../../elements/ModalPrimitives' +import { Box, HStack, VStack, Separator, SpanBox } from '../../elements/LayoutPrimitives' +import { Button } from '../../elements/Button' +import { StyledText } from '../../elements/StyledText' +import { CrossIcon } from '../../elements/images/CrossIcon' +import { CommentIcon } from '../../elements/images/CommentIcon' +import { TrashIcon } from '../../elements/images/TrashIcon' +import { styled, theme } from '../../tokens/stitches.config' +import type { Highlight } from '../../../lib/networking/fragments/highlightFragment' +import { HighlightView } from '../../patterns/HighlightView' +import { useCallback, useState } from 'react' +import { StyledTextArea } from '../../elements/StyledTextArea' +import { ConfirmationModal } from '../../patterns/ConfirmationModal' +import { AlignCenterHorizontalSimple, ArrowsInLineHorizontal, ArrowsOutLineHorizontal, Minus, Pen, Plus, Trash } from 'phosphor-react' +import { AIcon } from '../../elements/images/AIcon' +import { TickedRangeSlider } from '../../elements/TickedRangeSlider' + +type ReaderSettingsProps = { + +} + +export function ReaderSettings(props: ReaderSettingsProps): JSX.Element { + const VerticalDivider = styled(SpanBox, { + width: '1px', + height: '100%', + background: `${theme.colors.grayLine.toString()}`, + }) + return ( + + + + + + + + Margin: + + + {}} /> + + + + + Line Height: + + + {}} /> + + + + + ) +} diff --git a/packages/web/pages/[username]/[slug]/index.tsx b/packages/web/pages/[username]/[slug]/index.tsx index 14b61bcb2..f35f94e8a 100644 --- a/packages/web/pages/[username]/[slug]/index.tsx +++ b/packages/web/pages/[username]/[slug]/index.tsx @@ -29,6 +29,8 @@ import { ArchiveBox, DotsThree, HighlighterCircle, TagSimple, TextAa } from 'pho import { Separator } from '@radix-ui/react-separator' import { Article } from '../../../components/templates/article/Article' import { ArticleActionsMenu } from '../../../components/templates/article/ArticleActionsMenu' +import { HighlightsModal } from '../../../components/templates/article/HighlightsModal' +import { setLinkArchivedMutation } from '../../../lib/networking/mutations/setLinkArchivedMutation' const MenuSeparator = styled(Separator, { width: '100%', @@ -48,6 +50,7 @@ export default function Home(): JSX.Element { const scrollRef = useRef(null) const { slug } = router.query const [showLabelsModal, setShowLabelsModal] = useState(false) + const [showHighlightsModal, setShowHighlightsModal] = useState(false) // Populate data cache const { viewerData } = useGetViewerQuery() @@ -73,33 +76,51 @@ export default function Home(): JSX.Element { setMarginWidth(newMargin) } + const actionHandler = async (action: string, arg?: number) => { + switch (action) { + case 'archive': + if (article) { + await setLinkArchivedMutation({ + linkId: article.id, + archived: true, + }) + // TODO: merge from article actions PR + // removeItemFromCache(cache, mutate, props.article.id) + router.push(`/home`) + } + break + case 'openOriginalArticle': + const url = article?.url + if (url) { + window.open(url, '_blank') + } + break + case 'showHighlights': + setShowHighlightsModal(true) + break + case 'incrementFontSize': + await updateFontSize(Math.min(fontSize + 2, 28)) + break + case 'decrementFontSize': + await updateFontSize(Math.max(fontSize - 2, 10)) + break + case 'incrementMarginWidth': + updateMarginWidth(Math.min(marginWidth + 50, 560)) + break + case 'decrementMarginWidth': + updateMarginWidth(Math.max(marginWidth - 50, 200)) + break + case 'editLabels': + if (viewerData?.me && isVipUser(viewerData?.me)) { + setShowLabelsModal(true) + } + break + } + }; + useKeyboardShortcuts( articleKeyboardCommands(router, async (action) => { - switch (action) { - case 'openOriginalArticle': - const url = article?.url - if (url) { - window.open(url, '_blank') - } - break - case 'incrementFontSize': - await updateFontSize(Math.min(fontSize + 2, 28)) - break - case 'decrementFontSize': - await updateFontSize(Math.max(fontSize - 2, 10)) - break - case 'incrementMarginWidth': - updateMarginWidth(Math.min(marginWidth + 50, 560)) - break - case 'decrementMarginWidth': - updateMarginWidth(Math.max(marginWidth - 50, 200)) - break - case 'editLabels': - if (viewerData?.me && isVipUser(viewerData?.me)) { - setShowLabelsModal(true) - } - break - } + actionHandler(action) }) ) @@ -124,19 +145,22 @@ export default function Home(): JSX.Element { - - + position: 'fixed', + flexDirection: 'row-reverse', + top: '-120px', + left: 8, + height: '100%', + width: '48px', + '@lgDown': { + display: 'none', + }, + }} + > + + {article.contentReader == 'PDF' ? ( + + )} + {showHighlightsModal && ( + setShowHighlightsModal(false)} + deleteHighlightAction={(highlightId: string) => { + // removeHighlightCallback(highlightId) + }} + /> + )} {/* {showLabelsModal && ( )} */} - - )} ) } diff --git a/packages/web/styles/globals.css b/packages/web/styles/globals.css index 573f58d7f..dda271556 100644 --- a/packages/web/styles/globals.css +++ b/packages/web/styles/globals.css @@ -99,3 +99,39 @@ div#appleid-signin { border-bottom-color: transparent; border-left-color: transparent; } + +.slider { + -webkit-appearance: none; + appearance: none; + width: 100%; + height: 1px; + background: var(--colors-grayBorderHover); +} + +.slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 16px !important; + height: 16px; + border-radius: 50%; + background: var(--colors-utilityTextContrast); + cursor: pointer; +} + +.slider::-moz-range-thumb { + -webkit-appearance: none; + width: 16px !important; + height: 16px; + border-radius: 50%; + background: var(--colors-utilityTextContrast); + cursor: pointer; +} + +input[type=range]::-webkit-slider-thumb { + -webkit-appearance: none; + border: none; + height: 16px; + width: 16px; + border-radius: 50%; + background: var(--colors-grayTextContrast); +} \ No newline at end of file From ba301c4f0f428c6934ce618aaacf2245dc638f81 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Fri, 8 Apr 2022 15:44:52 -0700 Subject: [PATCH 010/125] Sliders --- .../web/components/patterns/PrimaryHeader.tsx | 2 +- .../templates/article/ArticleActionsMenu.tsx | 4 +- .../templates/article/ArticleContainer.tsx | 1 - .../templates/article/ReaderSettingsModal.tsx | 44 ++++++++++--------- .../web/pages/[username]/[slug]/index.tsx | 1 + 5 files changed, 29 insertions(+), 23 deletions(-) diff --git a/packages/web/components/patterns/PrimaryHeader.tsx b/packages/web/components/patterns/PrimaryHeader.tsx index 2d62089c7..f539fa5fd 100644 --- a/packages/web/components/patterns/PrimaryHeader.tsx +++ b/packages/web/components/patterns/PrimaryHeader.tsx @@ -195,7 +195,7 @@ function NavHeader(props: NavHeaderProps): JSX.Element { mr: '16px', }} > - + {/* */} diff --git a/packages/web/components/templates/article/ArticleActionsMenu.tsx b/packages/web/components/templates/article/ArticleActionsMenu.tsx index 0dab2698f..01cc1d26c 100644 --- a/packages/web/components/templates/article/ArticleActionsMenu.tsx +++ b/packages/web/components/templates/article/ArticleActionsMenu.tsx @@ -1,5 +1,6 @@ import { Separator } from "@radix-ui/react-separator" import { ArchiveBox, DotsThree, HighlighterCircle, TagSimple, TextAa } from "phosphor-react" +import { UserPreferences } from "../../../lib/networking/queries/useGetUserPreferences" import { Button } from "../../elements/Button" import { Dropdown } from "../../elements/DropdownElements" import { Box, SpanBox } from "../../elements/LayoutPrimitives" @@ -10,6 +11,7 @@ export type ArticleActionsMenuLayout = 'horizontal' | 'vertical' type ArticleActionsMenuProps = { layout: ArticleActionsMenuLayout + userPreferences?: UserPreferences articleActionHandler: (action: string, arg?: number) => void } @@ -54,7 +56,7 @@ export function ArticleActionsMenu(props: ArticleActionsMenuProps): JSX.Element } css={{ m: '0px', p: '0px', outlineStyle: 'solid', outlineWidth: '1px', outlineColor: theme.colors.grayLine.toString() }} > - + diff --git a/packages/web/components/templates/article/ArticleContainer.tsx b/packages/web/components/templates/article/ArticleContainer.tsx index 6dc5dd79a..de0866809 100644 --- a/packages/web/components/templates/article/ArticleContainer.tsx +++ b/packages/web/components/templates/article/ArticleContainer.tsx @@ -13,7 +13,6 @@ import { ArticleHeaderToolbar } from './ArticleHeaderToolbar' import { userPersonalizationMutation } from '../../../lib/networking/mutations/userPersonalizationMutation' import { updateThemeLocally } from '../../../lib/themeUpdater' import { ArticleMutations } from '../../../lib/articleActions' -import { TextAa } from 'phosphor-react' type ArticleContainerProps = { article: ArticleAttributes diff --git a/packages/web/components/templates/article/ReaderSettingsModal.tsx b/packages/web/components/templates/article/ReaderSettingsModal.tsx index b51669c2c..f6eccae5d 100644 --- a/packages/web/components/templates/article/ReaderSettingsModal.tsx +++ b/packages/web/components/templates/article/ReaderSettingsModal.tsx @@ -18,9 +18,11 @@ import { ConfirmationModal } from '../../patterns/ConfirmationModal' import { AlignCenterHorizontalSimple, ArrowsInLineHorizontal, ArrowsOutLineHorizontal, Minus, Pen, Plus, Trash } from 'phosphor-react' import { AIcon } from '../../elements/images/AIcon' import { TickedRangeSlider } from '../../elements/TickedRangeSlider' +import { UserPreferences } from '../../../lib/networking/queries/useGetUserPreferences' type ReaderSettingsProps = { - + userPreferences?: UserPreferences + articleActionHandler: (action: string, arg?: number) => void } export function ReaderSettings(props: ReaderSettingsProps): JSX.Element { @@ -34,40 +36,42 @@ export function ReaderSettings(props: ReaderSettingsProps): JSX.Element { - - - + Margin: - - {}} /> - - - + */} ) } diff --git a/packages/web/pages/[username]/[slug]/index.tsx b/packages/web/pages/[username]/[slug]/index.tsx index f35f94e8a..c0514ca29 100644 --- a/packages/web/pages/[username]/[slug]/index.tsx +++ b/packages/web/pages/[username]/[slug]/index.tsx @@ -158,6 +158,7 @@ export default function Home(): JSX.Element { > From 278a885f83a466cc7ed03efb003288cf640262f5 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Fri, 8 Apr 2022 20:19:18 -0700 Subject: [PATCH 011/125] Edit labels menu --- .../templates/article/ArticleActionsMenu.tsx | 8 +- .../templates/article/EditLabelsModal.tsx | 333 +++++++++++++----- .../templates/article/ReaderSettingsModal.tsx | 2 +- 3 files changed, 260 insertions(+), 83 deletions(-) diff --git a/packages/web/components/templates/article/ArticleActionsMenu.tsx b/packages/web/components/templates/article/ArticleActionsMenu.tsx index 01cc1d26c..1d989599c 100644 --- a/packages/web/components/templates/article/ArticleActionsMenu.tsx +++ b/packages/web/components/templates/article/ArticleActionsMenu.tsx @@ -5,6 +5,7 @@ import { Button } from "../../elements/Button" import { Dropdown } from "../../elements/DropdownElements" import { Box, SpanBox } from "../../elements/LayoutPrimitives" import { styled, theme } from "../../tokens/stitches.config" +import { EditLabelsModal } from "./EditLabelsModal" import { ReaderSettings } from "./ReaderSettingsModal" export type ArticleActionsMenuLayout = 'horizontal' | 'vertical' @@ -54,7 +55,7 @@ export function ArticleActionsMenu(props: ArticleActionsMenuProps): JSX.Element } - css={{ m: '0px', p: '0px', outlineStyle: 'solid', outlineWidth: '1px', outlineColor: theme.colors.grayLine.toString() }} + css={{ m: '0px', p: '0px' }} > @@ -63,13 +64,16 @@ export function ArticleActionsMenu(props: ArticleActionsMenuProps): JSX.Element } - css={{ background: 'red' }} + css={{ m: '0px', p: '0px' }} > + {/* */} diff --git a/packages/web/components/templates/article/EditLabelsModal.tsx b/packages/web/components/templates/article/EditLabelsModal.tsx index d06cc6e04..1d74f4574 100644 --- a/packages/web/components/templates/article/EditLabelsModal.tsx +++ b/packages/web/components/templates/article/EditLabelsModal.tsx @@ -3,108 +3,281 @@ import { ModalOverlay, ModalRoot, } from '../../elements/ModalPrimitives' -import { HStack, VStack } from '../../elements/LayoutPrimitives' +import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives' import { Button } from '../../elements/Button' import { StyledText } from '../../elements/StyledText' import { CrossIcon } from '../../elements/images/CrossIcon' -import { theme } from '../../tokens/stitches.config' +import { styled, theme } from '../../tokens/stitches.config' import { Label, useGetLabelsQuery } from '../../../lib/networking/queries/useGetLabelsQuery' -import { ChangeEvent, useCallback, useState } from 'react' +import { ChangeEvent, useCallback, useRef, useState, useMemo } from 'react' import { setLabelsMutation } from '../../../lib/networking/mutations/setLabelsMutation' import { ArticleAttributes } from '../../../lib/networking/queries/useGetArticleQuery' import { LabelChip } from '../../elements/LabelChip' +import { Check, Circle, Pen, PencilSimple, PencilSimpleLine, Plus, TagSimple } from 'phosphor-react' +import Link from 'next/link' type EditLabelsModalProps = { - article: ArticleAttributes - onOpenChange: (open: boolean) => void - setLabels: (labels: Label[]) => void + // labels: Label[] + // article: ArticleAttributes + // onOpenChange: (open: boolean) => void + // setLabels: (labels: Label[]) => void +} + +type HeaderProps = { + filterText: string + setFilterText: (text: string) => void +} + +const FormInput = styled('input', { + width: '100%', + fontSize: '16px', + fontFamily: 'inter', + fontWeight: 'normal', + lineHeight: '1.8', + color: '$grayTextContrast', + '&:focus': { + outline: 'none', + }, +}) + +const StyledLabel = styled('label', { + display: 'flex', + justifyContent: 'flex-start', + '&:focus-visible': { + backgroundColor: 'red', + }, +}) + +function Header(props: HeaderProps): JSX.Element { + const inputRef = useRef(null) + + return ( + + {/* */} + {/* + Apply labels to this page + */} + {/* */} + {/* */} + +
{ + // event.preventDefault() + // props.applySearchQuery(searchTerm || '') + inputRef.current?.blur() + }} + > + { + event.target.select() + //setFocused(true) + }} + onBlur={() => { + //setFocused(false) + console.log('blurred') + }} + onKeyDown={(event) => { + console.log('keydown', event.key) + }} + onChange={(event) => { + console.log('event', event) + props.setFilterText(event.target.value) + }} + css={{ + border: '1px solid $grayBorder', + borderRadius: '8px', + width: '100%', + bg: 'transparent', + fontSize: '16px', + fontFamily: 'inter', + fontWeight: 'normal', + marginBottom: '2px', + textIndent: '8px', + paddingLeft: '8px', + color: '$grayTextContrast', + '&:focus': { + outline: 'none', + boxShadow: '0px 0px 2px 2px rgba(255, 234, 159, 0.56)', + }, + }} + /> + +
+
) +} + +type LabelsListProps = { + // pageId: string + // selectedLabels: Label[] + availableLabels: Label[] +// setSelectedLabels: (labels: Label[]) => void +} + +function LabelsList(props: LabelsListProps): JSX.Element { + const isSelected = useCallback((label: Label) => { + // return props.selectedLabels.some((other) => { + // return other.id === label.id + // }) + return false + }, []) + + return ( + + {props.availableLabels && + props.availableLabels.map((label, idx) => ( + { + // console.log('selected label', label) + // if (props.selectedLabels.includes(label)) { + // props.setSelectedLabels( + // props.selectedLabels.filter((id) => id !== label) + // ) + // } else { + // props.setSelectedLabels([...props.selectedLabels, label]) + // } + // const result = await setLabelsMutation(props.pageId, props.selectedLabels.map((l) => l.id)) + // console.log('result', result) + }} + > + + {isSelected(label) && } + + + + + + {label.name} + + + {isSelected(label) && } + + {/* + + */} + {/* */} + + ))} + ) } export function EditLabelsModal(props: EditLabelsModalProps): JSX.Element { - const [selectedLabels, setSelectedLabels] = useState(props.labels) + const [filterText, setFilterText] = useState('') + const [selectedLabels, setSelectedLabels] = useState([]) const { labels } = useGetLabelsQuery() const saveAndExit = useCallback(async () => { - const result = await setLabelsMutation(props.article.id, selectedLabels.map((l) => l.id)) - console.log('result of setting labels', result) - props.onOpenChange(false) - props.setLabels(selectedLabels) + // const result = await setLabelsMutation(props.article.id, selectedLabels.map((l) => l.id)) + // console.log('result of setting labels', result) + // // props.onOpenChange(false) + // // props.setLabels(selectedLabels) }, [props, selectedLabels]) - const handleChange = useCallback( - (event: ChangeEvent) => { - // const label = event.target.value - // if (event.target.checked) { - // setSelectedLabels([...selectedLabels, label]) - // } else { - // setSelectedLabels(selectedLabels.filter((l) => l !== label)) - // } - }, - [selectedLabels] - ) + const filteredLabels = useMemo(() => { + if (!labels) { + return [] + } + return labels.filter((label) => { + return label.name.toLowerCase().includes(filterText.toLowerCase()) + }) + }, [labels, filterText]) return ( - - - { - event.preventDefault() - }} - css={{ overflow: 'auto', p: '0' }} - > - - - - Edit Labels - - - - {labels && - labels.map((label) => ( - { - // if (selectedLabels.includes(label.id)) { - // setSelectedLabels( - // selectedLabels.filter((id) => id !== label.id) - // ) - // } else { - // setSelectedLabels([...selectedLabels, label.id]) - // } - }} - > - - - - ))} - - + + + {`Create new label "${filterText}"`} + + )} + {/* Footer */} + + + Edit labels - - + // + // ) -} +} \ No newline at end of file diff --git a/packages/web/components/templates/article/ReaderSettingsModal.tsx b/packages/web/components/templates/article/ReaderSettingsModal.tsx index f6eccae5d..f1043837a 100644 --- a/packages/web/components/templates/article/ReaderSettingsModal.tsx +++ b/packages/web/components/templates/article/ReaderSettingsModal.tsx @@ -65,7 +65,7 @@ export function ReaderSettings(props: ReaderSettingsProps): JSX.Element { - {}} /> + {}} /> From a9988d47068e74a5b19ff8f386acfbd47b5b025a Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Fri, 8 Apr 2022 20:37:09 -0700 Subject: [PATCH 012/125] Re-enable top menu on small screens --- .../web/components/patterns/PrimaryHeader.tsx | 24 +++++++++---------- .../templates/article/ArticleActionsMenu.tsx | 9 +++---- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/packages/web/components/patterns/PrimaryHeader.tsx b/packages/web/components/patterns/PrimaryHeader.tsx index f539fa5fd..bcb8b22ab 100644 --- a/packages/web/components/patterns/PrimaryHeader.tsx +++ b/packages/web/components/patterns/PrimaryHeader.tsx @@ -186,19 +186,17 @@ function NavHeader(props: NavHeaderProps): JSX.Element { - {/* */} - - - + height: '100%', + width: '100%', + display: 'none', + '@lgDown': { + display: 'flex', + }, + mr: '16px', + }} + > + {}} /> + {props.username ? ( void } @@ -31,6 +30,8 @@ export function MenuSeparator(props: MenuSeparatorProps) { } export function ArticleActionsMenu(props: ArticleActionsMenuProps): JSX.Element { + const { preferencesData } = useGetUserPreferences() + return ( <> - + From 0998048163b4d8cb282db978ea7150f07f8f22c0 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Fri, 8 Apr 2022 20:50:53 -0700 Subject: [PATCH 013/125] Remove unused prop --- packages/web/pages/[username]/[slug]/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/web/pages/[username]/[slug]/index.tsx b/packages/web/pages/[username]/[slug]/index.tsx index c0514ca29..f35f94e8a 100644 --- a/packages/web/pages/[username]/[slug]/index.tsx +++ b/packages/web/pages/[username]/[slug]/index.tsx @@ -158,7 +158,6 @@ export default function Home(): JSX.Element { > From d8e064807050a30e9d867dbc39858ca489baf991 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Fri, 8 Apr 2022 21:06:51 -0700 Subject: [PATCH 014/125] Remove empty arrow function --- packages/web/components/patterns/PrimaryHeader.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/web/components/patterns/PrimaryHeader.tsx b/packages/web/components/patterns/PrimaryHeader.tsx index bcb8b22ab..f96f5138b 100644 --- a/packages/web/components/patterns/PrimaryHeader.tsx +++ b/packages/web/components/patterns/PrimaryHeader.tsx @@ -195,7 +195,9 @@ function NavHeader(props: NavHeaderProps): JSX.Element { mr: '16px', }} > - {}} /> + { + console.log('action handler') + }} /> {props.username ? ( From 0b4f223b2670b7dd99a71f0426fa969dd25946ca Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Fri, 8 Apr 2022 21:31:19 -0700 Subject: [PATCH 015/125] Spacing on top dropdowns --- .../templates/article/ArticleActionsMenu.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/web/components/templates/article/ArticleActionsMenu.tsx b/packages/web/components/templates/article/ArticleActionsMenu.tsx index 72e2d5c41..cd5ea9071 100644 --- a/packages/web/components/templates/article/ArticleActionsMenu.tsx +++ b/packages/web/components/templates/article/ArticleActionsMenu.tsx @@ -48,9 +48,9 @@ export function ArticleActionsMenu(props: ArticleActionsMenuProps): JSX.Element }} > @@ -64,9 +64,10 @@ export function ArticleActionsMenu(props: ArticleActionsMenuProps): JSX.Element From 0d16b32ae712e65730b34b002560e4b09c2ab4d0 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Fri, 8 Apr 2022 22:00:57 -0700 Subject: [PATCH 016/125] Test with bottom sheet --- .../components/elements/DropdownElements.tsx | 1 - .../templates/article/ArticleActionsMenu.tsx | 19 ++++++++++++++++--- .../templates/article/EditLabelsModal.tsx | 3 ++- .../templates/article/ReaderSettingsModal.tsx | 7 +++++-- packages/web/package.json | 1 + 5 files changed, 24 insertions(+), 7 deletions(-) diff --git a/packages/web/components/elements/DropdownElements.tsx b/packages/web/components/elements/DropdownElements.tsx index d5f3b78c6..6b63da219 100644 --- a/packages/web/components/elements/DropdownElements.tsx +++ b/packages/web/components/elements/DropdownElements.tsx @@ -115,7 +115,6 @@ export function Dropdown({ sideOffset = 0, css }: DropdownProps): JSX.Element { - console.log('side', side) return ( {triggerElement} diff --git a/packages/web/components/templates/article/ArticleActionsMenu.tsx b/packages/web/components/templates/article/ArticleActionsMenu.tsx index cd5ea9071..c806fa655 100644 --- a/packages/web/components/templates/article/ArticleActionsMenu.tsx +++ b/packages/web/components/templates/article/ArticleActionsMenu.tsx @@ -1,5 +1,7 @@ import { Separator } from "@radix-ui/react-separator" import { ArchiveBox, DotsThree, HighlighterCircle, TagSimple, TextAa } from "phosphor-react" +import { useState } from "react" +import { BottomSheet } from "react-spring-bottom-sheet" import { useGetUserPreferences, UserPreferences } from "../../../lib/networking/queries/useGetUserPreferences" import { Button } from "../../elements/Button" import { Dropdown } from "../../elements/DropdownElements" @@ -8,6 +10,10 @@ import { styled, theme } from "../../tokens/stitches.config" import { EditLabelsModal } from "./EditLabelsModal" import { ReaderSettings } from "./ReaderSettingsModal" +// if setting up the CSS is tricky, you can add this to your page somewhere: +// +import 'react-spring-bottom-sheet/dist/style.css' + export type ArticleActionsMenuLayout = 'horizontal' | 'vertical' type ArticleActionsMenuProps = { @@ -31,6 +37,7 @@ export function MenuSeparator(props: MenuSeparatorProps) { export function ArticleActionsMenu(props: ArticleActionsMenuProps): JSX.Element { const { preferencesData } = useGetUserPreferences() + const [open, setOpen] = useState(false) return ( <> @@ -56,7 +63,7 @@ export function ArticleActionsMenu(props: ArticleActionsMenuProps): JSX.Element } - css={{ m: '0px', p: '0px' }} + css={{ m: '0px', p: '0px' }} > @@ -83,8 +90,8 @@ export function ArticleActionsMenu(props: ArticleActionsMenuProps): JSX.Element - - {/* @@ -92,6 +99,12 @@ export function ArticleActionsMenu(props: ArticleActionsMenuProps): JSX.Element */} + minHeight} + open={open} onDismiss={() => setOpen(false)}> + + {/* */} + ) diff --git a/packages/web/components/templates/article/EditLabelsModal.tsx b/packages/web/components/templates/article/EditLabelsModal.tsx index 1d74f4574..c7205b43d 100644 --- a/packages/web/components/templates/article/EditLabelsModal.tsx +++ b/packages/web/components/templates/article/EditLabelsModal.tsx @@ -241,7 +241,8 @@ export function EditLabelsModal(props: EditLabelsModalProps): JSX.Element { // >
@@ -65,7 +66,9 @@ export function ReaderSettings(props: ReaderSettingsProps): JSX.Element { - {}} /> + { + console.log('range changed') + }} /> diff --git a/packages/web/package.json b/packages/web/package.json index 7858d890b..39c1eba08 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -40,6 +40,7 @@ "react-hot-toast": "^2.1.1", "react-intl": "^5.20.12", "react-loading-skeleton": "^3.0.2", + "react-spring-bottom-sheet": "^3.4.0", "react-twitter-widgets": "^1.10.0", "swr": "^1.0.1", "uuid": "^8.3.2" From f87db8bfad58599d3f28da115031295b908abded Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Sun, 10 Apr 2022 09:40:07 -0700 Subject: [PATCH 017/125] Revert "Test with bottom sheet" This reverts commit 44faad1172580bf4b7ca85d00f0495f76226dc2d. --- .../components/elements/DropdownElements.tsx | 1 + .../templates/article/ArticleActionsMenu.tsx | 19 +++---------------- .../templates/article/EditLabelsModal.tsx | 3 +-- .../templates/article/ReaderSettingsModal.tsx | 7 ++----- packages/web/package.json | 1 - 5 files changed, 7 insertions(+), 24 deletions(-) diff --git a/packages/web/components/elements/DropdownElements.tsx b/packages/web/components/elements/DropdownElements.tsx index 6b63da219..d5f3b78c6 100644 --- a/packages/web/components/elements/DropdownElements.tsx +++ b/packages/web/components/elements/DropdownElements.tsx @@ -115,6 +115,7 @@ export function Dropdown({ sideOffset = 0, css }: DropdownProps): JSX.Element { + console.log('side', side) return ( {triggerElement} diff --git a/packages/web/components/templates/article/ArticleActionsMenu.tsx b/packages/web/components/templates/article/ArticleActionsMenu.tsx index c806fa655..cd5ea9071 100644 --- a/packages/web/components/templates/article/ArticleActionsMenu.tsx +++ b/packages/web/components/templates/article/ArticleActionsMenu.tsx @@ -1,7 +1,5 @@ import { Separator } from "@radix-ui/react-separator" import { ArchiveBox, DotsThree, HighlighterCircle, TagSimple, TextAa } from "phosphor-react" -import { useState } from "react" -import { BottomSheet } from "react-spring-bottom-sheet" import { useGetUserPreferences, UserPreferences } from "../../../lib/networking/queries/useGetUserPreferences" import { Button } from "../../elements/Button" import { Dropdown } from "../../elements/DropdownElements" @@ -10,10 +8,6 @@ import { styled, theme } from "../../tokens/stitches.config" import { EditLabelsModal } from "./EditLabelsModal" import { ReaderSettings } from "./ReaderSettingsModal" -// if setting up the CSS is tricky, you can add this to your page somewhere: -// -import 'react-spring-bottom-sheet/dist/style.css' - export type ArticleActionsMenuLayout = 'horizontal' | 'vertical' type ArticleActionsMenuProps = { @@ -37,7 +31,6 @@ export function MenuSeparator(props: MenuSeparatorProps) { export function ArticleActionsMenu(props: ArticleActionsMenuProps): JSX.Element { const { preferencesData } = useGetUserPreferences() - const [open, setOpen] = useState(false) return ( <> @@ -63,7 +56,7 @@ export function ArticleActionsMenu(props: ArticleActionsMenuProps): JSX.Element } - css={{ m: '0px', p: '0px' }} + css={{ m: '0px', p: '0px' }} > @@ -90,8 +83,8 @@ export function ArticleActionsMenu(props: ArticleActionsMenuProps): JSX.Element -{/* // props.articleActionHandler('archive') */} - {/* @@ -99,12 +92,6 @@ export function ArticleActionsMenu(props: ArticleActionsMenuProps): JSX.Element */} - minHeight} - open={open} onDismiss={() => setOpen(false)}> - - {/* */} - ) diff --git a/packages/web/components/templates/article/EditLabelsModal.tsx b/packages/web/components/templates/article/EditLabelsModal.tsx index c7205b43d..1d74f4574 100644 --- a/packages/web/components/templates/article/EditLabelsModal.tsx +++ b/packages/web/components/templates/article/EditLabelsModal.tsx @@ -241,8 +241,7 @@ export function EditLabelsModal(props: EditLabelsModalProps): JSX.Element { // >
@@ -66,9 +65,7 @@ export function ReaderSettings(props: ReaderSettingsProps): JSX.Element { - { - console.log('range changed') - }} /> + {}} /> diff --git a/packages/web/package.json b/packages/web/package.json index 39c1eba08..7858d890b 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -40,7 +40,6 @@ "react-hot-toast": "^2.1.1", "react-intl": "^5.20.12", "react-loading-skeleton": "^3.0.2", - "react-spring-bottom-sheet": "^3.4.0", "react-twitter-widgets": "^1.10.0", "swr": "^1.0.1", "uuid": "^8.3.2" From d8e03c090688d5ceab78b3f3a844246b186664bd Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Sun, 10 Apr 2022 09:42:38 -0700 Subject: [PATCH 018/125] Make arrow styles more closely match design --- packages/web/components/elements/DropdownElements.tsx | 6 +++--- .../web/components/templates/article/ArticleActionsMenu.tsx | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/web/components/elements/DropdownElements.tsx b/packages/web/components/elements/DropdownElements.tsx index d5f3b78c6..853bc71bc 100644 --- a/packages/web/components/elements/DropdownElements.tsx +++ b/packages/web/components/elements/DropdownElements.tsx @@ -49,12 +49,12 @@ export const DropdownContent = styled(Content, { backgroundColor: '$grayBg', borderRadius: '0.5em', padding: 5, - border: '1px solid $grayBorder', + outline: '1px solid $grayBorder', boxShadow: '$cardBoxShadow', }) const StyledArrow = styled(Arrow, { - fill: '$grayBase', + fill: '$grayBg', }) const StyledLabel = styled(Label, { @@ -131,7 +131,7 @@ export function Dropdown({ > {labelText && {labelText}} {children} - {showArrow && } + {showArrow && } ) diff --git a/packages/web/components/templates/article/ArticleActionsMenu.tsx b/packages/web/components/templates/article/ArticleActionsMenu.tsx index cd5ea9071..72e5417e2 100644 --- a/packages/web/components/templates/article/ArticleActionsMenu.tsx +++ b/packages/web/components/templates/article/ArticleActionsMenu.tsx @@ -48,6 +48,7 @@ export function ArticleActionsMenu(props: ArticleActionsMenuProps): JSX.Element }} > Date: Sun, 10 Apr 2022 09:47:24 -0700 Subject: [PATCH 019/125] Dont autofocus the labels filter on touch devices as the keyboard uses too much screen space --- packages/web/components/templates/article/EditLabelsModal.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/web/components/templates/article/EditLabelsModal.tsx b/packages/web/components/templates/article/EditLabelsModal.tsx index 1d74f4574..0a3bf1499 100644 --- a/packages/web/components/templates/article/EditLabelsModal.tsx +++ b/packages/web/components/templates/article/EditLabelsModal.tsx @@ -15,6 +15,7 @@ import { ArticleAttributes } from '../../../lib/networking/queries/useGetArticle import { LabelChip } from '../../elements/LabelChip' import { Check, Circle, Pen, PencilSimple, PencilSimpleLine, Plus, TagSimple } from 'phosphor-react' import Link from 'next/link' +import { isTouchScreenDevice } from '../../../lib/deviceType' type EditLabelsModalProps = { // labels: Label[] @@ -96,7 +97,7 @@ function Header(props: HeaderProps): JSX.Element { { From 05e7ba7ced62731ec8976f8b14730baf048757e1 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Sun, 10 Apr 2022 09:48:43 -0700 Subject: [PATCH 020/125] Remove empty arrow function --- .../web/components/templates/article/ReaderSettingsModal.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/web/components/templates/article/ReaderSettingsModal.tsx b/packages/web/components/templates/article/ReaderSettingsModal.tsx index f1043837a..e8c70709b 100644 --- a/packages/web/components/templates/article/ReaderSettingsModal.tsx +++ b/packages/web/components/templates/article/ReaderSettingsModal.tsx @@ -65,7 +65,9 @@ export function ReaderSettings(props: ReaderSettingsProps): JSX.Element { - {}} /> + { + console.log('range changed', value) + }} /> From eeea5b7c28a87caac804c3ce3b54c97fdd7d241f Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Sun, 10 Apr 2022 10:20:34 -0700 Subject: [PATCH 021/125] Consistent dropdown layout for article actions We also offset a little to display the popover in the middle of the article action button on desktop Disable autofocus on touch devices so the keyboard doesn't take up too much space --- .../components/elements/DropdownElements.tsx | 27 ++++++------ .../templates/article/ArticleActionsMenu.tsx | 43 ++++++++++++------- .../templates/article/EditLabelsModal.tsx | 2 +- .../templates/article/ReaderSettingsModal.tsx | 8 ++-- 4 files changed, 48 insertions(+), 32 deletions(-) diff --git a/packages/web/components/elements/DropdownElements.tsx b/packages/web/components/elements/DropdownElements.tsx index 853bc71bc..61dfeaac2 100644 --- a/packages/web/components/elements/DropdownElements.tsx +++ b/packages/web/components/elements/DropdownElements.tsx @@ -8,6 +8,7 @@ import { Arrow, Label, } from '@radix-ui/react-dropdown-menu' +import { PopperContentProps } from '@radix-ui/react-popover'; import { CSS } from '@stitches/react'; import { styled } from './../tokens/stitches.config' @@ -104,18 +105,19 @@ export function DropdownOption(props: DropdownOptionProps): JSX.Element { ) } -export function Dropdown({ - children, - align, - triggerElement, - labelText, - showArrow = true, - disabled = false, - side = 'bottom', - sideOffset = 0, - css -}: DropdownProps): JSX.Element { - console.log('side', side) +export function Dropdown(props: DropdownProps & PopperContentProps): JSX.Element { + const { + children, + align, + triggerElement, + labelText, + showArrow = true, + disabled = false, + side = 'bottom', + sideOffset = 0, + alignOffset = 0, + css + } = props return ( {triggerElement} @@ -128,6 +130,7 @@ export function Dropdown({ side={side} sideOffset={sideOffset} align={align ? align : 'center'} + alignOffset={alignOffset} > {labelText && {labelText}} {children} diff --git a/packages/web/components/templates/article/ArticleActionsMenu.tsx b/packages/web/components/templates/article/ArticleActionsMenu.tsx index 72e5417e2..a97a4370d 100644 --- a/packages/web/components/templates/article/ArticleActionsMenu.tsx +++ b/packages/web/components/templates/article/ArticleActionsMenu.tsx @@ -19,7 +19,7 @@ type MenuSeparatorProps = { layout: ArticleActionsMenuLayout } -export function MenuSeparator(props: MenuSeparatorProps) { +const MenuSeparator = (props: MenuSeparatorProps): JSX.Element => { const LineSeparator = styled(Separator, { width: '100%', margin: 0, @@ -29,6 +29,26 @@ export function MenuSeparator(props: MenuSeparatorProps) { return (props.layout == 'vertical' ? : <>) } +type ActionDropdownProps = { + layout: ArticleActionsMenuLayout + triggerElement: JSX.Element + children: JSX.Element +} + +const ActionDropdown = (props: ActionDropdownProps): JSX.Element => { + return + {props.children} + +} + export function ArticleActionsMenu(props: ArticleActionsMenuProps): JSX.Element { const { preferencesData } = useGetUserPreferences() @@ -47,38 +67,29 @@ export function ArticleActionsMenu(props: ArticleActionsMenuProps): JSX.Element m: '0px', }} > - } - css={{ m: '0px', p: '0px' }} > - + - } - css={{ m: '0px', p: '0px' }} > - {/* */} - + - {/* - {}} /> + { + console.log('changed line spacing') + }} /> - */} + ) } From 1427ffc783d531f32045f33c4b38563ac98c336a Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Sun, 10 Apr 2022 10:42:02 -0700 Subject: [PATCH 022/125] Test some smaller sizes for mobile screens --- .../web/components/templates/article/EditLabelsModal.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/web/components/templates/article/EditLabelsModal.tsx b/packages/web/components/templates/article/EditLabelsModal.tsx index ba441f215..b66b951b5 100644 --- a/packages/web/components/templates/article/EditLabelsModal.tsx +++ b/packages/web/components/templates/article/EditLabelsModal.tsx @@ -97,6 +97,7 @@ function Header(props: HeaderProps): JSX.Element {
Date: Sun, 10 Apr 2022 11:13:30 -0700 Subject: [PATCH 023/125] Increase small screen maxheight --- packages/web/components/templates/article/EditLabelsModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/components/templates/article/EditLabelsModal.tsx b/packages/web/components/templates/article/EditLabelsModal.tsx index b66b951b5..c04f7321f 100644 --- a/packages/web/components/templates/article/EditLabelsModal.tsx +++ b/packages/web/components/templates/article/EditLabelsModal.tsx @@ -246,7 +246,7 @@ export function EditLabelsModal(props: EditLabelsModalProps): JSX.Element { maxWidth: '265px', '@mdDown': { maxWidth: '100%', - maxHeight: '280px', + maxHeight: '320px', }, }}>
From 0a4215c57ba18e28775a3845ea16ac77c43ae78b Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Sun, 10 Apr 2022 14:52:30 -0700 Subject: [PATCH 024/125] Handle toggling label state --- .../templates/article/ArticleActionsMenu.tsx | 1 + .../templates/article/EditLabelsModal.tsx | 358 ++++++++++-------- 2 files changed, 194 insertions(+), 165 deletions(-) diff --git a/packages/web/components/templates/article/ArticleActionsMenu.tsx b/packages/web/components/templates/article/ArticleActionsMenu.tsx index a97a4370d..42b8651a5 100644 --- a/packages/web/components/templates/article/ArticleActionsMenu.tsx +++ b/packages/web/components/templates/article/ArticleActionsMenu.tsx @@ -1,5 +1,6 @@ import { Separator } from "@radix-ui/react-separator" import { ArchiveBox, DotsThree, HighlighterCircle, TagSimple, TextAa } from "phosphor-react" +import { useRef } from "react" import { useGetUserPreferences, UserPreferences } from "../../../lib/networking/queries/useGetUserPreferences" import { Button } from "../../elements/Button" import { Dropdown } from "../../elements/DropdownElements" diff --git a/packages/web/components/templates/article/EditLabelsModal.tsx b/packages/web/components/templates/article/EditLabelsModal.tsx index c04f7321f..e9b2172e0 100644 --- a/packages/web/components/templates/article/EditLabelsModal.tsx +++ b/packages/web/components/templates/article/EditLabelsModal.tsx @@ -9,7 +9,7 @@ import { StyledText } from '../../elements/StyledText' import { CrossIcon } from '../../elements/images/CrossIcon' import { styled, theme } from '../../tokens/stitches.config' import { Label, useGetLabelsQuery } from '../../../lib/networking/queries/useGetLabelsQuery' -import { ChangeEvent, useCallback, useRef, useState, useMemo } from 'react' +import { ChangeEvent, useCallback, useRef, useState, useMemo, useEffect } from 'react' import { setLabelsMutation } from '../../../lib/networking/mutations/setLabelsMutation' import { ArticleAttributes } from '../../../lib/networking/queries/useGetArticleQuery' import { LabelChip } from '../../elements/LabelChip' @@ -26,6 +26,8 @@ type EditLabelsModalProps = { type HeaderProps = { filterText: string + focused: boolean + parentRef: React.RefObject setFilterText: (text: string) => void } @@ -44,70 +46,68 @@ const FormInput = styled('input', { const StyledLabel = styled('label', { display: 'flex', justifyContent: 'flex-start', - '&:focus-visible': { - backgroundColor: 'red', + '&:focus': { + bg: '$grayBgActive', + }, + 'input:&:focus-within': { + bg: '$grayBgActive', }, }) +const useToggleLabels = () => { + const [selectedLabels, setSelectedLabels] = useState([]) + + const isSelected = useCallback((label: Label): boolean => { + return selectedLabels.some((other) => { + return other.id === label.id + }) + }, [selectedLabels]) + + const toggleLabel = useCallback((label: Label) => { + if (isSelected(label)) { + setSelectedLabels(selectedLabels.filter((other) => { + return other.id !== label.id + })) + } else { + setSelectedLabels([...selectedLabels, label]) + } + }, [isSelected, selectedLabels]) + + return [isSelected, toggleLabel] +} + function Header(props: HeaderProps): JSX.Element { const inputRef = useRef(null) + useEffect(() => { + if (props.focused && inputRef.current) { + inputRef.current.focus() + } + }, [props.focused]) + return ( - {/* */} - {/* - Apply labels to this page - */} - {/* */} - {/* */} -
{ - // event.preventDefault() - // props.applySearchQuery(searchTerm || '') - inputRef.current?.blur() - }} - > { event.target.select() //setFocused(true) + console.log('input::focused()') }} onBlur={() => { - //setFocused(false) - console.log('blurred') + // setFocused(false) + console.log('input::blurred') }} onKeyDown={(event) => { console.log('keydown', event.key) @@ -134,94 +134,82 @@ function Header(props: HeaderProps): JSX.Element { }, }} /> -
) } -type LabelsListProps = { - // pageId: string - // selectedLabels: Label[] - availableLabels: Label[] -// setSelectedLabels: (labels: Label[]) => void +type LabelListItemProps = { + label: Label + focused: boolean + selected: boolean } -function LabelsList(props: LabelsListProps): JSX.Element { - const isSelected = useCallback((label: Label) => { - // return props.selectedLabels.some((other) => { - // return other.id === label.id - // }) - return false - }, []) +function LabelListItem(props: LabelListItemProps): JSX.Element { + const ref = useRef(null) + const { label, focused, selected } = props + + useEffect(() => { + if (props.focused && ref.current) { + ref.current.focus() + } + }, [props.focused]) return ( - - {props.availableLabels && - props.availableLabels.map((label, idx) => ( - { - // console.log('selected label', label) - // if (props.selectedLabels.includes(label)) { - // props.setSelectedLabels( - // props.selectedLabels.filter((id) => id !== label) - // ) - // } else { - // props.setSelectedLabels([...props.selectedLabels, label]) - // } - // const result = await setLabelsMutation(props.pageId, props.selectedLabels.map((l) => l.id)) - // console.log('result', result) - }} - > - - {isSelected(label) && } - - - - - - {label.name} - - - {isSelected(label) && } - - {/* - - */} - {/* */} - - ))} - ) + { + console.log('toggling label') + }} + > + + + {selected && } + + + + + + {label.name} + + + {selected && } + + + ) } export function EditLabelsModal(props: EditLabelsModalProps): JSX.Element { + const parentRef = useRef(null) const [filterText, setFilterText] = useState('') - const [selectedLabels, setSelectedLabels] = useState([]) const { labels } = useGetLabelsQuery() - const saveAndExit = useCallback(async () => { - // const result = await setLabelsMutation(props.article.id, selectedLabels.map((l) => l.id)) - // console.log('result of setting labels', result) - // // props.onOpenChange(false) - // // props.setLabels(selectedLabels) - }, [props, selectedLabels]) + const [selectedLabels, setSelectedLabels] = useState([]) + + const isSelected = useCallback((label: Label): boolean => { + return selectedLabels.some((other) => { + return other.id === label.id + }) + }, [selectedLabels]) + + const toggleLabel = useCallback((label: Label) => { + if (isSelected(label)) { + setSelectedLabels(selectedLabels.filter((other) => { + return other.id !== label.id + })) + } else { + setSelectedLabels([...selectedLabels, label]) + } + }, [isSelected, selectedLabels]) const filteredLabels = useMemo(() => { if (!labels) { @@ -232,58 +220,98 @@ export function EditLabelsModal(props: EditLabelsModalProps): JSX.Element { }) }, [labels, filterText]) + useEffect(() => { + setFocusedIndex(undefined) + }, [filterText]) + + // Move focus through the labels list on tab or arrow up/down keys + const [focusedIndex, setFocusedIndex] = useState(undefined) + const handleKeyDown = useCallback((event: React.KeyboardEvent) => { + if (event.key === 'ArrowUp') { + event.preventDefault() + if (focusedIndex) { + setFocusedIndex(Math.max(0, focusedIndex - 1)) + } else { + setFocusedIndex(undefined) + } + } + if (event.key === 'ArrowDown' || event.key === 'Tab') { + event.preventDefault() + if (focusedIndex === undefined) { + setFocusedIndex(0) + } else { + setFocusedIndex(Math.min(filteredLabels.length - 1, focusedIndex + 1)) + } + } + if (event.key === 'Enter') { + event.preventDefault() + if (focusedIndex !== undefined) { + const label = filteredLabels[focusedIndex] + if (label) { + toggleLabel(label) + } + } + } + }, [filteredLabels, focusedIndex]) + return ( - // - // - // { - // event.preventDefault() - // }} - // css={{ p: '0', width: '100%', maxWidth: '400px' }} - // > - +
+ + {labels && + labels.map((label, idx) => ( + + ))} + + {filterText && ( + )} + {/* Footer */} + -
- - {filterText && ( - )} - {/* Footer */} - - - Edit labels - - - // - // + 'a:visited': { + color: theme.colors.grayText.toString(), + }, + }} + > + + Edit labels + + ) } \ No newline at end of file From 6dd39d54887089b04b08e38308076935b9c32767 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Sun, 10 Apr 2022 15:14:49 -0700 Subject: [PATCH 025/125] Handle toggling labels with mouse --- .../templates/article/EditLabelsModal.tsx | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/packages/web/components/templates/article/EditLabelsModal.tsx b/packages/web/components/templates/article/EditLabelsModal.tsx index e9b2172e0..af3db7061 100644 --- a/packages/web/components/templates/article/EditLabelsModal.tsx +++ b/packages/web/components/templates/article/EditLabelsModal.tsx @@ -9,7 +9,7 @@ import { StyledText } from '../../elements/StyledText' import { CrossIcon } from '../../elements/images/CrossIcon' import { styled, theme } from '../../tokens/stitches.config' import { Label, useGetLabelsQuery } from '../../../lib/networking/queries/useGetLabelsQuery' -import { ChangeEvent, useCallback, useRef, useState, useMemo, useEffect } from 'react' +import { ChangeEvent, useCallback, useRef, useState, useMemo, useEffect, MouseEventHandler } from 'react' import { setLabelsMutation } from '../../../lib/networking/mutations/setLabelsMutation' import { ArticleAttributes } from '../../../lib/networking/queries/useGetArticleQuery' import { LabelChip } from '../../elements/LabelChip' @@ -49,9 +49,6 @@ const StyledLabel = styled('label', { '&:focus': { bg: '$grayBgActive', }, - 'input:&:focus-within': { - bg: '$grayBgActive', - }, }) const useToggleLabels = () => { @@ -142,6 +139,7 @@ type LabelListItemProps = { label: Label focused: boolean selected: boolean + toggleLabel: (label: Label) => void } function LabelListItem(props: LabelListItemProps): JSX.Element { @@ -164,8 +162,10 @@ function LabelListItem(props: LabelListItemProps): JSX.Element { bg: props.focused ? '$grayBgActive' : 'unset', }} tabIndex={props.focused ? 0 : -1} - onClick={async () => { - console.log('toggling label') + onClick={(event) => { + event.preventDefault() + props.toggleLabel(label) + ref.current?.blur() }} > @@ -260,13 +260,16 @@ export function EditLabelsModal(props: EditLabelsModalProps): JSX.Element { distribution="start" onKeyDown={handleKeyDown} css={{ - p: '0', - maxHeight: '92%', - maxWidth: '265px', - '@mdDown': { - maxWidth: '100%', + p: '0', + width: '265px', maxHeight: '92%', - }, + '@mdDown': { + maxHeight: '92%', + }, + '@smDown': { + maxHeight: '92%', + maxWidth: '92%', + } }}>
- {labels && - labels.map((label, idx) => ( + {filteredLabels && + filteredLabels.map((label, idx) => ( ))} From 92ea5efb9867b7dbf1c1032cbd6d29438397de00 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Sun, 10 Apr 2022 15:43:14 -0700 Subject: [PATCH 026/125] Update naming --- .../templates/article/EditLabelsControl.tsx | 321 ++++++++++++++++++ 1 file changed, 321 insertions(+) create mode 100644 packages/web/components/templates/article/EditLabelsControl.tsx diff --git a/packages/web/components/templates/article/EditLabelsControl.tsx b/packages/web/components/templates/article/EditLabelsControl.tsx new file mode 100644 index 000000000..33d064c3e --- /dev/null +++ b/packages/web/components/templates/article/EditLabelsControl.tsx @@ -0,0 +1,321 @@ +import { + ModalContent, + ModalOverlay, + ModalRoot, +} from '../../elements/ModalPrimitives' +import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives' +import { Button } from '../../elements/Button' +import { StyledText } from '../../elements/StyledText' +import { CrossIcon } from '../../elements/images/CrossIcon' +import { styled, theme } from '../../tokens/stitches.config' +import { Label, useGetLabelsQuery } from '../../../lib/networking/queries/useGetLabelsQuery' +import { ChangeEvent, useCallback, useRef, useState, useMemo, useEffect, MouseEventHandler } from 'react' +import { setLabelsMutation } from '../../../lib/networking/mutations/setLabelsMutation' +import { ArticleAttributes } from '../../../lib/networking/queries/useGetArticleQuery' +import { LabelChip } from '../../elements/LabelChip' +import { Check, Circle, Pen, PencilSimple, PencilSimpleLine, Plus, TagSimple } from 'phosphor-react' +import Link from 'next/link' +import { isTouchScreenDevice } from '../../../lib/deviceType' + +type EditLabelsControlProps = { + // labels: Label[] + // article: ArticleAttributes + // onOpenChange: (open: boolean) => void + // setLabels: (labels: Label[]) => void +} + +type HeaderProps = { + filterText: string + focused: boolean + parentRef: React.RefObject + setFilterText: (text: string) => void +} + +const FormInput = styled('input', { + width: '100%', + fontSize: '16px', + fontFamily: 'inter', + fontWeight: 'normal', + lineHeight: '1.8', + color: '$grayTextContrast', + '&:focus': { + outline: 'none', + }, +}) + +const StyledLabel = styled('label', { + display: 'flex', + justifyContent: 'flex-start', + '&:focus': { + bg: '$grayBgActive', + }, +}) + +const useToggleLabels = () => { + const [selectedLabels, setSelectedLabels] = useState([]) + + const isSelected = useCallback((label: Label): boolean => { + return selectedLabels.some((other) => { + return other.id === label.id + }) + }, [selectedLabels]) + + const toggleLabel = useCallback((label: Label) => { + if (isSelected(label)) { + setSelectedLabels(selectedLabels.filter((other) => { + return other.id !== label.id + })) + } else { + setSelectedLabels([...selectedLabels, label]) + } + }, [isSelected, selectedLabels]) + + return [isSelected, toggleLabel] +} + +function Header(props: HeaderProps): JSX.Element { + const inputRef = useRef(null) + + useEffect(() => { + if (props.focused && inputRef.current) { + inputRef.current.focus() + } + }, [props.focused]) + + return ( + + + { + event.target.select() + //setFocused(true) + console.log('input::focused()') + }} + onBlur={() => { + // setFocused(false) + console.log('input::blurred') + }} + onKeyDown={(event) => { + console.log('keydown', event.key) + }} + onChange={(event) => { + console.log('event', event) + props.setFilterText(event.target.value) + }} + css={{ + border: '1px solid $grayBorder', + borderRadius: '8px', + width: '100%', + bg: 'transparent', + fontSize: '16px', + fontFamily: 'inter', + fontWeight: 'normal', + marginBottom: '2px', + textIndent: '8px', + paddingLeft: '8px', + color: '$grayTextContrast', + '&:focus': { + outline: 'none', + boxShadow: '0px 0px 2px 2px rgba(255, 234, 159, 0.56)', + }, + }} + /> + + ) +} + +type LabelListItemProps = { + label: Label + focused: boolean + selected: boolean + toggleLabel: (label: Label) => void +} + +function LabelListItem(props: LabelListItemProps): JSX.Element { + const ref = useRef(null) + const { label, focused, selected } = props + + useEffect(() => { + if (props.focused && ref.current) { + ref.current.focus() + } + }, [props.focused]) + + return ( + { + event.preventDefault() + props.toggleLabel(label) + ref.current?.blur() + }} + > + + + {selected && } + + + + + + {label.name} + + + {selected && } + + + ) +} + +export function EditLabelsControl(props: EditLabelsControlProps): JSX.Element { + const parentRef = useRef(null) + const [filterText, setFilterText] = useState('') + const { labels } = useGetLabelsQuery() + + const [selectedLabels, setSelectedLabels] = useState([]) + + const isSelected = useCallback((label: Label): boolean => { + return selectedLabels.some((other) => { + return other.id === label.id + }) + }, [selectedLabels]) + + const toggleLabel = useCallback((label: Label) => { + if (isSelected(label)) { + setSelectedLabels(selectedLabels.filter((other) => { + return other.id !== label.id + })) + } else { + setSelectedLabels([...selectedLabels, label]) + } + }, [isSelected, selectedLabels]) + + const filteredLabels = useMemo(() => { + if (!labels) { + return [] + } + return labels.filter((label) => { + return label.name.toLowerCase().includes(filterText.toLowerCase()) + }) + }, [labels, filterText]) + + useEffect(() => { + setFocusedIndex(undefined) + }, [filterText]) + + // Move focus through the labels list on tab or arrow up/down keys + const [focusedIndex, setFocusedIndex] = useState(undefined) + const handleKeyDown = useCallback((event: React.KeyboardEvent) => { + if (event.key === 'ArrowUp') { + event.preventDefault() + if (focusedIndex) { + setFocusedIndex(Math.max(0, focusedIndex - 1)) + } else { + setFocusedIndex(undefined) + } + } + if (event.key === 'ArrowDown' || event.key === 'Tab') { + event.preventDefault() + if (focusedIndex === undefined) { + setFocusedIndex(0) + } else { + setFocusedIndex(Math.min(filteredLabels.length - 1, focusedIndex + 1)) + } + } + if (event.key === 'Enter') { + event.preventDefault() + if (focusedIndex !== undefined) { + const label = filteredLabels[focusedIndex] + if (label) { + toggleLabel(label) + } + } + } + }, [filteredLabels, focusedIndex]) + + return ( + +
+ + {filteredLabels && + filteredLabels.map((label, idx) => ( + + ))} + + {filterText && ( + )} + {/* Footer */} + + + Edit labels + + + ) +} \ No newline at end of file From de72df98282fdb5866fa487ce5e8591215a1c5ee Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Sun, 10 Apr 2022 15:54:51 -0700 Subject: [PATCH 027/125] Naming, update small screen layout --- .../templates/article/ArticleActionsMenu.tsx | 4 +- .../templates/article/EditLabelsControl.tsx | 28 +- .../templates/article/EditLabelsModal.tsx | 321 ------------------ .../web/pages/[username]/[slug]/index.tsx | 2 +- 4 files changed, 7 insertions(+), 348 deletions(-) delete mode 100644 packages/web/components/templates/article/EditLabelsModal.tsx diff --git a/packages/web/components/templates/article/ArticleActionsMenu.tsx b/packages/web/components/templates/article/ArticleActionsMenu.tsx index 42b8651a5..0f115e180 100644 --- a/packages/web/components/templates/article/ArticleActionsMenu.tsx +++ b/packages/web/components/templates/article/ArticleActionsMenu.tsx @@ -6,7 +6,7 @@ import { Button } from "../../elements/Button" import { Dropdown } from "../../elements/DropdownElements" import { Box, SpanBox } from "../../elements/LayoutPrimitives" import { styled, theme } from "../../tokens/stitches.config" -import { EditLabelsModal } from "./EditLabelsModal" +import { EditLabelsControl } from "./EditLabelsControl" import { ReaderSettings } from "./ReaderSettingsModal" export type ArticleActionsMenuLayout = 'horizontal' | 'vertical' @@ -89,7 +89,7 @@ export function ArticleActionsMenu(props: ArticleActionsMenuProps): JSX.Element } > - + )} - {/* Footer */} - - - Edit labels - - - ) -} \ No newline at end of file diff --git a/packages/web/pages/[username]/[slug]/index.tsx b/packages/web/pages/[username]/[slug]/index.tsx index f35f94e8a..0cc663a63 100644 --- a/packages/web/pages/[username]/[slug]/index.tsx +++ b/packages/web/pages/[username]/[slug]/index.tsx @@ -20,7 +20,7 @@ import { articleReadingProgressMutation } from '../../../lib/networking/mutation import { updateHighlightMutation } from '../../../lib/networking/mutations/updateHighlightMutation' import { userPersonalizationMutation } from '../../../lib/networking/mutations/userPersonalizationMutation' import Script from 'next/script' -import { EditLabelsModal } from '../../../components/templates/article/EditLabelsModal' +import { EditLabelsControl } from '../../../components/templates/article/EditLabelsControl' import { Label } from '../../../lib/networking/fragments/labelFragment' import { isVipUser } from '../../../lib/featureFlag' import { styled, theme } from '../../../components/tokens/stitches.config' From e7528cfc439351cbd2ea0040f10871fb0756677b Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Sun, 10 Apr 2022 16:07:25 -0700 Subject: [PATCH 028/125] Attempt to dismiss focus on mobile --- packages/web/components/templates/article/EditLabelsControl.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/components/templates/article/EditLabelsControl.tsx b/packages/web/components/templates/article/EditLabelsControl.tsx index 52f3604a4..7551fc7eb 100644 --- a/packages/web/components/templates/article/EditLabelsControl.tsx +++ b/packages/web/components/templates/article/EditLabelsControl.tsx @@ -92,7 +92,7 @@ function Header(props: HeaderProps): JSX.Element { Date: Sun, 10 Apr 2022 17:22:06 -0700 Subject: [PATCH 029/125] Wire up article actions to have an article instance when hosted in the header --- .../web/components/patterns/PrimaryHeader.tsx | 35 +++++++++---------- .../components/templates/PrimaryLayout.tsx | 3 +- .../templates/article/ArticleActionsMenu.tsx | 14 +++++++- .../web/pages/[username]/[slug]/index.tsx | 29 ++++++++------- 4 files changed, 45 insertions(+), 36 deletions(-) diff --git a/packages/web/components/patterns/PrimaryHeader.tsx b/packages/web/components/patterns/PrimaryHeader.tsx index f96f5138b..57b6cc0ed 100644 --- a/packages/web/components/patterns/PrimaryHeader.tsx +++ b/packages/web/components/patterns/PrimaryHeader.tsx @@ -17,8 +17,6 @@ import { UserBasicData } from '../../lib/networking/queries/useGetViewerQuery' import { setupAnalytics } from '../../lib/analytics' import { Button } from '../elements/Button' import Link from 'next/link' -import { ArchiveBox, ArrowSquareOut, DotsThree, HighlighterCircle, TagSimple, TextAa } from 'phosphor-react' -import { ArticleActionsMenu } from '../templates/article/ArticleActionsMenu' type HeaderProps = { user?: UserBasicData @@ -28,6 +26,7 @@ type HeaderProps = { isFixedPosition: boolean scrollElementRef?: React.RefObject displayFontStepper?: boolean + toolbarControl?: JSX.Element setShowLogoutConfirmation: (showShareModal: boolean) => void setShowKeyboardCommandsModal: (showShareModal: boolean) => void } @@ -56,9 +55,6 @@ export function PrimaryHeader(props: HeaderProps): JSX.Element { (changeset: ScrollOffsetChangeset) => { const isScrolledBeyondMinThreshold = changeset.current.y >= 50 const isScrollingDown = changeset.current.y > changeset.previous.y - - // setIsScrolled(isScrolledBeyondMinThreshold) - // setShowHeader(!(isScrollingDown && isScrolledBeyondMinThreshold)) }, 0 ) @@ -129,6 +125,7 @@ export function PrimaryHeader(props: HeaderProps): JSX.Element { isDisplayingShadow={isScrolled} isVisible={true} isFixedPosition={true} + toolbarControl={props.toolbarControl} displayFontStepper={props.displayFontStepper} /> @@ -145,6 +142,7 @@ type NavHeaderProps = { isVisible?: boolean isFixedPosition: boolean displayFontStepper?: boolean + toolbarControl?: JSX.Element } function NavHeader(props: NavHeaderProps): JSX.Element { @@ -185,20 +183,19 @@ function NavHeader(props: NavHeaderProps): JSX.Element { - - { - console.log('action handler') - }} /> - + {props.toolbarControl && ( + + {props.toolbarControl} + + )} {props.username ? ( displayFontStepper?: boolean + headerToolbarControl?: JSX.Element } export function PrimaryLayout(props: PrimaryLayoutProps): JSX.Element { @@ -76,6 +76,7 @@ export function PrimaryLayout(props: PrimaryLayoutProps): JSX.Element { userInitials={viewerData?.me?.name.charAt(0) ?? ''} profileImageURL={viewerData?.me?.profile.pictureUrl} isFixedPosition={true} + toolbarControl={props.headerToolbarControl} scrollElementRef={props.scrollElementRef} displayFontStepper={props.displayFontStepper} setShowLogoutConfirmation={setShowLogoutConfirmation} diff --git a/packages/web/components/templates/article/ArticleActionsMenu.tsx b/packages/web/components/templates/article/ArticleActionsMenu.tsx index 0f115e180..e28a41dca 100644 --- a/packages/web/components/templates/article/ArticleActionsMenu.tsx +++ b/packages/web/components/templates/article/ArticleActionsMenu.tsx @@ -1,6 +1,7 @@ import { Separator } from "@radix-ui/react-separator" import { ArchiveBox, DotsThree, HighlighterCircle, TagSimple, TextAa } from "phosphor-react" import { useRef } from "react" +import { ArticleAttributes } from "../../../lib/networking/queries/useGetArticleQuery" import { useGetUserPreferences, UserPreferences } from "../../../lib/networking/queries/useGetUserPreferences" import { Button } from "../../elements/Button" import { Dropdown } from "../../elements/DropdownElements" @@ -12,6 +13,7 @@ import { ReaderSettings } from "./ReaderSettingsModal" export type ArticleActionsMenuLayout = 'horizontal' | 'vertical' type ArticleActionsMenuProps = { + article: ArticleAttributes layout: ArticleActionsMenuLayout articleActionHandler: (action: string, arg?: number) => void } @@ -39,7 +41,7 @@ type ActionDropdownProps = { const ActionDropdown = (props: ActionDropdownProps): JSX.Element => { return +{/* + */} - { - console.log('range changed', value) + { + setMarginWidth(value) + props.articleActionHandler('setMarginWidth', value) }} /> { setMarginWidth(value) props.articleActionHandler('setMarginWidth', value) }} /> - + - { - console.log('changed line spacing') + { + setLineHeight(value) + props.articleActionHandler('setLineHeight', value) }} /> - { + { setLineHeight(value) props.articleActionHandler('setLineHeight', value) }} /> diff --git a/packages/web/pages/[username]/[slug]/index.tsx b/packages/web/pages/[username]/[slug]/index.tsx index cb820d9d8..4bd1c9559 100644 --- a/packages/web/pages/[username]/[slug]/index.tsx +++ b/packages/web/pages/[username]/[slug]/index.tsx @@ -38,14 +38,13 @@ export default function Home(): JSX.Element { const { slug } = router.query const [showHighlightsModal, setShowHighlightsModal] = useState(false) - // Populate data cache const { viewerData } = useGetViewerQuery() - const { preferencesData } = useGetUserPreferences() const [fontSize, setFontSize] = useState(preferencesData?.fontSize ?? 20) const [marginWidth, setMarginWidth] = useState(preferencesData?.margin ?? 360) + const [lineHeight, setLineHeight] = useState(preferencesData?.lineHeight ?? 150) - const { articleData, mutate } = useGetArticleQuery({ + const { articleData } = useGetArticleQuery({ username: router.query.username as string, slug: router.query.slug as string, includeFriendsHighlights: false, @@ -103,7 +102,7 @@ export default function Home(): JSX.Element { break case 'setMarginWidth': { const value = Number(arg) - if (value > 200 && value < 560) { + if (value >= 200 && value <= 560) { updateMarginWidth(value) } break @@ -114,6 +113,13 @@ export default function Home(): JSX.Element { case 'decrementMarginWidth': updateMarginWidth(Math.max(marginWidth - 45, 200)) break + case 'setLineHeight': { + const value = Number(arg) + if (value >= 100 && value <= 300) { + setLineHeight(value) + } + break + } } }; @@ -193,6 +199,7 @@ export default function Home(): JSX.Element { highlightsBaseURL={`${webBaseURL}/${viewerData.me?.profile?.username}/${slug}/highlights`} fontSize={fontSize} margin={marginWidth} + lineHeight={lineHeight} labels={labels} articleMutations={{ createHighlightMutation, From 9a627c225e229273e42ae223f1ba89e8e3a3a74e Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 11 Apr 2022 08:58:48 -0700 Subject: [PATCH 040/125] Add tooltips to article actions --- packages/web/components/elements/Tooltip.tsx | 18 +++++++- .../templates/article/ArticleActionsMenu.tsx | 43 +++++++++++++------ 2 files changed, 45 insertions(+), 16 deletions(-) diff --git a/packages/web/components/elements/Tooltip.tsx b/packages/web/components/elements/Tooltip.tsx index 241c20e8e..b1139061a 100644 --- a/packages/web/components/elements/Tooltip.tsx +++ b/packages/web/components/elements/Tooltip.tsx @@ -62,6 +62,15 @@ type TooltipWrappedProps = { style?: TooltipPrimitive.TooltipContentProps['style'] } +const DefaultTooltipStyle = { + backgroundColor: '#F9D354', + color: '#0A0806', +} + +const DefaultArrowStyle = { + fill: '#F9D354' +} + export const TooltipWrapped: FC = ({ children, active, @@ -73,9 +82,14 @@ export const TooltipWrapped: FC = ({ return ( {children} - + {tooltipContent} - + ) diff --git a/packages/web/components/templates/article/ArticleActionsMenu.tsx b/packages/web/components/templates/article/ArticleActionsMenu.tsx index 1a8762f84..abcec99a3 100644 --- a/packages/web/components/templates/article/ArticleActionsMenu.tsx +++ b/packages/web/components/templates/article/ArticleActionsMenu.tsx @@ -4,7 +4,8 @@ import { ArticleAttributes } from "../../../lib/networking/queries/useGetArticle import { useGetUserPreferences } from "../../../lib/networking/queries/useGetUserPreferences" import { Button } from "../../elements/Button" import { Dropdown } from "../../elements/DropdownElements" -import { Box, SpanBox } from "../../elements/LayoutPrimitives" +import { Box } from "../../elements/LayoutPrimitives" +import { TooltipWrapped } from "../../elements/Tooltip" import { styled, theme } from "../../tokens/stitches.config" import { EditLabelsControl } from "./EditLabelsControl" import { ReaderSettings } from "./ReaderSettingsModal" @@ -59,22 +60,23 @@ export function ArticleActionsMenu(props: ArticleActionsMenuProps): JSX.Element - - + + + } > @@ -85,9 +87,12 @@ export function ArticleActionsMenu(props: ArticleActionsMenuProps): JSX.Element - - + + + } > */} - + + + {/* From ebe22f3df8959726e6249ad9f8a3916fb8c70215 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 11 Apr 2022 09:10:38 -0700 Subject: [PATCH 041/125] Wrap the button so style isnt changed --- .../templates/article/ArticleActionsMenu.tsx | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/web/components/templates/article/ArticleActionsMenu.tsx b/packages/web/components/templates/article/ArticleActionsMenu.tsx index abcec99a3..f8fbb8c22 100644 --- a/packages/web/components/templates/article/ArticleActionsMenu.tsx +++ b/packages/web/components/templates/article/ArticleActionsMenu.tsx @@ -111,14 +111,15 @@ export function ArticleActionsMenu(props: ArticleActionsMenuProps): JSX.Element */} - - - + + + { setLineHeight(value) props.articleActionHandler('setLineHeight', value) }} /> - + + ) diff --git a/packages/web/pages/[username]/[slug]/index.tsx b/packages/web/pages/[username]/[slug]/index.tsx index 4bd1c9559..7eb2c194e 100644 --- a/packages/web/pages/[username]/[slug]/index.tsx +++ b/packages/web/pages/[username]/[slug]/index.tsx @@ -120,6 +120,12 @@ export default function Home(): JSX.Element { } break } + case 'resetReaderSettings': { + updateFontSize(20) + updateMarginWidth(360) + setLineHeight(150) + break + } } }; From c280b2cf01bf83277e3a1acd4a2bbcd9761d68a1 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 11 Apr 2022 11:13:58 -0700 Subject: [PATCH 045/125] Remove items from cache when archived from the article actions menu --- .../networking/queries/useGetArticleQuery.tsx | 64 ++++++++++++++----- .../queries/useGetLibraryItemsQuery.tsx | 2 +- .../web/pages/[username]/[slug]/index.tsx | 17 ++++- 3 files changed, 63 insertions(+), 20 deletions(-) diff --git a/packages/web/lib/networking/queries/useGetArticleQuery.tsx b/packages/web/lib/networking/queries/useGetArticleQuery.tsx index a75f5f72a..0d7422db2 100644 --- a/packages/web/lib/networking/queries/useGetArticleQuery.tsx +++ b/packages/web/lib/networking/queries/useGetArticleQuery.tsx @@ -1,11 +1,11 @@ import { gql } from 'graphql-request' -import useSWRImmutable from 'swr' -import useSWR from 'swr' +import useSWRImmutable, { Cache } from 'swr' import { makeGqlFetcher, RequestContext, ssrFetcher } from '../networkHelpers' import { articleFragment, ContentReader } from '../fragments/articleFragment' import { Highlight, highlightFragment } from '../fragments/highlightFragment' -import { KeyedMutator, ScopedMutator } from 'swr/dist/types' +import { ScopedMutator } from 'swr/dist/types' import { Label, labelFragment } from '../fragments/labelFragment' +import { LibraryItems } from './useGetLibraryItemsQuery' type ArticleQueryInput = { username?: string @@ -17,7 +17,6 @@ type ArticleQueryOutput = { articleData?: ArticleData articleFetchError: unknown isLoading: boolean - mutate: KeyedMutator } type ArticleData = { @@ -79,16 +78,7 @@ const query = gql` ${highlightFragment} ${labelFragment} ` -export const cacheArticle = ( - mutate: ScopedMutator, - username: string, - article: ArticleAttributes, - includeFriendsHighlights = false -) => { - mutate([query, username, article.slug, includeFriendsHighlights], { - article: { article: { ...article, cached: true } }, - }) -} + export function useGetArticleQuery({ username, @@ -101,7 +91,7 @@ export function useGetArticleQuery({ includeFriendsHighlights, } - const { data, error, mutate } = useSWR( + const { data, error, mutate } = useSWRImmutable( slug ? [query, username, slug, includeFriendsHighlights] : null, makeGqlFetcher(variables) ) @@ -118,7 +108,6 @@ export function useGetArticleQuery({ } return { - mutate, articleData: resultData, articleFetchError: resultError as unknown, isLoading: !error && !data, @@ -136,3 +125,46 @@ export async function articleQuery( return Promise.reject() } + +export const cacheArticle = ( + mutate: ScopedMutator, + username: string, + article: ArticleAttributes, + includeFriendsHighlights = false +) => { + mutate([query, username, article.slug, includeFriendsHighlights], { + article: { article: { ...article, cached: true } }, + }) +} + + +export const removeItemFromCache = ( + cache: Cache, + mutate: ScopedMutator, + itemId: string, +) => { + try { + const mappedCache = cache as Map + mappedCache.forEach((value: any, key) => { + if (typeof value == 'object' && 'articles' in value) { + const articles = value.articles as LibraryItems + const idx = articles.edges.findIndex((edge) => edge.node.id == itemId) + if (idx > -1) { + value.articles.edges.splice(idx, 1) + mutate(key, value, false) + } + } + }) + + mappedCache.forEach((value: any, key) => { + if (Array.isArray(value)) { + const idx = value.findIndex((item) => 'articles' in item) + if (idx > -1) { + mutate(key, value, false) + } + } + }) + } catch (error) { + console.log('error removing item from cache', error) + } +} diff --git a/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx b/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx index 3a761076e..ad02f5e96 100644 --- a/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx +++ b/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx @@ -40,7 +40,7 @@ export type LibraryItemsData = { articles: LibraryItems } -type LibraryItems = { +export type LibraryItems = { edges: LibraryItem[] pageInfo: PageInfo errorCodes?: string[] diff --git a/packages/web/pages/[username]/[slug]/index.tsx b/packages/web/pages/[username]/[slug]/index.tsx index 7eb2c194e..dbfbb69d6 100644 --- a/packages/web/pages/[username]/[slug]/index.tsx +++ b/packages/web/pages/[username]/[slug]/index.tsx @@ -1,7 +1,7 @@ import { PrimaryLayout } from '../../../components/templates/PrimaryLayout' import { LoadingView } from '../../../components/patterns/LoadingView' import { useGetViewerQuery } from '../../../lib/networking/queries/useGetViewerQuery' -import { useGetArticleQuery } from '../../../lib/networking/queries/useGetArticleQuery' +import { removeItemFromCache, useGetArticleQuery } from '../../../lib/networking/queries/useGetArticleQuery' import { useRouter } from 'next/router' import { VStack } from './../../../components/elements/LayoutPrimitives' import { ArticleContainer } from './../../../components/templates/article/ArticleContainer' @@ -25,6 +25,8 @@ import { ArticleActionsMenu } from '../../../components/templates/article/Articl import { HighlightsModal } from '../../../components/templates/article/HighlightsModal' import { setLinkArchivedMutation } from '../../../lib/networking/mutations/setLinkArchivedMutation' import { Label } from '../../../lib/networking/fragments/labelFragment' +import { useSWRConfig } from 'swr' +import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers' const PdfArticleContainerNoSSR = dynamic( @@ -34,6 +36,7 @@ const PdfArticleContainerNoSSR = dynamic( export default function Home(): JSX.Element { const router = useRouter() + const { cache, mutate } = useSWRConfig() const scrollRef = useRef(null) const { slug } = router.query const [showHighlightsModal, setShowHighlightsModal] = useState(false) @@ -73,12 +76,20 @@ export default function Home(): JSX.Element { switch (action) { case 'archive': if (article) { + removeItemFromCache(cache, mutate, article.id) + await setLinkArchivedMutation({ linkId: article.id, archived: true, + }).then((res) => { + if (res) { + showSuccessToast('Link archived', { position: 'bottom-right' }) + } else { + // todo: + showErrorToast('Error archiving link', { position: 'bottom-right' }) + } }) - // TODO: merge from article actions PR - // removeItemFromCache(cache, mutate, props.article.id) + router.push(`/home`) } break From 34d892a9a26748702461b44ed505ce2fa223012a Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 11 Apr 2022 11:16:04 -0700 Subject: [PATCH 046/125] Update comment --- packages/web/pages/[username]/[slug]/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/pages/[username]/[slug]/index.tsx b/packages/web/pages/[username]/[slug]/index.tsx index dbfbb69d6..b4d84cfda 100644 --- a/packages/web/pages/[username]/[slug]/index.tsx +++ b/packages/web/pages/[username]/[slug]/index.tsx @@ -85,7 +85,7 @@ export default function Home(): JSX.Element { if (res) { showSuccessToast('Link archived', { position: 'bottom-right' }) } else { - // todo: + // todo: revalidate or put back in cache? showErrorToast('Error archiving link', { position: 'bottom-right' }) } }) From 110a503927eb80bec2f84e7c85daa6df3898cd8c Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 11 Apr 2022 11:31:28 -0700 Subject: [PATCH 047/125] Implement the Create new Label button --- .../templates/article/EditLabelsControl.tsx | 21 ++++++++++++------- .../mutations/createLabelMutation.ts | 14 +++++++++++-- .../labels/labelColorObjects.tsx | 6 ++++++ 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/packages/web/components/templates/article/EditLabelsControl.tsx b/packages/web/components/templates/article/EditLabelsControl.tsx index a4910b2ca..ed70506a6 100644 --- a/packages/web/components/templates/article/EditLabelsControl.tsx +++ b/packages/web/components/templates/article/EditLabelsControl.tsx @@ -12,6 +12,9 @@ import { Check, Circle, PencilSimple, Plus } from 'phosphor-react' import { isTouchScreenDevice } from '../../../lib/deviceType' import { setLabelsMutation } from '../../../lib/networking/mutations/setLabelsMutation' +import { createLabelMutation } from '../../../lib/networking/mutations/createLabelMutation' +import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers' +import { randomLabelColorHex } from '../../../utils/settings-page/labels/labelColorObjects' type EditLabelsControlProps = { article: ArticleAttributes @@ -194,8 +197,6 @@ export function EditLabelsControl(props: EditLabelsControlProps): JSX.Element { ) props.article.labels = result props.articleActionHandler('refreshLabels', result) - - console.log('refreshing article with labels', props.article.labels) }, [isSelected, selectedLabels]) const filteredLabels = useMemo(() => { @@ -276,17 +277,23 @@ export function EditLabelsControl(props: EditLabelsControlProps): JSX.Element { )} - {/* Footer */} + + )} + { + const colorHexes = Object.keys(labelColorObjects).slice(0, -1) + const randomColorHex = colorHexes[Math.floor(Math.random() * colorHexes.length)] + return randomColorHex +} \ No newline at end of file From 96ba44df6e45a11fe90710c58b14bcd1f39cedf8 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 11 Apr 2022 11:53:43 -0700 Subject: [PATCH 048/125] Remove unused ref prop --- .../web/components/templates/article/EditLabelsControl.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/web/components/templates/article/EditLabelsControl.tsx b/packages/web/components/templates/article/EditLabelsControl.tsx index ed70506a6..1771b1fb8 100644 --- a/packages/web/components/templates/article/EditLabelsControl.tsx +++ b/packages/web/components/templates/article/EditLabelsControl.tsx @@ -24,7 +24,6 @@ type EditLabelsControlProps = { type HeaderProps = { filterText: string focused: boolean - parentRef: React.RefObject setFilterText: (text: string) => void } @@ -169,7 +168,6 @@ function LabelListItem(props: LabelListItemProps): JSX.Element { } export function EditLabelsControl(props: EditLabelsControlProps): JSX.Element { - const parentRef = useRef(null) const [filterText, setFilterText] = useState('') const { labels } = useGetLabelsQuery() const [selectedLabels, setSelectedLabels] = useState(props.article.labels || []) @@ -244,7 +242,6 @@ export function EditLabelsControl(props: EditLabelsControlProps): JSX.Element { return (
From 4888c75b9afca8b528afc02f81e162ca9efa7d75 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 11 Apr 2022 11:59:33 -0700 Subject: [PATCH 049/125] Reset focused index when search bar gets focus This is needed if someone combines keyboard navigation and using the mouse to select the textbox --- .../templates/article/EditLabelsControl.tsx | 33 +++++-------------- 1 file changed, 9 insertions(+), 24 deletions(-) diff --git a/packages/web/components/templates/article/EditLabelsControl.tsx b/packages/web/components/templates/article/EditLabelsControl.tsx index 1771b1fb8..5b034a14f 100644 --- a/packages/web/components/templates/article/EditLabelsControl.tsx +++ b/packages/web/components/templates/article/EditLabelsControl.tsx @@ -24,6 +24,7 @@ type EditLabelsControlProps = { type HeaderProps = { filterText: string focused: boolean + resetFocusedIndex: () => void setFilterText: (text: string) => void } @@ -47,28 +48,6 @@ const StyledLabel = styled('label', { }, }) -const useToggleLabels = () => { - const [selectedLabels, setSelectedLabels] = useState([]) - - const isSelected = useCallback((label: Label): boolean => { - return selectedLabels.some((other) => { - return other.id === label.id - }) - }, [selectedLabels]) - - const toggleLabel = useCallback((label: Label) => { - if (isSelected(label)) { - setSelectedLabels(selectedLabels.filter((other) => { - return other.id !== label.id - })) - } else { - setSelectedLabels([...selectedLabels, label]) - } - }, [isSelected, selectedLabels]) - - return [isSelected, toggleLabel] -} - function Header(props: HeaderProps): JSX.Element { const inputRef = useRef(null) @@ -95,6 +74,9 @@ function Header(props: HeaderProps): JSX.Element { onChange={(event) => { props.setFilterText(event.target.value) }} + onFocus={() => { + props.resetFocusedIndex() + }} css={{ border: '1px solid $grayBorder', borderRadius: '8px', @@ -213,6 +195,7 @@ export function EditLabelsControl(props: EditLabelsControlProps): JSX.Element { // Move focus through the labels list on tab or arrow up/down keys const [focusedIndex, setFocusedIndex] = useState(undefined) const handleKeyDown = useCallback((event: React.KeyboardEvent) => { + const maxIndex = filteredLabels.length + 2 if (event.key === 'ArrowUp') { event.preventDefault() if (focusedIndex) { @@ -226,8 +209,9 @@ export function EditLabelsControl(props: EditLabelsControlProps): JSX.Element { if (focusedIndex === undefined) { setFocusedIndex(0) } else { - setFocusedIndex(Math.min(filteredLabels.length - 1, focusedIndex + 1)) + setFocusedIndex(Math.min(maxIndex, focusedIndex + 1)) } + console.log('focusedIndex', focusedIndex) } if (event.key === 'Enter') { event.preventDefault() @@ -255,6 +239,7 @@ export function EditLabelsControl(props: EditLabelsControlProps): JSX.Element { }}>
setFocusedIndex(undefined)} setFilterText={setFilterText} filterText={filterText} /> @@ -290,7 +275,7 @@ export function EditLabelsControl(props: EditLabelsControlProps): JSX.Element { )} - Date: Mon, 11 Apr 2022 13:05:26 -0700 Subject: [PATCH 050/125] Allow selection of the bottom edit labels footer --- .../templates/article/EditLabelsControl.tsx | 51 ++++++++++++------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/packages/web/components/templates/article/EditLabelsControl.tsx b/packages/web/components/templates/article/EditLabelsControl.tsx index 5b034a14f..d4a09e38d 100644 --- a/packages/web/components/templates/article/EditLabelsControl.tsx +++ b/packages/web/components/templates/article/EditLabelsControl.tsx @@ -43,9 +43,6 @@ const FormInput = styled('input', { const StyledLabel = styled('label', { display: 'flex', justifyContent: 'flex-start', - '&:focus': { - bg: '$grayBgActive', - }, }) function Header(props: HeaderProps): JSX.Element { @@ -149,6 +146,39 @@ function LabelListItem(props: LabelListItemProps): JSX.Element { ) } +type EditLabelsButtonFooterProps = { + focused: boolean +} + +function EditLabelsButtonFooter(props: EditLabelsButtonFooterProps): JSX.Element { + const ref = useRef(null) + + useEffect(() => { + if (props.focused && ref.current) { + ref.current.focus() + } + }, [props.focused]) + + return ( + + + Edit labels + + ) +} + export function EditLabelsControl(props: EditLabelsControlProps): JSX.Element { const [filterText, setFilterText] = useState('') const { labels } = useGetLabelsQuery() @@ -211,7 +241,6 @@ export function EditLabelsControl(props: EditLabelsControlProps): JSX.Element { } else { setFocusedIndex(Math.min(maxIndex, focusedIndex + 1)) } - console.log('focusedIndex', focusedIndex) } if (event.key === 'Enter') { event.preventDefault() @@ -275,19 +304,7 @@ export function EditLabelsControl(props: EditLabelsControlProps): JSX.Element { )} - - - Edit labels - + ) } \ No newline at end of file From 454f379e700670b64620557d3543d3a34ee110fe Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 11 Apr 2022 13:27:43 -0700 Subject: [PATCH 051/125] Navigate via keyboard to edit labels page --- .../templates/article/EditLabelsControl.tsx | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/web/components/templates/article/EditLabelsControl.tsx b/packages/web/components/templates/article/EditLabelsControl.tsx index d4a09e38d..d0bfd9f31 100644 --- a/packages/web/components/templates/article/EditLabelsControl.tsx +++ b/packages/web/components/templates/article/EditLabelsControl.tsx @@ -15,6 +15,7 @@ import { setLabelsMutation } from '../../../lib/networking/mutations/setLabelsMu import { createLabelMutation } from '../../../lib/networking/mutations/createLabelMutation' import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers' import { randomLabelColorHex } from '../../../utils/settings-page/labels/labelColorObjects' +import Router, { useRouter } from 'next/router' type EditLabelsControlProps = { article: ArticleAttributes @@ -180,6 +181,7 @@ function EditLabelsButtonFooter(props: EditLabelsButtonFooterProps): JSX.Element } export function EditLabelsControl(props: EditLabelsControlProps): JSX.Element { + const router = useRouter() const [filterText, setFilterText] = useState('') const { labels } = useGetLabelsQuery() const [selectedLabels, setSelectedLabels] = useState(props.article.labels || []) @@ -207,7 +209,7 @@ export function EditLabelsControl(props: EditLabelsControlProps): JSX.Element { ) props.article.labels = result props.articleActionHandler('refreshLabels', result) - }, [isSelected, selectedLabels]) + }, [isSelected, selectedLabels, setSelectedLabels]) const filteredLabels = useMemo(() => { if (!labels) { @@ -244,6 +246,10 @@ export function EditLabelsControl(props: EditLabelsControlProps): JSX.Element { } if (event.key === 'Enter') { event.preventDefault() + if (focusedIndex === maxIndex) { + router.push('/settings/labels') + return + } if (focusedIndex !== undefined) { const label = filteredLabels[focusedIndex] if (label) { @@ -251,7 +257,7 @@ export function EditLabelsControl(props: EditLabelsControlProps): JSX.Element { } } } - }, [filteredLabels, focusedIndex]) + }, [filteredLabels, focusedIndex, isSelected, selectedLabels, setSelectedLabels]) return ( {filterText && ( )} From 113101fd6380ffb064eae991b9b0ef33cfb37193 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 11 Apr 2022 13:58:38 -0700 Subject: [PATCH 053/125] Handle creating labels via keyboard --- .../templates/article/EditLabelsControl.tsx | 41 +++++++++++-------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/packages/web/components/templates/article/EditLabelsControl.tsx b/packages/web/components/templates/article/EditLabelsControl.tsx index edb5ea129..f0b403c7e 100644 --- a/packages/web/components/templates/article/EditLabelsControl.tsx +++ b/packages/web/components/templates/article/EditLabelsControl.tsx @@ -183,9 +183,13 @@ function EditLabelsButtonFooter(props: EditLabelsButtonFooterProps): JSX.Element export function EditLabelsControl(props: EditLabelsControlProps): JSX.Element { const router = useRouter() const [filterText, setFilterText] = useState('') - const { labels } = useGetLabelsQuery() + const { labels, revalidate } = useGetLabelsQuery() const [selectedLabels, setSelectedLabels] = useState(props.article.labels || []) + useEffect(() => { + setFocusedIndex(undefined) + }, [filterText]) + const isSelected = useCallback((label: Label): boolean => { return selectedLabels.some((other) => { return other.id === label.id @@ -207,8 +211,11 @@ export function EditLabelsControl(props: EditLabelsControlProps): JSX.Element { props.article.linkId, newSelectedLabels.map((label) => label.id) ) + props.article.labels = result props.articleActionHandler('refreshLabels', result) + + revalidate() }, [isSelected, selectedLabels, setSelectedLabels]) const filteredLabels = useMemo(() => { @@ -220,13 +227,9 @@ export function EditLabelsControl(props: EditLabelsControlProps): JSX.Element { }) }, [labels, filterText]) - useEffect(() => { - setFocusedIndex(undefined) - }, [filterText]) - // Move focus through the labels list on tab or arrow up/down keys const [focusedIndex, setFocusedIndex] = useState(undefined) - const handleKeyDown = useCallback((event: React.KeyboardEvent) => { + const handleKeyDown = useCallback(async (event: React.KeyboardEvent) => { const maxIndex = filteredLabels.length + 1 if (event.key === 'ArrowUp') { event.preventDefault() @@ -264,6 +267,10 @@ export function EditLabelsControl(props: EditLabelsControlProps): JSX.Element { router.push('/settings/labels') return } + if (focusedIndex === maxIndex - 1) { + await createLabelFromFilterText() + return + } if (focusedIndex !== undefined) { const label = filteredLabels[focusedIndex] if (label) { @@ -271,7 +278,17 @@ export function EditLabelsControl(props: EditLabelsControlProps): JSX.Element { } } } - }, [filteredLabels, focusedIndex, isSelected, selectedLabels, setSelectedLabels]) + }, [filterText, filteredLabels, focusedIndex, isSelected, selectedLabels, setSelectedLabels]) + + const createLabelFromFilterText = useCallback(async () => { + const label = await createLabelMutation(filterText, randomLabelColorHex(), '') + if (label) { + showSuccessToast(`Created label ${label.name}`, { position: 'bottom-right' }) + toggleLabel(label) + } else { + showErrorToast('Failed to create label', { position: 'bottom-right' }) + } + }, [filterText, selectedLabels, setSelectedLabels, toggleLabel]) return ( { - const label = await createLabelMutation(filterText, randomLabelColorHex(), '') - if (label) { - showSuccessToast(`Created label ${label.name}`, { position: 'bottom-right' }) - toggleLabel(label) - } else { - showErrorToast('Failed to create label', { position: 'bottom-right' }) - } - }} + onClick={createLabelFromFilterText} > From aa9797f4d44b7d30d6be7e57865d713029586f41 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 11 Apr 2022 14:06:11 -0700 Subject: [PATCH 054/125] Dont reserve label space if there are no labels --- packages/web/components/templates/article/ArticleContainer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/components/templates/article/ArticleContainer.tsx b/packages/web/components/templates/article/ArticleContainer.tsx index cb4a31003..74fda11f6 100644 --- a/packages/web/components/templates/article/ArticleContainer.tsx +++ b/packages/web/components/templates/article/ArticleContainer.tsx @@ -148,7 +148,7 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element { href={props.article.url} /> {props.labels ? ( - + {props.labels?.map((label) => )} From 8d5425bc536cac21ce77719144405ac442ada68a Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 11 Apr 2022 14:45:29 -0700 Subject: [PATCH 055/125] Show a success message when reader settings are reset --- .../web/components/templates/article/ReaderSettingsModal.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/web/components/templates/article/ReaderSettingsModal.tsx b/packages/web/components/templates/article/ReaderSettingsModal.tsx index 0923b7e09..e8a312370 100644 --- a/packages/web/components/templates/article/ReaderSettingsModal.tsx +++ b/packages/web/components/templates/article/ReaderSettingsModal.tsx @@ -19,6 +19,7 @@ import { AlignCenterHorizontalSimple, ArrowsInLineHorizontal, ArrowsOutLineHoriz import { AIcon } from '../../elements/images/AIcon' import { TickedRangeSlider } from '../../elements/TickedRangeSlider' import { UserPreferences } from '../../../lib/networking/queries/useGetUserPreferences' +import { showSuccessToast } from '../../../lib/toastHelpers' type ReaderSettingsProps = { userPreferences?: UserPreferences @@ -124,6 +125,7 @@ export function ReaderSettings(props: ReaderSettingsProps): JSX.Element { setMarginWidth(360) setLineHeight(150) props.articleActionHandler('resetReaderSettings') + showSuccessToast('Display settings reset', { position: 'bottom-right' }) }} > From a6566e4a9b152d7ae67f5ce820ccfb00a2e33ee6 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 11 Apr 2022 14:46:02 -0700 Subject: [PATCH 056/125] Rename notes sidebar to highlights modal --- .../components/templates/article/ArticleContainer.tsx | 11 +++++++---- .../templates/article/ArticleHeaderToolbar.tsx | 4 ++-- .../components/templates/article/HighlightsLayer.tsx | 8 ++++---- packages/web/pages/[username]/[slug]/index.tsx | 11 ++--------- 4 files changed, 15 insertions(+), 19 deletions(-) diff --git a/packages/web/components/templates/article/ArticleContainer.tsx b/packages/web/components/templates/article/ArticleContainer.tsx index 74fda11f6..6d318bdbb 100644 --- a/packages/web/components/templates/article/ArticleContainer.tsx +++ b/packages/web/components/templates/article/ArticleContainer.tsx @@ -15,6 +15,7 @@ import { updateThemeLocally } from '../../../lib/themeUpdater' import { ArticleMutations } from '../../../lib/articleActions' import { LabelChip } from '../../elements/LabelChip' import { Label } from '../../../lib/networking/fragments/labelFragment' +import { HighlightsModal } from './HighlightsModal' type ArticleContainerProps = { article: ArticleAttributes @@ -28,11 +29,13 @@ type ArticleContainerProps = { fontSize?: number fontFamily?: string lineHeight?: number + showHighlightsModal?: boolean + setShowHighlightsModal?: (show: boolean) => void } export function ArticleContainer(props: ArticleContainerProps): JSX.Element { const [showShareModal, setShowShareModal] = useState(false) - const [showNotesSidebar, setShowNotesSidebar] = useState(false) + const [showHighlightsModal, setShowHighlightsModal] = useState(false) const [showReportIssuesModal, setShowReportIssuesModal] = useState(false) const [fontSize, setFontSize] = useState(props.fontSize ?? 20) @@ -157,7 +160,7 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element { 0} /> @@ -193,9 +196,9 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element { articleId={props.article.id} isAppleAppEmbed={props.isAppleAppEmbed} highlightBarDisabled={props.highlightBarDisabled} - showNotesSidebar={showNotesSidebar} + showHighlightsModal={showHighlightsModal} highlightsBaseURL={props.highlightsBaseURL} - setShowNotesSidebar={setShowNotesSidebar} + setShowHighlightsModal={setShowHighlightsModal} articleMutations={props.articleMutations} /> {showReportIssuesModal ? ( diff --git a/packages/web/components/templates/article/ArticleHeaderToolbar.tsx b/packages/web/components/templates/article/ArticleHeaderToolbar.tsx index c870a7682..ca2edb16d 100644 --- a/packages/web/components/templates/article/ArticleHeaderToolbar.tsx +++ b/packages/web/components/templates/article/ArticleHeaderToolbar.tsx @@ -14,7 +14,7 @@ type ArticleHeaderToolbarProps = { articleTitle: string articleShareURL: string hasHighlights: boolean - setShowNotesSidebar: (showNotesSidebar: boolean) => void + setShowHighlightsModal: (showHighlightsModal: boolean) => void setShowShareArticleModal: (showShareModal: boolean) => void } @@ -46,7 +46,7 @@ export function ArticleHeaderToolbar( return ( {props.hasHighlights && ( - From f0a725b7efc8e5f886113a15cd5f6cc0e3afb08e Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 11 Apr 2022 15:20:07 -0700 Subject: [PATCH 059/125] Dont let labels list expand too wide --- .../web/components/templates/article/ArticleActionsMenu.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/components/templates/article/ArticleActionsMenu.tsx b/packages/web/components/templates/article/ArticleActionsMenu.tsx index f8fbb8c22..6514f5000 100644 --- a/packages/web/components/templates/article/ArticleActionsMenu.tsx +++ b/packages/web/components/templates/article/ArticleActionsMenu.tsx @@ -41,7 +41,7 @@ type ActionDropdownProps = { const ActionDropdown = (props: ActionDropdownProps): JSX.Element => { return Date: Mon, 11 Apr 2022 15:24:19 -0700 Subject: [PATCH 060/125] Set the label modal width to be consistent --- .../web/components/templates/article/ArticleActionsMenu.tsx | 2 +- packages/web/components/templates/article/EditLabelsControl.tsx | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/web/components/templates/article/ArticleActionsMenu.tsx b/packages/web/components/templates/article/ArticleActionsMenu.tsx index 6514f5000..7c411dbad 100644 --- a/packages/web/components/templates/article/ArticleActionsMenu.tsx +++ b/packages/web/components/templates/article/ArticleActionsMenu.tsx @@ -41,7 +41,7 @@ type ActionDropdownProps = { const ActionDropdown = (props: ActionDropdownProps): JSX.Element => { return Date: Mon, 11 Apr 2022 15:36:19 -0700 Subject: [PATCH 061/125] Only show the old article toolbar on ios embedded web (/app) --- .../templates/article/ArticleContainer.tsx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/web/components/templates/article/ArticleContainer.tsx b/packages/web/components/templates/article/ArticleContainer.tsx index ab3b00800..3c879b7cb 100644 --- a/packages/web/components/templates/article/ArticleContainer.tsx +++ b/packages/web/components/templates/article/ArticleContainer.tsx @@ -157,13 +157,15 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element { )} ) : null} - 0} - /> + {props.isAppleAppEmbed && ( + 0} + /> + )}
Date: Mon, 11 Apr 2022 15:36:35 -0700 Subject: [PATCH 062/125] Dont set a max width on smaller devices for the labels modal --- .../web/components/templates/article/EditLabelsControl.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/web/components/templates/article/EditLabelsControl.tsx b/packages/web/components/templates/article/EditLabelsControl.tsx index b988fda21..de29a323e 100644 --- a/packages/web/components/templates/article/EditLabelsControl.tsx +++ b/packages/web/components/templates/article/EditLabelsControl.tsx @@ -297,10 +297,6 @@ export function EditLabelsControl(props: EditLabelsControlProps): JSX.Element { css={{ p: '0', maxHeight: '92%', - '@mdDown': { - maxWidth: '100%', - maxHeight: '92%', - }, }}>
Date: Mon, 11 Apr 2022 18:46:51 -0700 Subject: [PATCH 063/125] Use a modal instead of dropdown on small screens --- .../templates/article/ArticleActionsMenu.tsx | 56 +++++++++++-------- .../templates/article/ArticleContainer.tsx | 1 - .../templates/article/EditLabelsControl.tsx | 32 ++++++----- .../templates/article/EditLabelsModal.tsx | 42 ++++++++++++-- .../web/pages/[username]/[slug]/index.tsx | 15 +++++ 5 files changed, 103 insertions(+), 43 deletions(-) diff --git a/packages/web/components/templates/article/ArticleActionsMenu.tsx b/packages/web/components/templates/article/ArticleActionsMenu.tsx index 7c411dbad..3b13c4d5c 100644 --- a/packages/web/components/templates/article/ArticleActionsMenu.tsx +++ b/packages/web/components/templates/article/ArticleActionsMenu.tsx @@ -4,7 +4,7 @@ import { ArticleAttributes } from "../../../lib/networking/queries/useGetArticle import { useGetUserPreferences } from "../../../lib/networking/queries/useGetUserPreferences" import { Button } from "../../elements/Button" import { Dropdown } from "../../elements/DropdownElements" -import { Box } from "../../elements/LayoutPrimitives" +import { Box, SpanBox } from "../../elements/LayoutPrimitives" import { TooltipWrapped } from "../../elements/Tooltip" import { styled, theme } from "../../tokens/stitches.config" import { EditLabelsControl } from "./EditLabelsControl" @@ -84,32 +84,40 @@ export function ArticleActionsMenu(props: ArticleActionsMenuProps): JSX.Element - - - - } + - - -{/* - */} + + + + ) diff --git a/packages/web/pages/[username]/[slug]/index.tsx b/packages/web/pages/[username]/[slug]/index.tsx index c0dfc3589..5748a0013 100644 --- a/packages/web/pages/[username]/[slug]/index.tsx +++ b/packages/web/pages/[username]/[slug]/index.tsx @@ -27,6 +27,8 @@ import { setLinkArchivedMutation } from '../../../lib/networking/mutations/setLi import { Label } from '../../../lib/networking/fragments/labelFragment' import { useSWRConfig } from 'swr' import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers' +import { EditLabelsControl } from '../../../components/templates/article/EditLabelsControl' +import { EditLabelsModal } from '../../../components/templates/article/EditLabelsModal' const PdfArticleContainerNoSSR = dynamic( @@ -46,6 +48,7 @@ export default function Home(): JSX.Element { const [fontSize, setFontSize] = useState(preferencesData?.fontSize ?? 20) const [marginWidth, setMarginWidth] = useState(preferencesData?.margin ?? 360) const [lineHeight, setLineHeight] = useState(preferencesData?.lineHeight ?? 150) + const [showEditLabelsModal, setShowEditLabelsModal] = useState(false) const { articleData } = useGetArticleQuery({ username: router.query.username as string, @@ -131,6 +134,10 @@ export default function Home(): JSX.Element { } break } + case 'editLabels': { + setShowEditLabelsModal(true) + break + } case 'resetReaderSettings': { updateFontSize(20) updateMarginWidth(360) @@ -230,6 +237,14 @@ export default function Home(): JSX.Element { /> )} + + {showEditLabelsModal && ( + setShowEditLabelsModal(false)} + /> + )} ) } From 68933689f6118f9bfaaed7b20f04d5cc9eb9f117 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 11 Apr 2022 19:08:19 -0700 Subject: [PATCH 064/125] Close labels modal when tap outside --- packages/web/components/templates/article/EditLabelsModal.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/web/components/templates/article/EditLabelsModal.tsx b/packages/web/components/templates/article/EditLabelsModal.tsx index 2e048310e..ca45e65f8 100644 --- a/packages/web/components/templates/article/EditLabelsModal.tsx +++ b/packages/web/components/templates/article/EditLabelsModal.tsx @@ -23,10 +23,11 @@ export function EditLabelsModal(props: EditLabelsModalProps): JSX.Element { { event.preventDefault() + props.onOpenChange(false) }} - css={{ overflow: 'auto', p: '0', width: '100%' }} > Date: Mon, 11 Apr 2022 20:38:54 -0700 Subject: [PATCH 065/125] Smaller display settings dropdown on small screens. Better shadows --- .../templates/article/ArticleActionsMenu.tsx | 3 ++- .../templates/article/ReaderSettingsModal.tsx | 4 ++-- packages/web/pages/[username]/[slug]/index.tsx | 16 +++++++++++++++- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/packages/web/components/templates/article/ArticleActionsMenu.tsx b/packages/web/components/templates/article/ArticleActionsMenu.tsx index 3b13c4d5c..a1d3f951b 100644 --- a/packages/web/components/templates/article/ArticleActionsMenu.tsx +++ b/packages/web/components/templates/article/ArticleActionsMenu.tsx @@ -41,7 +41,7 @@ type ActionDropdownProps = { const ActionDropdown = (props: ActionDropdownProps): JSX.Element => { return + - Margin: + Margin: + + + + + + ) +} From 9834303b3c5e5e75e34deb66df3c3d7c5b209e28 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 11 Apr 2022 21:17:26 -0700 Subject: [PATCH 067/125] Refine box shadows in dark mode --- packages/web/components/tokens/stitches.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/components/tokens/stitches.config.ts b/packages/web/components/tokens/stitches.config.ts index de59a709b..1a45ef788 100644 --- a/packages/web/components/tokens/stitches.config.ts +++ b/packages/web/components/tokens/stitches.config.ts @@ -200,7 +200,7 @@ const darkThemeSpec = { }, shadows: { cardBoxShadow: - '0px 0px 9px -2px rgba(32, 31, 29, 0.09), 0px 7px 12px rgba(32, 31, 29, 0.07)', + '0px 0px 9px -2px rgba(255, 255, 255, 0.09), 0px 7px 12px rgba(255, 255, 255, 0.07)', }, } From cbff548b9854cb17ff2c75d7ae6ae2b5eb362a7c Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Tue, 12 Apr 2022 08:42:15 -0700 Subject: [PATCH 068/125] Tweak box shadows in dark mode --- packages/web/components/tokens/stitches.config.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/web/components/tokens/stitches.config.ts b/packages/web/components/tokens/stitches.config.ts index 1a45ef788..f9937aa33 100644 --- a/packages/web/components/tokens/stitches.config.ts +++ b/packages/web/components/tokens/stitches.config.ts @@ -102,8 +102,7 @@ export const { styled, css, theme, getCssText, globalCss, keyframes, config } = borderStyles: {}, shadows: { panelShadow: '0px 4px 18px rgba(120, 123, 134, 0.12)', - cardBoxShadow: - '0px 0px 9px -2px rgba(32, 31, 29, 0.09), 0px 7px 12px rgba(32, 31, 29, 0.07)', + cardBoxShadow: '0px 0px 4px 0px rgb(255 255 255 / 10%)', }, zIndices: {}, transitions: {}, From 244cced8b400f0a81625df82ca4f2f16bce37d7a Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Tue, 12 Apr 2022 12:35:56 -0700 Subject: [PATCH 069/125] Make the labels modal non-scrollable on small screens, add a border --- packages/web/components/templates/article/EditLabelsModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/components/templates/article/EditLabelsModal.tsx b/packages/web/components/templates/article/EditLabelsModal.tsx index ca45e65f8..1ef6da940 100644 --- a/packages/web/components/templates/article/EditLabelsModal.tsx +++ b/packages/web/components/templates/article/EditLabelsModal.tsx @@ -23,7 +23,7 @@ export function EditLabelsModal(props: EditLabelsModalProps): JSX.Element { { event.preventDefault() props.onOpenChange(false) From 0d1b05d4fc246e00e0ec0886c27cafa5e9108e41 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Tue, 12 Apr 2022 12:58:04 -0700 Subject: [PATCH 070/125] Rename reader settings --- .../web/components/templates/article/ArticleActionsMenu.tsx | 4 ++-- .../components/templates/article/DisplaySettingsModal.tsx | 5 ++--- .../{ReaderSettingsModal.tsx => ReaderSettingsControl.tsx} | 3 ++- 3 files changed, 6 insertions(+), 6 deletions(-) rename packages/web/components/templates/article/{ReaderSettingsModal.tsx => ReaderSettingsControl.tsx} (98%) diff --git a/packages/web/components/templates/article/ArticleActionsMenu.tsx b/packages/web/components/templates/article/ArticleActionsMenu.tsx index a1d3f951b..410ae3080 100644 --- a/packages/web/components/templates/article/ArticleActionsMenu.tsx +++ b/packages/web/components/templates/article/ArticleActionsMenu.tsx @@ -8,7 +8,7 @@ import { Box, SpanBox } from "../../elements/LayoutPrimitives" import { TooltipWrapped } from "../../elements/Tooltip" import { styled, theme } from "../../tokens/stitches.config" import { EditLabelsControl } from "./EditLabelsControl" -import { ReaderSettings } from "./ReaderSettingsModal" +import { ReaderSettingsControl } from "./ReaderSettingsControl" export type ArticleActionsMenuLayout = 'horizontal' | 'vertical' @@ -80,7 +80,7 @@ export function ArticleActionsMenu(props: ArticleActionsMenuProps): JSX.Element } > - + diff --git a/packages/web/components/templates/article/DisplaySettingsModal.tsx b/packages/web/components/templates/article/DisplaySettingsModal.tsx index b2d3b40ed..c7e0623cb 100644 --- a/packages/web/components/templates/article/DisplaySettingsModal.tsx +++ b/packages/web/components/templates/article/DisplaySettingsModal.tsx @@ -11,8 +11,7 @@ import { } from '../../elements/ModalPrimitives' import { StyledText } from '../../elements/StyledText' import { theme } from '../../tokens/stitches.config' -import { EditLabelsControl } from './EditLabelsControl' -import { ReaderSettings } from './ReaderSettingsModal' +import { ReaderSettingsControl } from './ReaderSettingsControl' type DisplaySettingsModalProps = { onOpenChange: (open: boolean) => void @@ -51,7 +50,7 @@ export function DisplaySettingsModal(props: DisplaySettingsModalProps): JSX.Elem /> - + diff --git a/packages/web/components/templates/article/ReaderSettingsModal.tsx b/packages/web/components/templates/article/ReaderSettingsControl.tsx similarity index 98% rename from packages/web/components/templates/article/ReaderSettingsModal.tsx rename to packages/web/components/templates/article/ReaderSettingsControl.tsx index 1f435978d..fd1e74004 100644 --- a/packages/web/components/templates/article/ReaderSettingsModal.tsx +++ b/packages/web/components/templates/article/ReaderSettingsControl.tsx @@ -9,6 +9,7 @@ import { TickedRangeSlider } from '../../elements/TickedRangeSlider' import { UserPreferences } from '../../../lib/networking/queries/useGetUserPreferences' import { showSuccessToast } from '../../../lib/toastHelpers' + type ReaderSettingsProps = { userPreferences?: UserPreferences articleActionHandler: (action: string, arg?: number) => void @@ -20,7 +21,7 @@ const VerticalDivider = styled(SpanBox, { background: `${theme.colors.grayLine.toString()}`, }) -export function ReaderSettings(props: ReaderSettingsProps): JSX.Element { +export function ReaderSettingsControl(props: ReaderSettingsProps): JSX.Element { const [marginWidth, setMarginWidth] = useState(props.userPreferences?.margin ?? 0) const [lineHeight, setLineHeight] = useState(props.userPreferences?.lineHeight ?? 150) From 372cd6afe2df25968984d9f98f1caa3a18ee5f0d Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Tue, 12 Apr 2022 13:16:25 -0700 Subject: [PATCH 071/125] More consistent boxshadows with themes --- packages/web/components/patterns/HighlightBar.tsx | 2 +- packages/web/components/tokens/stitches.config.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/web/components/patterns/HighlightBar.tsx b/packages/web/components/patterns/HighlightBar.tsx index 444fb0f56..ead951168 100644 --- a/packages/web/components/patterns/HighlightBar.tsx +++ b/packages/web/components/patterns/HighlightBar.tsx @@ -44,7 +44,7 @@ export function HighlightBar(props: HighlightBarProps): JSX.Element { background: '$grayBg', borderRadius: '4px', border: '1px solid $grayBorder', - boxShadow: '0px 0px 9px -2px rgba(32, 31, 29, 0.09), 0px 7px 12px rgba(32, 31, 29, 0.07)', + boxShadow: theme.shadows.cardBoxShadow.toString(), bottom: 'calc(38px + env(safe-area-inset-bottom, 40px))', '@smDown': { maxWidth: '80%', diff --git a/packages/web/components/tokens/stitches.config.ts b/packages/web/components/tokens/stitches.config.ts index f9937aa33..80a87e59f 100644 --- a/packages/web/components/tokens/stitches.config.ts +++ b/packages/web/components/tokens/stitches.config.ts @@ -102,7 +102,7 @@ export const { styled, css, theme, getCssText, globalCss, keyframes, config } = borderStyles: {}, shadows: { panelShadow: '0px 4px 18px rgba(120, 123, 134, 0.12)', - cardBoxShadow: '0px 0px 4px 0px rgb(255 255 255 / 10%)', + cardBoxShadow: '0px 0px 4px 0px rgba(0, 0, 0, 0.1)', }, zIndices: {}, transitions: {}, From 0fa9526924954d74f78d2e8c5ffbb365b8cee7eb Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Fri, 8 Apr 2022 12:56:08 -0700 Subject: [PATCH 072/125] use searchTerm and selected labels to compute searchQuery --- .../App/Views/Home/HomeFeedViewIOS.swift | 6 +++--- .../App/Views/Home/HomeFeedViewMac.swift | 6 +++--- .../App/Views/Home/HomeFeedViewModel.swift | 19 +++++++++++++++++-- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index 7c5e60d82..e6c0e494b 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -23,17 +23,17 @@ import Views viewModel.loadItems(dataService: dataService, isRefresh: true) } .searchable( - text: $viewModel.searchQuery, + text: $viewModel.searchTerm, placement: .sidebar ) { - if viewModel.searchQuery.isEmpty { + if viewModel.searchTerm.isEmpty { Text("Inbox").searchCompletion("in:inbox ") Text("All").searchCompletion("in:all ") Text("Archived").searchCompletion("in:archive ") Text("Files").searchCompletion("type:file ") } } - .onChange(of: viewModel.searchQuery) { _ in + .onChange(of: viewModel.searchTerm) { _ in // Maybe we should debounce this, but // it feels like it works ok without viewModel.loadItems(dataService: dataService, isRefresh: true) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewMac.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewMac.swift index 91cad42e8..640f6fdc5 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewMac.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewMac.swift @@ -77,17 +77,17 @@ import Views .listStyle(PlainListStyle()) .navigationTitle("Home") .searchable( - text: $viewModel.searchQuery, + text: $viewModel.searchTerm, placement: .toolbar ) { - if viewModel.searchQuery.isEmpty { + if viewModel.searchTerm.isEmpty { Text("Inbox").searchCompletion("in:inbox ") Text("All").searchCompletion("in:all ") Text("Archived").searchCompletion("in:archive ") Text("Files").searchCompletion("type:file ") } } - .onChange(of: viewModel.searchQuery) { _ in + .onChange(of: viewModel.searchTerm) { _ in // Maybe we should debounce this, but // it feels like it works ok without viewModel.loadItems(dataService: dataService, isRefresh: true) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index 7a9af7d98..61829eafd 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -15,7 +15,8 @@ final class HomeFeedViewModel: ObservableObject { @Published var isLoading = false @Published var showPushNotificationPrimer = false @Published var itemUnderLabelEdit: FeedItem? - @Published var searchQuery = "" + @Published var searchTerm = "" + @Published var selectedLabels = [FeedItemLabel]() @Published var snoozePresented = false @Published var itemToSnooze: FeedItem? @Published var selectedLinkItem: FeedItem? @@ -68,7 +69,7 @@ final class HomeFeedViewModel: ObservableObject { dataService.libraryItemsPublisher( limit: 10, sortDescending: true, - searchQuery: searchQuery.isEmpty ? nil : searchQuery, + searchQuery: searchQuery, cursor: isRefresh ? nil : cursor ) .sink( @@ -195,4 +196,18 @@ final class HomeFeedViewModel: ObservableObject { items[index].labels = labels } } + + private var searchQuery: String? { + if searchTerm.isEmpty, selectedLabels.isEmpty { + return nil + } + + var query = searchTerm + + for label in selectedLabels { + query.append(" label:\(label.name)") + } + + return query + } } From df291ecbab34f83d019d76fbea0140f1d68c472b Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Sun, 10 Apr 2022 19:25:06 -0700 Subject: [PATCH 073/125] add text chip buttons to represent labels used as filters --- .../App/Views/Home/HomeFeedViewIOS.swift | 91 ++++++++++++------- .../OmnivoreKit/Sources/Views/TextChip.swift | 68 ++++++++++++++ 2 files changed, 127 insertions(+), 32 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index e6c0e494b..bf5c6b885 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -24,7 +24,7 @@ import Views } .searchable( text: $viewModel.searchTerm, - placement: .sidebar + placement: .navigationBarDrawer ) { if viewModel.searchTerm.isEmpty { Text("Inbox").searchCompletion("in:inbox ") @@ -71,6 +71,7 @@ import Views } } .navigationTitle("Home") + .navigationBarTitleDisplayMode(.inline) .onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in // Don't refresh the list if the user is currently reading an article if viewModel.selectedLinkItem == nil { @@ -106,48 +107,74 @@ import Views struct HomeFeedView: View { @EnvironmentObject var dataService: DataService - @Binding var prefersListLayout: Bool - + @State private var showLabelsSheet = false @ObservedObject var viewModel: HomeFeedViewModel + // TODO: remove stub + let demoFilterChips = [ + FeedItemLabel(id: "1", name: "Inbox", color: "#039466", createdAt: nil, description: nil), + FeedItemLabel(id: "2", name: "NotInbox", color: "#039466", createdAt: nil, description: nil), + FeedItemLabel(id: "3", name: "Atari", color: "#039466", createdAt: nil, description: nil), + FeedItemLabel(id: "4", name: "iOS", color: "#039466", createdAt: nil, description: nil) + ] + var body: some View { - if prefersListLayout { - HomeFeedListView(prefersListLayout: $prefersListLayout, viewModel: viewModel) - } else { - HomeFeedGridView(viewModel: viewModel) - .toolbar { - ToolbarItem { - if #available(iOS 15.0, *) { - Button("", action: {}) - .disabled(true) - .overlay { - if viewModel.isLoading { - ProgressView() + VStack { + ScrollView(.horizontal, showsIndicators: false) { + HStack { + TextChipButton.makeAddLabelButton { + showLabelsSheet = true + } + ForEach(demoFilterChips, id: \.self) { label in + TextChipButton.makeRemovableLabelButton(feedItemLabel: label) { + print("tapped label named \(label.name)") + } + } + Spacer() + } + .padding(.horizontal) + .sheet(isPresented: $showLabelsSheet) { + Text("select labels stub") + } + } + if prefersListLayout { + HomeFeedListView(prefersListLayout: $prefersListLayout, viewModel: viewModel) + } else { + HomeFeedGridView(viewModel: viewModel) + .toolbar { + ToolbarItem { + if #available(iOS 15.0, *) { + Button("", action: {}) + .disabled(true) + .overlay { + if viewModel.isLoading { + ProgressView() + } } - } - } else { - if viewModel.isLoading { - Button(action: {}, label: { ProgressView() }) } else { + if viewModel.isLoading { + Button(action: {}, label: { ProgressView() }) + } else { + Button( + action: { viewModel.loadItems(dataService: dataService, isRefresh: true) }, + label: { Label("Refresh Feed", systemImage: "arrow.clockwise") } + ) + } + } + } + ToolbarItem { + if UIDevice.isIPad { Button( - action: { viewModel.loadItems(dataService: dataService, isRefresh: true) }, - label: { Label("Refresh Feed", systemImage: "arrow.clockwise") } + action: { prefersListLayout.toggle() }, + label: { + Label("Toggle Feed Layout", systemImage: prefersListLayout ? "square.grid.2x2" : "list.bullet") + } ) } } } - ToolbarItem { - if UIDevice.isIPad { - Button( - action: { prefersListLayout.toggle() }, - label: { - Label("Toggle Feed Layout", systemImage: prefersListLayout ? "square.grid.2x2" : "list.bullet") - } - ) - } - } - } + } } } } diff --git a/apple/OmnivoreKit/Sources/Views/TextChip.swift b/apple/OmnivoreKit/Sources/Views/TextChip.swift index cb55573e8..3fcd16e12 100644 --- a/apple/OmnivoreKit/Sources/Views/TextChip.swift +++ b/apple/OmnivoreKit/Sources/Views/TextChip.swift @@ -30,3 +30,71 @@ public struct TextChip: View { .cornerRadius(cornerRadius) } } + +public struct TextChipButton: View { + public static func makeAddLabelButton(onTap: @escaping () -> Void) -> TextChipButton { + TextChipButton(title: "Label Filter", color: .appYellow48, actionType: .add, onTap: onTap) + } + + public static func makeShowOptionsButton(title: String, onTap: @escaping () -> Void) -> TextChipButton { + TextChipButton(title: title, color: .appButtonBackground, actionType: .add, onTap: onTap) + } + + public static func makeRemovableLabelButton( + feedItemLabel: FeedItemLabel, + onTap: @escaping () -> Void + ) -> TextChipButton { + TextChipButton( + title: feedItemLabel.name, + color: Color(hex: feedItemLabel.color) ?? .appButtonBackground, + actionType: .remove, + onTap: onTap + ) + } + + public enum ActionType { + case remove + case add + case show + + var systemIconName: String { + switch self { + case .remove: + return "xmark" + case .add: + return "plus" + case .show: + return "chevron.down" + } + } + } + + init(title: String, color: Color, actionType: ActionType, onTap: @escaping () -> Void) { + self.text = title + self.color = color + self.onTap = onTap + self.actionType = actionType + } + + let text: String + let color: Color + let onTap: () -> Void + let actionType: ActionType + let cornerRadius = 20.0 + + public var body: some View { + Button(action: onTap) { + HStack { + Text(text) + Image(systemName: actionType.systemIconName) + } + .padding(.horizontal, 10) + .padding(.vertical, 5) + .font(.appFootnote) + .foregroundColor(color.isDark ? .white : .black) + .lineLimit(1) + .background(color) + .cornerRadius(cornerRadius) + } + } +} From bd652dc3edfd49e94696c9baa82c895bd2792434 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Sun, 10 Apr 2022 20:42:52 -0700 Subject: [PATCH 074/125] rename selectedLabelsForItemInContext to selectedLabels --- .../App/Views/Labels/ApplyLabelsView.swift | 6 ++-- .../App/Views/Labels/LabelsViewModel.swift | 34 +++++++++++++------ 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift b/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift index 1c1348c94..0d46e15b7 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift @@ -15,10 +15,10 @@ struct ApplyLabelsView: View { var innerBody: some View { List { Section(header: Text("Assigned Labels")) { - if viewModel.selectedLabelsForItemInContext.isEmpty { + if viewModel.selectedLabels.isEmpty { Text("No labels are currently assigned.") } - ForEach(viewModel.selectedLabelsForItemInContext.applySearchFilter(labelSearchFilter), id: \.self) { label in + ForEach(viewModel.selectedLabels.applySearchFilter(labelSearchFilter), id: \.self) { label in HStack { TextChip(feedItemLabel: label) Spacer() @@ -34,7 +34,7 @@ struct ApplyLabelsView: View { } } Section(header: Text("Available Labels")) { - ForEach(viewModel.unselectedLabelsForItemInContext.applySearchFilter(labelSearchFilter), id: \.self) { label in + ForEach(viewModel.unselectedLabels.applySearchFilter(labelSearchFilter), id: \.self) { label in HStack { TextChip(feedItemLabel: label) Spacer() diff --git a/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift index b38109c72..e8020c684 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift @@ -7,14 +7,20 @@ import Views final class LabelsViewModel: ObservableObject { private var hasLoadedInitialLabels = false @Published var isLoading = false - @Published var selectedLabelsForItemInContext = [FeedItemLabel]() - @Published var unselectedLabelsForItemInContext = [FeedItemLabel]() + @Published var selectedLabels = [FeedItemLabel]() + @Published var unselectedLabels = [FeedItemLabel]() @Published var labels = [FeedItemLabel]() @Published var showCreateEmailModal = false var subscriptions = Set() - func loadLabels(dataService: DataService, item: FeedItem?) { + + /// Loads initial set of labels when a edit labels list is displayed + /// - Parameters: + /// - dataService: `DataService` reference + /// - item: Optional `FeedItem` for applying labels to a single item + /// - initiallySelectedLabels: Optional `[FeedItem]` for filtering a list of items + func loadLabels(dataService: DataService, item: FeedItem?, initiallySelectedLabels: [FeedItem]?) { guard !hasLoadedInitialLabels else { return } isLoading = true @@ -25,11 +31,17 @@ final class LabelsViewModel: ObservableObject { self?.labels = allLabels self?.hasLoadedInitialLabels = true if let item = item { - self?.selectedLabelsForItemInContext = item.labels - self?.unselectedLabelsForItemInContext = allLabels.filter { label in + self?.selectedLabels = item.labels + self?.unselectedLabels = allLabels.filter { label in !item.labels.contains(where: { $0.id == label.id }) } } + if let initiallySelectedLabels = initiallySelectedLabels { + self?.selectedLabels = initiallySelectedLabels + self?.unselectedLabels = allLabels.filter { label in + !initiallySelectedLabels.contains(where: { $0.id == label.id }) + } + } } ) .store(in: &subscriptions) @@ -49,7 +61,7 @@ final class LabelsViewModel: ObservableObject { receiveValue: { [weak self] result in self?.isLoading = false self?.labels.insert(result, at: 0) - self?.unselectedLabelsForItemInContext.insert(result, at: 0) + self?.unselectedLabels.insert(result, at: 0) self?.showCreateEmailModal = false } ) @@ -73,7 +85,7 @@ final class LabelsViewModel: ObservableObject { func saveItemLabelChanges(itemID: String, dataService: DataService, onComplete: @escaping ([FeedItemLabel]) -> Void) { isLoading = true - dataService.updateArticleLabelsPublisher(itemID: itemID, labelIDs: selectedLabelsForItemInContext.map(\.id)).sink( + dataService.updateArticleLabelsPublisher(itemID: itemID, labelIDs: selectedLabels.map(\.id)).sink( receiveCompletion: { [weak self] _ in self?.isLoading = false }, @@ -83,12 +95,12 @@ final class LabelsViewModel: ObservableObject { } func addLabelToItem(_ label: FeedItemLabel) { - selectedLabelsForItemInContext.insert(label, at: 0) - unselectedLabelsForItemInContext.removeAll { $0.id == label.id } + selectedLabels.insert(label, at: 0) + unselectedLabels.removeAll { $0.id == label.id } } func removeLabelFromItem(_ label: FeedItemLabel) { - unselectedLabelsForItemInContext.insert(label, at: 0) - selectedLabelsForItemInContext.removeAll { $0.id == label.id } + unselectedLabels.insert(label, at: 0) + selectedLabels.removeAll { $0.id == label.id } } } From c8ad2f47a2c2e137cc582ed8bb8e1f8f0d121a4b Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Sun, 10 Apr 2022 21:09:42 -0700 Subject: [PATCH 075/125] adapt assign labels view to work on a list --- .../App/Views/Home/HomeFeedViewIOS.swift | 23 ++++------ .../App/Views/Labels/ApplyLabelsView.swift | 46 ++++++++++++++++--- .../App/Views/Labels/LabelsViewModel.swift | 5 +- 3 files changed, 52 insertions(+), 22 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index bf5c6b885..611979fc9 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -38,11 +38,14 @@ import Views // it feels like it works ok without viewModel.loadItems(dataService: dataService, isRefresh: true) } + .onChange(of: viewModel.selectedLabels) { _ in + viewModel.loadItems(dataService: dataService, isRefresh: true) + } .onSubmit(of: .search) { viewModel.loadItems(dataService: dataService, isRefresh: true) } .sheet(item: $viewModel.itemUnderLabelEdit) { item in - ApplyLabelsView(item: item) { labels in + ApplyLabelsView(mode: .item(item)) { labels in viewModel.updateLabels(itemID: item.id, labels: labels) } } @@ -52,7 +55,7 @@ import Views viewModel: viewModel ) .sheet(item: $viewModel.itemUnderLabelEdit) { item in - ApplyLabelsView(item: item) { labels in + ApplyLabelsView(mode: .item(item)) { labels in viewModel.updateLabels(itemID: item.id, labels: labels) } } @@ -111,14 +114,6 @@ import Views @State private var showLabelsSheet = false @ObservedObject var viewModel: HomeFeedViewModel - // TODO: remove stub - let demoFilterChips = [ - FeedItemLabel(id: "1", name: "Inbox", color: "#039466", createdAt: nil, description: nil), - FeedItemLabel(id: "2", name: "NotInbox", color: "#039466", createdAt: nil, description: nil), - FeedItemLabel(id: "3", name: "Atari", color: "#039466", createdAt: nil, description: nil), - FeedItemLabel(id: "4", name: "iOS", color: "#039466", createdAt: nil, description: nil) - ] - var body: some View { VStack { ScrollView(.horizontal, showsIndicators: false) { @@ -126,16 +121,18 @@ import Views TextChipButton.makeAddLabelButton { showLabelsSheet = true } - ForEach(demoFilterChips, id: \.self) { label in + ForEach(viewModel.selectedLabels, id: \.self) { label in TextChipButton.makeRemovableLabelButton(feedItemLabel: label) { - print("tapped label named \(label.name)") + viewModel.selectedLabels.removeAll { $0.id == label.id } } } Spacer() } .padding(.horizontal) .sheet(isPresented: $showLabelsSheet) { - Text("select labels stub") + ApplyLabelsView(mode: .list(viewModel.selectedLabels)) { labels in + viewModel.selectedLabels = labels + } } } if prefersListLayout { diff --git a/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift b/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift index 0d46e15b7..3b0c80a85 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift @@ -4,7 +4,30 @@ import SwiftUI import Views struct ApplyLabelsView: View { - let item: FeedItem + enum Mode { + case item(FeedItem) + case list([FeedItemLabel]) + + var navTitle: String { + switch self { + case .item: + return "Assign Labels" + case .list: + return "Apply Label Filters" + } + } + + var confirmButtonText: String { + switch self { + case .item: + return "Save" + case .list: + return "Apply" + } + } + } + + let mode: Mode let commitLabelChanges: ([FeedItemLabel]) -> Void @EnvironmentObject var dataService: DataService @@ -63,7 +86,7 @@ struct ApplyLabelsView: View { .disabled(viewModel.isLoading) } } - .navigationTitle("Assign Labels") + .navigationTitle(mode.navTitle) #if os(iOS) .navigationBarTitleDisplayMode(.inline) .toolbar { @@ -76,12 +99,18 @@ struct ApplyLabelsView: View { ToolbarItem(placement: .navigationBarTrailing) { Button( action: { - viewModel.saveItemLabelChanges(itemID: item.id, dataService: dataService) { labels in - commitLabelChanges(labels) + switch mode { + case let .item(feedItem): + viewModel.saveItemLabelChanges(itemID: feedItem.id, dataService: dataService) { labels in + commitLabelChanges(labels) + presentationMode.wrappedValue.dismiss() + } + case .list: + commitLabelChanges(viewModel.selectedLabels) presentationMode.wrappedValue.dismiss() } }, - label: { Text("Save").foregroundColor(.appGrayTextContrast) } + label: { Text(mode.confirmButtonText).foregroundColor(.appGrayTextContrast) } ) } } @@ -112,7 +141,12 @@ struct ApplyLabelsView: View { } } .onAppear { - viewModel.loadLabels(dataService: dataService, item: item) + switch mode { + case let .item(feedItem): + viewModel.loadLabels(dataService: dataService, item: feedItem) + case let .list(labels): + viewModel.loadLabels(dataService: dataService, initiallySelectedLabels: labels) + } } } } diff --git a/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift index e8020c684..35159e8ca 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift @@ -14,13 +14,12 @@ final class LabelsViewModel: ObservableObject { var subscriptions = Set() - /// Loads initial set of labels when a edit labels list is displayed /// - Parameters: /// - dataService: `DataService` reference /// - item: Optional `FeedItem` for applying labels to a single item - /// - initiallySelectedLabels: Optional `[FeedItem]` for filtering a list of items - func loadLabels(dataService: DataService, item: FeedItem?, initiallySelectedLabels: [FeedItem]?) { + /// - initiallySelectedLabels: Optional `[FeedItemLabel]` for filtering a list of items + func loadLabels(dataService: DataService, item: FeedItem? = nil, initiallySelectedLabels: [FeedItemLabel]? = nil) { guard !hasLoadedInitialLabels else { return } isLoading = true From be68971a0323952701ef8634a7c1ded5a7be9b49 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Sun, 10 Apr 2022 21:13:28 -0700 Subject: [PATCH 076/125] use xmark for remove label button --- .../OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift b/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift index 3b0c80a85..8cf320384 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift @@ -51,7 +51,7 @@ struct ApplyLabelsView: View { viewModel.removeLabelFromItem(label) } }, - label: { Image(systemName: "trash").foregroundColor(.appGrayTextContrast) } + label: { Image(systemName: "xmark.circle").foregroundColor(.appGrayTextContrast) } ) } } From d68c8e5e337bb50f80cb20fa38fac93838079518 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Sun, 10 Apr 2022 21:38:36 -0700 Subject: [PATCH 077/125] update search query to handle multiple labels --- .../Sources/App/Views/Home/HomeFeedViewModel.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index 61829eafd..763615607 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -204,8 +204,9 @@ final class HomeFeedViewModel: ObservableObject { var query = searchTerm - for label in selectedLabels { - query.append(" label:\(label.name)") + if !selectedLabels.isEmpty { + query.append(" label:") + query.append(selectedLabels.map(\.name).joined(separator: ",")) } return query From 6c96b256d1603d89c6da9ef316b219a46c18e9d9 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 12 Apr 2022 12:30:33 -0700 Subject: [PATCH 078/125] move create label submit button into navbar --- .../Sources/App/Views/Labels/LabelsView.swift | 32 ++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsView.swift b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsView.swift index 627305438..39c0049b6 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsView.swift @@ -92,6 +92,10 @@ struct CreateLabelView: View { @State private var newLabelName = "" @State private var newLabelColor = Color.clear + var shouldDisableCreateButton: Bool { + viewModel.isLoading || newLabelName.isEmpty || newLabelColor == .clear + } + var body: some View { NavigationView { VStack(spacing: 16) { @@ -104,32 +108,24 @@ struct CreateLabelView: View { newLabelColor == .clear ? "Select Color" : newLabelColor.description, selection: $newLabelColor ) - Button( - action: { - viewModel.createLabel( - dataService: dataService, - name: newLabelName, - color: newLabelColor, - description: nil - ) - }, - label: { Text("Create") } - ) - .buttonStyle(SolidCapsuleButtonStyle(color: .appDeepBackground, width: 300)) - .disabled(viewModel.isLoading || newLabelName.isEmpty || newLabelColor == .clear) Spacer() } .padding() .toolbar { - ToolbarItem(placement: .automatic) { + ToolbarItem(placement: .navigationBarLeading) { Button( action: { viewModel.showCreateEmailModal = false }, - label: { - Image(systemName: "xmark") - .foregroundColor(.appGrayTextContrast) - } + label: { Text("Cancel").foregroundColor(.appGrayTextContrast) } ) } + ToolbarItem(placement: .navigationBarTrailing) { + Button( + action: {}, + label: { Text("Create").foregroundColor(.appGrayTextContrast) } + ) + .opacity(shouldDisableCreateButton ? 0.2 : 1) + .disabled(shouldDisableCreateButton) + } } .navigationTitle("Create New Label") #if os(iOS) From 060313ab31f3f2ded2de811342714aea955d17af Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 12 Apr 2022 15:05:29 -0700 Subject: [PATCH 079/125] use hgrid to display label color selections --- .../Sources/App/Views/Labels/LabelsView.swift | 62 ++++++++++++++++--- 1 file changed, 55 insertions(+), 7 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsView.swift b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsView.swift index 39c0049b6..903726be1 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsView.swift @@ -19,7 +19,7 @@ struct LabelsView: View { Form { innerBody .alert("Are you sure you want to delete this label?", isPresented: $showDeleteConfirmation) { - Button("Remove Link", role: .destructive) { + Button("Delete Label", role: .destructive) { if let labelID = labelToRemoveID { withAnimation { viewModel.deleteLabel(dataService: dataService, labelID: labelID) @@ -96,18 +96,53 @@ struct CreateLabelView: View { viewModel.isLoading || newLabelName.isEmpty || newLabelColor == .clear } + let rows = [ + GridItem(.fixed(60)), + GridItem(.fixed(60)), + GridItem(.fixed(60)) + ] + + let swatches = (0 ... 200).map { _ in Color.random } + var body: some View { NavigationView { - VStack(spacing: 16) { + VStack { + HStack { + if !newLabelName.isEmpty, newLabelColor != .clear { + TextChip(text: newLabelName, color: newLabelColor) + } else { + Text("Assign a name and color.") + } + Spacer() + } + TextField("Label Name", text: $newLabelName) #if os(iOS) .keyboardType(.alphabet) #endif .textFieldStyle(StandardTextFieldStyle()) - ColorPicker( - newLabelColor == .clear ? "Select Color" : newLabelColor.description, - selection: $newLabelColor - ) + + ScrollView(.horizontal, showsIndicators: false) { + LazyHGrid(rows: rows, alignment: .top, spacing: 20) { + ForEach(swatches, id: \.self) { swatch in + ZStack { + Circle() + .fill(swatch) + .frame(width: 50, height: 50) + .onTapGesture { + newLabelColor = swatch + } + .padding(10) + + if newLabelColor == swatch { + Circle() + .stroke(swatch, lineWidth: 5) + .frame(width: 60, height: 60) + } + } + } + } + } Spacer() } .padding() @@ -120,7 +155,14 @@ struct CreateLabelView: View { } ToolbarItem(placement: .navigationBarTrailing) { Button( - action: {}, + action: { + viewModel.createLabel( + dataService: dataService, + name: newLabelName, + color: newLabelColor, + description: nil + ) + }, label: { Text("Create").foregroundColor(.appGrayTextContrast) } ) .opacity(shouldDisableCreateButton ? 0.2 : 1) @@ -134,3 +176,9 @@ struct CreateLabelView: View { } } } + +extension Color { + static var random: Color { + Color(hue: .random(in: 0 ... 1), saturation: .random(in: 0.2 ... 0.8), brightness: .random(in: 0.5 ... 0.8)) + } +} From 751095b9834fb489c331b70f82ceb480aa2fc91b Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 12 Apr 2022 15:28:14 -0700 Subject: [PATCH 080/125] add padding below text chip preview --- apple/OmnivoreKit/Sources/App/Views/Labels/LabelsView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsView.swift b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsView.swift index 903726be1..fd2ab65c0 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsView.swift @@ -115,6 +115,7 @@ struct CreateLabelView: View { } Spacer() } + .padding(.bottom, 8) TextField("Label Name", text: $newLabelName) #if os(iOS) From 57b8b763b071cd7104e95cbfb68be8f6499fd969 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 12 Apr 2022 16:09:53 -0700 Subject: [PATCH 081/125] run swift gql gen --- .../Services/DataService/GQLSchema.swift | 1425 ++++++++++++++++- .../UpdateArticleLabelsPublisher.swift | 2 +- apple/swiftgraphql.yml | 4 +- 3 files changed, 1401 insertions(+), 30 deletions(-) diff --git a/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift b/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift index 6f50d6b8b..162b121e0 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift @@ -5177,7 +5177,6 @@ extension Objects { struct Highlight { let __typename: TypeName = .highlight let annotation: [String: String] - let article: [String: Objects.Article] let createdAt: [String: DateTime] let createdByMe: [String: Bool] let id: [String: String] @@ -5214,10 +5213,6 @@ extension Objects.Highlight: Decodable { if let value = try container.decode(String?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) } - case "article": - if let value = try container.decode(Objects.Article?.self, forKey: codingKey) { - map.set(key: field, hash: alias, value: value as Any) - } case "createdAt": if let value = try container.decode(DateTime?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) @@ -5281,7 +5276,6 @@ extension Objects.Highlight: Decodable { } annotation = map["annotation"] - article = map["article"] createdAt = map["createdAt"] createdByMe = map["createdByMe"] id = map["id"] @@ -5354,25 +5348,6 @@ extension Fields where TypeLock == Objects.Highlight { } } - func article(selection: Selection) throws -> Type { - let field = GraphQLField.composite( - name: "article", - arguments: [], - selection: selection.selection - ) - select(field) - - switch response { - case let .decoding(data): - if let data = data.article[field.alias!] { - return try selection.decode(data: data) - } - throw HttpError.badpayload - case .mocking: - return selection.mock() - } - } - func quote() throws -> String { let field = GraphQLField.leaf( name: "quote", @@ -11260,6 +11235,137 @@ extension Selection where TypeLock == Never, Type == Never { typealias DeleteLabelError = Selection } +extension Objects { + struct UpdateLabelSuccess { + let __typename: TypeName = .updateLabelSuccess + let label: [String: Objects.Label] + + enum TypeName: String, Codable { + case updateLabelSuccess = "UpdateLabelSuccess" + } + } +} + +extension Objects.UpdateLabelSuccess: Decodable { + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: DynamicCodingKeys.self) + + var map = HashMap() + for codingKey in container.allKeys { + if codingKey.isTypenameKey { continue } + + let alias = codingKey.stringValue + let field = GraphQLField.getFieldNameFromAlias(alias) + + switch field { + case "label": + if let value = try container.decode(Objects.Label?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + default: + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Unknown key \(field)." + ) + ) + } + } + + label = map["label"] + } +} + +extension Fields where TypeLock == Objects.UpdateLabelSuccess { + func label(selection: Selection) throws -> Type { + let field = GraphQLField.composite( + name: "label", + arguments: [], + selection: selection.selection + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.label[field.alias!] { + return try selection.decode(data: data) + } + throw HttpError.badpayload + case .mocking: + return selection.mock() + } + } +} + +extension Selection where TypeLock == Never, Type == Never { + typealias UpdateLabelSuccess = Selection +} + +extension Objects { + struct UpdateLabelError { + let __typename: TypeName = .updateLabelError + let errorCodes: [String: [Enums.UpdateLabelErrorCode]] + + enum TypeName: String, Codable { + case updateLabelError = "UpdateLabelError" + } + } +} + +extension Objects.UpdateLabelError: Decodable { + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: DynamicCodingKeys.self) + + var map = HashMap() + for codingKey in container.allKeys { + if codingKey.isTypenameKey { continue } + + let alias = codingKey.stringValue + let field = GraphQLField.getFieldNameFromAlias(alias) + + switch field { + case "errorCodes": + if let value = try container.decode([Enums.UpdateLabelErrorCode]?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + default: + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Unknown key \(field)." + ) + ) + } + } + + errorCodes = map["errorCodes"] + } +} + +extension Fields where TypeLock == Objects.UpdateLabelError { + func errorCodes() throws -> [Enums.UpdateLabelErrorCode] { + let field = GraphQLField.leaf( + name: "errorCodes", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.errorCodes[field.alias!] { + return data + } + throw HttpError.badpayload + case .mocking: + return [] + } + } +} + +extension Selection where TypeLock == Never, Type == Never { + typealias UpdateLabelError = Selection +} + extension Objects { struct SignupSuccess { let __typename: TypeName = .signupSuccess @@ -11522,6 +11628,910 @@ extension Selection where TypeLock == Never, Type == Never { typealias SetLabelsError = Selection } +extension Objects { + struct GenerateApiKeySuccess { + let __typename: TypeName = .generateApiKeySuccess + let apiKey: [String: String] + + enum TypeName: String, Codable { + case generateApiKeySuccess = "GenerateApiKeySuccess" + } + } +} + +extension Objects.GenerateApiKeySuccess: Decodable { + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: DynamicCodingKeys.self) + + var map = HashMap() + for codingKey in container.allKeys { + if codingKey.isTypenameKey { continue } + + let alias = codingKey.stringValue + let field = GraphQLField.getFieldNameFromAlias(alias) + + switch field { + case "apiKey": + if let value = try container.decode(String?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + default: + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Unknown key \(field)." + ) + ) + } + } + + apiKey = map["apiKey"] + } +} + +extension Fields where TypeLock == Objects.GenerateApiKeySuccess { + func apiKey() throws -> String { + let field = GraphQLField.leaf( + name: "apiKey", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.apiKey[field.alias!] { + return data + } + throw HttpError.badpayload + case .mocking: + return String.mockValue + } + } +} + +extension Selection where TypeLock == Never, Type == Never { + typealias GenerateApiKeySuccess = Selection +} + +extension Objects { + struct GenerateApiKeyError { + let __typename: TypeName = .generateApiKeyError + let errorCodes: [String: [Enums.GenerateApiKeyErrorCode]] + + enum TypeName: String, Codable { + case generateApiKeyError = "GenerateApiKeyError" + } + } +} + +extension Objects.GenerateApiKeyError: Decodable { + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: DynamicCodingKeys.self) + + var map = HashMap() + for codingKey in container.allKeys { + if codingKey.isTypenameKey { continue } + + let alias = codingKey.stringValue + let field = GraphQLField.getFieldNameFromAlias(alias) + + switch field { + case "errorCodes": + if let value = try container.decode([Enums.GenerateApiKeyErrorCode]?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + default: + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Unknown key \(field)." + ) + ) + } + } + + errorCodes = map["errorCodes"] + } +} + +extension Fields where TypeLock == Objects.GenerateApiKeyError { + func errorCodes() throws -> [Enums.GenerateApiKeyErrorCode] { + let field = GraphQLField.leaf( + name: "errorCodes", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.errorCodes[field.alias!] { + return data + } + throw HttpError.badpayload + case .mocking: + return [] + } + } +} + +extension Selection where TypeLock == Never, Type == Never { + typealias GenerateApiKeyError = Selection +} + +extension Objects { + struct SearchItem { + let __typename: TypeName = .searchItem + let annotation: [String: String] + let author: [String: String] + let contentReader: [String: Enums.ContentReader] + let createdAt: [String: DateTime] + let description: [String: String] + let id: [String: String] + let image: [String: String] + let isArchived: [String: Bool] + let labels: [String: [Objects.Label]] + let originalArticleUrl: [String: String] + let ownedByViewer: [String: Bool] + let pageId: [String: String] + let pageType: [String: Enums.PageType] + let publishedAt: [String: DateTime] + let quote: [String: String] + let readingProgressAnchorIndex: [String: Int] + let readingProgressPercent: [String: Double] + let shortId: [String: String] + let slug: [String: String] + let title: [String: String] + let uploadFileId: [String: String] + let url: [String: String] + + enum TypeName: String, Codable { + case searchItem = "SearchItem" + } + } +} + +extension Objects.SearchItem: Decodable { + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: DynamicCodingKeys.self) + + var map = HashMap() + for codingKey in container.allKeys { + if codingKey.isTypenameKey { continue } + + let alias = codingKey.stringValue + let field = GraphQLField.getFieldNameFromAlias(alias) + + switch field { + case "annotation": + if let value = try container.decode(String?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + case "author": + if let value = try container.decode(String?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + case "contentReader": + if let value = try container.decode(Enums.ContentReader?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + case "createdAt": + if let value = try container.decode(DateTime?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + case "description": + if let value = try container.decode(String?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + case "id": + if let value = try container.decode(String?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + case "image": + if let value = try container.decode(String?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + case "isArchived": + if let value = try container.decode(Bool?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + case "labels": + if let value = try container.decode([Objects.Label]?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + case "originalArticleUrl": + if let value = try container.decode(String?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + case "ownedByViewer": + if let value = try container.decode(Bool?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + case "pageId": + if let value = try container.decode(String?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + case "pageType": + if let value = try container.decode(Enums.PageType?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + case "publishedAt": + if let value = try container.decode(DateTime?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + case "quote": + if let value = try container.decode(String?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + case "readingProgressAnchorIndex": + if let value = try container.decode(Int?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + case "readingProgressPercent": + if let value = try container.decode(Double?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + case "shortId": + if let value = try container.decode(String?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + case "slug": + if let value = try container.decode(String?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + case "title": + if let value = try container.decode(String?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + case "uploadFileId": + if let value = try container.decode(String?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + case "url": + if let value = try container.decode(String?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + default: + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Unknown key \(field)." + ) + ) + } + } + + annotation = map["annotation"] + author = map["author"] + contentReader = map["contentReader"] + createdAt = map["createdAt"] + description = map["description"] + id = map["id"] + image = map["image"] + isArchived = map["isArchived"] + labels = map["labels"] + originalArticleUrl = map["originalArticleUrl"] + ownedByViewer = map["ownedByViewer"] + pageId = map["pageId"] + pageType = map["pageType"] + publishedAt = map["publishedAt"] + quote = map["quote"] + readingProgressAnchorIndex = map["readingProgressAnchorIndex"] + readingProgressPercent = map["readingProgressPercent"] + shortId = map["shortId"] + slug = map["slug"] + title = map["title"] + uploadFileId = map["uploadFileId"] + url = map["url"] + } +} + +extension Fields where TypeLock == Objects.SearchItem { + func id() throws -> String { + let field = GraphQLField.leaf( + name: "id", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.id[field.alias!] { + return data + } + throw HttpError.badpayload + case .mocking: + return String.mockValue + } + } + + func title() throws -> String { + let field = GraphQLField.leaf( + name: "title", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.title[field.alias!] { + return data + } + throw HttpError.badpayload + case .mocking: + return String.mockValue + } + } + + func slug() throws -> String { + let field = GraphQLField.leaf( + name: "slug", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.slug[field.alias!] { + return data + } + throw HttpError.badpayload + case .mocking: + return String.mockValue + } + } + + func url() throws -> String { + let field = GraphQLField.leaf( + name: "url", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.url[field.alias!] { + return data + } + throw HttpError.badpayload + case .mocking: + return String.mockValue + } + } + + func pageType() throws -> Enums.PageType { + let field = GraphQLField.leaf( + name: "pageType", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.pageType[field.alias!] { + return data + } + throw HttpError.badpayload + case .mocking: + return Enums.PageType.allCases.first! + } + } + + func contentReader() throws -> Enums.ContentReader { + let field = GraphQLField.leaf( + name: "contentReader", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.contentReader[field.alias!] { + return data + } + throw HttpError.badpayload + case .mocking: + return Enums.ContentReader.allCases.first! + } + } + + func createdAt() throws -> DateTime { + let field = GraphQLField.leaf( + name: "createdAt", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.createdAt[field.alias!] { + return data + } + throw HttpError.badpayload + case .mocking: + return DateTime.mockValue + } + } + + func isArchived() throws -> Bool { + let field = GraphQLField.leaf( + name: "isArchived", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.isArchived[field.alias!] { + return data + } + throw HttpError.badpayload + case .mocking: + return Bool.mockValue + } + } + + func readingProgressPercent() throws -> Double? { + let field = GraphQLField.leaf( + name: "readingProgressPercent", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + return data.readingProgressPercent[field.alias!] + case .mocking: + return nil + } + } + + func readingProgressAnchorIndex() throws -> Int? { + let field = GraphQLField.leaf( + name: "readingProgressAnchorIndex", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + return data.readingProgressAnchorIndex[field.alias!] + case .mocking: + return nil + } + } + + func author() throws -> String? { + let field = GraphQLField.leaf( + name: "author", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + return data.author[field.alias!] + case .mocking: + return nil + } + } + + func image() throws -> String? { + let field = GraphQLField.leaf( + name: "image", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + return data.image[field.alias!] + case .mocking: + return nil + } + } + + func description() throws -> String? { + let field = GraphQLField.leaf( + name: "description", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + return data.description[field.alias!] + case .mocking: + return nil + } + } + + func publishedAt() throws -> DateTime? { + let field = GraphQLField.leaf( + name: "publishedAt", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + return data.publishedAt[field.alias!] + case .mocking: + return nil + } + } + + func ownedByViewer() throws -> Bool? { + let field = GraphQLField.leaf( + name: "ownedByViewer", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + return data.ownedByViewer[field.alias!] + case .mocking: + return nil + } + } + + func originalArticleUrl() throws -> String? { + let field = GraphQLField.leaf( + name: "originalArticleUrl", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + return data.originalArticleUrl[field.alias!] + case .mocking: + return nil + } + } + + func uploadFileId() throws -> String? { + let field = GraphQLField.leaf( + name: "uploadFileId", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + return data.uploadFileId[field.alias!] + case .mocking: + return nil + } + } + + func pageId() throws -> String? { + let field = GraphQLField.leaf( + name: "pageId", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + return data.pageId[field.alias!] + case .mocking: + return nil + } + } + + func shortId() throws -> String? { + let field = GraphQLField.leaf( + name: "shortId", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + return data.shortId[field.alias!] + case .mocking: + return nil + } + } + + func quote() throws -> String? { + let field = GraphQLField.leaf( + name: "quote", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + return data.quote[field.alias!] + case .mocking: + return nil + } + } + + func annotation() throws -> String? { + let field = GraphQLField.leaf( + name: "annotation", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + return data.annotation[field.alias!] + case .mocking: + return nil + } + } + + func labels(selection: Selection) throws -> Type { + let field = GraphQLField.composite( + name: "labels", + arguments: [], + selection: selection.selection + ) + select(field) + + switch response { + case let .decoding(data): + return try selection.decode(data: data.labels[field.alias!]) + case .mocking: + return selection.mock() + } + } +} + +extension Selection where TypeLock == Never, Type == Never { + typealias SearchItem = Selection +} + +extension Objects { + struct SearchItemEdge { + let __typename: TypeName = .searchItemEdge + let cursor: [String: String] + let node: [String: Objects.SearchItem] + + enum TypeName: String, Codable { + case searchItemEdge = "SearchItemEdge" + } + } +} + +extension Objects.SearchItemEdge: Decodable { + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: DynamicCodingKeys.self) + + var map = HashMap() + for codingKey in container.allKeys { + if codingKey.isTypenameKey { continue } + + let alias = codingKey.stringValue + let field = GraphQLField.getFieldNameFromAlias(alias) + + switch field { + case "cursor": + if let value = try container.decode(String?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + case "node": + if let value = try container.decode(Objects.SearchItem?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + default: + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Unknown key \(field)." + ) + ) + } + } + + cursor = map["cursor"] + node = map["node"] + } +} + +extension Fields where TypeLock == Objects.SearchItemEdge { + func cursor() throws -> String { + let field = GraphQLField.leaf( + name: "cursor", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.cursor[field.alias!] { + return data + } + throw HttpError.badpayload + case .mocking: + return String.mockValue + } + } + + func node(selection: Selection) throws -> Type { + let field = GraphQLField.composite( + name: "node", + arguments: [], + selection: selection.selection + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.node[field.alias!] { + return try selection.decode(data: data) + } + throw HttpError.badpayload + case .mocking: + return selection.mock() + } + } +} + +extension Selection where TypeLock == Never, Type == Never { + typealias SearchItemEdge = Selection +} + +extension Objects { + struct SearchSuccess { + let __typename: TypeName = .searchSuccess + let edges: [String: [Objects.SearchItemEdge]] + let pageInfo: [String: Objects.PageInfo] + + enum TypeName: String, Codable { + case searchSuccess = "SearchSuccess" + } + } +} + +extension Objects.SearchSuccess: Decodable { + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: DynamicCodingKeys.self) + + var map = HashMap() + for codingKey in container.allKeys { + if codingKey.isTypenameKey { continue } + + let alias = codingKey.stringValue + let field = GraphQLField.getFieldNameFromAlias(alias) + + switch field { + case "edges": + if let value = try container.decode([Objects.SearchItemEdge]?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + case "pageInfo": + if let value = try container.decode(Objects.PageInfo?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + default: + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Unknown key \(field)." + ) + ) + } + } + + edges = map["edges"] + pageInfo = map["pageInfo"] + } +} + +extension Fields where TypeLock == Objects.SearchSuccess { + func edges(selection: Selection) throws -> Type { + let field = GraphQLField.composite( + name: "edges", + arguments: [], + selection: selection.selection + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.edges[field.alias!] { + return try selection.decode(data: data) + } + throw HttpError.badpayload + case .mocking: + return selection.mock() + } + } + + func pageInfo(selection: Selection) throws -> Type { + let field = GraphQLField.composite( + name: "pageInfo", + arguments: [], + selection: selection.selection + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.pageInfo[field.alias!] { + return try selection.decode(data: data) + } + throw HttpError.badpayload + case .mocking: + return selection.mock() + } + } +} + +extension Selection where TypeLock == Never, Type == Never { + typealias SearchSuccess = Selection +} + +extension Objects { + struct SearchError { + let __typename: TypeName = .searchError + let errorCodes: [String: [Enums.SearchErrorCode]] + + enum TypeName: String, Codable { + case searchError = "SearchError" + } + } +} + +extension Objects.SearchError: Decodable { + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: DynamicCodingKeys.self) + + var map = HashMap() + for codingKey in container.allKeys { + if codingKey.isTypenameKey { continue } + + let alias = codingKey.stringValue + let field = GraphQLField.getFieldNameFromAlias(alias) + + switch field { + case "errorCodes": + if let value = try container.decode([Enums.SearchErrorCode]?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + default: + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Unknown key \(field)." + ) + ) + } + } + + errorCodes = map["errorCodes"] + } +} + +extension Fields where TypeLock == Objects.SearchError { + func errorCodes() throws -> [Enums.SearchErrorCode] { + let field = GraphQLField.leaf( + name: "errorCodes", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.errorCodes[field.alias!] { + return data + } + throw HttpError.badpayload + case .mocking: + return [] + } + } +} + +extension Selection where TypeLock == Never, Type == Never { + typealias SearchError = Selection +} + extension Objects { struct Mutation { let __typename: TypeName = .mutation @@ -11539,6 +12549,7 @@ extension Objects { let deleteNewsletterEmail: [String: Unions.DeleteNewsletterEmailResult] let deleteReaction: [String: Unions.DeleteReactionResult] let deleteReminder: [String: Unions.DeleteReminderResult] + let generateApiKey: [String: Unions.GenerateApiKeyResult] let googleLogin: [String: Unions.LoginResult] let googleSignup: [String: Unions.GoogleSignupResult] let logOut: [String: Unions.LogOutResult] @@ -11560,6 +12571,7 @@ extension Objects { let signup: [String: Unions.SignupResult] let updateHighlight: [String: Unions.UpdateHighlightResult] let updateHighlightReply: [String: Unions.UpdateHighlightReplyResult] + let updateLabel: [String: Unions.UpdateLabelResult] let updateLinkShareInfo: [String: Unions.UpdateLinkShareInfoResult] let updateReminder: [String: Unions.UpdateReminderResult] let updateSharedComment: [String: Unions.UpdateSharedCommentResult] @@ -11641,6 +12653,10 @@ extension Objects.Mutation: Decodable { if let value = try container.decode(Unions.DeleteReminderResult?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) } + case "generateApiKey": + if let value = try container.decode(Unions.GenerateApiKeyResult?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } case "googleLogin": if let value = try container.decode(Unions.LoginResult?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) @@ -11725,6 +12741,10 @@ extension Objects.Mutation: Decodable { if let value = try container.decode(Unions.UpdateHighlightReplyResult?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) } + case "updateLabel": + if let value = try container.decode(Unions.UpdateLabelResult?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } case "updateLinkShareInfo": if let value = try container.decode(Unions.UpdateLinkShareInfoResult?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) @@ -11773,6 +12793,7 @@ extension Objects.Mutation: Decodable { deleteNewsletterEmail = map["deleteNewsletterEmail"] deleteReaction = map["deleteReaction"] deleteReminder = map["deleteReminder"] + generateApiKey = map["generateApiKey"] googleLogin = map["googleLogin"] googleSignup = map["googleSignup"] logOut = map["logOut"] @@ -11794,6 +12815,7 @@ extension Objects.Mutation: Decodable { signup = map["signup"] updateHighlight = map["updateHighlight"] updateHighlightReply = map["updateHighlightReply"] + updateLabel = map["updateLabel"] updateLinkShareInfo = map["updateLinkShareInfo"] updateReminder = map["updateReminder"] updateSharedComment = map["updateSharedComment"] @@ -12507,6 +13529,25 @@ extension Fields where TypeLock == Objects.Mutation { } } + func updateLabel(input: InputObjects.UpdateLabelInput, selection: Selection) throws -> Type { + let field = GraphQLField.composite( + name: "updateLabel", + arguments: [Argument(name: "input", type: "UpdateLabelInput!", value: input)], + selection: selection.selection + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.updateLabel[field.alias!] { + return try selection.decode(data: data) + } + throw HttpError.badpayload + case .mocking: + return selection.mock() + } + } + func deleteLabel(id: String, selection: Selection) throws -> Type { let field = GraphQLField.composite( name: "deleteLabel", @@ -12582,6 +13623,25 @@ extension Fields where TypeLock == Objects.Mutation { return selection.mock() } } + + func generateApiKey(scope: OptionalArgument = .absent(), selection: Selection) throws -> Type { + let field = GraphQLField.composite( + name: "generateApiKey", + arguments: [Argument(name: "scope", type: "String", value: scope)], + selection: selection.selection + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.generateApiKey[field.alias!] { + return try selection.decode(data: data) + } + throw HttpError.badpayload + case .mocking: + return selection.mock() + } + } } extension Selection where TypeLock == Never, Type == Never { @@ -12603,6 +13663,7 @@ extension Objects { let me: [String: Objects.User] let newsletterEmails: [String: Unions.NewsletterEmailsResult] let reminder: [String: Unions.ReminderResult] + let search: [String: Unions.SearchResult] let sharedArticle: [String: Unions.SharedArticleResult] let user: [String: Unions.UserResult] let users: [String: Unions.UsersResult] @@ -12674,6 +13735,10 @@ extension Objects.Query: Decodable { if let value = try container.decode(Unions.ReminderResult?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) } + case "search": + if let value = try container.decode(Unions.SearchResult?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } case "sharedArticle": if let value = try container.decode(Unions.SharedArticleResult?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) @@ -12712,6 +13777,7 @@ extension Objects.Query: Decodable { me = map["me"] newsletterEmails = map["newsletterEmails"] reminder = map["reminder"] + search = map["search"] sharedArticle = map["sharedArticle"] user = map["user"] users = map["users"] @@ -13015,6 +14081,25 @@ extension Fields where TypeLock == Objects.Query { return selection.mock() } } + + func search(after: OptionalArgument = .absent(), first: OptionalArgument = .absent(), query: OptionalArgument = .absent(), selection: Selection) throws -> Type { + let field = GraphQLField.composite( + name: "search", + arguments: [Argument(name: "after", type: "String", value: after), Argument(name: "first", type: "Int", value: first), Argument(name: "query", type: "String", value: query)], + selection: selection.selection + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.search[field.alias!] { + return try selection.decode(data: data) + } + throw HttpError.badpayload + case .mocking: + return selection.mock() + } + } } extension Selection where TypeLock == Never, Type == Never { @@ -16658,6 +17743,80 @@ extension Selection where TypeLock == Never, Type == Never { typealias DeleteLabelResult = Selection } +extension Unions { + struct UpdateLabelResult { + let __typename: TypeName + let errorCodes: [String: [Enums.UpdateLabelErrorCode]] + let label: [String: Objects.Label] + + enum TypeName: String, Codable { + case updateLabelSuccess = "UpdateLabelSuccess" + case updateLabelError = "UpdateLabelError" + } + } +} + +extension Unions.UpdateLabelResult: Decodable { + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: DynamicCodingKeys.self) + + var map = HashMap() + for codingKey in container.allKeys { + if codingKey.isTypenameKey { continue } + + let alias = codingKey.stringValue + let field = GraphQLField.getFieldNameFromAlias(alias) + + switch field { + case "errorCodes": + if let value = try container.decode([Enums.UpdateLabelErrorCode]?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + case "label": + if let value = try container.decode(Objects.Label?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + default: + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Unknown key \(field)." + ) + ) + } + } + + __typename = try container.decode(TypeName.self, forKey: DynamicCodingKeys(stringValue: "__typename")!) + + errorCodes = map["errorCodes"] + label = map["label"] + } +} + +extension Fields where TypeLock == Unions.UpdateLabelResult { + func on(updateLabelSuccess: Selection, updateLabelError: Selection) throws -> Type { + select([GraphQLField.fragment(type: "UpdateLabelSuccess", selection: updateLabelSuccess.selection), GraphQLField.fragment(type: "UpdateLabelError", selection: updateLabelError.selection)]) + + switch response { + case let .decoding(data): + switch data.__typename { + case .updateLabelSuccess: + let data = Objects.UpdateLabelSuccess(label: data.label) + return try updateLabelSuccess.decode(data: data) + case .updateLabelError: + let data = Objects.UpdateLabelError(errorCodes: data.errorCodes) + return try updateLabelError.decode(data: data) + } + case .mocking: + return updateLabelSuccess.mock() + } + } +} + +extension Selection where TypeLock == Never, Type == Never { + typealias UpdateLabelResult = Selection +} + extension Unions { struct SignupResult { let __typename: TypeName @@ -16806,6 +17965,160 @@ extension Selection where TypeLock == Never, Type == Never { typealias SetLabelsResult = Selection } +extension Unions { + struct GenerateApiKeyResult { + let __typename: TypeName + let apiKey: [String: String] + let errorCodes: [String: [Enums.GenerateApiKeyErrorCode]] + + enum TypeName: String, Codable { + case generateApiKeySuccess = "GenerateApiKeySuccess" + case generateApiKeyError = "GenerateApiKeyError" + } + } +} + +extension Unions.GenerateApiKeyResult: Decodable { + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: DynamicCodingKeys.self) + + var map = HashMap() + for codingKey in container.allKeys { + if codingKey.isTypenameKey { continue } + + let alias = codingKey.stringValue + let field = GraphQLField.getFieldNameFromAlias(alias) + + switch field { + case "apiKey": + if let value = try container.decode(String?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + case "errorCodes": + if let value = try container.decode([Enums.GenerateApiKeyErrorCode]?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + default: + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Unknown key \(field)." + ) + ) + } + } + + __typename = try container.decode(TypeName.self, forKey: DynamicCodingKeys(stringValue: "__typename")!) + + apiKey = map["apiKey"] + errorCodes = map["errorCodes"] + } +} + +extension Fields where TypeLock == Unions.GenerateApiKeyResult { + func on(generateApiKeySuccess: Selection, generateApiKeyError: Selection) throws -> Type { + select([GraphQLField.fragment(type: "GenerateApiKeySuccess", selection: generateApiKeySuccess.selection), GraphQLField.fragment(type: "GenerateApiKeyError", selection: generateApiKeyError.selection)]) + + switch response { + case let .decoding(data): + switch data.__typename { + case .generateApiKeySuccess: + let data = Objects.GenerateApiKeySuccess(apiKey: data.apiKey) + return try generateApiKeySuccess.decode(data: data) + case .generateApiKeyError: + let data = Objects.GenerateApiKeyError(errorCodes: data.errorCodes) + return try generateApiKeyError.decode(data: data) + } + case .mocking: + return generateApiKeySuccess.mock() + } + } +} + +extension Selection where TypeLock == Never, Type == Never { + typealias GenerateApiKeyResult = Selection +} + +extension Unions { + struct SearchResult { + let __typename: TypeName + let edges: [String: [Objects.SearchItemEdge]] + let errorCodes: [String: [Enums.SearchErrorCode]] + let pageInfo: [String: Objects.PageInfo] + + enum TypeName: String, Codable { + case searchSuccess = "SearchSuccess" + case searchError = "SearchError" + } + } +} + +extension Unions.SearchResult: Decodable { + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: DynamicCodingKeys.self) + + var map = HashMap() + for codingKey in container.allKeys { + if codingKey.isTypenameKey { continue } + + let alias = codingKey.stringValue + let field = GraphQLField.getFieldNameFromAlias(alias) + + switch field { + case "edges": + if let value = try container.decode([Objects.SearchItemEdge]?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + case "errorCodes": + if let value = try container.decode([Enums.SearchErrorCode]?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + case "pageInfo": + if let value = try container.decode(Objects.PageInfo?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + default: + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Unknown key \(field)." + ) + ) + } + } + + __typename = try container.decode(TypeName.self, forKey: DynamicCodingKeys(stringValue: "__typename")!) + + edges = map["edges"] + errorCodes = map["errorCodes"] + pageInfo = map["pageInfo"] + } +} + +extension Fields where TypeLock == Unions.SearchResult { + func on(searchSuccess: Selection, searchError: Selection) throws -> Type { + select([GraphQLField.fragment(type: "SearchSuccess", selection: searchSuccess.selection), GraphQLField.fragment(type: "SearchError", selection: searchError.selection)]) + + switch response { + case let .decoding(data): + switch data.__typename { + case .searchSuccess: + let data = Objects.SearchSuccess(edges: data.edges, pageInfo: data.pageInfo) + return try searchSuccess.decode(data: data) + case .searchError: + let data = Objects.SearchError(errorCodes: data.errorCodes) + return try searchError.decode(data: data) + } + case .mocking: + return searchSuccess.mock() + } + } +} + +extension Selection where TypeLock == Never, Type == Never { + typealias SearchResult = Selection +} + // MARK: - Enums enum Enums {} @@ -16957,6 +18270,8 @@ extension Enums { case website = "WEBSITE" + case highlights = "HIGHLIGHTS" + case unknown = "UNKNOWN" } } @@ -17421,6 +18736,19 @@ extension Enums { } } +extension Enums { + /// UpdateLabelErrorCode + enum UpdateLabelErrorCode: String, CaseIterable, Codable { + case unauthorized = "UNAUTHORIZED" + + case badRequest = "BAD_REQUEST" + + case notFound = "NOT_FOUND" + + case forbidden = "FORBIDDEN" + } +} + extension Enums { /// SetLabelsErrorCode enum SetLabelsErrorCode: String, CaseIterable, Codable { @@ -17432,6 +18760,20 @@ extension Enums { } } +extension Enums { + /// GenerateApiKeyErrorCode + enum GenerateApiKeyErrorCode: String, CaseIterable, Codable { + case badRequest = "BAD_REQUEST" + } +} + +extension Enums { + /// SearchErrorCode + enum SearchErrorCode: String, CaseIterable, Codable { + case unauthorized = "UNAUTHORIZED" + } +} + // MARK: - Input Objects enum InputObjects {} @@ -18293,6 +19635,33 @@ extension InputObjects { } } +extension InputObjects { + struct UpdateLabelInput: Encodable, Hashable { + var labelId: String + + var color: String + + var description: OptionalArgument = .absent() + + var name: String + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(labelId, forKey: .labelId) + try container.encode(color, forKey: .color) + if description.hasValue { try container.encode(description, forKey: .description) } + try container.encode(name, forKey: .name) + } + + enum CodingKeys: String, CodingKey { + case labelId + case color + case description + case name + } + } +} + extension InputObjects { struct LoginInput: Encodable, Hashable { var password: String @@ -18349,18 +19718,18 @@ extension InputObjects { extension InputObjects { struct SetLabelsInput: Encodable, Hashable { - var linkId: String + var pageId: String var labelIds: [String] func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(linkId, forKey: .linkId) + try container.encode(pageId, forKey: .pageId) try container.encode(labelIds, forKey: .labelIds) } enum CodingKeys: String, CodingKey { - case linkId + case pageId case labelIds } } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateArticleLabelsPublisher.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateArticleLabelsPublisher.swift index fce103d6a..0809b693a 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateArticleLabelsPublisher.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateArticleLabelsPublisher.swift @@ -20,7 +20,7 @@ public extension DataService { let mutation = Selection.Mutation { try $0.setLabels( input: InputObjects.SetLabelsInput( - linkId: itemID, + pageId: itemID, labelIds: labelIDs ), selection: selection diff --git a/apple/swiftgraphql.yml b/apple/swiftgraphql.yml index 9a2b8bd2b..dd96fa3fc 100644 --- a/apple/swiftgraphql.yml +++ b/apple/swiftgraphql.yml @@ -4,10 +4,12 @@ scalars: SanitizedString_undefined_15: String SanitizedString_undefined_40: String SanitizedString_undefined_50: String + SanitizedString_undefined_64: String SanitizedString_undefined_95: String + SanitizedString_undefined_100: String SanitizedString_undefined_300: String SanitizedString_undefined_400: String SanitizedString_undefined_2000: String SanitizedString_undefined_4000: String SanitizedString_undefined_8000: String - SanitizedString_undefined_undefined: String \ No newline at end of file + SanitizedString_undefined_undefined: String From d92b526583117d9ca7d115ae4dabe2d9c2c7a2c7 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Tue, 12 Apr 2022 18:09:43 -0700 Subject: [PATCH 082/125] Give the labels control a max height --- packages/web/components/templates/article/EditLabelsControl.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/components/templates/article/EditLabelsControl.tsx b/packages/web/components/templates/article/EditLabelsControl.tsx index 77f56a526..ead74eb03 100644 --- a/packages/web/components/templates/article/EditLabelsControl.tsx +++ b/packages/web/components/templates/article/EditLabelsControl.tsx @@ -300,7 +300,7 @@ export function EditLabelsControl(props: EditLabelsControlProps): JSX.Element { css={{ p: '0', width: '100%', - maxHeight: '80%', + maxHeight: '380px', }}>
Date: Tue, 12 Apr 2022 18:10:43 -0700 Subject: [PATCH 083/125] Fix tooltip tags -> labels --- .../web/components/templates/article/ArticleActionsMenu.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/components/templates/article/ArticleActionsMenu.tsx b/packages/web/components/templates/article/ArticleActionsMenu.tsx index 410ae3080..64aa0b20c 100644 --- a/packages/web/components/templates/article/ArticleActionsMenu.tsx +++ b/packages/web/components/templates/article/ArticleActionsMenu.tsx @@ -95,7 +95,7 @@ export function ArticleActionsMenu(props: ArticleActionsMenuProps): JSX.Element layout={props.layout} triggerElement={ From 11429dddc060d00c6539ae319ff7d0f494480075 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Tue, 12 Apr 2022 20:08:18 -0700 Subject: [PATCH 084/125] Dont autofocus the text input on mobile On mobile we dont want to auto focus because the keyboard opens and takes up most of the screen space. --- .../web/components/templates/article/EditLabelsControl.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/web/components/templates/article/EditLabelsControl.tsx b/packages/web/components/templates/article/EditLabelsControl.tsx index ead74eb03..aa62b492e 100644 --- a/packages/web/components/templates/article/EditLabelsControl.tsx +++ b/packages/web/components/templates/article/EditLabelsControl.tsx @@ -50,14 +50,14 @@ function Header(props: HeaderProps): JSX.Element { const inputRef = useRef(null) useEffect(() => { - if (props.focused && inputRef.current) { + if (!isTouchScreenDevice() && props.focused && inputRef.current) { inputRef.current.focus() } }, [props.focused]) return ( - Date: Tue, 12 Apr 2022 20:09:03 -0700 Subject: [PATCH 085/125] Set the link colour. Safari needs this --- packages/web/components/templates/article/EditLabelsControl.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/web/components/templates/article/EditLabelsControl.tsx b/packages/web/components/templates/article/EditLabelsControl.tsx index aa62b492e..96e297242 100644 --- a/packages/web/components/templates/article/EditLabelsControl.tsx +++ b/packages/web/components/templates/article/EditLabelsControl.tsx @@ -167,6 +167,7 @@ function EditLabelsButtonFooter(props: EditLabelsButtonFooterProps): JSX.Element css={{ width: '100%', height: '42px', bg: props.focused ? '$grayBgActive' : 'unset', + color: theme.colors.grayText.toString(), 'a:link': { textDecoration: 'none', }, From a728bcf88699006d00a967691b20d0149d575b5c Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Tue, 12 Apr 2022 20:36:00 -0700 Subject: [PATCH 086/125] Improve sizing of scrollable content area on mobile --- .../web/components/templates/article/EditLabelsControl.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/web/components/templates/article/EditLabelsControl.tsx b/packages/web/components/templates/article/EditLabelsControl.tsx index 96e297242..6eff4bacb 100644 --- a/packages/web/components/templates/article/EditLabelsControl.tsx +++ b/packages/web/components/templates/article/EditLabelsControl.tsx @@ -301,14 +301,17 @@ export function EditLabelsControl(props: EditLabelsControlProps): JSX.Element { css={{ p: '0', width: '100%', - maxHeight: '380px', }}>
setFocusedIndex(undefined)} setFilterText={setFilterText} filterText={filterText} /> - + {filteredLabels && filteredLabels.map((label, idx) => ( Date: Tue, 12 Apr 2022 20:50:06 -0700 Subject: [PATCH 087/125] Update to use pageId instead of linkId for SetLabels API --- packages/web/lib/networking/mutations/setLabelsMutation.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/web/lib/networking/mutations/setLabelsMutation.ts b/packages/web/lib/networking/mutations/setLabelsMutation.ts index a7d3aa0bb..6953bcc45 100644 --- a/packages/web/lib/networking/mutations/setLabelsMutation.ts +++ b/packages/web/lib/networking/mutations/setLabelsMutation.ts @@ -12,7 +12,7 @@ type SetLabels = { } export async function setLabelsMutation( - linkId: string, + pageId: string, labelIds: string[] ): Promise { const mutation = gql` @@ -32,7 +32,7 @@ export async function setLabelsMutation( ` try { - const data = await gqlFetcher(mutation, { input: { linkId, labelIds } }) as SetLabelsResult + const data = await gqlFetcher(mutation, { input: { pageId, labelIds } }) as SetLabelsResult return data.errorCodes ? undefined : data.setLabels.labels } catch (error) { console.log('SetLabelsOutput error', error) From 928ddd69a94089ec6983783799f5695185161e18 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Wed, 13 Apr 2022 12:17:12 +0800 Subject: [PATCH 088/125] validate color input in label as rgb hex value --- packages/api/src/directives.ts | 13 ++++++++++--- packages/api/src/generated/graphql.ts | 1 + packages/api/src/generated/schema.graphql | 2 +- packages/api/src/scalars.ts | 9 +++++++-- packages/api/src/schema.ts | 3 ++- 5 files changed, 21 insertions(+), 7 deletions(-) diff --git a/packages/api/src/directives.ts b/packages/api/src/directives.ts index fd54cbb66..b4a32bf5e 100644 --- a/packages/api/src/directives.ts +++ b/packages/api/src/directives.ts @@ -1,4 +1,4 @@ -import { mapSchema, getDirective, MapperKind } from '@graphql-tools/utils' +import { getDirective, MapperKind, mapSchema } from '@graphql-tools/utils' import { GraphQLNonNull, GraphQLScalarType, GraphQLSchema } from 'graphql' import { SanitizedString } from './scalars' @@ -14,19 +14,26 @@ export const sanitizeDirectiveTransformer = (schema: GraphQLSchema) => { const allowedTags = sanitizeDirective[0].allowedTags as | string[] | undefined + const pattern = sanitizeDirective[0].pattern as RegExp | undefined if ( fieldConfig.type instanceof GraphQLNonNull && fieldConfig.type.ofType instanceof GraphQLScalarType ) { fieldConfig.type = new GraphQLNonNull( - new SanitizedString(fieldConfig.type.ofType, allowedTags, maxLength) + new SanitizedString( + fieldConfig.type.ofType, + allowedTags, + maxLength, + pattern + ) ) } else if (fieldConfig.type instanceof GraphQLScalarType) { fieldConfig.type = new SanitizedString( fieldConfig.type, allowedTags, - maxLength + maxLength, + pattern ) } else { // eslint-disable-next-line @typescript-eslint/restrict-template-expressions diff --git a/packages/api/src/generated/graphql.ts b/packages/api/src/generated/graphql.ts index d14428b39..d1e733acc 100644 --- a/packages/api/src/generated/graphql.ts +++ b/packages/api/src/generated/graphql.ts @@ -2501,6 +2501,7 @@ export type ResolversParentTypes = { export type SanitizeDirectiveArgs = { allowedTags?: Maybe>>; maxLength?: Maybe; + pattern?: Maybe; }; export type SanitizeDirectiveResolver = DirectiveResolverFn; diff --git a/packages/api/src/generated/schema.graphql b/packages/api/src/generated/schema.graphql index e3c5630a8..2f77183e1 100644 --- a/packages/api/src/generated/schema.graphql +++ b/packages/api/src/generated/schema.graphql @@ -1,4 +1,4 @@ -directive @sanitize(allowedTags: [String], maxLength: Int) on INPUT_FIELD_DEFINITION +directive @sanitize(allowedTags: [String], maxLength: Int, pattern: String) on INPUT_FIELD_DEFINITION type ArchiveLinkError { errorCodes: [ArchiveLinkErrorCode!]! diff --git a/packages/api/src/scalars.ts b/packages/api/src/scalars.ts index 2be928e6e..454b7c38e 100644 --- a/packages/api/src/scalars.ts +++ b/packages/api/src/scalars.ts @@ -8,10 +8,11 @@ export class SanitizedString extends GraphQLScalarType { constructor( type: GraphQLScalarType, allowedTags?: string[], - maxLength?: number + maxLength?: number, + pattern?: RegExp ) { super({ - name: `SanitizedString_${allowedTags}_${maxLength}`, + name: `SanitizedString_${allowedTags}_${maxLength}_${pattern}`, description: 'Source string that was sanitized', serialize(value: string) { @@ -24,6 +25,8 @@ export class SanitizedString extends GraphQLScalarType { throw new Error( `Specified value cannot be longer than ${maxLength} characters` ) + } else if (pattern && !pattern.test(value)) { + throw new Error(`Specified value does not match pattern`) } return sanitize(value, { allowedTags: allowedTags || [] }) }, @@ -35,6 +38,8 @@ export class SanitizedString extends GraphQLScalarType { throw new Error( `Specified value cannot be longer than ${maxLength} characters` ) + } else if (pattern && !pattern.test(value)) { + throw new Error(`Specified value does not match pattern`) } return sanitize(value, { allowedTags: allowedTags || [] }) }, diff --git a/packages/api/src/schema.ts b/packages/api/src/schema.ts index 43fa75719..08def9c86 100755 --- a/packages/api/src/schema.ts +++ b/packages/api/src/schema.ts @@ -8,6 +8,7 @@ const schema = gql` directive @sanitize( allowedTags: [String] maxLength: Int + pattern: String ) on INPUT_FIELD_DEFINITION enum SortOrder { @@ -1273,7 +1274,7 @@ const schema = gql` input CreateLabelInput { name: String! @sanitize(maxLength: 64) - color: String! + color: String! @sanitize(pattern: "^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$") description: String @sanitize(maxLength: 100) } From 3b831fecb80d6c9bb2fee6012ea7299a192064c2 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Wed, 13 Apr 2022 12:20:41 +0800 Subject: [PATCH 089/125] test both max length and pattern if declared --- packages/api/src/scalars.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/api/src/scalars.ts b/packages/api/src/scalars.ts index 454b7c38e..2f0de333a 100644 --- a/packages/api/src/scalars.ts +++ b/packages/api/src/scalars.ts @@ -25,7 +25,8 @@ export class SanitizedString extends GraphQLScalarType { throw new Error( `Specified value cannot be longer than ${maxLength} characters` ) - } else if (pattern && !pattern.test(value)) { + } + if (pattern && !pattern.test(value)) { throw new Error(`Specified value does not match pattern`) } return sanitize(value, { allowedTags: allowedTags || [] }) @@ -38,7 +39,8 @@ export class SanitizedString extends GraphQLScalarType { throw new Error( `Specified value cannot be longer than ${maxLength} characters` ) - } else if (pattern && !pattern.test(value)) { + } + if (pattern && !pattern.test(value)) { throw new Error(`Specified value does not match pattern`) } return sanitize(value, { allowedTags: allowedTags || [] }) From 5a4e4baae6e6a71163cced7b0314eb08bb036f97 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Tue, 12 Apr 2022 21:49:48 -0700 Subject: [PATCH 090/125] Handle changes to createLabel response. Trim label names --- packages/web/pages/settings/labels.tsx | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/web/pages/settings/labels.tsx b/packages/web/pages/settings/labels.tsx index 2c144f55a..d03e5eec9 100644 --- a/packages/web/pages/settings/labels.tsx +++ b/packages/web/pages/settings/labels.tsx @@ -183,18 +183,14 @@ export default function LabelsPage(): JSX.Element { async function createLabel(): Promise { const res = await createLabelMutation( - nameInputText, + nameInputText.trim(), labelColorHex.value, descriptionInputText ) if (res) { - if (res.createLabel.errorCodes && res.createLabel.errorCodes.length > 0) { - showErrorToast(res.createLabel.errorCodes[0]) - } else { - showSuccessToast('Label created', { position: 'bottom-right' }) - resetLabelState() - revalidate() - } + showSuccessToast('Label created', { position: 'bottom-right' }) + resetLabelState() + revalidate() } else { showErrorToast('Failed to create label') } From 8ba1be456c7aa7cd130f2f3c9bd2382c435f4c85 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Wed, 13 Apr 2022 13:23:21 +0800 Subject: [PATCH 091/125] fix sanitizer invalid name --- packages/api/src/directives.ts | 16 +++++++++------- packages/api/src/scalars.ts | 12 ++++++++---- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/packages/api/src/directives.ts b/packages/api/src/directives.ts index b4a32bf5e..435166539 100644 --- a/packages/api/src/directives.ts +++ b/packages/api/src/directives.ts @@ -5,16 +5,18 @@ import { SanitizedString } from './scalars' export const sanitizeDirectiveTransformer = (schema: GraphQLSchema) => { return mapSchema(schema, { [MapperKind.FIELD]: (fieldConfig) => { - const sanitizeDirective = getDirective(schema, fieldConfig, 'sanitize') - if (!sanitizeDirective || sanitizeDirective.length < 1) { + const sanitizeDirective = getDirective( + schema, + fieldConfig, + 'sanitize' + )?.[0] + if (!sanitizeDirective) { return fieldConfig } - const maxLength = sanitizeDirective[0].maxLength as number | undefined - const allowedTags = sanitizeDirective[0].allowedTags as - | string[] - | undefined - const pattern = sanitizeDirective[0].pattern as RegExp | undefined + const maxLength = sanitizeDirective.maxLength as number | undefined + const allowedTags = sanitizeDirective.allowedTags as string[] | undefined + const pattern = sanitizeDirective.pattern as string | undefined if ( fieldConfig.type instanceof GraphQLNonNull && diff --git a/packages/api/src/scalars.ts b/packages/api/src/scalars.ts index 2f0de333a..22404534f 100644 --- a/packages/api/src/scalars.ts +++ b/packages/api/src/scalars.ts @@ -9,10 +9,14 @@ export class SanitizedString extends GraphQLScalarType { type: GraphQLScalarType, allowedTags?: string[], maxLength?: number, - pattern?: RegExp + pattern?: string ) { super({ - name: `SanitizedString_${allowedTags}_${maxLength}_${pattern}`, + // Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ as per graphql-js + name: `SanitizedString_${allowedTags}_${maxLength}_${pattern}`.replace( + /\W/g, + '' + ), description: 'Source string that was sanitized', serialize(value: string) { @@ -26,7 +30,7 @@ export class SanitizedString extends GraphQLScalarType { `Specified value cannot be longer than ${maxLength} characters` ) } - if (pattern && !pattern.test(value)) { + if (pattern && !new RegExp(pattern).test(value)) { throw new Error(`Specified value does not match pattern`) } return sanitize(value, { allowedTags: allowedTags || [] }) @@ -40,7 +44,7 @@ export class SanitizedString extends GraphQLScalarType { `Specified value cannot be longer than ${maxLength} characters` ) } - if (pattern && !pattern.test(value)) { + if (pattern && !new RegExp(pattern).test(value)) { throw new Error(`Specified value does not match pattern`) } return sanitize(value, { allowedTags: allowedTags || [] }) From a806f55233e220ffbd70fb3fa36f655ea0f38b8a Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Wed, 13 Apr 2022 21:18:58 +0800 Subject: [PATCH 092/125] upload highlight to gcs --- packages/api/src/datalayer/pubsub.ts | 26 ++++++++++++++++++- packages/api/src/elastic/highlights.ts | 18 ++++++++++--- .../src/routers/svc/{pages.ts => upload.ts} | 7 +++-- packages/api/src/server.ts | 4 +-- 4 files changed, 45 insertions(+), 10 deletions(-) rename packages/api/src/routers/svc/{pages.ts => upload.ts} (89%) diff --git a/packages/api/src/datalayer/pubsub.ts b/packages/api/src/datalayer/pubsub.ts index 690e9376b..ba98f2952 100644 --- a/packages/api/src/datalayer/pubsub.ts +++ b/packages/api/src/datalayer/pubsub.ts @@ -2,7 +2,7 @@ import { PubSub } from '@google-cloud/pubsub' import { env } from '../env' import { ReportType } from '../generated/graphql' import express from 'express' -import { Page } from '../elastic/types' +import { Highlight, Page } from '../elastic/types' export const createPubSubClient = (): PubsubClient => { const client = new PubSub() @@ -49,6 +49,24 @@ export const createPubSubClient = (): PubsubClient => { pageDeleted: (id: string, userId: string): Promise => { return publish('pageDeleted', Buffer.from(JSON.stringify({ id, userId }))) }, + highlightCreated: (highlight: Highlight): Promise => { + return publish('highlightCreated', Buffer.from(JSON.stringify(highlight))) + }, + highlightUpdated: ( + highlight: Partial, + userId: string + ): Promise => { + return publish( + 'highlightUpdated', + Buffer.from(JSON.stringify({ ...highlight, userId })) + ) + }, + highlightDeleted: (id: string, userId: string): Promise => { + return publish( + 'highlightDeleted', + Buffer.from(JSON.stringify({ id, userId })) + ) + }, reportSubmitted: ( submitterId: string, itemUrl: string, @@ -75,6 +93,12 @@ export interface PubsubClient { pageCreated: (page: Page) => Promise pageUpdated: (page: Partial, userId: string) => Promise pageDeleted: (id: string, userId: string) => Promise + highlightCreated: (highlight: Highlight) => Promise + highlightUpdated: ( + highlight: Partial, + userId: string + ) => Promise + highlightDeleted: (id: string, userId: string) => Promise reportSubmitted( submitterId: string | undefined, itemUrl: string, diff --git a/packages/api/src/elastic/highlights.ts b/packages/api/src/elastic/highlights.ts index 5a499237e..7bd6a6adf 100644 --- a/packages/api/src/elastic/highlights.ts +++ b/packages/api/src/elastic/highlights.ts @@ -35,7 +35,11 @@ export const addHighlightToPage = async ( retry_on_conflict: 3, }) - return body.result === 'updated' + if (body.result !== 'updated') return false + + await ctx.pubsub.highlightCreated(highlight) + + return true } catch (e) { if ( e instanceof ResponseError && @@ -125,7 +129,11 @@ export const deleteHighlight = async ( refresh: ctx.refresh, }) - return !!body.updated + if (body.result !== 'updated') return false + + await ctx.pubsub.highlightDeleted(highlightId, ctx.uid) + + return true } catch (e) { console.error('failed to delete a highlight in elastic', e) @@ -266,7 +274,11 @@ export const updateHighlight = async ( refresh: ctx.refresh, }) - return !!body.updated + if (body.result !== 'updated') return false + + await ctx.pubsub.highlightUpdated(highlight, ctx.uid) + + return true } catch (e) { if ( e instanceof ResponseError && diff --git a/packages/api/src/routers/svc/pages.ts b/packages/api/src/routers/svc/upload.ts similarity index 89% rename from packages/api/src/routers/svc/pages.ts rename to packages/api/src/routers/svc/upload.ts index d1e139094..b26be4cad 100644 --- a/packages/api/src/routers/svc/pages.ts +++ b/packages/api/src/routers/svc/upload.ts @@ -6,14 +6,13 @@ import { readPushSubscription } from '../../datalayer/pubsub' import { generateUploadSignedUrl, uploadToSignedUrl } from '../../utils/uploads' import { v4 as uuidv4 } from 'uuid' import { env } from '../../env' -import { Page } from '../../elastic/types' import { DateTime } from 'luxon' -export function pageServiceRouter() { +export function uploadServiceRouter() { const router = express.Router() router.post('/upload/:folder', async (req, res) => { - console.log('upload page data req', req.params.folder) + console.log('upload data req', req.params.folder) const { message: msgStr, expired } = readPushSubscription(req) if (!msgStr) { @@ -28,7 +27,7 @@ export function pageServiceRouter() { } try { - const data: Partial = JSON.parse(msgStr) + const data: { userId: string } = JSON.parse(msgStr) if (!data.userId) { console.log('No userId found in message') res.status(400).send('Bad Request') diff --git a/packages/api/src/server.ts b/packages/api/src/server.ts index 27967ce5a..df70f693e 100755 --- a/packages/api/src/server.ts +++ b/packages/api/src/server.ts @@ -40,7 +40,7 @@ import { ApolloServer } from 'apollo-server-express' import { pdfAttachmentsRouter } from './routers/svc/pdf_attachments' import { corsConfig } from './utils/corsConfig' import { initElasticsearch } from './elastic' -import { pageServiceRouter } from './routers/svc/pages' +import { uploadServiceRouter } from './routers/svc/upload' const PORT = process.env.PORT || 4000 @@ -98,7 +98,7 @@ export const createApp = (): { app.use('/svc/pubsub/links', linkServiceRouter()) app.use('/svc/pubsub/newsletters', newsletterServiceRouter()) app.use('/svc/pubsub/emails', emailsServiceRouter()) - app.use('/svc/pubsub/pages', pageServiceRouter()) + app.use('/svc/pubsub/upload', uploadServiceRouter()) app.use('/svc/reminders', remindersServiceRouter()) app.use('/svc/pdf-attachments', pdfAttachmentsRouter()) From 0b0b521fabc633c549e3aabd8e280c9c93294466 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Wed, 13 Apr 2022 22:21:20 +0800 Subject: [PATCH 093/125] use generic method for uploading --- packages/api/src/datalayer/pubsub.ts | 63 ++++++++++++------------- packages/api/src/elastic/highlights.ts | 15 ++++-- packages/api/src/elastic/labels.ts | 26 ++++++++-- packages/api/src/elastic/pages.ts | 11 +++-- packages/api/src/routers/svc/upload.ts | 16 +++---- packages/api/test/routers/pages.test.ts | 6 +-- 6 files changed, 83 insertions(+), 54 deletions(-) diff --git a/packages/api/src/datalayer/pubsub.ts b/packages/api/src/datalayer/pubsub.ts index ba98f2952..e4f397acd 100644 --- a/packages/api/src/datalayer/pubsub.ts +++ b/packages/api/src/datalayer/pubsub.ts @@ -2,7 +2,6 @@ import { PubSub } from '@google-cloud/pubsub' import { env } from '../env' import { ReportType } from '../generated/graphql' import express from 'express' -import { Highlight, Page } from '../elastic/types' export const createPubSubClient = (): PubsubClient => { const client = new PubSub() @@ -37,34 +36,34 @@ export const createPubSubClient = (): PubsubClient => { Buffer.from(JSON.stringify({ userId, email, name, username })) ) }, - pageUpdated: (page: Partial, userId: string): Promise => { - return publish( - 'pageUpdated', - Buffer.from(JSON.stringify({ ...page, userId })) - ) - }, - pageCreated: (page: Page): Promise => { - return publish('pageCreated', Buffer.from(JSON.stringify(page))) - }, - pageDeleted: (id: string, userId: string): Promise => { - return publish('pageDeleted', Buffer.from(JSON.stringify({ id, userId }))) - }, - highlightCreated: (highlight: Highlight): Promise => { - return publish('highlightCreated', Buffer.from(JSON.stringify(highlight))) - }, - highlightUpdated: ( - highlight: Partial, + entityCreated: ( + type: EntityType, + data: T, userId: string ): Promise => { return publish( - 'highlightUpdated', - Buffer.from(JSON.stringify({ ...highlight, userId })) + 'entityCreated', + Buffer.from(JSON.stringify({ type, userId, ...data })) ) }, - highlightDeleted: (id: string, userId: string): Promise => { + entityUpdated: ( + type: EntityType, + data: T, + userId: string + ): Promise => { return publish( - 'highlightDeleted', - Buffer.from(JSON.stringify({ id, userId })) + 'entityUpdated', + Buffer.from(JSON.stringify({ type, userId, ...data })) + ) + }, + entityDeleted: ( + type: EntityType, + id: string, + userId: string + ): Promise => { + return publish( + 'entityDeleted', + Buffer.from(JSON.stringify({ type, id, userId })) ) }, reportSubmitted: ( @@ -83,6 +82,12 @@ export const createPubSubClient = (): PubsubClient => { } } +export enum EntityType { + PAGE = 'page', + HIGHLIGHT = 'highlight', + LABEL = 'label', +} + export interface PubsubClient { userCreated: ( userId: string, @@ -90,15 +95,9 @@ export interface PubsubClient { name: string, username: string ) => Promise - pageCreated: (page: Page) => Promise - pageUpdated: (page: Partial, userId: string) => Promise - pageDeleted: (id: string, userId: string) => Promise - highlightCreated: (highlight: Highlight) => Promise - highlightUpdated: ( - highlight: Partial, - userId: string - ) => Promise - highlightDeleted: (id: string, userId: string) => Promise + entityCreated: (type: EntityType, data: T, userId: string) => Promise + entityUpdated: (type: EntityType, data: T, userId: string) => Promise + entityDeleted: (type: EntityType, id: string, userId: string) => Promise reportSubmitted( submitterId: string | undefined, itemUrl: string, diff --git a/packages/api/src/elastic/highlights.ts b/packages/api/src/elastic/highlights.ts index 7bd6a6adf..934563ca3 100644 --- a/packages/api/src/elastic/highlights.ts +++ b/packages/api/src/elastic/highlights.ts @@ -8,6 +8,7 @@ import { import { ResponseError } from '@elastic/elasticsearch/lib/errors' import { client, INDEX_ALIAS } from './index' import { SortBy, SortOrder, SortParams } from '../utils/search' +import { EntityType } from '../datalayer/pubsub' export const addHighlightToPage = async ( id: string, @@ -37,7 +38,11 @@ export const addHighlightToPage = async ( if (body.result !== 'updated') return false - await ctx.pubsub.highlightCreated(highlight) + await ctx.pubsub.entityCreated( + EntityType.HIGHLIGHT, + highlight, + ctx.uid + ) return true } catch (e) { @@ -131,7 +136,7 @@ export const deleteHighlight = async ( if (body.result !== 'updated') return false - await ctx.pubsub.highlightDeleted(highlightId, ctx.uid) + await ctx.pubsub.entityDeleted(EntityType.HIGHLIGHT, highlightId, ctx.uid) return true } catch (e) { @@ -276,7 +281,11 @@ export const updateHighlight = async ( if (body.result !== 'updated') return false - await ctx.pubsub.highlightUpdated(highlight, ctx.uid) + await ctx.pubsub.entityUpdated( + EntityType.HIGHLIGHT, + highlight, + ctx.uid + ) return true } catch (e) { diff --git a/packages/api/src/elastic/labels.ts b/packages/api/src/elastic/labels.ts index ec1ac5188..2bb00bba8 100644 --- a/packages/api/src/elastic/labels.ts +++ b/packages/api/src/elastic/labels.ts @@ -1,5 +1,6 @@ import { Label, PageContext } from './types' import { client, INDEX_ALIAS } from './index' +import { EntityType } from '../datalayer/pubsub' export const addLabelInPage = async ( id: string, @@ -27,9 +28,17 @@ export const addLabelInPage = async ( retry_on_conflict: 3, }) - return body.result === 'updated' + if (body.result !== 'updated') return false + + await ctx.pubsub.entityCreated - displayFontStepper?: boolean toolbarControl?: JSX.Element setShowLogoutConfirmation: (showShareModal: boolean) => void setShowKeyboardCommandsModal: (showShareModal: boolean) => void @@ -126,7 +125,6 @@ export function PrimaryHeader(props: HeaderProps): JSX.Element { isVisible={true} isFixedPosition={true} toolbarControl={props.toolbarControl} - displayFontStepper={props.displayFontStepper} /> ) @@ -141,7 +139,6 @@ type NavHeaderProps = { isDisplayingShadow?: boolean isVisible?: boolean isFixedPosition: boolean - displayFontStepper?: boolean toolbarControl?: JSX.Element } @@ -211,7 +208,6 @@ function NavHeader(props: NavHeaderProps): JSX.Element { /> } actionHandler={props.actionHandler} - displayFontStepper={props.displayFontStepper} /> ) : ( diff --git a/packages/web/components/templates/PrimaryLayout.tsx b/packages/web/components/templates/PrimaryLayout.tsx index c73a02ab2..56e85a419 100644 --- a/packages/web/components/templates/PrimaryLayout.tsx +++ b/packages/web/components/templates/PrimaryLayout.tsx @@ -22,7 +22,6 @@ type PrimaryLayoutProps = { hideHeader?: boolean pageMetaDataProps?: PageMetaDataProps scrollElementRef?: MutableRefObject - displayFontStepper?: boolean headerToolbarControl?: JSX.Element } @@ -78,7 +77,6 @@ export function PrimaryLayout(props: PrimaryLayoutProps): JSX.Element { isFixedPosition={true} toolbarControl={props.headerToolbarControl} scrollElementRef={props.scrollElementRef} - displayFontStepper={props.displayFontStepper} setShowLogoutConfirmation={setShowLogoutConfirmation} setShowKeyboardCommandsModal={setShowKeyboardCommandsModal} /> diff --git a/packages/web/pages/[username]/[slug]/index.tsx b/packages/web/pages/[username]/[slug]/index.tsx index c950b11b3..598f7e0cf 100644 --- a/packages/web/pages/[username]/[slug]/index.tsx +++ b/packages/web/pages/[username]/[slug]/index.tsx @@ -164,7 +164,6 @@ export default function Home(): JSX.Element { Date: Wed, 13 Apr 2022 10:34:46 -0700 Subject: [PATCH 102/125] Add dropdown link to labels --- packages/web/components/patterns/DropdownMenu.tsx | 5 +++++ packages/web/components/patterns/PrimaryHeader.tsx | 3 +++ 2 files changed, 8 insertions(+) diff --git a/packages/web/components/patterns/DropdownMenu.tsx b/packages/web/components/patterns/DropdownMenu.tsx index adf9f25ae..317af1ade 100644 --- a/packages/web/components/patterns/DropdownMenu.tsx +++ b/packages/web/components/patterns/DropdownMenu.tsx @@ -16,6 +16,7 @@ export type HeaderDropdownAction = | 'apply-lighter-theme' | 'navigate-to-install' | 'navigate-to-emails' + | 'navigate-to-labels' | 'navigate-to-profile' | 'increaseFontSize' | 'decreaseFontSize' @@ -62,6 +63,10 @@ export function DropdownMenu(props: DropdownMenuProps): JSX.Element { onSelect={() => props.actionHandler('navigate-to-emails')} title="Emails" /> + props.actionHandler('navigate-to-labels')} + title="Labels" + /> window.Intercom('show')} title="Feedback" diff --git a/packages/web/components/patterns/PrimaryHeader.tsx b/packages/web/components/patterns/PrimaryHeader.tsx index 50c64844c..79294d02f 100644 --- a/packages/web/components/patterns/PrimaryHeader.tsx +++ b/packages/web/components/patterns/PrimaryHeader.tsx @@ -102,6 +102,9 @@ export function PrimaryHeader(props: HeaderProps): JSX.Element { case 'navigate-to-emails': router.push('/settings/emails') break + case 'navigate-to-labels': + router.push('/settings/labels') + break case 'navigate-to-profile': if (props.user) { router.push(`/${props.user.profile.username}`) From 1e2901c4e71b5ab26394c2118ae4d3df4af12278 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 13 Apr 2022 10:48:25 -0700 Subject: [PATCH 103/125] add label editing to reader view --- .../App/Views/Home/HomeFeedViewIOS.swift | 2 +- .../App/Views/Home/HomeFeedViewModel.swift | 18 ++++++++++++++++-- .../Views/WebReader/WebReaderContainer.swift | 4 ++++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index 799146733..7f6893916 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -103,7 +103,7 @@ import Views } } .onChange(of: viewModel.selectedLinkItem) { _ in - viewModel.commitProgressUpdates() + viewModel.commitItemUpdates() } } } diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index 763615607..4ee67a777 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -11,6 +11,9 @@ final class HomeFeedViewModel: ObservableObject { /// Track progress updates to be committed when user navigates back to grid view var uncommittedReadingProgressUpdates = [String: Double]() + /// Track label updates to be committed when user navigates back to grid view + var uncommittedLabelUpdates = [String: [FeedItemLabel]]() + @Published var items = [FeedItem]() @Published var isLoading = false @Published var showPushNotificationPrimer = false @@ -173,14 +176,18 @@ final class HomeFeedViewModel: ObservableObject { .store(in: &subscriptions) } - /// Update `FeedItem`s with the cached reading progress values so it can animate when the + /// Update `FeedItem`s with the cached reading progress and label values so it can animate when the /// user navigates back to the grid view (and also avoid mutations of the grid items /// that can cause the `NavigationView` to pop. - func commitProgressUpdates() { + func commitItemUpdates() { for (key, value) in uncommittedReadingProgressUpdates { updateProgress(itemID: key, progress: value) } + for (key, value) in uncommittedLabelUpdates { + updateLabels(itemID: key, labels: value) + } uncommittedReadingProgressUpdates = [:] + uncommittedLabelUpdates = [:] } private func updateProgress(itemID: String, progress: Double) { @@ -191,6 +198,13 @@ final class HomeFeedViewModel: ObservableObject { } func updateLabels(itemID: String, labels: [FeedItemLabel]) { + // If item is being being displayed then delay the state update of labels until + // user is no longer reading the item. + if selectedLinkItem != nil { + uncommittedLabelUpdates[itemID] = labels + return + } + guard let item = items.first(where: { $0.id == itemID }) else { return } if let index = items.firstIndex(of: item) { items[index].labels = labels diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift index 4759c527b..ecf6144a8 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift @@ -132,6 +132,10 @@ import WebKit Menu( content: { Group { + Button( + action: { homeFeedViewModel.itemUnderLabelEdit = item }, + label: { Label("Edit Labels", systemImage: "tag") } + ) Button( action: { homeFeedViewModel.setLinkArchived( From 4e4dae89d7776dbaaa245a473d5b11efd9fac308 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 13 Apr 2022 10:51:56 -0700 Subject: [PATCH 104/125] Consistent naming on label controls - Edit Labels: Edit all of a user's labels - Set Labels: Set the labels assigned to a link --- .../templates/article/ArticleActionsMenu.tsx | 6 +++--- ...EditLabelsControl.tsx => SetLabelsControl.tsx} | 11 +++++------ .../{EditLabelsModal.tsx => SetLabelsModal.tsx} | 11 +++++------ .../lib/keyboardShortcuts/navigationShortcuts.ts | 4 ++-- packages/web/pages/[username]/[slug]/index.tsx | 15 +++++++-------- 5 files changed, 22 insertions(+), 25 deletions(-) rename packages/web/components/templates/article/{EditLabelsControl.tsx => SetLabelsControl.tsx} (97%) rename packages/web/components/templates/article/{EditLabelsModal.tsx => SetLabelsModal.tsx} (83%) diff --git a/packages/web/components/templates/article/ArticleActionsMenu.tsx b/packages/web/components/templates/article/ArticleActionsMenu.tsx index 39bef218c..75be3afcc 100644 --- a/packages/web/components/templates/article/ArticleActionsMenu.tsx +++ b/packages/web/components/templates/article/ArticleActionsMenu.tsx @@ -7,7 +7,7 @@ import { Dropdown } from "../../elements/DropdownElements" import { Box, SpanBox } from "../../elements/LayoutPrimitives" import { TooltipWrapped } from "../../elements/Tooltip" import { styled, theme } from "../../tokens/stitches.config" -import { EditLabelsControl } from "./EditLabelsControl" +import { SetLabelsControl } from "./SetLabelsControl" import { ReaderSettingsControl } from "./ReaderSettingsControl" export type ArticleActionsMenuLayout = 'horizontal' | 'vertical' @@ -101,7 +101,7 @@ export function ArticleActionsMenu(props: ArticleActionsMenuProps): JSX.Element } > - @@ -109,7 +109,7 @@ export function ArticleActionsMenu(props: ArticleActionsMenuProps): JSX.Element )} - +