From 03e95a029b2a554f7c97fe5bc2129ceb8d82ccf4 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 24 Aug 2023 12:00:35 +0800 Subject: [PATCH] Add multiple highlight colors --- packages/api/src/apollo.ts | 2 + packages/web/components/elements/Button.tsx | 12 + .../web/components/patterns/HighlightBar.tsx | 239 ++++++++++-------- .../web/components/patterns/HighlightView.tsx | 13 +- .../templates/article/HighlightsLayer.tsx | 51 +++- .../web/components/tokens/stitches.config.ts | 15 ++ .../web/lib/highlights/createHighlight.ts | 5 +- .../web/lib/highlights/highlightGenerator.ts | 24 +- .../web/lib/highlights/highlightHelpers.ts | 6 +- .../networking/fragments/highlightFragment.ts | 2 + .../mutations/createHighlightMutation.ts | 1 + .../mutations/mergeHighlightMutation.ts | 1 + packages/web/lib/themeUpdater.tsx | 34 +++ packages/web/styles/articleInnerStyling.css | 29 ++- 14 files changed, 301 insertions(+), 133 deletions(-) diff --git a/packages/api/src/apollo.ts b/packages/api/src/apollo.ts index 65435b78d..693f57f63 100644 --- a/packages/api/src/apollo.ts +++ b/packages/api/src/apollo.ts @@ -102,6 +102,8 @@ export function makeApolloServer(): ApolloServer { const apollo = new ApolloServer({ schema: schema, context: contextFunc, + cache: 'bounded', + persistedQueries: false, formatError: (err) => { logger.info('server error', err) Sentry.captureException(err) diff --git a/packages/web/components/elements/Button.tsx b/packages/web/components/elements/Button.tsx index 3aadaf088..6a407e770 100644 --- a/packages/web/components/elements/Button.tsx +++ b/packages/web/components/elements/Button.tsx @@ -246,6 +246,18 @@ export const Button = styled('button', { }, '&:focus': { outline: 'none' }, }, + highlightBarIcon: { + p: '0px', + lineHeight: '0px', + bg: 'transparent', + border: 'none', + cursor: 'pointer', + '&:hover': { + opacity: 0.5, + }, + + '&:focus': { outline: 'none' }, + }, articleActionIcon: { bg: 'transparent', border: 'none', diff --git a/packages/web/components/patterns/HighlightBar.tsx b/packages/web/components/patterns/HighlightBar.tsx index 0888b82cd..a352c90f0 100644 --- a/packages/web/components/patterns/HighlightBar.tsx +++ b/packages/web/components/patterns/HighlightBar.tsx @@ -6,9 +6,20 @@ import { StyledText } from '../elements/StyledText' import { Button } from '../elements/Button' import { HStack, Box } from '../elements/LayoutPrimitives' import { PenWithColorIcon } from '../elements/images/PenWithColorIcon' -import { Note, Tag, Trash, Copy } from 'phosphor-react' +import { + Note, + Tag, + Trash, + Copy, + Circle, + CopySimple, + CheckCircle, +} from 'phosphor-react' import { TrashIcon } from '../elements/icons/TrashIcon' import { LabelIcon } from '../elements/icons/LabelIcon' +import { NotebookIcon } from '../elements/icons/NotebookIcon' +import { highlightColor, highlightColors } from '../../lib/themeUpdater' +import { useState } from 'react' type PageCoordinates = { pageX: number @@ -24,27 +35,29 @@ export type HighlightAction = | 'unshare' | 'setHighlightLabels' | 'copy' + | 'updateColor' type HighlightBarProps = { anchorCoordinates: PageCoordinates isNewHighlight: boolean isSharedToFeed: boolean displayAtBottom: boolean - handleButtonClick: (action: HighlightAction) => void + highlightColor?: string + handleButtonClick: (action: HighlightAction, param?: string) => void } export function HighlightBar(props: HighlightBarProps): JSX.Element { return ( - - {iconElement} - - {text} - - - - ) -} +const Separator = styled('div', { + width: '1px', + height: '20px', + mx: '5px', + background: '$thHighlightBar', +}) function BarContent(props: HighlightBarProps): JSX.Element { - const Separator = styled('div', { - width: '1px', - maxWidth: '1px', - height: '100%', - background: '$grayBorder', - }) + const [hovered, setHovered] = useState(undefined) return ( - {props.isNewHighlight ? ( - } - onClick={() => props.handleButtonClick('create')} - /> - ) : ( - <> - { + return ( + + ) + })} + + {!props.isNewHighlight && ( + <> + )} - - - } + + + ) } diff --git a/packages/web/components/patterns/HighlightView.tsx b/packages/web/components/patterns/HighlightView.tsx index b258cbe88..28063dc59 100644 --- a/packages/web/components/patterns/HighlightView.tsx +++ b/packages/web/components/patterns/HighlightView.tsx @@ -13,7 +13,11 @@ import { styled, theme } from '../tokens/stitches.config' import { HighlightViewNote } from './HighlightNotes' import ReactMarkdown from 'react-markdown' import remarkGfm from 'remark-gfm' -import { isDarkTheme } from '../../lib/themeUpdater' +import { + highlightColor, + highlightColorVar, + isDarkTheme, +} from '../../lib/themeUpdater' import { ReadableItem } from '../../lib/networking/queries/useGetLibraryItemsQuery' import { UserBasicData } from '../../lib/networking/queries/useGetViewerQuery' import { @@ -70,7 +74,7 @@ export function HighlightView(props: HighlightViewProps): JSX.Element { const hover = useHover(context) const { getReferenceProps, getFloatingProps } = useInteractions([hover]) - const highlightAlpha = isDark ? 0.5 : 0.35 + const highlightColor = highlightColorVar(props.highlight.color) return ( *': { display: 'inline', - padding: '2px', - backgroundColor: `rgba(var(--colors-highlightBackground), ${highlightAlpha})`, - boxShadow: `3px 0 0 rgba(var(--colors-highlightBackground), ${highlightAlpha}), -3px 0 0 rgba(var(--colors-highlightBackground), ${highlightAlpha})`, + padding: '3px', + backgroundColor: `rgba(${highlightColor}, var(--colors-highlightBackgroundAlpha))`, boxDecorationBreak: 'clone', borderRadius: '2px', }, diff --git a/packages/web/components/templates/article/HighlightsLayer.tsx b/packages/web/components/templates/article/HighlightsLayer.tsx index 95b07f762..c3fee48bb 100644 --- a/packages/web/components/templates/article/HighlightsLayer.tsx +++ b/packages/web/components/templates/article/HighlightsLayer.tsx @@ -30,6 +30,7 @@ import 'react-sliding-pane/dist/react-sliding-pane.css' import { NotebookContent } from './Notebook' import { NotebookHeader } from './NotebookHeader' import useGetWindowDimensions from '../../../lib/hooks/useGetWindowDimensions' +import { ConfirmationModal } from '../../patterns/ConfirmationModal' type HighlightsLayerProps = { viewer: UserBasicData @@ -88,20 +89,26 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element { undefined ) + const [ + confirmDeleteHighlightWithNoteId, + setConfirmDeleteHighlightWithNoteId, + ] = useState(undefined) + const windowDimensions = useGetWindowDimensions() const createHighlightFromSelection = useCallback( async ( selection: SelectionAttributes, - note?: string + options: { annotation?: string; color?: string } | undefined ): Promise => { const result = await createHighlight( { selection: selection, articleId: props.articleId, existingHighlights: highlights, + color: options?.color, highlightStartEndOffsets: highlightLocations, - annotation: note, + annotation: options?.annotation, highlightPositionPercent: selectionPercentPos(selection.selection), highlightPositionAnchorIndex: selectionAnchorIndex( selection.selection @@ -190,8 +197,10 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element { setHighlights(highlights.filter(($0) => $0.id !== highlightId)) setFocusedHighlight(undefined) document.dispatchEvent(new Event('highlightsUpdated')) + showSuccessToast('Highlight removed') } else { console.error('Failed to delete highlight') + showErrorToast('Error removing highlight') } }, [focusedHighlight, highlights, highlightLocations, props.articleMutations] @@ -271,14 +280,14 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element { } const createHighlightCallback = useCallback( - async (annotation?: string) => { + async (options: { annotation?: string; color?: string } | undefined) => { if (!selectionData) { return } try { const result = await createHighlightFromSelection( selectionData, - annotation + options ) if (!result) { showErrorToast('Error saving highlight', { position: 'bottom-right' }) @@ -436,13 +445,25 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element { }, [handleSingleClick, handleDoubleClick]) const handleAction = useCallback( - async (action: HighlightAction) => { + async (action: HighlightAction, param?: string) => { switch (action) { case 'delete': - await removeHighlightCallback() + if (focusedHighlight?.annotation == undefined) { + await removeHighlightCallback() + } else { + setConfirmDeleteHighlightWithNoteId(focusedHighlight?.id) + } break case 'create': - await createHighlightCallback() + await createHighlightCallback({ + color: param, + }) + break + case 'updateColor': + if (focusedHighlight) { + focusedHighlight.color = param + await updateHighlightsCallback(focusedHighlight) + } break case 'comment': if (props.highlightBarDisabled || focusedHighlight) { @@ -517,6 +538,7 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element { removeHighlightCallback, selectionData, setSelectionData, + confirmDeleteHighlightWithNoteId, ] ) @@ -751,6 +773,20 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element { onOpenChange={() => setLabelsTarget(undefined)} /> )} + {confirmDeleteHighlightWithNoteId && ( + { + ;(async () => { + await removeHighlightCallback(confirmDeleteHighlightWithNoteId) + setConfirmDeleteHighlightWithNoteId(undefined) + })() + }} + onOpenChange={() => { + setConfirmDeleteHighlightWithNoteId(undefined) + }} + /> + )} {/* // Display the button bar if we are not in the native app and there // is a focused highlight or selection data */} {!props.highlightBarDisabled && (focusedHighlight || selectionData) && ( @@ -761,6 +797,7 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element { handleButtonClick={handleAction} isSharedToFeed={focusedHighlight?.sharedAt != undefined} displayAtBottom={isTouchScreenDevice()} + highlightColor={focusedHighlight?.color ?? 'yellow'} /> )} diff --git a/packages/web/components/tokens/stitches.config.ts b/packages/web/components/tokens/stitches.config.ts index 74a105863..1218cfe67 100644 --- a/packages/web/components/tokens/stitches.config.ts +++ b/packages/web/components/tokens/stitches.config.ts @@ -1,5 +1,6 @@ import type * as Stitches from '@stitches/react' import { createStitches, createTheme } from '@stitches/react' +import { highlightColor } from '../../lib/themeUpdater' export enum ThemeId { Light = 'Light', @@ -198,6 +199,16 @@ export const { styled, css, theme, getCssText, globalCss, keyframes, config } = thProgressFg: '#FFD234', thHighContrast: '#3D3D3D', + thHighlightBar: '#D9D9D9', + + highlightBackgroundGreen: '85, 198, 137', + highlightBackgroundBlue: '106, 177, 255', + highlightBackgroundOrange: '254, 181, 109', + highlightBackgroundYellow: '255, 210, 52', + highlightBackgroundRed: '251, 154, 154', + + highlightBackgroundAlpha: '0.2', + highlightUnderlineAlpha: '1', }, }, media: { @@ -298,6 +309,10 @@ const darkThemeSpec = { thProgressFg: '#FFD234', thHighContrast: '#D9D9D9', + + thHighlightBar: '#6A6968', + highlightUnderlineAlpha: '0.5', + highlightBackgroundAlpha: '0.35', }, shadows: { cardBoxShadow: diff --git a/packages/web/lib/highlights/createHighlight.ts b/packages/web/lib/highlights/createHighlight.ts index bbe5b8653..36fe80422 100644 --- a/packages/web/lib/highlights/createHighlight.ts +++ b/packages/web/lib/highlights/createHighlight.ts @@ -17,6 +17,7 @@ type CreateHighlightInput = { selection: SelectionAttributes articleId: string annotation?: string + color?: string existingHighlights: Highlight[] highlightStartEndOffsets: HighlightLocation[] highlightPositionPercent?: number @@ -94,7 +95,8 @@ export async function createHighlight( const highlightAttributes = makeHighlightNodeAttributes( patch, id, - annotations.length > 0 + annotations.length > 0, + input.color ) const newHighlightAttributes = { @@ -102,6 +104,7 @@ export async function createHighlight( shortId: nanoid(8), patch, + color: input.color, prefix: highlightAttributes.prefix, suffix: highlightAttributes.suffix, quote: htmlToMarkdown(container.innerHTML), diff --git a/packages/web/lib/highlights/highlightGenerator.ts b/packages/web/lib/highlights/highlightGenerator.ts index 392d29d11..a62c02016 100644 --- a/packages/web/lib/highlights/highlightGenerator.ts +++ b/packages/web/lib/highlights/highlightGenerator.ts @@ -68,9 +68,7 @@ function nodeAttributesFromHighlight( const id = highlight.id const withNote = !!highlight.annotation const tooltip = undefined - const customColor = highlight.createdByMe - ? undefined - : 'var(--colors-recommendedHighlightBackground)' + const customColor = highlight.color return makeHighlightNodeAttributes(patch, id, withNote, customColor, tooltip) } @@ -142,15 +140,17 @@ export function makeHighlightNodeAttributes( } const newHighlightSpan = document.createElement('span') - newHighlightSpan.className = withNote - ? highlightWithNoteClassName - : highlightClassname + newHighlightSpan.className = highlightClassname + + if (withNote) { + newHighlightSpan.className = `${newHighlightSpan.className} ${highlightWithNoteClassName}` + } + + if (customColor) { + newHighlightSpan.className = `${newHighlightSpan.className} highlight_${customColor}` + } + newHighlightSpan.setAttribute(highlightIdAttribute, id) - customColor && - newHighlightSpan.setAttribute( - 'style', - `background-color: ${customColor} !important` - ) tooltip && newHighlightSpan.setAttribute('title', tooltip) newHighlightSpan.appendChild(newTextNode) lastElement = newHighlightSpan @@ -162,7 +162,7 @@ export function makeHighlightNodeAttributes( if (withNote && lastElement) { lastElement.classList.add('last_element') - const svg = noteImage() + const svg = noteImage(customColor) svg.setAttribute(highlightNoteIdAttribute, id) const ctr = document.createElement('div') diff --git a/packages/web/lib/highlights/highlightHelpers.ts b/packages/web/lib/highlights/highlightHelpers.ts index 3f45c9a51..f000ca207 100644 --- a/packages/web/lib/highlights/highlightHelpers.ts +++ b/packages/web/lib/highlights/highlightHelpers.ts @@ -1,3 +1,5 @@ +import { highlightColor, highlightColorVar } from '../themeUpdater' + export type SelectionAttributes = { selection: Selection range: Range @@ -25,7 +27,7 @@ export function getHighlightNoteButton(highlightId: string): Element[] { ) } -export function noteImage(): SVGSVGElement { +export function noteImage(color: string | undefined): SVGSVGElement { const svgURI = 'http://www.w3.org/2000/svg' const svg = document.createElementNS(svgURI, 'svg') svg.setAttribute('viewBox', '0 0 14 14') @@ -38,7 +40,7 @@ export function noteImage(): SVGSVGElement { 'd', 'M1 5.66602C1 3.7804 1 2.83759 1.58579 2.2518C2.17157 1.66602 3.11438 1.66602 5 1.66602H9C10.8856 1.66602 11.8284 1.66602 12.4142 2.2518C13 2.83759 13 3.7804 13 5.66602V7.66601C13 9.55163 13 10.4944 12.4142 11.0802C11.8284 11.666 10.8856 11.666 9 11.666H4.63014C4.49742 11.666 4.43106 11.666 4.36715 11.6701C3.92582 11.6984 3.50632 11.8722 3.17425 12.1642C3.12616 12.2065 3.07924 12.2534 2.98539 12.3473V12.3473C2.75446 12.5782 2.639 12.6937 2.55914 12.7475C1.96522 13.1481 1.15512 12.8125 1.01838 12.1093C1 12.0148 1 11.8515 1 11.5249V5.66602Z' ) - path.setAttribute('stroke', 'rgba(255, 210, 52, 0.8)') + path.setAttribute('stroke', `rgba(${highlightColorVar(color)}, 0.8)`) path.setAttribute('stroke-width', '1.8') path.setAttribute('stroke-linejoin', 'round') svg.appendChild(path) diff --git a/packages/web/lib/networking/fragments/highlightFragment.ts b/packages/web/lib/networking/fragments/highlightFragment.ts index a6cf2e4c6..0185bd524 100644 --- a/packages/web/lib/networking/fragments/highlightFragment.ts +++ b/packages/web/lib/networking/fragments/highlightFragment.ts @@ -10,6 +10,7 @@ export const highlightFragment = gql` prefix suffix patch + color annotation createdByMe createdAt @@ -41,6 +42,7 @@ export type Highlight = { updatedAt: string sharedAt: string labels?: Label[] + color?: string highlightPositionPercent?: number highlightPositionAnchorIndex?: number } diff --git a/packages/web/lib/networking/mutations/createHighlightMutation.ts b/packages/web/lib/networking/mutations/createHighlightMutation.ts index 2193b46a1..f21268b96 100644 --- a/packages/web/lib/networking/mutations/createHighlightMutation.ts +++ b/packages/web/lib/networking/mutations/createHighlightMutation.ts @@ -15,6 +15,7 @@ export type CreateHighlightInput = { suffix?: string quote?: string html?: string + color?: string annotation?: string patch?: string diff --git a/packages/web/lib/networking/mutations/mergeHighlightMutation.ts b/packages/web/lib/networking/mutations/mergeHighlightMutation.ts index 11b05d434..4b7d23bda 100644 --- a/packages/web/lib/networking/mutations/mergeHighlightMutation.ts +++ b/packages/web/lib/networking/mutations/mergeHighlightMutation.ts @@ -40,6 +40,7 @@ export async function mergeHighlightMutation( prefix suffix patch + color createdAt updatedAt annotation diff --git a/packages/web/lib/themeUpdater.tsx b/packages/web/lib/themeUpdater.tsx index 6c57109a6..017b9424b 100644 --- a/packages/web/lib/themeUpdater.tsx +++ b/packages/web/lib/themeUpdater.tsx @@ -111,3 +111,37 @@ export function isDarkTheme(): boolean { currentTheme == 'Black' ) } + +export const highlightColors = ['yellow', 'red', 'green', 'blue'] + +export const highlightColor = (name: string | undefined) => { + switch (name) { + case 'green': + return '#55C689' + case 'blue': + return '#6AB1FF' + case 'yellow': + return '#FFD234' + case 'orange': + return '#FEB56D' + case 'red': + return '#FB9A9A' + } + return '#FFD234' +} + +export const highlightColorVar = (name: string | undefined) => { + switch (name) { + case 'green': + return 'var(--colors-highlightBackgroundGreen)' + case 'blue': + return 'var(--colors-highlightBackgroundBlue)' + case 'yellow': + return 'var(--colors-highlightBackgroundYellow)' + case 'orange': + return 'var(--colors-highlightBackgroundOrange)' + case 'red': + return 'var(--colors-highlightBackgroundRed)' + } + return 'var(--colors-highlightBackground)' +} diff --git a/packages/web/styles/articleInnerStyling.css b/packages/web/styles/articleInnerStyling.css index 461e59f3c..8d42c2aa1 100644 --- a/packages/web/styles/articleInnerStyling.css +++ b/packages/web/styles/articleInnerStyling.css @@ -19,12 +19,29 @@ cursor: pointer; } -.highlight_with_note { - color: var(--colors-highlightText); - background-color: rgba(var(--colors-highlightBackground), 0.35); - border-bottom: 2px rgb(var(--colors-highlightBackground)) solid; - border-radius: 2px; - cursor: pointer; +.highlight_green { + background-color: rgba(var(--colors-highlightBackgroundGreen), var(--colors-highlightBackgroundAlpha)); + border-bottom: 2px rgba(var(--colors-highlightBackgroundGreen), var(--colors-highlightUnderlineAlpha)) solid; +} + +.highlight_red { + background-color: rgba(var(--colors-highlightBackgroundRed), var(--colors-highlightBackgroundAlpha)); + border-bottom: 2px rgba(var(--colors-highlightBackgroundRed), var(--colors-highlightUnderlineAlpha)) solid; +} + +.highlight_blue { + background-color: rgba(var(--colors-highlightBackgroundBlue), var(--colors-highlightBackgroundAlpha)); + border-bottom: 2px rgba(var(--colors-highlightBackgroundBlue), var(--colors-highlightUnderlineAlpha)) solid; +} + +.highlight_yellow { + background-color: rgba(var(--colors-highlightBackgroundYellow), var(--colors-highlightBackgroundAlpha)); + border-bottom: 2px rgba(var(--colors-highlightBackgroundYellow), var(--colors-highlightUnderlineAlpha)) solid; +} + +.highlight_orange { + background-color: rgba(var(--colors-highlightBackgroundOrange), var(--colors-highlightBackgroundAlpha)); + border-bottom: 2px rgba(var(--colors-highlightBackgroundOrange), var(--colors-highlightUnderlineAlpha)) solid; } .article-inner-css {