mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Merge pull request #2412 from omnivore-app/feat/web-notebook-view
Improved notebook modal
This commit is contained in:
commit
178fa5e42d
36 changed files with 1777 additions and 1068 deletions
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
// )
|
||||
}
|
||||
|
|
|
|||
217
packages/web/components/patterns/ArticleNotes.tsx
Normal file
217
packages/web/components/patterns/ArticleNotes.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
113
packages/web/components/patterns/HighlightHoverActions.tsx
Normal file
113
packages/web/components/patterns/HighlightHoverActions.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
30
packages/web/components/patterns/MDEditorSavePlugin.tsx
Normal file
30
packages/web/components/patterns/MDEditorSavePlugin.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
33
packages/web/components/patterns/RcEditorStyles.tsx
Normal file
33
packages/web/components/patterns/RcEditorStyles.tsx
Normal 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',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -125,7 +125,7 @@ const readerCommands = () => {
|
|||
callback: () => {},
|
||||
},
|
||||
{
|
||||
actionDescription: 'Open Notebook',
|
||||
actionDescription: 'Toggle Notebook open',
|
||||
shortcutKeys: ['t'],
|
||||
shortcutKeyDescription: 't',
|
||||
callback: () => {},
|
||||
|
|
|
|||
|
|
@ -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: ',
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
77
packages/web/components/templates/article/NotebookHeader.tsx
Normal file
77
packages/web/components/templates/article/NotebookHeader.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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': {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
||||
|
|
|
|||
|
|
@ -131,6 +131,8 @@ export async function createHighlight(
|
|||
)
|
||||
}
|
||||
|
||||
document.dispatchEvent(new Event('highlightsUpdated'))
|
||||
|
||||
if (highlight) {
|
||||
const highlights = [...keptHighlights, highlight]
|
||||
return {
|
||||
|
|
|
|||
25
packages/web/lib/hooks/useGetWindowDimensions.tsx
Normal file
25
packages/web/lib/hooks/useGetWindowDimensions.tsx
Normal 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
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -107,6 +107,7 @@ export function isDarkTheme(): boolean {
|
|||
return (
|
||||
currentTheme === 'Dark' ||
|
||||
currentTheme === 'Darker' ||
|
||||
currentTheme === 'Apollo' ||
|
||||
currentTheme == 'Black'
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
70
yarn.lock
70
yarn.lock
|
|
@ -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==
|
||||
|
|
|
|||
Loading…
Reference in a new issue