Merge pull request #2412 from omnivore-app/feat/web-notebook-view

Improved notebook modal
This commit is contained in:
Jackson Harper 2023-06-27 17:46:35 +08:00 committed by GitHub
commit 178fa5e42d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 1777 additions and 1068 deletions

View file

@ -242,15 +242,31 @@ export const Button = styled('button', {
border: 'none',
cursor: 'pointer',
'&:hover': {
opacity: 0.8,
opacity: 0.7,
},
},
articleActionIcon: {
bg: 'transparent',
border: 'none',
cursor: 'pointer',
padding: '4px',
borderRadius: '5px',
'&:hover': {
opacity: 0.8,
opacity: 0.7,
},
},
hoverActionIcon: {
bg: 'transparent',
border: 'none',
cursor: 'pointer',
padding: '4px',
height: '100%',
pt: '6px',
minWidth: '25px',
'&:hover': {
bg: '$grayBgHover',
},
},
ghost: {

View file

@ -172,7 +172,10 @@ export function Dropdown(
} = props
return (
<Root modal={modal} onOpenChange={props.onOpenChange}>
<DropdownTrigger disabled={disabled} css={{ cursor: 'pointer' }}>
<DropdownTrigger
disabled={disabled}
css={{ height: '100%', cursor: 'pointer' }}
>
{triggerElement}
</DropdownTrigger>
<DropdownContent

View file

@ -11,93 +11,67 @@ type LabelChipProps = {
color: string // expected to be a RGB hex color string
isSelected?: boolean
useAppAppearance?: boolean
xAction?: () => void
}
export function LabelChip(props: LabelChipProps): JSX.Element {
const router = useRouter()
const isDark = isDarkTheme()
const luminance = getLuminance(props.color)
const textColor = luminance > 0.5 ? '#000000' : '#ffffff'
const selectedBorder = isDark ? '#FFEA9F' : 'black'
const unSelectedBorder = isDark ? '#6A6968' : '#D9D9D9'
if (props.useAppAppearance) {
return (
<SpanBox
css={{
display: 'inline-table',
margin: '2px',
fontSize: '11px',
fontWeight: '500',
fontFamily: '$inter',
padding: '4px 10px',
whiteSpace: 'nowrap',
cursor: 'pointer',
backgroundClip: 'padding-box',
borderRadius: '5px',
borderWidth: '1px',
borderStyle: 'solid',
color: isDark ? '#EBEBEB' : '#2A2A2A',
borderColor: props.isSelected ? selectedBorder : unSelectedBorder,
backgroundColor: isDark ? '#2A2A2A' : '#F5F5F5',
}}
>
<HStack alignment="center" css={{ gap: '10px' }}>
<Circle size={14} color={props.color} weight="fill" />
<SpanBox css={{ pt: '1px' }}>{props.text}</SpanBox>
{props.xAction && (
<Button
style="ghost"
css={{ display: 'flex', pt: '1px' }}
onClick={(event) => {
if (props.xAction) {
props.xAction()
event.preventDefault()
}
}}
>
<X
size={14}
color={
props.isSelected
? '#FFEA9F'
: theme.colors.thBorderSubtle.toString()
}
/>
</Button>
)}
</HStack>
</SpanBox>
)
}
return (
<Button
style="plainIcon"
onClick={(e) => {
router.push(`/home?q=label:"${props.text}"`)
e.stopPropagation()
<SpanBox
css={{
display: 'inline-table',
margin: '2px',
fontSize: '11px',
fontWeight: '500',
fontFamily: '$inter',
padding: '1px 7px',
whiteSpace: 'nowrap',
cursor: 'pointer',
backgroundClip: 'padding-box',
borderRadius: '5px',
borderWidth: '1px',
borderStyle: 'solid',
color: isDark ? '#EBEBEB' : '#2A2A2A',
borderColor: props.isSelected ? selectedBorder : unSelectedBorder,
backgroundColor: isDark ? '#2A2A2A' : '#F5F5F5',
}}
>
<SpanBox
css={{
display: 'inline-table',
margin: '2px',
borderRadius: '4px',
color: textColor,
fontSize: '13px',
fontWeight: '500',
padding: '3px 6px',
whiteSpace: 'nowrap',
cursor: 'pointer',
backgroundClip: 'padding-box',
backgroundColor: props.color,
}}
>
{props.text}
</SpanBox>
</Button>
<HStack alignment="center" css={{ gap: '5px' }}>
<Circle size={14} color={props.color} weight="fill" />
<SpanBox css={{ pt: '1px' }}>{props.text}</SpanBox>
</HStack>
</SpanBox>
)
// }
// return (
// <Button
// style="plainIcon"
// onClick={(e) => {
// router.push(`/home?q=label:"${props.text}"`)
// e.stopPropagation()
// }}
// >
// <SpanBox
// css={{
// display: 'inline-table',
// margin: '2px',
// borderRadius: '4px',
// color: textColor,
// fontSize: '13px',
// fontWeight: '500',
// padding: '3px 6px',
// whiteSpace: 'nowrap',
// cursor: 'pointer',
// backgroundClip: 'padding-box',
// backgroundColor: props.color,
// }}
// >
// {props.text}
// </SpanBox>
// </Button>
// )
}

View file

@ -0,0 +1,217 @@
/* eslint-disable react/no-children-prop */
import {
ChangeEvent,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react'
import { VStack } from '../elements/LayoutPrimitives'
import MarkdownIt from 'markdown-it'
import MdEditor, { Plugins } from 'react-markdown-editor-lite'
import 'react-markdown-editor-lite/lib/index.css'
import throttle from 'lodash/throttle'
import { updateHighlightMutation } from '../../lib/networking/mutations/updateHighlightMutation'
import { Highlight } from '../../lib/networking/fragments/highlightFragment'
import Counter from './MDEditorSavePlugin'
import { isDarkTheme } from '../../lib/themeUpdater'
import { RcEditorStyles } from './RcEditorStyles'
const mdParser = new MarkdownIt()
MdEditor.use(Plugins.TabInsert, {
tabMapValue: 1, // note that 1 means a '\t' instead of ' '.
})
console.log()
MdEditor.use(Counter)
type NoteSectionProps = {
targetId: string
placeHolder: string
text: string
setText: (text: string) => void
saveText: (text: string) => void
}
export function ArticleNotes(props: NoteSectionProps): JSX.Element {
const saveText = useCallback(
(text) => {
props.saveText(text)
},
[props]
)
return (
<MarkdownNote
targetId={props.targetId}
placeHolder={props.placeHolder}
text={props.text}
setText={props.setText}
saveText={saveText}
fillBackground={false}
/>
)
}
type HighlightViewNoteProps = {
targetId: string
placeHolder: string
mode: 'edit' | 'preview'
highlight: Highlight
setEditMode: (set: 'edit' | 'preview') => void
text: string
setText: (text: string) => void
updateHighlight: (highlight: Highlight) => void
}
export function HighlightViewNote(props: HighlightViewNoteProps): JSX.Element {
const [lastSaved, setLastSaved] = useState<Date | undefined>(undefined)
const saveText = useCallback(
(text) => {
;(async () => {
const success = await updateHighlightMutation({
annotation: text,
highlightId: props.highlight?.id,
})
if (success) {
// setLastSaved(updateTime)
props.highlight.annotation = text
props.updateHighlight(props.highlight)
}
})()
},
[props]
)
return (
<MarkdownNote
targetId={props.targetId}
placeHolder={props.placeHolder}
text={props.text}
setText={props.setText}
saveText={saveText}
fillBackground={true}
/>
)
}
type MarkdownNote = {
targetId: string
placeHolder: string
text: string | undefined
setText: (text: string) => void
fillBackground: boolean | undefined
saveText: (text: string) => void
}
export function MarkdownNote(props: MarkdownNote): JSX.Element {
const editorRef = useRef<MdEditor | null>(null)
const isDark = isDarkTheme()
const saveRef = useRef(props.saveText)
useEffect(() => {
saveRef.current = props.saveText
}, [props])
const debouncedSave = useMemo<(text: string) => void>(() => {
const func = (text: string) => {
saveRef.current?.(text)
}
return throttle(func, 3000)
}, [])
const handleEditorChange = useCallback(
(
data: { text: string; html: string },
event?: ChangeEvent<HTMLTextAreaElement> | undefined
) => {
props.setText(data.text)
if (event) {
event.preventDefault()
}
debouncedSave(data.text)
},
[]
)
useEffect(() => {
const saveMarkdownNote = () => {
const md = editorRef.current?.getMdValue()
if (md) {
props.saveText(md)
}
}
document.addEventListener('saveMarkdownNote', saveMarkdownNote)
return () => {
document.removeEventListener('saveMarkdownNote', saveMarkdownNote)
}
}, [props, editorRef])
return (
<VStack
css={{
width: '100%',
...RcEditorStyles(isDark, true),
}}
onKeyDown={(event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.code.toLowerCase() === 'escape') {
event.preventDefault()
event.stopPropagation()
}
}}
>
<MdEditor
key="note-editor"
ref={editorRef}
value={props.text}
placeholder={props.placeHolder}
view={{ menu: true, md: true, html: false }}
canView={{
menu: true,
md: true,
html: true,
both: false,
fullScreen: false,
hideMenu: false,
}}
plugins={[
'tab-insert',
'header',
'font-bold',
'font-italic',
'font-underline',
'font-strikethrough',
'list-unordered',
'list-ordered',
'block-quote',
'link',
'auto-resize',
'save',
]}
style={{
width: '100%',
height: '180px',
}}
renderHTML={(text: string) => mdParser.render(text)}
onChange={handleEditorChange}
/>
</VStack>
)
}

View file

@ -0,0 +1,113 @@
import { useState } from 'react'
import { Box, SpanBox } from '../elements/LayoutPrimitives'
import { LibraryItemNode } from '../../lib/networking/queries/useGetLibraryItemsQuery'
import { Button } from '../elements/Button'
import { theme } from '../tokens/stitches.config'
import {
ArchiveBox,
Book,
BookOpen,
Copy,
DotsThree,
Notebook,
Tag,
Trash,
Tray,
} from 'phosphor-react'
//import { CardMenu } from '../CardMenu'
import { UserBasicData } from '../../lib/networking/queries/useGetViewerQuery'
import { Highlight } from '../../lib/networking/fragments/highlightFragment'
import { showErrorToast, showSuccessToast } from '../../lib/toastHelpers'
type HighlightHoverActionsProps = {
viewer: UserBasicData
highlight: Highlight
isHovered: boolean
viewInReader: (highlightId: string) => void
setLabelsTarget: (target: Highlight) => void
setShowConfirmDeleteHighlightId: (set: string) => void
}
export const HighlightHoverActions = (props: HighlightHoverActionsProps) => {
const [menuOpen, setMenuOpen] = useState(false)
return (
<Box
css={{
height: '33px',
width: '135px',
bg: '$thBackground',
display: 'flex',
pt: '0px',
alignItems: 'center',
justifyContent: 'center',
border: '1px solid $thBackground5',
borderRadius: '5px',
gap: '5px',
px: '5px',
visibility: props.isHovered || menuOpen ? 'unset' : 'hidden',
'&:hover': {
boxShadow:
'0 1px 3px 0 rgba(0, 0, 0, 0.1),0 1px 2px 0 rgba(0, 0, 0, 0.06);',
},
}}
>
<Button
style="hoverActionIcon"
onClick={(event) => {
const quote = props.highlight.quote
if (quote && navigator.clipboard) {
;(async () => {
const text = props.highlight.annotation
? `> ${quote}\n${props.highlight.annotation}`
: quote
await navigator.clipboard.writeText(text)
showSuccessToast('Highlight copied', {
position: 'bottom-right',
})
})()
} else {
showErrorToast('No highlight text.', {
position: 'bottom-right',
})
}
event.preventDefault()
}}
>
<Copy size={19} color={theme.colors.thNotebookSubtle.toString()} />
</Button>
<Button
style="hoverActionIcon"
onClick={(event) => {
props.setLabelsTarget(props.highlight)
event.preventDefault()
}}
>
<Tag size={18} color={theme.colors.thNotebookSubtle.toString()} />
</Button>
<Button
style="hoverActionIcon"
onClick={(event) => {
props.viewInReader(props.highlight.id)
event.preventDefault()
}}
>
<BookOpen size={18} color={theme.colors.thNotebookSubtle.toString()} />
</Button>
<Button
style="hoverActionIcon"
onClick={(event) => {
props.setShowConfirmDeleteHighlightId(props.highlight.id)
event.preventDefault()
}}
>
<Trash size={18} color={theme.colors.thNotebookSubtle.toString()} />
</Button>
</Box>
)
}

View file

@ -8,7 +8,7 @@ import {
useState,
} from 'react'
import { formattedShortTime } from '../../lib/dateFormatting'
import { HStack, SpanBox, VStack } from '../elements/LayoutPrimitives'
import { Box, HStack, SpanBox, VStack } from '../elements/LayoutPrimitives'
import MarkdownIt from 'markdown-it'
import MdEditor, { Plugins } from 'react-markdown-editor-lite'
@ -18,14 +18,10 @@ import throttle from 'lodash/throttle'
import { updateHighlightMutation } from '../../lib/networking/mutations/updateHighlightMutation'
import { Highlight } from '../../lib/networking/fragments/highlightFragment'
import { Button } from '../elements/Button'
import {
ModalContent,
ModalOverlay,
ModalRoot,
} from '../elements/ModalPrimitives'
import { CloseButton } from '../elements/CloseButton'
import { StyledText } from '../elements/StyledText'
import remarkGfm from 'remark-gfm'
import { RcEditorStyles } from './RcEditorStyles'
import { isDarkTheme } from '../../lib/themeUpdater'
import { showErrorToast, showSuccessToast } from '../../lib/toastHelpers'
const mdParser = new MarkdownIt()
@ -33,52 +29,14 @@ MdEditor.use(Plugins.TabInsert, {
tabMapValue: 1, // note that 1 means a '\t' instead of ' '.
})
type NoteSectionProps = {
placeHolder: string
mode: 'edit' | 'preview'
sizeMode: 'normal' | 'maximized'
setEditMode: (set: 'edit' | 'preview') => void
text: string | undefined
saveText: (text: string, completed: (success: boolean) => void) => void
}
export function HighlightNoteBox(props: NoteSectionProps): JSX.Element {
const [lastSaved, setLastSaved] = useState<Date | undefined>(undefined)
const saveText = useCallback(
(text, updateTime) => {
props.saveText(text, (success) => {
if (success) {
setLastSaved(updateTime)
}
})
},
[props]
)
return (
<MarkdownNote
placeHolder={props.placeHolder}
mode={props.mode}
sizeMode={props.sizeMode}
setEditMode={props.setEditMode}
text={props.text}
saveText={saveText}
lastSaved={lastSaved}
fillBackground={false}
/>
)
}
type HighlightViewNoteProps = {
targetId: string
placeHolder: string
mode: 'edit' | 'preview'
highlight: Highlight
sizeMode: 'normal' | 'maximized'
setEditMode: (set: 'edit' | 'preview') => void
text: string | undefined
@ -87,9 +45,10 @@ type HighlightViewNoteProps = {
export function HighlightViewNote(props: HighlightViewNoteProps): JSX.Element {
const [lastSaved, setLastSaved] = useState<Date | undefined>(undefined)
const [errorSaving, setErrorSaving] = useState<string | undefined>(undefined)
const saveText = useCallback(
(text, updateTime) => {
(text, updateTime, interactive) => {
;(async () => {
const success = await updateHighlightMutation({
annotation: text,
@ -99,6 +58,13 @@ export function HighlightViewNote(props: HighlightViewNoteProps): JSX.Element {
setLastSaved(updateTime)
props.highlight.annotation = text
props.updateHighlight(props.highlight)
if (interactive) {
showSuccessToast('Note saved', {
position: 'bottom-right',
})
}
} else {
setErrorSaving('Error saving note.')
}
})()
},
@ -107,48 +73,52 @@ export function HighlightViewNote(props: HighlightViewNoteProps): JSX.Element {
return (
<MarkdownNote
targetId={props.targetId}
placeHolder={props.placeHolder}
mode={props.mode}
sizeMode={props.sizeMode}
setEditMode={props.setEditMode}
text={props.text}
saveText={saveText}
lastSaved={lastSaved}
errorSaving={errorSaving}
fillBackground={true}
/>
)
}
type MarkdownNote = {
targetId: string
placeHolder: string
mode: 'edit' | 'preview'
sizeMode: 'normal' | 'maximized'
setEditMode: (set: 'edit' | 'preview') => void
text: string | undefined
fillBackground: boolean | undefined
lastSaved: Date | undefined
saveText: (text: string, updateTime: Date) => void
errorSaving: string | undefined
saveText: (text: string, updateTime: Date, interactive: boolean) => void
}
export function MarkdownNote(props: MarkdownNote): JSX.Element {
const editorRef = useRef<MdEditor | null>(null)
const isDark = isDarkTheme()
const [lastChanged, setLastChanged] = useState<Date | undefined>(undefined)
const [errorSaving, setErrorSaving] = useState<string | undefined>(undefined)
const saveRef = useRef(props.saveText)
useEffect(() => {
saveRef.current = props.saveText
}, [props.lastSaved, lastChanged])
}, [props])
const debouncedSave = useMemo<
(text: string, updateTime: Date) => void
>(() => {
const func = (text: string, updateTime: Date) => {
saveRef.current?.(text, updateTime)
saveRef.current?.(text, updateTime, false)
}
return throttle(func, 3000)
}, [])
@ -164,9 +134,10 @@ export function MarkdownNote(props: MarkdownNote): JSX.Element {
const updateTime = new Date()
setLastChanged(updateTime)
debouncedSave(data.text, updateTime)
},
[props.lastSaved, lastChanged]
[]
)
return (
@ -174,29 +145,15 @@ export function MarkdownNote(props: MarkdownNote): JSX.Element {
{props.mode == 'edit' ? (
<VStack
css={{
pt: '5px',
width: '100%',
mt: '15px',
'.rc-md-editor': {
borderRadius: '5px',
},
'.rc-md-navigation': {
borderRadius: '5px',
borderBottomLeftRadius: '0px',
borderBottomRightRadius: '0px',
},
'.rc-md-editor .editor-container >.section': {
borderRight: 'unset',
},
'.rc-md-editor .editor-container .sec-md .input': {
padding: '10px',
borderRadius: '5px',
fontSize: '16px',
},
...RcEditorStyles(isDark, false),
}}
onKeyDown={(event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.code.toLowerCase() === 'escape') {
props.setEditMode('preview')
event.preventDefault()
event.stopPropagation()
}
}}
>
@ -230,7 +187,7 @@ export function MarkdownNote(props: MarkdownNote): JSX.Element {
]}
style={{
width: '100%',
height: props.sizeMode == 'normal' ? '160px' : '320px',
height: '160px',
}}
renderHTML={(text: string) => mdParser.render(text)}
onChange={handleEditorChange}
@ -246,7 +203,7 @@ export function MarkdownNote(props: MarkdownNote): JSX.Element {
alignment="start"
distribution="start"
>
{errorSaving && (
{props.errorSaving && (
<SpanBox
css={{
width: '100%',
@ -255,7 +212,7 @@ export function MarkdownNote(props: MarkdownNote): JSX.Element {
color: 'red',
}}
>
{errorSaving}
{props.errorSaving}
</SpanBox>
)}
{props.lastSaved !== undefined ? (
@ -267,65 +224,60 @@ export function MarkdownNote(props: MarkdownNote): JSX.Element {
)}`}
</>
) : null}
{lastChanged !== props.lastSaved && (
<SpanBox
css={{
fontSize: '9px',
mt: '1px',
color: 'green',
marginLeft: 'auto',
<SpanBox
css={{
fontSize: '9px',
mt: '10px',
color: 'green',
marginLeft: 'auto',
}}
>
<Button
css={{ marginRight: '10px' }}
style="ctaOutlineYellow"
onClick={(event) => {
props.setEditMode('preview')
event.preventDefault()
}}
>
<Button
css={{
textDecoration: 'underline',
border: 'unset',
background: 'unset',
'&:hover': {
border: 'unset',
background: 'unset',
},
}}
onClick={(event) => {
const value = editorRef.current?.getMdValue()
if (value) {
props.saveText(value, new Date())
}
event.preventDefault()
}}
>
Save
</Button>
</SpanBox>
)}
Cancel
</Button>
<Button
style="ctaDarkYellow"
onClick={(event) => {
if (editorRef.current) {
const value = editorRef.current.getMdValue()
const updateTime = new Date()
setLastChanged(updateTime)
props.saveText(value, updateTime, true)
props.setEditMode('preview')
} else {
showErrorToast('Error saving note.', {
position: 'bottom-right',
})
}
event.preventDefault()
}}
>
Save
</Button>
</SpanBox>
</HStack>
</VStack>
) : (
<>
<SpanBox
css={{
p: '5px',
p: props.text ? '10px' : '0px',
width: '100%',
fontSize: '15px',
borderRadius: '3px',
marginTop: props.fillBackground || !props.text ? '10px' : '0px',
paddingLeft:
props.fillBackground && props.text
? '10px'
: !props.text
? '5px'
: '0px',
paddingRight:
props.fillBackground && props.text
? '10px'
: !props.text
? '5px'
: '0px',
fontSize: '12px',
marginTop: '0px',
color: props.text ? '$thHighContrast' : '#898989',
border: props.text ? 'unset' : '1px solid $thBorderColor',
borderRadius: '5px',
background:
props.text && props.fillBackground ? '$thBackground5' : 'unset',
props.text && props.fillBackground
? '$thNotebookTextBackground'
: 'unset',
'> *': {
m: '0px',
},
@ -342,111 +294,3 @@ export function MarkdownNote(props: MarkdownNote): JSX.Element {
</>
)
}
type MarkdownModalProps = {
placeHolder: string
mode: 'edit' | 'preview'
sizeMode: 'normal' | 'maximized'
setEditMode: (set: 'edit' | 'preview') => void
text: string | undefined
saveText: (text: string, completed: (success: boolean) => void) => void
}
export function MarkdownModal(props: MarkdownModalProps): JSX.Element {
const [lastSaved, setLastSaved] = useState<Date | undefined>(undefined)
const saveText = useCallback(
(text, updateTime) => {
props.saveText(text, (success) => {
if (success) {
setLastSaved(updateTime)
}
})
},
[props]
)
const handleClose = useCallback(() => {
console.log('onOpenChange')
}, [])
return (
<ModalRoot
defaultOpen
onOpenChange={handleClose}
css={{ width: '100%', height: '100%' }}
>
<ModalOverlay css={{ width: '100%', height: '100%' }} />
<ModalContent
css={{
bg: '$grayBg',
zIndex: '30',
width: '100%',
height: '100%',
maxHeight: 'unset',
maxWidth: 'unset',
}}
>
<VStack>
<HStack
distribution="between"
alignment="center"
css={{
width: '100%',
position: 'sticky',
top: '0px',
height: '50px',
p: '20px',
bg: '$grayBg',
zIndex: 10,
}}
>
<StyledText style="modalHeadline" css={{ color: '$thTextSubtle2' }}>
Edit Note
</StyledText>
<HStack
css={{
ml: 'auto',
cursor: 'pointer',
gap: '15px',
mr: '-5px',
}}
distribution="center"
alignment="center"
>
{/* <Dropdown triggerElement={<MenuTrigger />}>
<DropdownOption
onSelect={() => {
exportHighlights()
}}
title="Export Notebook"
/>
<DropdownOption
onSelect={() => {
setShowConfirmDeleteNote(true)
}}
title="Delete Document Note"
/>
</Dropdown> */}
<CloseButton close={handleClose} />
</HStack>
</HStack>
<SpanBox css={{ padding: '20px', width: '100%', height: '100%' }}>
<MarkdownNote
placeHolder={props.placeHolder}
mode={props.mode}
sizeMode={props.sizeMode}
setEditMode={props.setEditMode}
text={props.text}
saveText={saveText}
lastSaved={lastSaved}
fillBackground={false}
/>
</SpanBox>
</VStack>
</ModalContent>
</ModalRoot>
)
}

View file

@ -1,6 +1,5 @@
/* eslint-disable react/no-children-prop */
import { BookOpen, PencilLine } from 'phosphor-react'
import { useState } from 'react'
import { useMemo, useState } from 'react'
import type { Highlight } from '../../lib/networking/fragments/highlightFragment'
import { LabelChip } from '../elements/LabelChip'
import {
@ -10,61 +9,105 @@ import {
SpanBox,
HStack,
} from '../elements/LayoutPrimitives'
import { styled } from '../tokens/stitches.config'
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 { HighlightsMenu } from '../templates/homeFeed/HighlightItem'
import { ReadableItem } from '../../lib/networking/queries/useGetLibraryItemsQuery'
import { UserBasicData } from '../../lib/networking/queries/useGetViewerQuery'
import {
autoUpdate,
offset,
size,
useFloating,
useHover,
useInteractions,
} from '@floating-ui/react'
import { LibraryHoverActions } from './LibraryCards/LibraryHoverActions'
import { HighlightHoverActions } from './HighlightHoverActions'
type HighlightViewProps = {
item: ReadableItem
viewer: UserBasicData
highlight: Highlight
author?: string
title?: string
updateHighlight: (highlight: Highlight) => void
viewInReader: (highlightId: string) => void
setLabelsTarget: (target: Highlight) => void
setShowConfirmDeleteHighlightId: (set: string) => void
}
const StyledQuote = styled(Blockquote, {
p: '0px',
margin: '0px 0px 0px 0px',
fontSize: '18px',
lineHeight: '27px',
borderRadius: '4px',
width: '100%',
})
export function HighlightView(props: HighlightViewProps): JSX.Element {
const isDark = isDarkTheme()
const [noteMode, setNoteMode] = useState<'preview' | 'edit'>('preview')
const [isHovered, setIsHovered] = useState(false)
const [isOpen, setIsOpen] = useState(false)
const { refs, floatingStyles, context } = useFloating({
open: isOpen,
onOpenChange: setIsOpen,
middleware: [
offset({
mainAxis: -25,
}),
size(),
],
placement: 'top-end',
whileElementsMounted: autoUpdate,
})
const hover = useHover(context)
const { getReferenceProps, getFloatingProps } = useInteractions([hover])
const highlightAlpha = isDark ? 1.0 : 0.35
return (
<HStack
<VStack
ref={refs.setReference}
{...getReferenceProps()}
css={{
p: '0px',
width: '100%',
height: '100%',
alignItems: 'stretch',
}}
>
<VStack css={{ minHeight: '100%', width: '10px' }}>
<Box
css={{
mt: '5px',
width: '10px',
height: '10px',
background: '#FFD234',
borderRadius: '7px',
}}
<Box
ref={refs.setFloating}
style={floatingStyles}
{...getFloatingProps()}
>
<HighlightHoverActions
viewer={props.viewer}
highlight={props.highlight}
isHovered={isOpen ?? false}
viewInReader={props.viewInReader}
setLabelsTarget={props.setLabelsTarget}
setShowConfirmDeleteHighlightId={
props.setShowConfirmDeleteHighlightId
}
/>
<Box
css={{
width: '2px',
flexGrow: '1',
background: '#FFD234',
marginLeft: '4px',
flex: '1',
marginBottom: '10px',
}}
/>
</VStack>
</Box>
<VStack
css={{
width: '100%',
padding: '0px',
paddingLeft: '15px',
'@mdDown': {
padding: '0px',
},
}}
>
<StyledQuote>
@ -72,10 +115,16 @@ export function HighlightView(props: HighlightViewProps): JSX.Element {
css={{
'> *': {
m: '0px',
display: 'inline',
padding: '2px',
backgroundColor: `rgba(var(--colors-highlightBackground), ${highlightAlpha})`,
boxShadow: `1px 0 0 rgba(var(--colors-highlightBackground), ${highlightAlpha}), -1px 0 0 rgba(var(--colors-highlightBackground), ${highlightAlpha})`,
boxDecorationBreak: 'clone',
borderRadius: '2px',
},
fontSize: '15px',
lineHeight: 1.5,
color: '$grayText',
color: '$thTextSubtle2',
img: {
display: 'block',
margin: '0.5em auto !important',
@ -96,43 +145,27 @@ export function HighlightView(props: HighlightViewProps): JSX.Element {
))}
</Box>
<HStack
css={{ width: '100%', height: '100%', pt: '15px' }}
css={{
width: '100%',
pt: '15px',
'@mdDown': {
p: '10px',
},
}}
alignment="start"
distribution="start"
>
<HighlightViewNote
targetId={props.highlight.id}
text={props.highlight.annotation}
placeHolder="Add notes to this highlight..."
highlight={props.highlight}
sizeMode={'normal'}
mode={noteMode}
setEditMode={setNoteMode}
updateHighlight={props.updateHighlight}
/>
<SpanBox
css={{
lineHeight: '1',
marginLeft: '20px',
marginTop: '15px',
cursor: 'pointer',
borderRadius: '1000px',
'&:hover': {
background: '#EBEBEB',
},
}}
onClick={(event) => {
setNoteMode(noteMode == 'preview' ? 'edit' : 'preview')
event.preventDefault()
}}
>
{noteMode === 'edit' ? (
<BookOpen size={15} color="#898989" />
) : (
<PencilLine size={15} color="#898989" />
)}
</SpanBox>
</HStack>
</VStack>
</HStack>
</VStack>
)
}

View file

@ -6,14 +6,6 @@ import { Box, SpanBox } from '../../elements/LayoutPrimitives'
dayjs.extend(relativeTime)
export const MetaStyle = {
width: '100%',
color: '$thTextSubtle3',
fontSize: '13px',
fontWeight: '400',
fontFamily: '$display',
}
export const MenuStyle = {
display: 'flex',
marginLeft: 'auto',
@ -30,6 +22,14 @@ export const MenuStyle = {
},
}
export const MetaStyle = {
width: '100%',
color: '$thTextSubtle3',
fontSize: '13px',
fontWeight: '400',
fontFamily: '$display',
}
export const TitleStyle = {
color: '$thTextContrast2',
fontSize: '16px',
@ -119,7 +119,9 @@ export function LibraryItemMetadata(
props: LibraryItemMetadataProps
): JSX.Element {
const highlightCount = useMemo(() => {
return props.item.highlights?.length ?? 0
return (
props.item.highlights?.filter((h) => h.type == 'HIGHLIGHT').length ?? 0
)
}, [props.item.highlights])
return (

View file

@ -5,20 +5,30 @@ import { CoverImage } from '../../elements/CoverImage'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import { useCallback, useState } from 'react'
import { DotsThreeVertical } from 'phosphor-react'
import Link from 'next/link'
import { CardMenu } from '../CardMenu'
import {
AuthorInfoStyle,
CardCheckbox,
DescriptionStyle,
LibraryItemMetadata,
MenuStyle,
MetaStyle,
siteName,
TitleStyle,
MenuStyle,
} from './LibraryCardStyles'
import { sortedLabels } from '../../../lib/labelsSort'
import { LibraryHoverActions } from './LibraryHoverActions'
import {
useHover,
useFloating,
useInteractions,
size,
offset,
autoUpdate,
} from '@floating-ui/react'
import { CardMenu } from '../CardMenu'
import { DotsThree } from 'phosphor-react'
import { isTouchScreenDevice } from '../../../lib/deviceType'
dayjs.extend(relativeTime)
@ -54,9 +64,29 @@ export function ProgressBar(props: ProgressBarProps): JSX.Element {
export function LibraryGridCard(props: LinkedItemCardProps): JSX.Element {
const [isHovered, setIsHovered] = useState(false)
const [isOpen, setIsOpen] = useState(false)
const { refs, floatingStyles, context } = useFloating({
open: isOpen,
onOpenChange: setIsOpen,
middleware: [
offset({
mainAxis: -25,
}),
size(),
],
placement: 'top-end',
whileElementsMounted: autoUpdate,
})
const hover = useHover(context)
const { getReferenceProps, getFloatingProps } = useInteractions([hover])
return (
<VStack
ref={refs.setReference}
{...getReferenceProps()}
css={{
pl: '20px',
padding: '15px',
@ -86,18 +116,34 @@ export function LibraryGridCard(props: LinkedItemCardProps): JSX.Element {
{props.inMultiSelect ? (
<LibraryGridCardContent {...props} isHovered={isHovered} />
) : (
<Link
href={`${props.viewer.profile.username}/${props.item.slug}`}
passHref
>
<a
<>
{!isTouchScreenDevice() && (
<Box
ref={refs.setFloating}
style={floatingStyles}
{...getFloatingProps()}
>
<LibraryHoverActions
item={props.item}
viewer={props.viewer}
handleAction={props.handleAction}
isHovered={isHovered ?? false}
/>
</Box>
)}
<Link
href={`${props.viewer.profile.username}/${props.item.slug}`}
style={{ textDecoration: 'unset', width: '100%', height: '100%' }}
tabIndex={-1}
passHref
>
<LibraryGridCardContent {...props} isHovered={isHovered} />
</a>
</Link>
<a
href={`${props.viewer.profile.username}/${props.item.slug}`}
style={{ textDecoration: 'unset', width: '100%', height: '100%' }}
tabIndex={-1}
>
<LibraryGridCardContent {...props} isHovered={isHovered} />
</a>
</Link>
</>
)}
</VStack>
)
@ -133,7 +179,7 @@ const LibraryGridCardContent = (props: LinkedItemCardProps): JSX.Element => {
<Box
css={{
...MenuStyle,
visibility: props.isHovered || menuOpen ? 'unset' : 'hidden',
visibility: menuOpen ? 'visible' : 'hidden',
'@media (hover: none)': {
visibility: 'unset',
},
@ -145,12 +191,13 @@ const LibraryGridCardContent = (props: LinkedItemCardProps): JSX.Element => {
onOpenChange={(open) => setMenuOpen(open)}
actionHandler={props.handleAction}
triggerElement={
<DotsThreeVertical size={25} weight="bold" color="#ADADAD" />
<DotsThree size={25} weight="bold" color="#ADADAD" />
}
/>
</Box>
)}
</HStack>
<VStack
alignment="start"
distribution="start"

View file

@ -1,5 +1,5 @@
import { Box, VStack, HStack, SpanBox } from '../../elements/LayoutPrimitives'
import { useMemo, useState } from 'react'
import { useCallback, useMemo, useState } from 'react'
import { CaretDown, CaretUp } from 'phosphor-react'
import { MetaStyle, timeAgo, TitleStyle } from './LibraryCardStyles'
import { styled } from '@stitches/react'
@ -10,6 +10,8 @@ import { theme } from '../../tokens/stitches.config'
import { getHighlightLocation } from '../../templates/article/NotebookModal'
import { Highlight } from '../../../lib/networking/fragments/highlightFragment'
import { HighlightView } from '../HighlightView'
import { useRouter } from 'next/router'
import { showErrorToast } from '../../../lib/toastHelpers'
export const GridSeparator = styled(Box, {
height: '1px',
@ -28,8 +30,32 @@ export function LibraryHighlightGridCard(
props: LibraryHighlightGridCardProps
): JSX.Element {
const [expanded, setExpanded] = useState(false)
const higlightCount = props.item.highlights?.length ?? 0
const router = useRouter()
const viewInReader = useCallback(
(highlightId) => {
if (!router || !router.isReady || !props.viewer) {
showErrorToast('Error navigating to highlight')
return
}
console.log('pushing user: ', props.viewer, 'slug: ', props.item.slug)
router.push(
{
pathname: '/[username]/[slug]',
query: {
username: props.viewer.profile.username,
slug: props.item.slug,
},
hash: highlightId,
},
`${props.viewer.profile.username}/${props.item.slug}#${highlightId}`,
{
scroll: false,
}
)
},
[router, props]
)
const sortedHighlights = useMemo(() => {
const sorted = (a: number, b: number) => {
@ -123,14 +149,23 @@ export function LibraryHighlightGridCard(
<>
<GridSeparator css={{ width: '100%' }} />
<VStack
css={{ height: '100%', width: '100%', mt: '20px' }}
css={{ height: '100%', width: '100%', mt: '20px', gap: '20px' }}
distribution="start"
>
{sortedHighlights.map((highlight) => (
<SpanBox key={`hv-${highlight.id}`}>
<SpanBox key={`hv-${highlight.id}`} css={{ width: '100%' }}>
<HighlightView
key={highlight.id}
viewer={props.viewer}
item={props.item}
highlight={highlight}
viewInReader={viewInReader}
setLabelsTarget={() => {
console.log('TODO: set labels')
}}
setShowConfirmDeleteHighlightId={() => {
console.log('TODO: confirm delete')
}}
updateHighlight={(highlight) => {
console.log('updated highlight: ', highlight)
}}

View file

@ -0,0 +1,125 @@
import { useState } from 'react'
import { Box, SpanBox } from '../../elements/LayoutPrimitives'
import { LibraryItemNode } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
import { LinkedItemCardAction } from './CardTypes'
import { Button } from '../../elements/Button'
import { theme } from '../../tokens/stitches.config'
import {
ArchiveBox,
DotsThree,
Notebook,
Tag,
Trash,
Tray,
} from 'phosphor-react'
import { CardMenu } from '../CardMenu'
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
type LibraryHoverActionsProps = {
viewer: UserBasicData
isHovered: boolean
item: LibraryItemNode
handleAction: (action: LinkedItemCardAction) => void
}
export const LibraryHoverActions = (props: LibraryHoverActionsProps) => {
const [menuOpen, setMenuOpen] = useState(false)
return (
<Box
css={{
overflow: 'clip',
height: '33px',
width: '162px',
bg: '$thBackground',
display: 'flex',
pt: '0px',
alignItems: 'center',
justifyContent: 'center',
border: '1px solid $thBackground5',
borderRadius: '5px',
gap: '5px',
px: '5px',
visibility: props.isHovered || menuOpen ? 'unset' : 'hidden',
'&:hover': {
boxShadow:
'0 1px 3px 0 rgba(0, 0, 0, 0.1),0 1px 2px 0 rgba(0, 0, 0, 0.06);',
},
}}
>
<Button
style="hoverActionIcon"
onClick={(event) => {
props.handleAction('open-notebook')
event.preventDefault()
}}
>
<Notebook size={19} color={theme.colors.thNotebookSubtle.toString()} />
</Button>
<Button
style="hoverActionIcon"
onClick={(event) => {
const action = props.item.isArchived ? 'unarchive' : 'archive'
props.handleAction(action)
event.preventDefault()
}}
>
{props.item.isArchived ? (
<Tray size={18} color={theme.colors.thNotebookSubtle.toString()} />
) : (
<ArchiveBox
size={18}
color={theme.colors.thNotebookSubtle.toString()}
/>
)}
</Button>
<Button
style="hoverActionIcon"
onClick={(event) => {
props.handleAction('delete')
event.preventDefault()
}}
>
<Trash size={18} color={theme.colors.thNotebookSubtle.toString()} />
</Button>
<Button
style="hoverActionIcon"
onClick={(event) => {
props.handleAction('set-labels')
event.preventDefault()
}}
>
<Tag size={18} color={theme.colors.thNotebookSubtle.toString()} />
</Button>
<CardMenu
item={props.item}
viewer={props.viewer}
onOpenChange={(open) => setMenuOpen(open)}
actionHandler={props.handleAction}
triggerElement={
<SpanBox
css={{
display: 'flex',
pt: '2.5px',
height: '33px',
'&:hover': {
bg: '$grayBgHover',
},
}}
>
<DotsThree
size={25}
weight="bold"
color={theme.colors.thNotebookSubtle.toString()}
/>
</SpanBox>
}
/>
</Box>
)
}

View file

@ -2,34 +2,67 @@ import { Box, VStack, HStack, SpanBox } from '../../elements/LayoutPrimitives'
import { LabelChip } from '../../elements/LabelChip'
import type { LinkedItemCardProps } from './CardTypes'
import { useCallback, useState } from 'react'
import { DotsThree } from 'phosphor-react'
import Link from 'next/link'
import { CardMenu } from '../CardMenu'
import {
AuthorInfoStyle,
CardCheckbox,
LibraryItemMetadata,
MenuStyle,
MetaStyle,
siteName,
TitleStyle,
MenuStyle,
} from './LibraryCardStyles'
import { sortedLabels } from '../../../lib/labelsSort'
import { LIBRARY_LEFT_MENU_WIDTH } from '../../templates/homeFeed/LibraryFilterMenu'
import { LibraryHoverActions } from './LibraryHoverActions'
import {
useHover,
useFloating,
useInteractions,
size,
offset,
autoUpdate,
} from '@floating-ui/react'
import { CardMenu } from '../CardMenu'
import { DotsThree } from 'phosphor-react'
import { isTouchScreenDevice } from '../../../lib/deviceType'
export function LibraryListCard(props: LinkedItemCardProps): JSX.Element {
const [isHovered, setIsHovered] = useState(false)
const [isOpen, setIsOpen] = useState(false)
const { refs, floatingStyles, context } = useFloating({
open: isOpen,
onOpenChange: setIsOpen,
middleware: [
offset({
mainAxis: -25,
}),
size(),
],
placement: 'top-end',
whileElementsMounted: autoUpdate,
})
const hover = useHover(context)
const { getReferenceProps, getFloatingProps } = useInteractions([hover])
return (
<VStack
ref={refs.setReference}
{...getReferenceProps()}
css={{
px: '15px',
py: '10px',
px: '20px',
pt: '20px',
pb: '20px',
height: '100%',
cursor: 'pointer',
gap: '10px',
border: '1px solid $grayBorder',
borderBottom: 'none',
borderRadius: '6px',
width: '100vw',
'@media (min-width: 768px)': {
width: `calc(100vw - ${LIBRARY_LEFT_MENU_WIDTH})`,
@ -43,6 +76,12 @@ export function LibraryListCard(props: LinkedItemCardProps): JSX.Element {
'@media (min-width: 1600px)': {
width: '1340px',
},
boxShadow:
'0 1px 3px 0 rgba(0, 0, 0, 0.1),0 1px 2px 0 rgba(0, 0, 0, 0.06);',
'@media (max-width: 930px)': {
boxShadow: 'unset',
borderRadius: 'unset',
},
}}
alignment="start"
distribution="start"
@ -56,18 +95,34 @@ export function LibraryListCard(props: LinkedItemCardProps): JSX.Element {
{props.inMultiSelect ? (
<LibraryListCardContent {...props} isHovered={isHovered} />
) : (
<Link
href={`${props.viewer.profile.username}/${props.item.slug}`}
passHref
>
<a
<>
{!isTouchScreenDevice() && (
<Box
ref={refs.setFloating}
style={floatingStyles}
{...getFloatingProps()}
>
<LibraryHoverActions
item={props.item}
viewer={props.viewer}
handleAction={props.handleAction}
isHovered={isHovered ?? false}
/>
</Box>
)}
<Link
href={`${props.viewer.profile.username}/${props.item.slug}`}
style={{ textDecoration: 'unset', width: '100%', height: '100%' }}
tabIndex={-1}
passHref
>
<LibraryListCardContent {...props} isHovered={isHovered} />
</a>
</Link>
<a
href={`${props.viewer.profile.username}/${props.item.slug}`}
style={{ textDecoration: 'unset', width: '100%', height: '100%' }}
tabIndex={-1}
>
<LibraryListCardContent {...props} isHovered={isHovered} />
</a>
</Link>
</>
)}
</VStack>
)
@ -76,8 +131,8 @@ export function LibraryListCard(props: LinkedItemCardProps): JSX.Element {
export function LibraryListCardContent(
props: LinkedItemCardProps
): JSX.Element {
const { isChecked, setIsChecked, item } = props
const [menuOpen, setMenuOpen] = useState(false)
const { isChecked, setIsChecked, item } = props
const originText = siteName(props.item.originalArticleUrl, props.item.url)
const handleCheckChanged = useCallback(() => {
@ -99,7 +154,7 @@ export function LibraryListCardContent(
<Box
css={{
...MenuStyle,
visibility: props.isHovered || menuOpen ? 'unset' : 'hidden',
visibility: menuOpen ? 'visible' : 'hidden',
'@media (hover: none)': {
visibility: 'unset',
},
@ -122,11 +177,14 @@ export function LibraryListCardContent(
distribution="start"
css={{ height: '100%', width: '100%' }}
>
<Box css={{ ...TitleStyle, width: '80%' }}>{props.item.title}</Box>
<Box css={{ ...TitleStyle, fontSize: '18px', width: '80%' }}>
{props.item.title}
</Box>
<SpanBox
css={{
mt: '5px',
...AuthorInfoStyle,
maxWidth: '90%',
}}
>
{props.item.author}

View file

@ -0,0 +1,30 @@
/* eslint-disable functional/no-class */
import { FloppyDisk } from 'phosphor-react'
import { PluginComponent } from 'react-markdown-editor-lite'
import { Button } from '../elements/Button'
export default class MDEditorSavePlugin extends PluginComponent {
static pluginName = 'save'
static align = 'right'
constructor(props: any) {
super(props)
}
render() {
return (
<Button
style="plainIcon"
css={{ display: 'flex', pr: '5px' }}
onClick={(event) => {
document.dispatchEvent(new Event('saveMarkdownNote'))
event.preventDefault()
}}
>
<FloppyDisk size={18} weight="bold" color="#757575" />
</Button>
)
}
}

View file

@ -0,0 +1,33 @@
export const RcEditorStyles = (isDark: boolean, shadow: boolean) => {
return {
'.rc-md-editor .rc-md-navigation': {
background: '$grayBg',
borderBottom: '1px solid $thBorderSubtle',
},
'.rc-md-editor': {
borderRadius: '5px',
backgroundColor: isDark ? '#2A2A2A' : 'white',
border: '1px solid $thBorderSubtle',
},
'.rc-md-navigation': {
borderRadius: '5px',
borderBottomLeftRadius: '0px',
borderBottomRightRadius: '0px',
background: 'var(--colors-grayBg)',
},
'.rc-md-editor .editor-container >.section': {
borderRight: 'unset',
},
'.rc-md-editor .editor-container .sec-md .input': {
padding: '10px',
borderRadius: '5px',
fontSize: '16px',
color: isDark ? '#EBEBEB' : 'black',
backgroundColor: isDark ? '#2A2A2A' : 'white',
},
'.rc-md-editor .drop-wrap': {
border: '1px solid $thBorderSubtle',
backgroundColor: isDark ? '#2A2A2A' : 'white',
},
}
}

View file

@ -125,7 +125,7 @@ const readerCommands = () => {
callback: () => {},
},
{
actionDescription: 'Open Notebook',
actionDescription: 'Toggle Notebook open',
shortcutKeys: ['t'],
shortcutKeyDescription: 't',
callback: () => {},

View file

@ -333,7 +333,6 @@ export default function EpubContainer(props: EpubContainerProps): JSX.Element {
key={notebookKey}
viewer={props.viewer}
item={props.article}
highlights={highlightsRef.current}
onClose={(updatedHighlights, deletedAnnotations) => {
console.log(
'closed PDF notebook: ',

View file

@ -2,9 +2,8 @@ import { useState } from 'react'
import { Highlight } from '../../../lib/networking/fragments/highlightFragment'
import { ReadableItem } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
import { HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
import { HighlightView } from '../../patterns/HighlightView'
import { HighlightsMenu } from '../homeFeed/HighlightItem'
type HighlightViewItemProps = {
viewer: UserBasicData
@ -25,38 +24,28 @@ export function HighlightViewItem(props: HighlightViewItemProps): JSX.Element {
return (
<HStack
css={{ width: '100%', py: '20px' }}
css={{
width: '100%',
pt: '0px',
pb: '0px',
}}
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
>
<VStack css={{ width: '100%' }}>
<VStack css={{ width: '100%', height: '100%' }}>
<HighlightView
viewer={props.viewer}
item={props.item}
highlight={props.highlight}
updateHighlight={props.updateHighlight}
/>
<SpanBox css={{ mb: '15px' }} />
</VStack>
<SpanBox
css={{
marginLeft: 'auto',
width: '20px',
visibility: hover ? 'unset' : 'hidden',
'@media (hover: none)': {
visibility: 'unset',
},
}}
>
<HighlightsMenu
item={props.item}
viewer={props.viewer}
highlight={props.highlight}
viewInReader={props.viewInReader}
setLabelsTarget={props.setSetLabelsTarget}
setShowConfirmDeleteHighlightId={
props.setShowConfirmDeleteHighlightId
}
/>
</SpanBox>
<SpanBox css={{ mb: '15px' }} />
</VStack>
</HStack>
)
}

View file

@ -19,13 +19,17 @@ import { HighlightBar, HighlightAction } from '../../patterns/HighlightBar'
import { removeHighlights } from '../../../lib/highlights/deleteHighlight'
import { createHighlight } from '../../../lib/highlights/createHighlight'
import { HighlightNoteModal } from './HighlightNoteModal'
import { NotebookModal } from './NotebookModal'
import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'
import { ArticleMutations } from '../../../lib/articleActions'
import { isTouchScreenDevice } from '../../../lib/deviceType'
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
import { ReadableItem } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
import { SetHighlightLabelsModalPresenter } from './SetLabelsModalPresenter'
import SlidingPane from 'react-sliding-pane'
import 'react-sliding-pane/dist/react-sliding-pane.css'
import { NotebookContent } from './Notebook'
import { NotebookHeader } from './NotebookHeader'
import useGetWindowDimensions from '../../../lib/hooks/useGetWindowDimensions'
type HighlightsLayerProps = {
viewer: UserBasicData
@ -74,15 +78,15 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
const focusedHighlightMousePos = useRef({ pageX: 0, pageY: 0 })
const [currentHighlightIdx, setCurrentHighlightIdx] = useState(0)
const [focusedHighlight, setFocusedHighlight] = useState<
Highlight | undefined
>(undefined)
const [focusedHighlight, setFocusedHighlight] =
useState<Highlight | undefined>(undefined)
const [selectionData, setSelectionData] = useSelection(highlightLocations)
const [labelsTarget, setLabelsTarget] = useState<Highlight | undefined>(
undefined
)
const [labelsTarget, setLabelsTarget] =
useState<Highlight | undefined>(undefined)
const windowDimensions = useGetWindowDimensions()
const createHighlightFromSelection = useCallback(
async (
@ -183,6 +187,7 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
)
setHighlights(highlights.filter(($0) => $0.id !== highlightId))
setFocusedHighlight(undefined)
document.dispatchEvent(new Event('highlightsUpdated'))
} else {
console.error('Failed to delete highlight')
}
@ -439,7 +444,7 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
await removeHighlightCallback()
break
case 'create':
await createHighlightCallback('none')
await createHighlightCallback()
break
case 'comment':
if (props.highlightBarDisabled || focusedHighlight) {
@ -541,6 +546,21 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
}
}
const deleteHighlightById = useCallback(
(event: Event) => {
const annotationId = (event as CustomEvent).detail as string
if (annotationId) {
removeHighlights(
highlights.map((h) => h.id),
highlightLocations
)
const keptHighlights = highlights.filter(($0) => $0.id !== annotationId)
setHighlights([...keptHighlights])
}
},
[highlights, highlightLocations]
)
useEffect(() => {
const safeHandleAction = async (action: HighlightAction) => {
try {
@ -652,7 +672,7 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
dispatchHighlightMessage('noteCreated')
} else {
try {
await createHighlightCallback('none')
await createHighlightCallback()
dispatchHighlightMessage('noteCreated')
} catch (error) {
dispatchHighlightError('saveAnnotation', error)
@ -671,6 +691,7 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
document.addEventListener('setHighlightLabels', setHighlightLabels)
document.addEventListener('scrollToNextHighlight', goToNextHighlight)
document.addEventListener('scrollToPrevHighlight', goToPreviousHighlight)
document.addEventListener('deleteHighlightbyId', deleteHighlightById)
return () => {
document.removeEventListener('annotate', annotate)
@ -687,91 +708,95 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
'scrollToPrevHighlight',
goToPreviousHighlight
)
document.removeEventListener('deleteHighlightbyId', deleteHighlightById)
}
})
if (highlightModalAction?.highlightModalAction == 'addComment') {
return (
<HighlightNoteModal
highlight={highlightModalAction.highlight}
author={props.articleAuthor}
title={props.articleTitle}
onUpdate={updateHighlightsCallback}
onOpenChange={() =>
setHighlightModalAction({ highlightModalAction: 'none' })
}
createHighlightForNote={highlightModalAction?.createHighlightForNote}
/>
)
}
if (labelsTarget) {
return (
<SetHighlightLabelsModalPresenter
highlight={labelsTarget}
highlightId={labelsTarget.id}
onOpenChange={() => setLabelsTarget(undefined)}
/>
)
}
// Display the button bar if we are not in the native app and there
// is a focused highlight or selection data
if (!props.highlightBarDisabled && (focusedHighlight || selectionData)) {
const anchorCoordinates = () => {
return {
pageX:
selectionData?.focusPosition.x ??
focusedHighlightMousePos.current?.pageX ??
0,
pageY:
selectionData?.focusPosition.y ??
focusedHighlightMousePos.current?.pageY ??
0,
}
const anchorCoordinates = () => {
return {
pageX:
selectionData?.focusPosition.x ??
focusedHighlightMousePos.current?.pageX ??
0,
pageY:
selectionData?.focusPosition.y ??
focusedHighlightMousePos.current?.pageY ??
0,
}
return (
<>
<HighlightBar
anchorCoordinates={anchorCoordinates()}
isNewHighlight={!!selectionData}
handleButtonClick={handleAction}
isSharedToFeed={focusedHighlight?.sharedAt != undefined}
displayAtBottom={isTouchScreenDevice()}
/>
</>
)
}
if (props.showHighlightsModal) {
return (
<NotebookModal
viewer={props.viewer}
item={props.item}
highlights={highlights}
onClose={handleCloseNotebook}
viewHighlightInReader={(highlightId) => {
// The timeout here is a bit of a hack to work around rerendering
setTimeout(() => {
const target = document.querySelector(
`[omnivore-highlight-id="${highlightId}"]`
)
target?.scrollIntoView({
block: 'center',
behavior: 'auto',
})
}, 1)
history.replaceState(
undefined,
window.location.href,
`#${highlightId}`
)
return (
<>
{highlightModalAction?.highlightModalAction == 'addComment' && (
<HighlightNoteModal
highlight={highlightModalAction.highlight}
author={props.articleAuthor}
title={props.articleTitle}
onUpdate={updateHighlightsCallback}
onOpenChange={() =>
setHighlightModalAction({ highlightModalAction: 'none' })
}
createHighlightForNote={highlightModalAction?.createHighlightForNote}
/>
)}
{labelsTarget && (
<SetHighlightLabelsModalPresenter
highlight={labelsTarget}
highlightId={labelsTarget.id}
onOpenChange={() => setLabelsTarget(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) && (
<>
<HighlightBar
anchorCoordinates={anchorCoordinates()}
isNewHighlight={!!selectionData}
handleButtonClick={handleAction}
isSharedToFeed={focusedHighlight?.sharedAt != undefined}
displayAtBottom={isTouchScreenDevice()}
/>
</>
)}
<SlidingPane
className="sliding-pane-class"
isOpen={props.showHighlightsModal}
width={windowDimensions.width < 600 ? '100%' : '420px'}
hideHeader={true}
from="right"
overlayClassName="slide-panel-overlay"
onRequestClose={() => {
props.setShowHighlightsModal(false)
}}
/>
)
}
return <></>
>
<>
<NotebookHeader setShowNotebook={props.setShowHighlightsModal} />
<NotebookContent
viewer={props.viewer}
item={props.item}
// highlights={highlights}
// onClose={handleCloseNotebook}
viewInReader={(highlightId) => {
// The timeout here is a bit of a hack to work around rerendering
setTimeout(() => {
const target = document.querySelector(
`[omnivore-highlight-id="${highlightId}"]`
)
target?.scrollIntoView({
block: 'center',
behavior: 'auto',
})
}, 1)
history.replaceState(
undefined,
window.location.href,
`#${highlightId}`
)
}}
/>
</>
</SlidingPane>
</>
)
}

View file

@ -2,8 +2,8 @@ import { Box, HStack, VStack, SpanBox } from '../../elements/LayoutPrimitives'
import { StyledText } from '../../elements/StyledText'
import { theme } from '../../tokens/stitches.config'
import type { Highlight } from '../../../lib/networking/fragments/highlightFragment'
import { useCallback, useEffect, useMemo, useReducer, useState } from 'react'
import { BookOpen, PencilLine, X } from 'phosphor-react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { CaretDown, CaretRight } from 'phosphor-react'
import { updateHighlightMutation } from '../../../lib/networking/mutations/updateHighlightMutation'
import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'
import { diff_match_patch } from 'diff-match-patch'
@ -12,28 +12,26 @@ import { createHighlightMutation } from '../../../lib/networking/mutations/creat
import { v4 as uuidv4 } from 'uuid'
import { nanoid } from 'nanoid'
import { deleteHighlightMutation } from '../../../lib/networking/mutations/deleteHighlightMutation'
import { HighlightNoteBox } from '../../patterns/HighlightNotes'
import { HighlightViewItem } from './HighlightViewItem'
import { ConfirmationModal } from '../../patterns/ConfirmationModal'
import { TrashIcon } from '../../elements/images/TrashIcon'
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
import { ReadableItem } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
import { SetHighlightLabelsModalPresenter } from './SetLabelsModalPresenter'
import { Button } from '../../elements/Button'
import { ArticleNotes } from '../../patterns/ArticleNotes'
import { useGetArticleQuery } from '../../../lib/networking/queries/useGetArticleQuery'
import { formattedShortTime } from '../../../lib/dateFormatting'
import { isDarkTheme } from '../../../lib/themeUpdater'
type NotebookProps = {
type NotebookContentProps = {
viewer: UserBasicData
item: ReadableItem
highlights: Highlight[]
sizeMode: 'normal' | 'maximized'
viewInReader: (highlightId: string) => void
onAnnotationsChanged?: (
highlights: Highlight[],
deletedAnnotations: Highlight[]
) => void
onAnnotationsChanged?: (highlights: Highlight[]) => void
showConfirmDeleteNote?: boolean
setShowConfirmDeleteNote?: (show: boolean) => void
@ -45,161 +43,95 @@ export const getHighlightLocation = (patch: string): number | undefined => {
return patches[0].start1 || undefined
}
type AnnotationInfo = {
loaded: boolean
type NoteState = {
isCreating: boolean
note: Highlight | undefined
noteId: string
allAnnotations: Highlight[]
deletedAnnotations: Highlight[]
createStarted: Date | undefined
}
export function Notebook(props: NotebookProps): JSX.Element {
export function NotebookContent(props: NotebookContentProps): JSX.Element {
const isDark = isDarkTheme()
const { articleData, mutate } = useGetArticleQuery({
slug: props.item.slug,
username: props.viewer.profile.username,
includeFriendsHighlights: false,
})
const [noteText, setNoteText] = useState<string>('')
const [showConfirmDeleteHighlightId, setShowConfirmDeleteHighlightId] =
useState<undefined | string>(undefined)
const [labelsTarget, setLabelsTarget] = useState<Highlight | undefined>(
undefined
)
const [notesEditMode, setNotesEditMode] = useState<'edit' | 'preview'>(
'preview'
)
const [, updateState] = useState({})
const annotationsReducer = (
state: AnnotationInfo,
action: {
type: string
allHighlights?: Highlight[]
note?: Highlight | undefined
updateHighlight?: Highlight | undefined
deleteHighlightId?: string | undefined
}
) => {
switch (action.type) {
case 'RESET': {
const note = action.allHighlights?.find((h) => h.type == 'NOTE')
return {
...state,
loaded: true,
note: note,
noteId: note?.id ?? state.noteId,
allAnnotations: [...(action.allHighlights ?? [])],
}
}
case 'CREATE_NOTE': {
if (!action.note) {
throw new Error('No note on CREATE_NOTE action')
}
return {
...state,
note: action.note,
noteId: action.note.id,
allAnnotations: [...state.allAnnotations, action.note],
}
}
case 'DELETE_NOTE': {
// If there is no note to delete, just make sure we have cleared out the note
const noteId = action.note?.id
if (!action.note?.id) {
return {
...state,
node: undefined,
noteId: uuidv4(),
}
}
const idx = state.allAnnotations.findIndex((h) => h.id === noteId)
return {
...state,
note: undefined,
noteId: uuidv4(),
allAnnotations: state.allAnnotations.splice(idx, 1),
}
}
case 'DELETE_HIGHLIGHT': {
const highlightId = action.deleteHighlightId
if (!highlightId) {
throw new Error('No highlightId for delete action.')
}
const idx = state.allAnnotations.findIndex((h) => h.id === highlightId)
if (idx < 0) {
return { ...state }
}
const deleted = state.deletedAnnotations
deleted.push(state.allAnnotations[idx])
return {
...state,
deletedAnnotations: deleted,
allAnnotations: state.allAnnotations.splice(idx, 1),
}
}
case 'UPDATE_HIGHLIGHT': {
const highlight = action.updateHighlight
if (!highlight) {
throw new Error('No highlightId for delete action.')
}
const idx = state.allAnnotations.findIndex((h) => h.id === highlight.id)
if (idx !== -1) {
state.allAnnotations[idx] = highlight
}
return {
...state,
}
}
default:
return state
}
}
const [annotations, dispatchAnnotations] = useReducer(annotationsReducer, {
loaded: false,
const [labelsTarget, setLabelsTarget] =
useState<Highlight | undefined>(undefined)
const noteState = useRef<NoteState>({
isCreating: false,
note: undefined,
noteId: uuidv4(),
allAnnotations: [],
deletedAnnotations: [],
createStarted: undefined,
})
useEffect(() => {
dispatchAnnotations({
type: 'RESET',
allHighlights: props.highlights,
})
}, [props.highlights])
const newNoteId = useMemo(() => {
return uuidv4()
}, [])
useEffect(() => {
if (props.onAnnotationsChanged) {
props.onAnnotationsChanged(
annotations.allAnnotations,
annotations.deletedAnnotations
)
}
}, [annotations])
const updateNote = useCallback(
(note: Highlight, text: string, startTime: Date) => {
;(async () => {
const result = await updateHighlightMutation({
highlightId: note.id,
annotation: text,
})
if (result) {
setLastSaved(startTime)
} else {
setErrorSaving('Error saving')
}
})()
},
[]
)
const deleteDocumentNote = useCallback(() => {
const note = annotations.note
if (!note) {
showErrorToast('No note found')
return
}
const createNote = useCallback((text: string) => {
console.log('creating note: ', newNoteId, noteState.current.isCreating)
noteState.current.isCreating = true
noteState.current.createStarted = new Date()
;(async () => {
try {
const result = await deleteHighlightMutation(note.id)
if (!result) {
throw new Error()
}
showSuccessToast('Note deleted')
dispatchAnnotations({
note,
type: 'DELETE_NOTE',
const success = await createHighlightMutation({
id: newNoteId,
shortId: nanoid(8),
type: 'NOTE',
articleId: props.item.id,
annotation: text,
})
} catch (err) {
console.log('error deleting note', err)
showErrorToast('Error deleting note')
if (success) {
noteState.current.note = success
noteState.current.isCreating = false
} else {
setErrorSaving('Error creating note')
}
} catch (error) {
console.error('error creating note: ', error)
noteState.current.isCreating = false
setErrorSaving('Error creating note')
}
})()
}, [annotations])
}, [])
const highlights = useMemo(() => {
const result = articleData?.article.article.highlights
const note = result?.find((h) => h.type === 'NOTE')
if (note) {
noteState.current.note = note
noteState.current.isCreating = false
setNoteText(note.annotation || '')
}
return result
}, [articleData])
useEffect(() => {
if (highlights && props.onAnnotationsChanged) {
props.onAnnotationsChanged(highlights)
}
}, [highlights])
const sortedHighlights = useMemo(() => {
const sorted = (a: number, b: number) => {
@ -212,7 +144,7 @@ export function Notebook(props: NotebookProps): JSX.Element {
return 0
}
return annotations.allAnnotations
return (highlights ?? [])
.filter((h) => h.type === 'HIGHLIGHT')
.sort((a: Highlight, b: Highlight) => {
if (a.highlightPositionPercent && b.highlightPositionPercent) {
@ -229,83 +161,123 @@ export function Notebook(props: NotebookProps): JSX.Element {
} catch {}
return a.createdAt.localeCompare(b.createdAt)
})
}, [annotations])
}, [highlights])
const handleSaveNoteText = useCallback(
(text, cb: (success: boolean) => void) => {
if (!annotations.loaded) {
// We haven't loaded the user's annotations yet, so we can't
// find or create their highlight note.
return
}
(text) => {
const changeTime = new Date()
if (!annotations.note) {
const noteId = annotations.noteId
;(async () => {
const success = await createHighlightMutation({
id: noteId,
shortId: nanoid(8),
type: 'NOTE',
articleId: props.item.id,
annotation: text,
})
console.log('success creating annotation note: ', success)
if (success) {
dispatchAnnotations({
type: 'CREATE_NOTE',
note: success,
})
}
cb(!!success)
})()
setLastChanged(changeTime)
if (noteState.current.note) {
updateNote(noteState.current.note, text, changeTime)
return
}
if (noteState.current.isCreating) {
if (noteState.current.createStarted) {
const timeSinceStart =
new Date().getTime() - noteState.current.createStarted.getTime()
if (annotations.note) {
const note = annotations.note
;(async () => {
const success = await updateHighlightMutation({
highlightId: note.id,
annotation: text,
})
console.log('success updating annotation note: ', success)
if (success) {
note.annotation = text
dispatchAnnotations({
type: 'UPDATE_NOTE',
note: note,
})
if (timeSinceStart > 4000) {
createNote(text)
return
}
cb(!!success)
})()
}
return
}
createNote(text)
},
[annotations, props.item]
[noteText, noteState, createNote, updateNote, highlights]
)
const deleteDocumentNote = useCallback(() => {
;(async () => {
highlights
?.filter((h) => h.type === 'NOTE')
.forEach(async (h) => {
const result = await deleteHighlightMutation(h.id)
if (!result) {
showErrorToast('Error deleting note')
}
})
noteState.current.note = undefined
})()
setNoteText('')
}, [noteState, highlights])
const [errorSaving, setErrorSaving] = useState<string | undefined>(undefined)
const [lastChanged, setLastChanged] = useState<Date | undefined>(undefined)
const [lastSaved, setLastSaved] = useState<Date | undefined>(undefined)
useEffect(() => {
const highlightsUpdated = () => {
mutate()
}
document.addEventListener('highlightsUpdated', highlightsUpdated)
return () => {
document.removeEventListener('highlightsUpdated', highlightsUpdated)
}
}, [mutate])
return (
<VStack
tabIndex={-1}
distribution="start"
css={{ height: '100%', width: '100%', p: '20px' }}
>
<TitledSection
title="ARTICLE NOTES"
editMode={notesEditMode == 'edit'}
setEditMode={(edit) => setNotesEditMode(edit ? 'edit' : 'preview')}
/>
<HighlightNoteBox
mode={notesEditMode}
sizeMode={props.sizeMode}
setEditMode={setNotesEditMode}
text={annotations.note?.annotation}
placeHolder="Add notes to this document..."
saveText={handleSaveNoteText}
/>
<SpanBox css={{ mt: '10px', mb: '25px' }} />
<Box css={{ width: '100%' }}>
<TitledSection title="HIGHLIGHTS" />
css={{
height: '100%',
width: '100%',
px: '20px',
bg: '$thLibrarySearchbox',
'@mdDown': { p: '15px' },
}}
>
<>
<HStack
alignment="start"
distribution="start"
css={{ width: '100%', gap: '10px', mt: '25px' }}
>
<ArticleNotes
targetId={props.item.id}
text={noteText}
setText={setNoteText}
placeHolder="Add notes to this document..."
saveText={handleSaveNoteText}
/>
</HStack>
<HStack
css={{
minHeight: '15px',
width: '100%',
fontSize: '9px',
mt: '5px',
color: '$thTextSubtle',
}}
alignment="start"
distribution="start"
>
{errorSaving && (
<SpanBox
css={{
width: '100%',
fontSize: '9px',
mt: '5px',
}}
>
{errorSaving}
</SpanBox>
)}
{lastSaved !== undefined ? (
<>
{lastChanged === lastSaved
? 'Saved'
: `Last saved ${formattedShortTime(lastSaved.toISOString())}`}
</>
) : null}
</HStack>
</>
<VStack css={{ mt: '25px', gap: '25px' }}>
{sortedHighlights.map((highlight) => (
<HighlightViewItem
key={highlight.id}
@ -316,23 +288,24 @@ export function Notebook(props: NotebookProps): JSX.Element {
setSetLabelsTarget={setLabelsTarget}
setShowConfirmDeleteHighlightId={setShowConfirmDeleteHighlightId}
updateHighlight={() => {
dispatchAnnotations({
type: 'UPDATE_HIGHLIGHT',
updateHighlight: highlight,
})
mutate()
}}
/>
))}
{sortedHighlights.length === 0 && (
<Box
css={{
p: '10px',
mt: '15px',
width: '100%',
fontSize: '9px',
fontSize: '13px',
color: '$thTextSubtle',
alignItems: 'center',
justifyContent: 'center',
mb: '100px',
bg: isDark ? '#3D3D3D' : '$thBackground',
borderRadius: '6px',
boxShadow: '0px 4px 4px rgba(33, 33, 33, 0.1)',
}}
>
You have not added any highlights to this document.
@ -340,32 +313,34 @@ export function Notebook(props: NotebookProps): JSX.Element {
)}
<Box
css={{
'@mdDown': {
height: '320px',
width: '100%',
background: 'transparent',
},
width: '100%',
height: '320px',
}}
/>
</Box>
></Box>
</VStack>
{showConfirmDeleteHighlightId && (
<ConfirmationModal
message={'Are you sure you want to delete this highlight?'}
onAccept={() => {
;(async () => {
const highlightId = showConfirmDeleteHighlightId
const success = await deleteHighlightMutation(
showConfirmDeleteHighlightId
)
console.log(' ConfirmationModal::DeleteHighlight', success)
mutate()
if (success) {
dispatchAnnotations({
type: 'DELETE_HIGHLIGHT',
deleteHighlightId: showConfirmDeleteHighlightId,
showSuccessToast('Highlight deleted.', {
position: 'bottom-right',
})
showSuccessToast('Highlight deleted.')
const event = new CustomEvent('deleteHighlightbyId', {
detail: highlightId,
})
document.dispatchEvent(event)
} else {
showErrorToast('Error deleting highlight')
showErrorToast('Error deleting highlight', {
position: 'bottom-right',
})
}
})()
setShowConfirmDeleteHighlightId(undefined)
@ -383,7 +358,10 @@ export function Notebook(props: NotebookProps): JSX.Element {
<SetHighlightLabelsModalPresenter
highlight={labelsTarget}
highlightId={labelsTarget.id}
onOpenChange={() => setLabelsTarget(undefined)}
onOpenChange={() => {
mutate()
setLabelsTarget(undefined)
}}
/>
)}
{props.showConfirmDeleteNote && (
@ -407,62 +385,46 @@ export function Notebook(props: NotebookProps): JSX.Element {
)
}
type TitledSectionProps = {
type SectionTitleProps = {
title: string
editMode?: boolean
setEditMode?: (set: boolean) => void
selected: boolean
setSelected: (set: boolean) => void
}
function TitledSection(props: TitledSectionProps): JSX.Element {
function SectionTitle(props: SectionTitleProps): JSX.Element {
return (
<>
<HStack
css={{ width: '100%', borderBottom: '1px solid $thBorderColor' }}
alignment="start"
distribution="start"
<Button
style="plainIcon"
css={{
display: 'flex',
alignItems: 'center',
gap: '5px',
color: props.selected ? '$thTextContrast' : '$thTextSubtle',
borderBottom: props.selected
? '1px solid $thTextContrast'
: '1px solid transparent',
}}
onClick={(event) => {
props.setSelected(true)
event.stopPropagation()
}}
>
<StyledText
css={{
fontFamily: '$display',
fontStyle: 'normal',
fontWeight: '700',
fontSize: '12px',
lineHeight: '20px',
color: '#898989',
marginBottom: '1px',
m: '0px',
pt: '2px',
pb: '2px',
px: '5px',
fontFamily: '$inter',
fontWeight: '500',
fontSize: '13px',
color: '$thNotebookSubtle',
}}
>
{props.title}
</StyledText>
{props.setEditMode && (
<SpanBox
css={{
marginLeft: 'auto',
justifyContent: 'end',
lineHeight: '1',
alignSelf: 'end',
padding: '2px',
cursor: 'pointer',
borderRadius: '1000px',
'&:hover': {
background: '#EBEBEB',
},
}}
onClick={(event) => {
if (props.setEditMode) {
props.setEditMode(!props.editMode)
}
event.preventDefault()
}}
>
{props.editMode ? (
<BookOpen size={15} color="#898989" />
) : (
<PencilLine size={15} color="#898989" />
)}
</SpanBox>
)}
</HStack>
</Button>
</>
)
}

View file

@ -0,0 +1,77 @@
import { useCallback } from 'react'
import { Highlight } from '../../../lib/networking/fragments/highlightFragment'
import { ReadableItem } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
import {
UserBasicData,
useGetViewerQuery,
} from '../../../lib/networking/queries/useGetViewerQuery'
import { CloseButton } from '../../elements/CloseButton'
import { Dropdown, DropdownOption } from '../../elements/DropdownElements'
import { HStack } from '../../elements/LayoutPrimitives'
import { MenuTrigger } from '../../elements/MenuTrigger'
import { StyledText } from '../../elements/StyledText'
import { NotebookModal } from './NotebookModal'
import { Sidebar } from 'phosphor-react'
import { theme } from '../../tokens/stitches.config'
import { Button } from '../../elements/Button'
type NotebookHeaderProps = {
setShowNotebook: (set: boolean) => void
}
export const NotebookHeader = (props: NotebookHeaderProps) => {
const handleClose = useCallback(() => {
props.setShowNotebook(false)
}, [props])
return (
<HStack
distribution="center"
alignment="center"
css={{
width: '100%',
position: 'sticky',
top: '0px',
height: '50px',
p: '20px',
borderTopLeftRadius: '10px',
overflow: 'clip',
background: '$thLibrarySearchbox',
zIndex: 10,
borderBottom: '1px solid $thNotebookBorder',
}}
>
<StyledText style="modalHeadline" css={{ color: '$thNotebookSubtle' }}>
Notebook
</StyledText>
<HStack
css={{
ml: 'auto',
cursor: 'pointer',
gap: '15px',
mr: '-5px',
}}
distribution="center"
alignment="center"
>
{/* <Dropdown triggerElement={<MenuTrigger />}>
<DropdownOption
onSelect={() => {
// exportHighlights()
}}
title="Export Notebook"
/>
<DropdownOption
onSelect={() => {
// setShowConfirmDeleteNote(true)
}}
title="Delete Article Note"
/>
</Dropdown> */}
<Button style="plainIcon" onClick={() => props.setShowNotebook(false)}>
<Sidebar size={25} color={theme.colors.thNotebookSubtle.toString()} />
</Button>
</HStack>
</HStack>
)
}

View file

@ -16,7 +16,7 @@ import { diff_match_patch } from 'diff-match-patch'
import { MenuTrigger } from '../../elements/MenuTrigger'
import { highlightsAsMarkdown } from '../homeFeed/HighlightItem'
import 'react-markdown-editor-lite/lib/index.css'
import { Notebook } from './Notebook'
import { NotebookContent } from './Notebook'
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
import { ReadableItem } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
@ -24,10 +24,9 @@ type NotebookModalProps = {
viewer: UserBasicData
item: ReadableItem
highlights: Highlight[]
viewHighlightInReader: (arg: string) => void
onClose: (highlights: Highlight[], deletedAnnotations: Highlight[]) => void
onClose: (highlights: Highlight[], deletedHighlights: Highlight[]) => void
}
export const getHighlightLocation = (patch: string): number | undefined => {
@ -37,26 +36,22 @@ export const getHighlightLocation = (patch: string): number | undefined => {
}
export function NotebookModal(props: NotebookModalProps): JSX.Element {
const [sizeMode, setSizeMode] = useState<'normal' | 'maximized'>('normal')
const [showConfirmDeleteNote, setShowConfirmDeleteNote] = useState(false)
const [allAnnotations, setAllAnnotations] = useState<Highlight[] | undefined>(
undefined
)
const [deletedAnnotations, setDeletedAnnotations] = useState<
const [deletedHighlights, setDeletedAnnotations] = useState<
Highlight[] | undefined
>(undefined)
const handleClose = useCallback(() => {
props.onClose(allAnnotations ?? [], deletedAnnotations ?? [])
}, [props, allAnnotations, deletedAnnotations])
props.onClose(allAnnotations ?? [], deletedHighlights ?? [])
}, [props, allAnnotations])
const handleAnnotationsChange = useCallback(
(allAnnotations, deletedAnnotations) => {
setAllAnnotations(allAnnotations)
setDeletedAnnotations(deletedAnnotations)
},
[]
)
const handleAnnotationsChange = useCallback((allAnnotations) => {
setAllAnnotations(allAnnotations)
}, [])
const exportHighlights = useCallback(() => {
;(async () => {
@ -88,9 +83,11 @@ export function NotebookModal(props: NotebookModalProps): JSX.Element {
}}
css={{
overflow: 'auto',
height: sizeMode === 'normal' ? 'unset' : '100%',
maxWidth: sizeMode === 'normal' ? '640px' : '100%',
minHeight: sizeMode === 'normal' ? '525px' : 'unset',
bg: '$thLibraryBackground',
width: '100%',
height: 'unset',
maxWidth: '748px',
minHeight: '525px',
'@mdDown': {
top: '20px',
width: '100%',
@ -99,9 +96,18 @@ export function NotebookModal(props: NotebookModalProps): JSX.Element {
transform: 'translate(-50%)',
},
}}
onKeyUp={(event) => {
switch (event.key) {
case 'Escape':
handleClose()
event.preventDefault()
event.stopPropagation()
break
}
}}
>
<HStack
distribution="between"
distribution="center"
alignment="center"
css={{
width: '100%',
@ -126,7 +132,6 @@ export function NotebookModal(props: NotebookModalProps): JSX.Element {
distribution="center"
alignment="center"
>
<SizeToggle mode={sizeMode} setMode={setSizeMode} />
<Dropdown triggerElement={<MenuTrigger />}>
<DropdownOption
onSelect={() => {
@ -144,9 +149,8 @@ export function NotebookModal(props: NotebookModalProps): JSX.Element {
<CloseButton close={handleClose} />
</HStack>
</HStack>
<Notebook
<NotebookContent
{...props}
sizeMode={sizeMode}
viewInReader={viewInReader}
onAnnotationsChanged={handleAnnotationsChange}
showConfirmDeleteNote={showConfirmDeleteNote}
@ -188,33 +192,3 @@ function CloseButton(props: { close: () => void }): JSX.Element {
</Button>
)
}
function SizeToggle(props: SizeToggleProps): JSX.Element {
return (
<Button
style="plainIcon"
css={{
display: 'flex',
padding: '2px',
alignItems: 'center',
borderRadius: '9999px',
'&:hover': {
bg: '#898989',
},
'@mdDown': {
display: 'none',
},
}}
onClick={(event) => {
props.setMode(props.mode == 'normal' ? 'maximized' : 'normal')
event.preventDefault()
}}
>
{props.mode == 'normal' ? (
<ArrowsOut size="15" color={theme.colors.thTextContrast2.toString()} />
) : (
<ArrowsIn size="15" color={theme.colors.thTextContrast2.toString()} />
)}
</Button>
)
}

View file

@ -1,33 +1,59 @@
import { Highlight } from '../../../lib/networking/fragments/highlightFragment'
import { ReadableItem } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
import {
UserBasicData,
useGetViewerQuery,
} from '../../../lib/networking/queries/useGetViewerQuery'
import { NotebookModal } from './NotebookModal'
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
import SlidingPane from 'react-sliding-pane'
import 'react-sliding-pane/dist/react-sliding-pane.css'
import { NotebookContent } from './Notebook'
import { NotebookHeader } from './NotebookHeader'
import useGetWindowDimensions from '../../../lib/hooks/useGetWindowDimensions'
type NotebookPresenterProps = {
viewer: UserBasicData
item: ReadableItem
highlights: Highlight[]
onClose: (highlights: Highlight[]) => void
open: boolean
setOpen: (open: boolean) => void
}
export const NotebookPresenter = (props: NotebookPresenterProps) => {
const windowDimensions = useGetWindowDimensions()
return (
<NotebookModal
viewer={props.viewer}
item={props.item}
highlights={props.highlights}
onClose={(highlights: Highlight[], deletedAnnotations: Highlight[]) => {
console.log('NotebookModal: ', highlights, deletedAnnotations)
props.onClose(highlights)
<SlidingPane
className="sliding-pane-class"
isOpen={props.open}
width={windowDimensions.width < 600 ? '100%' : '420px'}
hideHeader={true}
from="right"
overlayClassName="slide-panel-overlay"
onRequestClose={() => {
props.setOpen(false)
}}
viewHighlightInReader={(highlightId) => {
window.location.href = `/${props.viewer.profile.username}/${props.item.slug}#${highlightId}`
}}
/>
>
<>
<NotebookHeader setShowNotebook={props.setOpen} />
<NotebookContent
viewer={props.viewer}
item={props.item}
viewInReader={(highlightId) => {
// The timeout here is a bit of a hack to work around rerendering
setTimeout(() => {
const target = document.querySelector(
`[omnivore-highlight-id="${highlightId}"]`
)
target?.scrollIntoView({
block: 'center',
behavior: 'auto',
})
}, 1)
history.replaceState(
undefined,
window.location.href,
`#${highlightId}`
)
}}
/>
</>
</SlidingPane>
)
}

View file

@ -13,11 +13,15 @@ import { articleReadingProgressMutation } from '../../../lib/networking/mutation
import { mergeHighlightMutation } from '../../../lib/networking/mutations/mergeHighlightMutation'
import { useCanShareNative } from '../../../lib/hooks/useCanShareNative'
import { pspdfKitKey } from '../../../lib/appConfig'
import { NotebookModal } from './NotebookModal'
import { HighlightNoteModal } from './HighlightNoteModal'
import { showErrorToast } from '../../../lib/toastHelpers'
import { HEADER_HEIGHT } from '../homeFeed/HeaderSpacer'
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
import SlidingPane from 'react-sliding-pane'
import 'react-sliding-pane/dist/react-sliding-pane.css'
import { NotebookContent } from './Notebook'
import { NotebookHeader } from './NotebookHeader'
import useWindowDimensions from '../../../lib/hooks/useGetWindowDimensions'
export type PdfArticleContainerProps = {
viewer: UserBasicData
@ -30,14 +34,12 @@ export default function PdfArticleContainer(
props: PdfArticleContainerProps
): JSX.Element {
const containerRef = useRef<HTMLDivElement | null>(null)
const [shareTarget, setShareTarget] = useState<Highlight | undefined>(
undefined
)
const [shareTarget, setShareTarget] =
useState<Highlight | undefined>(undefined)
const [notebookKey, setNotebookKey] = useState<string>(uuidv4())
const [noteTarget, setNoteTarget] = useState<Highlight | undefined>(undefined)
const [noteTargetPageIndex, setNoteTargetPageIndex] = useState<
number | undefined
>(undefined)
const [noteTargetPageIndex, setNoteTargetPageIndex] =
useState<number | undefined>(undefined)
const highlightsRef = useRef<Highlight[]>([])
const canShareNative = useCanShareNative()
@ -475,6 +477,8 @@ export default function PdfArticleContainer(
// the PSPDFKit instance if the theme, article URL, or page URL changes. Everything else
// should be handled by the PSPDFKit instance callbacks.
const windowDimensions = useWindowDimensions()
return (
<Box
id="article-wrapper"
@ -505,35 +509,31 @@ export default function PdfArticleContainer(
}}
/>
)}
{props.showHighlightsModal && (
<NotebookModal
key={notebookKey}
viewer={props.viewer}
item={props.article}
highlights={highlightsRef.current}
onClose={(updatedHighlights, deletedAnnotations) => {
console.log(
'closed PDF notebook: ',
updatedHighlights,
deletedAnnotations
)
deletedAnnotations.forEach((highlight) => {
const event = new CustomEvent('deleteHighlightbyId', {
detail: highlight.id,
<SlidingPane
className="sliding-pane-class"
isOpen={props.showHighlightsModal}
width={windowDimensions.width < 600 ? '100%' : '420px'}
hideHeader={true}
from="right"
overlayClassName="slide-panel-overlay"
onRequestClose={() => {
props.setShowHighlightsModal(false)
}}
>
<>
<NotebookHeader setShowNotebook={props.setShowHighlightsModal} />
<NotebookContent
viewer={props.viewer}
item={props.article}
viewInReader={(highlightId) => {
const event = new CustomEvent('scrollToHighlightId', {
detail: highlightId,
})
document.dispatchEvent(event)
})
props.setShowHighlightsModal(false)
}}
viewHighlightInReader={(highlightId) => {
const event = new CustomEvent('scrollToHighlightId', {
detail: highlightId,
})
document.dispatchEvent(event)
props.setShowHighlightsModal(false)
}}
/>
)}
}}
/>
</>
</SlidingPane>
</Box>
)
}

View file

@ -14,6 +14,7 @@ import { useGetLabelsQuery } from '../../../lib/networking/queries/useGetLabelsQ
import { v4 as uuidv4 } from 'uuid'
import { randomLabelColorHex } from '../../../utils/settings-page/labels/labelColorObjects'
import { LabelsDispatcher } from '../../../lib/hooks/useSetPageLabels'
import * as Dialog from '@radix-ui/react-dialog'
type SetLabelsModalProps = {
provider: LabelsProvider
@ -30,9 +31,8 @@ export function SetLabelsModal(props: SetLabelsModalProps): JSX.Element {
const availableLabels = useGetLabelsQuery()
const [tabCount, setTabCount] = useState(-1)
const [tabStartValue, setTabStartValue] = useState('')
const [errorMessage, setErrorMessage] = useState<string | undefined>(
undefined
)
const [errorMessage, setErrorMessage] =
useState<string | undefined>(undefined)
const errorTimeoutRef = useRef<NodeJS.Timeout | undefined>()
const [highlightLastLabel, setHighlightLastLabel] = useState(false)
@ -171,44 +171,46 @@ export function SetLabelsModal(props: SetLabelsModalProps): JSX.Element {
return (
<ModalRoot defaultOpen onOpenChange={props.onOpenChange}>
<ModalOverlay />
<ModalContent
tabIndex={0}
css={{
border: '1px solid $grayBorder',
backgroundColor: '$thBackground',
}}
onPointerDownOutside={(event) => {
event.preventDefault()
props.onOpenChange(false)
}}
onEscapeKeyDown={(event) => {
props.onOpenChange(false)
event.preventDefault()
}}
>
<VStack distribution="start" css={{ height: '100%' }}>
<SpanBox css={{ pt: '0px', px: '16px', width: '100%' }}>
<ModalTitleBar title="Labels" onOpenChange={props.onOpenChange} />
</SpanBox>
<SetLabelsControl
inputValue={inputValue}
setInputValue={setInputValue}
clearInputState={clearInputState}
selectedLabels={props.selectedLabels}
dispatchLabels={props.dispatchLabels}
tabCount={tabCount}
setTabCount={setTabCount}
tabStartValue={tabStartValue}
setTabStartValue={setTabStartValue}
highlightLastLabel={highlightLastLabel}
setHighlightLastLabel={setHighlightLastLabel}
deleteLastLabel={deleteLastLabel}
selectOrCreateLabel={selectOrCreateLabel}
errorMessage={errorMessage}
/>
</VStack>
</ModalContent>
<Dialog.Portal>
<ModalOverlay />
<ModalContent
tabIndex={0}
css={{
border: '1px solid $grayBorder',
backgroundColor: '$thBackground',
}}
onPointerDownOutside={(event) => {
event.preventDefault()
props.onOpenChange(false)
}}
onEscapeKeyDown={(event) => {
props.onOpenChange(false)
event.preventDefault()
}}
>
<VStack distribution="start" css={{ height: '100%' }}>
<SpanBox css={{ pt: '0px', px: '16px', width: '100%' }}>
<ModalTitleBar title="Labels" onOpenChange={props.onOpenChange} />
</SpanBox>
<SetLabelsControl
inputValue={inputValue}
setInputValue={setInputValue}
clearInputState={clearInputState}
selectedLabels={props.selectedLabels}
dispatchLabels={props.dispatchLabels}
tabCount={tabCount}
setTabCount={setTabCount}
tabStartValue={tabStartValue}
setTabStartValue={setTabStartValue}
highlightLastLabel={highlightLastLabel}
setHighlightLastLabel={setHighlightLastLabel}
deleteLastLabel={deleteLastLabel}
selectOrCreateLabel={selectOrCreateLabel}
errorMessage={errorMessage}
/>
</VStack>
</ModalContent>
</Dialog.Portal>
</ModalRoot>
)
}

View file

@ -10,7 +10,7 @@ import {
DropdownOption,
DropdownSeparator,
} from '../../elements/DropdownElements'
import { Box } from '../../elements/LayoutPrimitives'
import { Box, VStack } from '../../elements/LayoutPrimitives'
import { styled, theme } from '../../tokens/stitches.config'
@ -57,70 +57,77 @@ export function HighlightsMenu(props: HighlightsMenuProps): JSX.Element {
}, [props.highlight])
return (
<Dropdown
triggerElement={
<Box
css={{
display: 'flex',
height: '20px',
width: '20px',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '1000px',
'&:hover': {
bg: '#898989',
},
}}
>
<DotsThreeVertical
size={20}
color={theme.colors.thTextContrast2.toString()}
weight="bold"
/>
</Box>
}
<VStack
distribution="center"
alignment="center"
css={{ height: '100%', pl: '5px', pt: '5px' }}
>
<DropdownOption
onSelect={async () => {
copyHighlight()
}}
title="Copy"
/>
<DropdownOption
onSelect={() => {
props.setLabelsTarget(props.highlight)
}}
title="Labels"
/>
<DropdownOption
onSelect={() => {
props.setShowConfirmDeleteHighlightId(props.highlight.id)
}}
title="Delete"
/>
<DropdownSeparator />
<Link
href={`/${props.viewer.profile.username}/${props.item.slug}#${props.highlight.id}`}
<Dropdown
triggerElement={
<Box
css={{
marginLeft: 'auto',
display: 'flex',
height: '20px',
width: '20px',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '1000px',
'&:hover': {
bg: '#898989',
},
}}
>
<DotsThreeVertical
size={20}
color={theme.colors.thTextContrast2.toString()}
weight="bold"
/>
</Box>
}
>
<StyledLinkItem
onClick={(event) => {
console.log('event.ctrlKey: ', event.ctrlKey, event.metaKey)
if (event.ctrlKey || event.metaKey) {
window.open(
`/${props.viewer.profile.username}/${props.item.slug}#${props.highlight.id}`,
'_blank'
)
return
}
props.viewInReader(props.highlight.id)
event.preventDefault()
event.stopPropagation()
<DropdownOption
onSelect={async () => {
copyHighlight()
}}
title="Copy"
/>
<DropdownOption
onSelect={() => {
props.setLabelsTarget(props.highlight)
}}
title="Labels"
/>
<DropdownOption
onSelect={() => {
props.setShowConfirmDeleteHighlightId(props.highlight.id)
}}
title="Delete"
/>
<DropdownSeparator />
<Link
href={`/${props.viewer.profile.username}/${props.item.slug}#${props.highlight.id}`}
>
View In Reader
</StyledLinkItem>
</Link>
</Dropdown>
<StyledLinkItem
onClick={(event) => {
console.log('event.ctrlKey: ', event.ctrlKey, event.metaKey)
if (event.ctrlKey || event.metaKey) {
window.open(
`/${props.viewer.profile.username}/${props.item.slug}#${props.highlight.id}`,
'_blank'
)
return
}
props.viewInReader(props.highlight.id)
event.preventDefault()
event.stopPropagation()
}}
>
View In Reader
</StyledLinkItem>
</Link>
</Dropdown>
</VStack>
)
}

View file

@ -19,7 +19,7 @@ import {
timeAgo,
} from '../../patterns/LibraryCards/LibraryCardStyles'
import { LibraryHighlightGridCard } from '../../patterns/LibraryCards/LibraryHighlightGridCard'
import { Notebook } from '../article/Notebook'
import { NotebookContent } from '../article/Notebook'
import { EmptyHighlights } from './EmptyHighlights'
import { HEADER_HEIGHT } from './HeaderSpacer'
import { highlightsAsMarkdown } from './HighlightItem'
@ -34,9 +34,8 @@ type HighlightItemsLayoutProps = {
export function HighlightItemsLayout(
props: HighlightItemsLayoutProps
): JSX.Element {
const [currentItem, setCurrentItem] = useState<LibraryItem | undefined>(
undefined
)
const [currentItem, setCurrentItem] =
useState<LibraryItem | undefined>(undefined)
const listReducer = (
state: LibraryItem[],
@ -183,6 +182,7 @@ export function HighlightItemsLayout(
flexGrow: '1',
justifyContent: 'center',
overflowY: 'scroll',
bg: '$thLibrarySearchbox',
'@lgDown': {
display: 'none',
flexGrow: 'unset',
@ -415,24 +415,11 @@ function HighlightList(props: HighlightListProps): JSX.Element {
<HStack
css={{
width: '100%',
borderBottom: '1px solid $thBorderColor',
height: '100%',
}}
alignment="start"
distribution="center"
distribution="end"
>
<StyledText
css={{
fontWeight: '600',
fontSize: '15px',
fontFamily: '$display',
width: '100%',
color: 'thTextContrast2',
m: '0px',
pb: '5px',
}}
>
NOTEBOOK
</StyledText>
<Dropdown triggerElement={<MenuTrigger />}>
<DropdownOption
onSelect={() => {
@ -442,13 +429,13 @@ function HighlightList(props: HighlightListProps): JSX.Element {
/>
</Dropdown>
</HStack>
<HStack css={{ width: '100%', height: '100%' }}>
<HStack
css={{ width: '100%', height: '100%', bg: '$thLibrarySearchbox' }}
>
{props.viewer && (
<Notebook
sizeMode="normal"
<NotebookContent
viewer={props.viewer}
item={props.item.node}
highlights={props.item.node.highlights ?? []}
viewInReader={viewInReader}
/>
)}

View file

@ -84,13 +84,11 @@ export function HomeFeedContainer(): JSX.Element {
const gridContainerRef = useRef<HTMLDivElement>(null)
const [labelsTarget, setLabelsTarget] = useState<LibraryItem | undefined>(
undefined
)
const [labelsTarget, setLabelsTarget] =
useState<LibraryItem | undefined>(undefined)
const [notebookTarget, setNotebookTarget] = useState<LibraryItem | undefined>(
undefined
)
const [notebookTarget, setNotebookTarget] =
useState<LibraryItem | undefined>(undefined)
const [showAddLinkModal, setShowAddLinkModal] = useState(false)
const [showEditTitleModal, setShowEditTitleModal] = useState(false)
@ -207,6 +205,11 @@ export function HomeFeedContainer(): JSX.Element {
}
setActiveCardId(id)
scrollToActiveCard(id, true)
const newItem = getItem(id)
if (notebookTarget && newItem) {
setNotebookTarget(newItem)
}
},
[libraryItems]
)
@ -261,6 +264,13 @@ export function HomeFeedContainer(): JSX.Element {
return libraryItems.find((item) => item.node.id === activeCardId)
}, [libraryItems, activeCardId])
const getItem = useCallback(
(itemId) => {
return libraryItems.find((item) => item.node.id === itemId)
},
[libraryItems]
)
const activeItemIndex = useMemo(() => {
if (!activeCardId) {
return undefined
@ -278,8 +288,6 @@ export function HomeFeedContainer(): JSX.Element {
alreadyScrolled.current = true
if (activeItem) {
console.log('refreshing')
// refresh items on home feed
performActionOnItem('refresh', activeItem)
}
}
@ -342,7 +350,11 @@ export function HomeFeedContainer(): JSX.Element {
setLabelsTarget(item)
break
case 'open-notebook':
setNotebookTarget(item)
if (!notebookTarget) {
setNotebookTarget(item)
} else {
setNotebookTarget(undefined)
}
break
case 'unsubscribe':
performActionOnItem('unsubscribe', item)
@ -481,6 +493,7 @@ export function HomeFeedContainer(): JSX.Element {
handleCardAction('set-labels', activeItem)
break
case 'openNotebook':
console.log('openNotebook: ', notebookTarget)
handleCardAction('open-notebook', activeItem)
break
case 'sortDescending':
@ -1051,12 +1064,13 @@ function LibraryItemsLayout(props: LibraryItemsLayoutProps): JSX.Element {
<NotebookPresenter
viewer={props.viewer}
item={props.notebookTarget?.node}
highlights={props.notebookTarget?.node.highlights ?? []}
onClose={(highlights: Highlight[]) => {
if (props.notebookTarget?.node.highlights) {
props.notebookTarget.node.highlights = highlights
}
props.setNotebookTarget(undefined)
open={props.notebookTarget?.node !== undefined}
setOpen={(open: boolean) => {
// onClose={(highlights: Highlight[]) => {
// if (props.notebookTarget?.node.highlights) {
// props.notebookTarget.node.highlights = highlights
// }
props.setNotebookTarget(open ? props.notebookTarget : undefined)
}}
/>
)}
@ -1101,18 +1115,16 @@ function LibraryItems(props: LibraryItemsProps): JSX.Element {
width: '100%',
gridAutoRows: 'auto',
borderRadius: '6px',
gridGap: props.layout == 'LIST_LAYOUT' ? '0' : '20px',
gridGap: props.layout == 'LIST_LAYOUT' ? '10px' : '20px',
marginTop: '10px',
marginBottom: '0px',
paddingTop: '0',
paddingBottom: '0px',
overflow: 'hidden',
boxShadow:
props.layout == 'LIST_LAYOUT'
? '0 1px 3px 0 rgba(0, 0, 0, 0.1),0 1px 2px 0 rgba(0, 0, 0, 0.06);'
: 'unset',
'@media (max-width: 930px)': {
gridGap: props.layout == 'LIST_LAYOUT' ? '0px' : '20px',
},
'@xlgDown': {
border: 'unset',
borderRadius: props.layout == 'LIST_LAYOUT' ? 0 : undefined,
},
'@smDown': {

View file

@ -180,6 +180,11 @@ export const { styled, css, theme, getCssText, globalCss, keyframes, config } =
thLibraryMenuUnselected: '#898989',
thLibrarySelectionColor: '#FFEA9F',
thNotebookSubtle: '#6A6968',
thNotebookBorder: '#D9D9D9',
thNotebookBackground: '#FCFCFC',
thNotebookTextBackground: '#EBEBEB',
thTextContrast: '#1E1E1E',
thTextContrast2: '#3D3D3D',
@ -274,6 +279,12 @@ const darkThemeSpec = {
thLibraryMenuUnselected: '#898989',
thLibrarySelectionColor: '#3D3D3D',
thNotebookSubtle: '#898989',
thNotebookBorder: '#898989',
thNotebookBackground: '#3B3938',
thNotebookTextBackground: '#3D3D3D',
thNotebookHighContrast: '#2A2A2A',
thTextContrast: '#FFFFFF',
thTextContrast2: '#EBEBEB',

View file

@ -131,6 +131,8 @@ export async function createHighlight(
)
}
document.dispatchEvent(new Event('highlightsUpdated'))
if (highlight) {
const highlights = [...keptHighlights, highlight]
return {

View file

@ -0,0 +1,25 @@
import { useEffect, useState } from 'react'
function getWindowDimensions() {
const { innerWidth: width, innerHeight: height } = window
return {
width,
height,
}
}
export default function useWindowDimensions() {
const [windowDimensions, setWindowDimensions] = useState(
getWindowDimensions()
)
useEffect(() => {
function handleResize() {
setWindowDimensions(getWindowDimensions())
}
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
return windowDimensions
}

View file

@ -14,6 +14,7 @@ import {
Recommendation,
recommendationFragment,
} from './useGetLibraryItemsQuery'
import useSWR from 'swr'
type ArticleQueryInput = {
username?: string
@ -25,6 +26,8 @@ type ArticleQueryOutput = {
articleData?: ArticleData
isLoading: boolean
articleFetchError: string[] | null
mutate: () => void
}
type ArticleData = {
@ -107,7 +110,7 @@ export function useGetArticleQuery({
includeFriendsHighlights,
}
const { data, error } = useSWRImmutable(
const { data, error, mutate } = useSWR(
slug ? [query, username, slug, includeFriendsHighlights] : null,
makeGqlFetcher(variables)
)
@ -124,6 +127,7 @@ export function useGetArticleQuery({
}
return {
mutate: mutate,
articleData: resultData,
isLoading: !error && !data,
articleFetchError: resultError ? (resultError as string[]) : null,

View file

@ -107,6 +107,7 @@ export function isDarkTheme(): boolean {
return (
currentTheme === 'Dark' ||
currentTheme === 'Darker' ||
currentTheme === 'Apollo' ||
currentTheme == 'Black'
)
}

View file

@ -18,6 +18,7 @@
"build-storybook": "build-storybook -s public"
},
"dependencies": {
"@floating-ui/react": "^0.24.3",
"@radix-ui/react-avatar": "^0.1.1",
"@radix-ui/react-checkbox": "^0.1.5",
"@radix-ui/react-dialog": "^0.1.1",
@ -61,6 +62,7 @@
"react-markdown-editor-lite": "^1.3.4",
"react-masonry-css": "^1.0.16",
"react-pro-sidebar": "^0.7.1",
"react-sliding-pane": "^7.3.0",
"react-spinners": "^0.13.7",
"react-super-responsive-table": "^5.2.1",
"react-topbar-progress-indicator": "^4.1.1",

View file

@ -259,6 +259,10 @@ export default function Home(): JSX.Element {
) {
return
}
if (showHighlightsModal) {
setShowHighlightsModal(false)
return
}
const query = window.sessionStorage.getItem('q')
if (query) {
router.push(`/home?${query}`)
@ -350,7 +354,7 @@ export default function Home(): JSX.Element {
name: 'Notebook',
shortcut: ['t'],
perform: () => {
setShowHighlightsModal(true)
setShowHighlightsModal(!showHighlightsModal)
},
},
{
@ -361,7 +365,7 @@ export default function Home(): JSX.Element {
perform: () => setShowEditModal(true),
},
],
[readerSettings]
[readerSettings, showHighlightsModal]
)
const [labels, dispatchLabels] = useSetPageLabels(article?.id)

View file

@ -419,20 +419,22 @@ button {
margin: 0px;
}
.omnivore-masonry-grid {
display: -webkit-box; /* Not needed if autoprefixing */
display: -ms-flexbox; /* Not needed if autoprefixing */
display: flex;
margin-left: -16px; /* gutter size offset */
margin-right: 14px;
width: auto;
.slide-panel-overlay {
z-index: 100 !important;
background: transparent !important;
pointer-events: none;
}
.omnivore-masonry-grid_column {
padding-left: 16px; /* gutter size */
background-clip: padding-box;
.slide-pane__content {
padding: 0px !important;
pointer-events: all;
border-top-left-radius: 10px;
border-bottom-right-radius: 10px;
border-left: 1px solid var(--colors-thNotebookBorder);
background: var(--colors-thNotebookBackground);
}
/* .omnivore-masonry-grid_column > div {
background: grey;
margin-bottom: 16px;
} */
.slide-pane {
background: transparent !important;
box-shadow: 0px 4px 4px rgba(33, 33, 33, 0.1) !important;
}

View file

@ -2504,6 +2504,34 @@
dependencies:
tslib "^2.1.0"
"@floating-ui/core@^1.3.1":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.3.1.tgz#4d795b649cc3b1cbb760d191c80dcb4353c9a366"
integrity sha512-Bu+AMaXNjrpjh41znzHqaz3r2Nr8hHuHZT6V2LBKMhyMl0FgKA62PNYbqnfgmzOhoWZj70Zecisbo4H1rotP5g==
"@floating-ui/dom@^1.3.0":
version "1.4.2"
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.4.2.tgz#eb3a37f7506c4f95ef735967dc3496b5012e11cb"
integrity sha512-VKmvHVatWnewmGGy+7Mdy4cTJX71Pli6v/Wjb5RQBuq5wjUYx+Ef+kRThi8qggZqDgD8CogCpqhRoVp3+yQk+g==
dependencies:
"@floating-ui/core" "^1.3.1"
"@floating-ui/react-dom@^2.0.1":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.0.1.tgz#7972a4fc488a8c746cded3cfe603b6057c308a91"
integrity sha512-rZtAmSht4Lry6gdhAJDrCp/6rKN7++JnL1/Anbr/DdeyYXQPxvg/ivrbYvJulbRf4vL8b212suwMM2lxbv+RQA==
dependencies:
"@floating-ui/dom" "^1.3.0"
"@floating-ui/react@^0.24.3":
version "0.24.3"
resolved "https://registry.yarnpkg.com/@floating-ui/react/-/react-0.24.3.tgz#4f11f09c7245555724f5167dd6925133457db89c"
integrity sha512-wWC9duiog4HmbgKSKObDRuXqMjZR/6m75MIG+slm5CVWbridAjK9STcnCsGYmdpK78H/GmzYj4ADVP8paZVLYQ==
dependencies:
"@floating-ui/react-dom" "^2.0.1"
aria-hidden "^1.1.3"
tabbable "^6.0.1"
"@google-cloud/common@^3.8.1":
version "3.9.0"
resolved "https://registry.yarnpkg.com/@google-cloud/common/-/common-3.9.0.tgz#d93e62d13e66edacfad1cd25b20fdbbc11d9f6dd"
@ -10070,6 +10098,13 @@ aria-hidden@^1.1.1:
dependencies:
tslib "^1.0.0"
aria-hidden@^1.1.3:
version "1.2.3"
resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.3.tgz#14aeb7fb692bbb72d69bebfa47279c1fd725e954"
integrity sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ==
dependencies:
tslib "^2.0.0"
aria-query@^4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-4.2.2.tgz#0d2ca6c9aceb56b8977e9fed6aed7e15bbd2f83b"
@ -14432,6 +14467,11 @@ executable@^4.1.1:
dependencies:
pify "^2.2.0"
exenv@^1.2.0:
version "1.2.2"
resolved "https://registry.yarnpkg.com/exenv/-/exenv-1.2.2.tgz#2ae78e85d9894158670b03d47bec1f03bd91bb9d"
integrity sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw==
exit@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c"
@ -23806,6 +23846,11 @@ react-is@^18.0.0:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"
integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==
react-lifecycles-compat@^3.0.0:
version "3.0.4"
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
react-markdown-editor-lite@^1.3.4:
version "1.3.4"
resolved "https://registry.yarnpkg.com/react-markdown-editor-lite/-/react-markdown-editor-lite-1.3.4.tgz#77992d2389b9427a06595c63d95f52be66e5fea9"
@ -23842,6 +23887,16 @@ react-masonry-css@^1.0.16:
resolved "https://registry.yarnpkg.com/react-masonry-css/-/react-masonry-css-1.0.16.tgz#72b28b4ae3484e250534700860597553a10f1a2c"
integrity sha512-KSW0hR2VQmltt/qAa3eXOctQDyOu7+ZBevtKgpNDSzT7k5LA/0XntNa9z9HKCdz3QlxmJHglTZ18e4sX4V8zZQ==
react-modal@^3.14.3:
version "3.16.1"
resolved "https://registry.yarnpkg.com/react-modal/-/react-modal-3.16.1.tgz#34018528fc206561b1a5467fc3beeaddafb39b2b"
integrity sha512-VStHgI3BVcGo7OXczvnJN7yT2TWHJPDXZWyI/a0ssFNhGZWsPmB8cF0z33ewDXq4VfYMO1vXgiv/g8Nj9NDyWg==
dependencies:
exenv "^1.2.0"
prop-types "^15.7.2"
react-lifecycles-compat "^3.0.0"
warning "^4.0.3"
react-popper-tooltip@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/react-popper-tooltip/-/react-popper-tooltip-3.1.1.tgz#329569eb7b287008f04fcbddb6370452ad3f9eac"
@ -23925,6 +23980,14 @@ react-slidedown@^2.4.5:
dependencies:
tslib "^2.0.0"
react-sliding-pane@^7.3.0:
version "7.3.0"
resolved "https://registry.yarnpkg.com/react-sliding-pane/-/react-sliding-pane-7.3.0.tgz#a6a03b90db216e7ec6f746c7e649d19ba03ff4e0"
integrity sha512-KCyxw2BBvXjwYm1UX83Vk67D4kxec2icJxrSPidNus8voh1yB1K6bluwShAe3OvN5zk8H9InL22jGomTUOOudw==
dependencies:
prop-types "^15.7.2"
react-modal "^3.14.3"
react-spinners@^0.13.7:
version "0.13.7"
resolved "https://registry.yarnpkg.com/react-spinners/-/react-spinners-0.13.7.tgz#0f423c415bfa56765ce9fb36ff604e52a92b37a9"
@ -26195,6 +26258,11 @@ synchronous-promise@^2.0.15:
resolved "https://registry.yarnpkg.com/synchronous-promise/-/synchronous-promise-2.0.15.tgz#07ca1822b9de0001f5ff73595f3d08c4f720eb8e"
integrity sha512-k8uzYIkIVwmT+TcglpdN50pS2y1BDcUnBPK9iJeGu0Pl1lOI8pD6wtzgw91Pjpe+RxtTncw32tLxs/R0yNL2Mg==
tabbable@^6.0.1:
version "6.1.2"
resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-6.1.2.tgz#b0d3ca81d582d48a80f71b267d1434b1469a3703"
integrity sha512-qCN98uP7i9z0fIS4amQ5zbGBOq+OSigYeGvPy7NDk8Y9yncqDZ9pRPgfsc2PJIVM9RrJj7GIfuRgmjoUU9zTHQ==
tapable@^1.0.0, tapable@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2"
@ -27739,7 +27807,7 @@ walker@~1.0.5:
dependencies:
makeerror "1.0.12"
warning@^4.0.2:
warning@^4.0.2, warning@^4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3"
integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==