diff --git a/packages/api/src/entity/user_personalization.ts b/packages/api/src/entity/user_personalization.ts index 7fa36b470..6411b3885 100644 --- a/packages/api/src/entity/user_personalization.ts +++ b/packages/api/src/entity/user_personalization.ts @@ -8,6 +8,23 @@ import { UpdateDateColumn, } from 'typeorm' import { User } from './user' +import { Label } from './label' + +export type ShortcutType = 'search' | 'label' | 'newsletter' | 'feed' | 'folder' + +export type Shortcut = { + type: ShortcutType + + id: string + name: string + section: string + filter?: string + + icon?: string + label?: Label + + children?: Shortcut[] +} @Entity({ name: 'user_personalization' }) export class UserPersonalization { @@ -59,4 +76,7 @@ export class UserPersonalization { @Column('jsonb') digestConfig?: any | null + + @Column({ type: 'jsonb', nullable: true }) + shortcuts?: any | null // Explicitly allow null values } diff --git a/packages/api/src/routers/shortcuts_router.ts b/packages/api/src/routers/shortcuts_router.ts new file mode 100644 index 000000000..a6dd88f8a --- /dev/null +++ b/packages/api/src/routers/shortcuts_router.ts @@ -0,0 +1,107 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import cors from 'cors' +import express from 'express' +import { env } from '../env' +import { getClaimsByToken, getTokenByRequest } from '../utils/auth' +import { corsConfig } from '../utils/corsConfig' +import { logger } from '../utils/logger' +import { + getShortcuts, + resetShortcuts, + setShortcuts, +} from '../services/user_personalization' + +export function shortcutsRouter() { + const router = express.Router() + + router.get('/', cors(corsConfig), async (req, res) => { + logger.info('get shortcuts router') + const token = getTokenByRequest(req) + + let claims + try { + claims = await getClaimsByToken(token) + if (!claims) { + logger.info('failed to authorize') + return res.status(401).send('UNAUTHORIZED') + } + } catch (e) { + logger.info('failed to authorize', e) + return res.status(401).send('UNAUTHORIZED') + } + + try { + const shortcuts = await getShortcuts(claims.uid) + return res.send({ + shortcuts: shortcuts ?? [], + }) + } catch (e) { + logger.info('error getting shortcuts', e) + } + + return res.status(500).send('UNKNOWN') + }) + + router.options('/', cors({ ...corsConfig, maxAge: 600 })) + router.put('/', cors(corsConfig), async (req, res) => { + logger.info('put shortcuts router') + const token = getTokenByRequest(req) + + let claims + try { + claims = await getClaimsByToken(token) + if (!claims) { + logger.info('failed to authorize') + return res.status(401).send('UNAUTHORIZED') + } + } catch (e) { + logger.info('failed to authorize', e) + return res.status(401).send('UNAUTHORIZED') + } + + try { + const shortcuts = await setShortcuts(claims.uid, req.body.shortcuts) + return res.send({ + shortcuts: shortcuts ?? [], + }) + } catch (e) { + logger.info('error settings shortcuts', e) + } + + return res.status(500).send('UNKNOWN') + }) + + router.delete('/', cors(corsConfig), async (req, res) => { + logger.info('delete shortcuts router') + const token = getTokenByRequest(req) + + let claims + try { + claims = await getClaimsByToken(token) + if (!claims) { + logger.info('failed to authorize') + return res.status(401).send('UNAUTHORIZED') + } + } catch (e) { + logger.info('failed to authorize', e) + return res.status(401).send('UNAUTHORIZED') + } + + try { + const success = await resetShortcuts(claims.uid) + if (success) { + const shortcuts = await getShortcuts(claims.uid) + return res.send({ + shortcuts: shortcuts ?? [], + }) + } + } catch (e) { + logger.info('error settings shortcuts', e) + } + + return res.status(500).send('UNKNOWN') + }) + + return router +} diff --git a/packages/api/src/server.ts b/packages/api/src/server.ts index 7952cf5f8..3efaf69af 100755 --- a/packages/api/src/server.ts +++ b/packages/api/src/server.ts @@ -47,6 +47,7 @@ import { corsConfig } from './utils/corsConfig' import { getClientFromUserAgent } from './utils/helpers' import { buildLogger, buildLoggerTransport, logger } from './utils/logger' import { apiLimiter, authLimiter } from './utils/rate_limit' +import { shortcutsRouter } from './routers/shortcuts_router' const PORT = process.env.PORT || 4000 @@ -94,6 +95,7 @@ export const createApp = (): Express => { app.use('/api/mobile-auth', authLimiter, mobileAuthRouter()) app.use('/api/page', pageRouter()) app.use('/api/user', userRouter()) + app.use('/api/shortcuts', shortcutsRouter()) app.use('/api/article', articleRouter()) app.use('/api/ai-summary', aiSummariesRouter()) app.use('/api/explain', explainRouter()) diff --git a/packages/api/src/services/user_personalization.ts b/packages/api/src/services/user_personalization.ts index 40e39e459..5539ea462 100644 --- a/packages/api/src/services/user_personalization.ts +++ b/packages/api/src/services/user_personalization.ts @@ -1,6 +1,10 @@ -import { DeepPartial } from 'typeorm' -import { UserPersonalization } from '../entity/user_personalization' +import { DeepPartial, IsNull } from 'typeorm' +import { Shortcut, UserPersonalization } from '../entity/user_personalization' import { authTrx } from '../repository' +import { findLabelsByUserId } from './labels' +import { findSubscriptionById } from './subscriptions' +import { Filter } from '../entity/filter' +import { Subscription, SubscriptionStatus } from '../entity/subscription' export const findUserPersonalization = async (userId: string) => { return authTrx( @@ -34,3 +38,132 @@ export const saveUserPersonalization = async ( userId ) } + +export const getShortcuts = async (userId: string): Promise => { + const personalization = await authTrx( + (t) => + t.getRepository(UserPersonalization).findOneBy({ + user: { id: userId }, + }), + undefined, + userId + ) + if (personalization?.shortcuts) { + return personalization?.shortcuts as Shortcut[] + } + + return await userDefaultShortcuts(userId) +} + +export const resetShortcuts = async (userId: string): Promise => { + const result = await authTrx( + (t) => { + return t + .createQueryBuilder() + .update(UserPersonalization) + .set({ shortcuts: () => 'null' }) // Use a raw SQL string to set the value to null + .where({ + user: { id: userId }, + }) + .execute() + }, + undefined, + userId + ) + if (!result) { + throw Error('Could not update shortcuts') + } + return true +} + +export const setShortcuts = async ( + userId: string, + shortcuts: Shortcut[] +): Promise => { + const result = await authTrx( + (t) => + t.getRepository(UserPersonalization).update( + { + user: { id: userId }, + }, + { + shortcuts: shortcuts, + } + ), + undefined, + userId + ) + if (!result.affected || result.affected < 1) { + throw Error('Could not update shortcuts') + } + return shortcuts +} + +const userDefaultShortcuts = async (userId: string): Promise => { + const labels = await findLabelsByUserId(userId) + const savedSearches = await authTrx((t) => + t.getRepository(Filter).find({ + where: { user: { id: userId } }, + order: { position: 'ASC' }, + }) + ) + const subscriptions = await authTrx((t) => + t.getRepository(Subscription).find({ + where: { user: { id: userId }, status: SubscriptionStatus.Active }, + order: { mostRecentItemDate: 'DESC' }, + }) + ) + + return [ + { + id: '1', + type: 'folder', + name: 'Labels', + section: 'library', + children: labels.map((label) => { + return { + id: label.id, + type: 'label', + name: label.name, + section: 'library', + label: label, + filter: `in:all label:"${label.name}"`, + } + }), + }, + { + id: '2', + type: 'folder', + name: 'Subscriptions', + section: 'subscriptions', + children: subscriptions.map((subscription) => { + return { + id: subscription.id, + type: subscription.type == 'NEWSLETTER' ? 'newsletter' : 'feed', + name: subscription.name, + section: 'subscriptions', + icon: subscription.icon ?? undefined, + filter: + subscription.type == 'NEWSLETTER' + ? `in:following subscription:"${subscription.name}"` + : `in:following rss:"${subscription.url ?? ''}"`, + } + }), + }, + { + id: '3', + type: 'folder', + name: 'Saved Searches', + section: 'library', + children: savedSearches.map((search) => { + return { + id: search.id, + type: 'search', + name: search.name, + section: 'library', + filter: search.filter, + } + }), + }, + ] +} diff --git a/packages/db/migrations/0180.do.add_shortcuts_to_user_personalization.sql b/packages/db/migrations/0180.do.add_shortcuts_to_user_personalization.sql new file mode 100755 index 000000000..e5b6bfafe --- /dev/null +++ b/packages/db/migrations/0180.do.add_shortcuts_to_user_personalization.sql @@ -0,0 +1,9 @@ +-- Type: DO +-- Name: add_shortcuts_to_user_personalization +-- Description: Add a new shortcuts column to the user personalization table + +BEGIN; + +ALTER TABLE omnivore.user_personalization ADD COLUMN shortcuts JSONB DEFAULT NULL; + +COMMIT; diff --git a/packages/db/migrations/0180.undo.add_shortcuts_to_user_personalization.sql b/packages/db/migrations/0180.undo.add_shortcuts_to_user_personalization.sql new file mode 100755 index 000000000..31b6a5911 --- /dev/null +++ b/packages/db/migrations/0180.undo.add_shortcuts_to_user_personalization.sql @@ -0,0 +1,9 @@ +-- Type: UNDO +-- Name: add_shortcuts_to_user_personalization +-- Description: Add a new shortcuts column to the user personalization table + +BEGIN; + +ALTER TABLE omnivore.user_personalization DROP COLUMN shortcuts ; + +COMMIT; diff --git a/packages/web/components/elements/AndroidInstallHelp.tsx b/packages/web/components/elements/AndroidInstallHelp.tsx index f8acd5ece..c79cee33f 100644 --- a/packages/web/components/elements/AndroidInstallHelp.tsx +++ b/packages/web/components/elements/AndroidInstallHelp.tsx @@ -3,7 +3,7 @@ import { Desktop, DeviceTabletSpeaker, DeviceMobileCamera, -} from 'phosphor-react' +} from '@phosphor-icons/react' import { Box, HStack } from './LayoutPrimitives' import { StyledText, StyledAnchor } from './StyledText' diff --git a/packages/web/components/elements/Button.tsx b/packages/web/components/elements/Button.tsx index e0a999ce3..010e0bdba 100644 --- a/packages/web/components/elements/Button.tsx +++ b/packages/web/components/elements/Button.tsx @@ -438,6 +438,13 @@ export const Button = styled('button', { borderRadius: '5px', '&:hover': { bg: '$readerHoverBg', opacity: '1' }, }, + menuAction: { + display: 'flex', + border: 'none', + bg: 'transparent', + cursor: 'pointer', + '&:hover': { bg: 'transparent', opacity: '1' }, + }, themeSwitch: { p: '0px', m: '0px', diff --git a/packages/web/components/elements/CloseButton.tsx b/packages/web/components/elements/CloseButton.tsx index d4468c629..9dabf9102 100644 --- a/packages/web/components/elements/CloseButton.tsx +++ b/packages/web/components/elements/CloseButton.tsx @@ -1,4 +1,4 @@ -import { X } from 'phosphor-react' +import { X } from '@phosphor-icons/react' import { useState } from 'react' import { Button } from './Button' import { Box } from './LayoutPrimitives' diff --git a/packages/web/components/elements/DiscoverMenu.tsx b/packages/web/components/elements/DiscoverMenu.tsx index 9d28e6fc4..c178e955d 100644 --- a/packages/web/components/elements/DiscoverMenu.tsx +++ b/packages/web/components/elements/DiscoverMenu.tsx @@ -1,6 +1,6 @@ import { HStack, SpanBox, VStack } from './LayoutPrimitives' import { StyledText } from './StyledText' -import { NewspaperClipping } from 'phosphor-react' +import { NewspaperClipping } from '@phosphor-icons/react' import { theme } from '../tokens/stitches.config' import { useEffect, useState } from 'react' import { useRouter } from 'next/router' diff --git a/packages/web/components/elements/EditLabelChip.tsx b/packages/web/components/elements/EditLabelChip.tsx index 930f9f739..605d292be 100644 --- a/packages/web/components/elements/EditLabelChip.tsx +++ b/packages/web/components/elements/EditLabelChip.tsx @@ -1,6 +1,6 @@ import { Button } from './Button' import { SpanBox, HStack } from './LayoutPrimitives' -import { Circle, X } from 'phosphor-react' +import { Circle, X } from '@phosphor-icons/react' import { isDarkTheme } from '../../lib/themeUpdater' import { theme } from '../tokens/stitches.config' diff --git a/packages/web/components/elements/EditLabelChipStack.tsx b/packages/web/components/elements/EditLabelChipStack.tsx index 2844f2264..535cfd568 100644 --- a/packages/web/components/elements/EditLabelChipStack.tsx +++ b/packages/web/components/elements/EditLabelChipStack.tsx @@ -1,6 +1,6 @@ import { Button } from './Button' import { SpanBox, HStack } from './LayoutPrimitives' -import { Circle, X } from 'phosphor-react' +import { Circle, X } from '@phosphor-icons/react' import { isDarkTheme } from '../../lib/themeUpdater' import { Label } from '../../lib/networking/fragments/labelFragment' import { useMemo } from 'react' diff --git a/packages/web/components/elements/FeatureHelpBox.tsx b/packages/web/components/elements/FeatureHelpBox.tsx index 9390dddb3..0bb350259 100644 --- a/packages/web/components/elements/FeatureHelpBox.tsx +++ b/packages/web/components/elements/FeatureHelpBox.tsx @@ -3,7 +3,7 @@ import { theme } from '../tokens/stitches.config' import { Button } from './Button' import { CloseIcon } from './icons/CloseIcon' import { HelpfulSlothImage } from './images/HelpfulSlothImage' -import { ArrowSquareOut } from 'phosphor-react' +import { ArrowSquareOut } from '@phosphor-icons/react' import { useEffect, useState } from 'react' type FeatureHelpBoxProps = { diff --git a/packages/web/components/elements/IOSInstallHelp.tsx b/packages/web/components/elements/IOSInstallHelp.tsx index 5eb96fc36..1c7ad9c28 100644 --- a/packages/web/components/elements/IOSInstallHelp.tsx +++ b/packages/web/components/elements/IOSInstallHelp.tsx @@ -3,7 +3,7 @@ import { Desktop, DeviceTabletSpeaker, DeviceMobileCamera, -} from 'phosphor-react' +} from '@phosphor-icons/react' import { Box, HStack } from './LayoutPrimitives' import { StyledText, StyledAnchor } from './StyledText' diff --git a/packages/web/components/elements/LabelChip.tsx b/packages/web/components/elements/LabelChip.tsx index 9aa959897..c807ef3ac 100644 --- a/packages/web/components/elements/LabelChip.tsx +++ b/packages/web/components/elements/LabelChip.tsx @@ -1,5 +1,5 @@ import { SpanBox, HStack } from './LayoutPrimitives' -import { Circle, X } from 'phosphor-react' +import { Circle, X } from '@phosphor-icons/react' type LabelChipProps = { text: string diff --git a/packages/web/components/elements/MenuTrigger.tsx b/packages/web/components/elements/MenuTrigger.tsx index 4ec61eef5..63f105653 100644 --- a/packages/web/components/elements/MenuTrigger.tsx +++ b/packages/web/components/elements/MenuTrigger.tsx @@ -1,4 +1,4 @@ -import { DotsThreeVertical } from 'phosphor-react' +import { DotsThreeVertical } from '@phosphor-icons/react' import { theme } from '../tokens/stitches.config' import { Box } from './LayoutPrimitives' diff --git a/packages/web/components/elements/OutlinedLabelChip.tsx b/packages/web/components/elements/OutlinedLabelChip.tsx index 5991fd378..cd7da0827 100644 --- a/packages/web/components/elements/OutlinedLabelChip.tsx +++ b/packages/web/components/elements/OutlinedLabelChip.tsx @@ -1,5 +1,5 @@ import { SpanBox, HStack } from './LayoutPrimitives' -import { Circle, X } from 'phosphor-react' +import { Circle, X } from '@phosphor-icons/react' type LabelChipProps = { text: string diff --git a/packages/web/components/elements/SuggestionBox.tsx b/packages/web/components/elements/SuggestionBox.tsx index 5118708af..5ffaeb822 100644 --- a/packages/web/components/elements/SuggestionBox.tsx +++ b/packages/web/components/elements/SuggestionBox.tsx @@ -40,14 +40,16 @@ const InternalOrExternalLink = (props: InternalOrExternalLinkProps) => { }} > {!isExternal ? ( - {props.children} + + {props.children} + ) : ( {props.children} )} - ); + ) } export const SuggestionBox = (props: SuggestionBoxProps) => { @@ -59,7 +61,7 @@ export const SuggestionBox = (props: SuggestionBoxProps) => { flexDirection: props.size == 'large' ? 'column' : 'row', width: 'fit-content', borderRadius: '5px', - background: props.background ?? '$thBackground3', + background: props.background ?? 'unset', fontSize: '15px', fontFamily: '$inter', fontWeight: '500', diff --git a/packages/web/components/elements/Table.tsx b/packages/web/components/elements/Table.tsx index ce81966bf..468076597 100644 --- a/packages/web/components/elements/Table.tsx +++ b/packages/web/components/elements/Table.tsx @@ -8,7 +8,7 @@ import { Td, } from 'react-super-responsive-table' import 'react-super-responsive-table/dist/SuperResponsiveTableStyle.css' -import { PencilSimple, Plus, Trash } from 'phosphor-react' +import { PencilSimple, Plus, Trash } from '@phosphor-icons/react' import { Box, SpanBox, VStack } from './LayoutPrimitives' import { styled } from '../tokens/stitches.config' import { StyledText } from './StyledText' diff --git a/packages/web/components/elements/icons/ArchiveSectionIcon.tsx b/packages/web/components/elements/icons/ArchiveSectionIcon.tsx new file mode 100644 index 000000000..b37c0447b --- /dev/null +++ b/packages/web/components/elements/icons/ArchiveSectionIcon.tsx @@ -0,0 +1,32 @@ +/* eslint-disable functional/no-class */ +/* eslint-disable functional/no-this-expression */ +import { IconProps } from './IconProps' + +import React from 'react' + +export class ArchiveSectionIcon extends React.Component { + render() { + const color = (this.props.color || '#2A2A2A').toString() + + return ( + + + + + + + ) + } +} diff --git a/packages/web/components/elements/icons/HomeIcon.tsx b/packages/web/components/elements/icons/HomeIcon.tsx index 0f9e7ce0d..2bba94f9f 100644 --- a/packages/web/components/elements/icons/HomeIcon.tsx +++ b/packages/web/components/elements/icons/HomeIcon.tsx @@ -1,5 +1,6 @@ /* eslint-disable functional/no-class */ /* eslint-disable functional/no-this-expression */ +import { SpanBox } from '../LayoutPrimitives' import { IconProps } from './IconProps' import React from 'react' @@ -19,7 +20,7 @@ export class HomeIcon extends React.Component { > diff --git a/packages/web/components/elements/icons/NavMoreButtonDown.tsx b/packages/web/components/elements/icons/NavMoreButtonDown.tsx new file mode 100644 index 000000000..4d76c4c0a --- /dev/null +++ b/packages/web/components/elements/icons/NavMoreButtonDown.tsx @@ -0,0 +1,31 @@ +/* eslint-disable functional/no-class */ +/* eslint-disable functional/no-this-expression */ +import { IconProps } from './IconProps' + +import React from 'react' + +export class NavMoreButtonDownIcon extends React.Component { + render() { + const color = (this.props.color || '#2A2A2A').toString() + + return ( + + + + + + ) + } +} diff --git a/packages/web/components/elements/icons/NavMoreButtonUp.tsx b/packages/web/components/elements/icons/NavMoreButtonUp.tsx new file mode 100644 index 000000000..4270cad1e --- /dev/null +++ b/packages/web/components/elements/icons/NavMoreButtonUp.tsx @@ -0,0 +1,29 @@ +/* eslint-disable functional/no-class */ +/* eslint-disable functional/no-this-expression */ +import { IconProps } from './IconProps' + +import React from 'react' + +export class NavMoreButtonUpIcon extends React.Component { + render() { + const color = (this.props.color || '#2A2A2A').toString() + + return ( + + + + ) + } +} diff --git a/packages/web/components/elements/icons/ShortcutFolderClosed.tsx b/packages/web/components/elements/icons/ShortcutFolderClosed.tsx new file mode 100644 index 000000000..558cd8acf --- /dev/null +++ b/packages/web/components/elements/icons/ShortcutFolderClosed.tsx @@ -0,0 +1,29 @@ +/* eslint-disable functional/no-class */ +/* eslint-disable functional/no-this-expression */ +import { IconProps } from './IconProps' + +import React from 'react' + +export class ShortcutFolderClosed extends React.Component { + render() { + const size = (this.props.size || 26).toString() + const color = (this.props.color || '#2A2A2A').toString() + + return ( + + + + + + ) + } +} diff --git a/packages/web/components/elements/icons/ShortcutFolderOpen.tsx b/packages/web/components/elements/icons/ShortcutFolderOpen.tsx new file mode 100644 index 000000000..7c196bca9 --- /dev/null +++ b/packages/web/components/elements/icons/ShortcutFolderOpen.tsx @@ -0,0 +1,31 @@ +/* eslint-disable functional/no-class */ +/* eslint-disable functional/no-this-expression */ +import { IconProps } from './IconProps' + +import React from 'react' + +export class ShortcutFolderOpen extends React.Component { + render() { + const color = (this.props.color || '#2A2A2A').toString() + + return ( + + + + + + ) + } +} diff --git a/packages/web/components/elements/icons/TrashSectionIcon.tsx b/packages/web/components/elements/icons/TrashSectionIcon.tsx new file mode 100644 index 000000000..a42cea548 --- /dev/null +++ b/packages/web/components/elements/icons/TrashSectionIcon.tsx @@ -0,0 +1,32 @@ +/* eslint-disable functional/no-class */ +/* eslint-disable functional/no-this-expression */ +import { IconProps } from './IconProps' + +import React from 'react' + +export class TrashSectionIcon extends React.Component { + render() { + const color = (this.props.color || '#2A2A2A').toString() + + return ( + + + + + + + ) + } +} diff --git a/packages/web/components/elements/icons/home/AddToLibraryActionIcon.tsx b/packages/web/components/elements/icons/home/AddToLibraryActionIcon.tsx index b2db19fe2..ae852aac8 100644 --- a/packages/web/components/elements/icons/home/AddToLibraryActionIcon.tsx +++ b/packages/web/components/elements/icons/home/AddToLibraryActionIcon.tsx @@ -7,7 +7,6 @@ import React from 'react' export class AddToLibraryActionIcon extends React.Component { render() { const strokeColor = (this.props.color || '#D9D9D9').toString() - const backgroundColor = (this.props.color || '#3D3D3D').toString() return ( - ) : ( - - ) + + ) : ( + + ) } diff --git a/packages/web/components/nav-containers/highlights.tsx b/packages/web/components/nav-containers/highlights.tsx new file mode 100644 index 000000000..ef1358249 --- /dev/null +++ b/packages/web/components/nav-containers/highlights.tsx @@ -0,0 +1,319 @@ +import { NavigationLayout } from '../templates/NavigationLayout' +import { Box, HStack, VStack } from '../elements/LayoutPrimitives' +import { useFetchMore } from '../../lib/hooks/useFetchMoreScroll' +import { useCallback, useMemo, useState } from 'react' +import { useGetHighlights } from '../../lib/networking/queries/useGetHighlights' +import { Highlight } from '../../lib/networking/fragments/highlightFragment' +import { NextRouter, useRouter } from 'next/router' +import { + UserBasicData, + useGetViewerQuery, +} from '../../lib/networking/queries/useGetViewerQuery' +import { SetHighlightLabelsModalPresenter } from '../templates/article/SetLabelsModalPresenter' +import { TrashIcon } from '../elements/icons/TrashIcon' +import { showErrorToast, showSuccessToast } from '../../lib/toastHelpers' +import { ConfirmationModal } from '../patterns/ConfirmationModal' +import { deleteHighlightMutation } from '../../lib/networking/mutations/deleteHighlightMutation' +import { LabelChip } from '../elements/LabelChip' +import ReactMarkdown from 'react-markdown' +import remarkGfm from 'remark-gfm' +import { timeAgo } from '../patterns/LibraryCards/LibraryCardStyles' +import { HighlightHoverActions } from '../patterns/HighlightHoverActions' +import { + autoUpdate, + offset, + size, + useFloating, + useHover, + useInteractions, +} from '@floating-ui/react' +import { highlightColor } from '../../lib/themeUpdater' + +import { HighlightViewNote } from '../patterns/HighlightNotes' +import { theme } from '../tokens/stitches.config' + +const PAGE_SIZE = 10 + +export function HighlightsContainer(): JSX.Element { + const router = useRouter() + const viewer = useGetViewerQuery() + const [showFilterMenu, setShowFilterMenu] = useState(false) + const [_, setShowAddLinkModal] = useState(false) + + const { isLoading, setSize, size, data, mutate } = useGetHighlights({ + first: PAGE_SIZE, + }) + + const hasMore = useMemo(() => { + if (!data) { + return false + } + return data[data.length - 1].highlights.pageInfo.hasNextPage + }, [data]) + + const handleFetchMore = useCallback(() => { + if (isLoading || !hasMore) { + return + } + setSize(size + 1) + }, [isLoading, hasMore, setSize, size]) + + useFetchMore(handleFetchMore) + + const highlights = useMemo(() => { + if (!data) { + return [] + } + return data.flatMap((res) => res.highlights.edges.map((edge) => edge.node)) + }, [data]) + + return ( + + {highlights.map((highlight) => { + return ( + viewer.viewerData?.me && ( + + ) + ) + })} + + ) +} + +type HighlightCardProps = { + highlight: Highlight + viewer: UserBasicData + router: NextRouter + mutate: () => void +} + +type HighlightAnnotationProps = { + highlight: Highlight +} + +function HighlightAnnotation({ + highlight, +}: HighlightAnnotationProps): JSX.Element { + const [noteMode, setNoteMode] = useState<'edit' | 'preview'>('preview') + const [annotation, setAnnotation] = useState(highlight.annotation) + + return ( + { + setAnnotation(highlight.annotation) + }} + /> + ) +} + +function HighlightCard(props: HighlightCardProps): JSX.Element { + const [isOpen, setIsOpen] = useState(false) + const [showConfirmDeleteHighlightId, setShowConfirmDeleteHighlightId] = + useState(undefined) + const [labelsTarget, setLabelsTarget] = useState( + undefined + ) + + const viewInReader = useCallback( + (highlightId: string) => { + const router = props.router + const viewer = props.viewer + const item = props.highlight.libraryItem + + if (!router || !router.isReady || !viewer || !item) { + showErrorToast('Error navigating to highlight') + return + } + + router.push( + { + pathname: '/[username]/[slug]', + query: { + username: viewer.profile.username, + slug: item.slug, + }, + hash: highlightId, + }, + `${viewer.profile.username}/${item.slug}#${highlightId}`, + { + scroll: false, + } + ) + }, + [props.highlight.libraryItem, props.viewer, props.router] + ) + + 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 ( + + + + + + + {timeAgo(props.highlight.updatedAt)} + + {props.highlight.quote && ( + + {props.highlight.quote} + + )} + + {props.highlight.labels && ( + + {props.highlight.labels.map((label) => { + return ( + + ) + })} + + )} + + {props.highlight.libraryItem?.title} + + + {props.highlight.libraryItem?.author} + + {showConfirmDeleteHighlightId && ( + { + ;(async () => { + const highlightId = showConfirmDeleteHighlightId + const success = await deleteHighlightMutation( + props.highlight.libraryItem?.id || '', + showConfirmDeleteHighlightId + ) + props.mutate() + if (success) { + showSuccessToast('Highlight deleted.', { + position: 'bottom-right', + }) + const event = new CustomEvent('deleteHighlightbyId', { + detail: highlightId, + }) + document.dispatchEvent(event) + } else { + showErrorToast('Error deleting highlight', { + position: 'bottom-right', + }) + } + })() + setShowConfirmDeleteHighlightId(undefined) + }} + onOpenChange={() => setShowConfirmDeleteHighlightId(undefined)} + icon={ + + } + /> + )} + {labelsTarget && ( + { + // Don't actually need to do something here + console.log('update highlight: ', highlight) + }} + onOpenChange={() => { + props.mutate() + setLabelsTarget(undefined) + }} + /> + )} + + ) +} diff --git a/packages/web/pages/justread/index.tsx b/packages/web/components/nav-containers/home.tsx similarity index 70% rename from packages/web/pages/justread/index.tsx rename to packages/web/components/nav-containers/home.tsx index ef491bf40..8ae31204b 100644 --- a/packages/web/pages/justread/index.tsx +++ b/packages/web/components/nav-containers/home.tsx @@ -1,16 +1,16 @@ import * as HoverCard from '@radix-ui/react-hover-card' import { styled } from '@stitches/react' import { useRouter } from 'next/router' -import { useCallback, useMemo, useState } from 'react' -import { Button } from '../../components/elements/Button' -import { AddToLibraryActionIcon } from '../../components/elements/icons/home/AddToLibraryActionIcon' -import { ArchiveActionIcon } from '../../components/elements/icons/home/ArchiveActionIcon' -import { CommentActionIcon } from '../../components/elements/icons/home/CommentActionIcon' -import { RemoveActionIcon } from '../../components/elements/icons/home/RemoveActionIcon' -import { ShareActionIcon } from '../../components/elements/icons/home/ShareActionIcon' -import Pagination from '../../components/elements/Pagination' -import { timeAgo } from '../../components/patterns/LibraryCards/LibraryCardStyles' -import { theme } from '../../components/tokens/stitches.config' +import { useMemo, useState } from 'react' +import { Button } from '../elements/Button' +import { AddToLibraryActionIcon } from '../elements/icons/home/AddToLibraryActionIcon' +import { ArchiveActionIcon } from '../elements/icons/home/ArchiveActionIcon' +import { CommentActionIcon } from '../elements/icons/home/CommentActionIcon' +import { RemoveActionIcon } from '../elements/icons/home/RemoveActionIcon' +import { ShareActionIcon } from '../elements/icons/home/ShareActionIcon' +import Pagination from '../elements/Pagination' +import { timeAgo } from '../patterns/LibraryCards/LibraryCardStyles' +import { theme } from '../tokens/stitches.config' import { useApplyLocalTheme } from '../../lib/hooks/useApplyLocalTheme' import { useGetHiddenHomeSection } from '../../lib/networking/queries/useGetHiddenHomeSection' import { @@ -24,20 +24,10 @@ import { SubscriptionType, useGetSubscriptionsQuery, } from '../../lib/networking/queries/useGetSubscriptionsQuery' -import { - Box, - HStack, - SpanBox, - VStack, -} from './../../components/elements/LayoutPrimitives' -import { List, ThumbsDown, ThumbsUp } from 'phosphor-react' -import { showErrorToast, showSuccessToast } from '../../lib/toastHelpers' +import { Box, HStack, SpanBox, VStack } from '../elements/LayoutPrimitives' import { Toaster } from 'react-hot-toast' -import { DEFAULT_HEADER_HEIGHT } from '../../components/templates/homeFeed/HeaderSpacer' -import { NavigationMenu } from '../../components/templates/navMenu/NavigationMenu' -export default function Home(): JSX.Element { - const [showLeftMenu, setShowLeftMenu] = useState(false) +export function HomeContainer(): JSX.Element { const homeData = useGetHomeItems() useApplyLocalTheme() @@ -53,24 +43,6 @@ export default function Home(): JSX.Element { }} > -
{ - setShowLeftMenu(!showLeftMenu) - }} - /> - {showLeftMenu && ( - {}} - searchTerm={''} - // eslint-disable-next-line @typescript-eslint/no-empty-function - applySearchQuery={(searchQuery: string) => {}} - showFilterMenu={showLeftMenu} - setShowFilterMenu={(show) => { - setShowLeftMenu(show) - }} - /> - )} { fontFamily: '$inter', fontSize: '16px', fontWeight: '600', - color: '$readerText', + color: '$homeTextTitle', }} > {props.homeSection.title} - {props.homeSection.items.map((homeItem) => { return })} @@ -169,7 +140,7 @@ const TopPicksHomeSection = (props: HomeSectionProps): JSX.Element => { fontFamily: '$inter', fontSize: '16px', fontWeight: '600', - color: '$readerText', + color: '$homeTextTitle', }} > {props.homeSection.title} @@ -252,7 +223,7 @@ const HiddenHomeSection = (props: HomeSectionProps): JSX.Element => { fontFamily: '$inter', fontSize: '16px', fontWeight: '600', - color: '$readerText', + color: '$homeTextTitle', }} > {props.homeSection.title} @@ -319,7 +290,7 @@ const TimeAgo = (props: HomeItemViewProps): JSX.Element => { fontSize: '12px', fontWeight: 'medium', fontFamily: '$inter', - color: '$readerTextSubtle', + color: '$homeTextSubtle', }} > {timeAgo(props.homeItem.date)} @@ -337,7 +308,7 @@ const Title = (props: HomeItemViewProps): JSX.Element => { lineHeight: '20px', fontWeight: '600', fontFamily: '$inter', - color: '$readerText', + color: '$homeTextTitle', overflow: 'hidden', textOverflow: 'ellipsis', wordBreak: 'break-word', @@ -386,7 +357,7 @@ const JustAddedItemView = (props: HomeItemViewProps): JSX.Element => { padding: '5px', borderRadius: '5px', '&:hover': { - bg: '$thBackground', + bg: '$homeCardHover', borderRadius: '0px', }, }} @@ -423,7 +394,7 @@ const TopicPickHomeItemView = (props: HomeItemViewProps): JSX.Element => { borderRadius: '5px', '&:hover': { - bg: '#323232', + bg: '$homeCardHover', borderRadius: '0px', }, }} @@ -470,22 +441,26 @@ const TopicPickHomeItemView = (props: HomeItemViewProps): JSX.Element => { - + ) } @@ -557,7 +532,7 @@ const SourceInfo = (props: HomeItemViewProps) => ( fontFamily: '$inter', fontWeight: '500', fontSize: '13px', - color: '$readerFont', + color: '$homeTextSource', textDecoration: 'underline', }} > @@ -645,7 +620,7 @@ const SubscriptionSourceHoverContent = ( css={{ fontFamily: '$inter', fontSize: '13px', - color: '$thTextSubtle4', + color: '$homeTextBody', }} > {subscription ? <>{subscription.description} : <>} @@ -653,144 +628,3 @@ const SubscriptionSourceHoverContent = ( ) } - -// const SiteSourceHoverContent = ( -// props: SourceHoverContentProps -// ): JSX.Element => { -// const sendHomeFeedback = useCallback( -// async (feedbackType: SendHomeFeedbackType) => { -// const feedback: SendHomeFeedbackInput = { -// feedbackType, -// } -// feedback.site = props.source.name -// const result = await sendHomeFeedbackMutation(feedback) -// if (result) { -// showSuccessToast('Feedback sent') -// } else { -// showErrorToast('Error sending feedback') -// } -// }, -// [props] -// ) - -// return ( -// -// -// {props.source.icon && ( -// -// )} -// -// {props.source.name} -// -// -// {/* -// {subscription ? <>{subscription.description} : <>} -// */} -// -// -// ) -// } - -// type FeedbackViewProps = { -// sendFeedback: (type: SendHomeFeedbackType) => void -// } - -// const FeedbackView = (props: FeedbackViewProps): JSX.Element => { -// return ( -// -// -// -// -// ) -// } - -type HeaderProps = { - toggleMenu: () => void -} - -const Header = (props: HeaderProps): JSX.Element => { - const small = false - - return ( - - - - - - ) -} diff --git a/packages/web/components/patterns/HighlightBar.tsx b/packages/web/components/patterns/HighlightBar.tsx index c85f41301..4fc42080d 100644 --- a/packages/web/components/patterns/HighlightBar.tsx +++ b/packages/web/components/patterns/HighlightBar.tsx @@ -2,7 +2,7 @@ import { isAndroid } from '../../lib/deviceType' import { styled, theme } from '../tokens/stitches.config' import { Button } from '../elements/Button' import { HStack, Box } from '../elements/LayoutPrimitives' -import { Circle, CheckCircle } from 'phosphor-react' +import { Circle, CheckCircle } from '@phosphor-icons/react' import { LabelIcon } from '../elements/icons/LabelIcon' import { NotebookIcon } from '../elements/icons/NotebookIcon' import { highlightColor, highlightColors } from '../../lib/themeUpdater' diff --git a/packages/web/components/patterns/HighlightHoverActions.tsx b/packages/web/components/patterns/HighlightHoverActions.tsx index 4903fa116..95cdfc173 100644 --- a/packages/web/components/patterns/HighlightHoverActions.tsx +++ b/packages/web/components/patterns/HighlightHoverActions.tsx @@ -2,7 +2,7 @@ import { useState } from 'react' import { Box } from '../elements/LayoutPrimitives' import { Button } from '../elements/Button' import { theme } from '../tokens/stitches.config' -import { BookOpen, Copy } from 'phosphor-react' +import { BookOpen, Copy } from '@phosphor-icons/react' import { UserBasicData } from '../../lib/networking/queries/useGetViewerQuery' import { Highlight } from '../../lib/networking/fragments/highlightFragment' import { showErrorToast, showSuccessToast } from '../../lib/toastHelpers' diff --git a/packages/web/components/patterns/LibraryCards/CardTypes.tsx b/packages/web/components/patterns/LibraryCards/CardTypes.tsx index ab9d9dfcb..fbf713688 100644 --- a/packages/web/components/patterns/LibraryCards/CardTypes.tsx +++ b/packages/web/components/patterns/LibraryCards/CardTypes.tsx @@ -31,4 +31,6 @@ export type LinkedItemCardProps = { isHovered?: boolean isLoading?: boolean + + legacyLayout?: boolean } diff --git a/packages/web/components/patterns/LibraryCards/LibraryGridCard.tsx b/packages/web/components/patterns/LibraryCards/LibraryGridCard.tsx index c824c82e9..c5e388d81 100644 --- a/packages/web/components/patterns/LibraryCards/LibraryGridCard.tsx +++ b/packages/web/components/patterns/LibraryCards/LibraryGridCard.tsx @@ -26,7 +26,7 @@ import { autoUpdate, } from '@floating-ui/react' import { CardMenu } from '../CardMenu' -import { DotsThree } from 'phosphor-react' +import { DotsThree } from '@phosphor-icons/react' import { isTouchScreenDevice } from '../../../lib/deviceType' import { LoadingBarOverlay, ProgressBarOverlay } from './LibraryListCard' import { GridFallbackImage } from './FallbackImage' @@ -64,7 +64,7 @@ export function LibraryGridCard(props: LinkedItemCardProps): JSX.Element { css={{ pl: '0px', padding: '0px', - width: '293px', + width: '100%', height: '100%', minHeight: '270px', background: 'white', diff --git a/packages/web/components/patterns/LibraryCards/LibraryHighlightGridCard.tsx b/packages/web/components/patterns/LibraryCards/LibraryHighlightGridCard.tsx index 0ad21d885..4145c7d41 100644 --- a/packages/web/components/patterns/LibraryCards/LibraryHighlightGridCard.tsx +++ b/packages/web/components/patterns/LibraryCards/LibraryHighlightGridCard.tsx @@ -1,6 +1,6 @@ import { Box, VStack, HStack, SpanBox } from '../../elements/LayoutPrimitives' import { useCallback, useMemo, useState } from 'react' -import { CaretDown, CaretUp } from 'phosphor-react' +import { CaretDown, CaretUp } from '@phosphor-icons/react' import { MetaStyle, timeAgo, TitleStyle } from './LibraryCardStyles' import { styled } from '@stitches/react' import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery' @@ -197,7 +197,9 @@ export function LibraryHighlightGridCard( event.preventDefault() }} > - {`View ${highlightCount} highlight${highlightCount > 1 ? 's' : ''}`} + {`View ${highlightCount} highlight${ + highlightCount > 1 ? 's' : '' + }`} ({ + key: 'nav-show-menu', + isSessionStorage: false, + initialValue: true, + }) + + useKeyboardShortcuts(navigationCommands(router)) + + useKeyboardShortcuts( + primaryCommands((action) => { + switch (action) { + case 'toggleShortcutHelpModalDisplay': + setShowKeyboardCommandsModal(true) + break + } + }) + ) + + useRegisterActions( + [ + { + id: 'home', + section: 'Navigation', + name: 'Go to Home (Library) ', + shortcut: ['g h'], + keywords: 'go home', + perform: () => router?.push('/home'), + }, + { + id: 'lightTheme', + section: 'Preferences', + name: 'Change theme (light) ', + shortcut: ['v', 'l'], + keywords: 'light theme', + priority: Priority.LOW, + perform: () => updateTheme(ThemeId.Light), + }, + { + id: 'darkTheme', + section: 'Preferences', + name: 'Change theme (dark) ', + shortcut: ['v', 'd'], + keywords: 'dark theme', + priority: Priority.LOW, + perform: () => updateTheme(ThemeId.Dark), + }, + ], + [router] + ) + + // Attempt to identify the user if they are logged in. + useEffect(() => { + setupAnalytics(viewerData?.me) + }, [viewerData?.me]) + + const showLogout = useCallback(() => { + setShowLogoutConfirmation(true) + }, [setShowLogoutConfirmation]) + + useEffect(() => { + document.addEventListener('logout', showLogout) + + return () => { + document.removeEventListener('logout', showLogout) + } + }, [showLogout]) + + return ( + + {props.pageMetaDataProps ? ( + + ) : null} +
{ + setShowNavMenu(!showNavMenu) + }} + /> + {showNavMenu && ( + {}} + showMenu={showNavMenu} + setShowMenu={setShowNavMenu} + /> + )} + {props.children} + {showLogoutConfirmation ? ( + setShowLogoutConfirmation(false)} + /> + ) : null} + {showKeyboardCommandsModal ? ( + setShowKeyboardCommandsModal(false)} + /> + ) : null} +
+ ) +} + +type HeaderProps = { + toggleMenu: () => void +} + +const Header = (props: HeaderProps): JSX.Element => { + const small = false + + return ( + + + + + + ) +} diff --git a/packages/web/components/templates/PrimaryDropdown.tsx b/packages/web/components/templates/PrimaryDropdown.tsx index f5698a5f5..2785f8a57 100644 --- a/packages/web/components/templates/PrimaryDropdown.tsx +++ b/packages/web/components/templates/PrimaryDropdown.tsx @@ -1,5 +1,5 @@ import { useRouter } from 'next/router' -import { Moon, Sun } from 'phosphor-react' +import { Moon, Sun } from '@phosphor-icons/react' import { ReactNode, useCallback } from 'react' import { useGetViewerQuery } from '../../lib/networking/queries/useGetViewerQuery' import { Avatar } from '../elements/Avatar' @@ -20,8 +20,6 @@ import { ThemeSelector } from './article/ReaderSettingsControl' type PrimaryDropdownProps = { children?: ReactNode - showThemeSection: boolean - showFullThemeSection: boolean layout?: LayoutType updateLayout?: (layout: LayoutType) => void @@ -194,8 +192,7 @@ export function PrimaryDropdown(props: PrimaryDropdownProps): JSX.Element { - {props.showThemeSection && } - {props.showFullThemeSection && } + headerDropdownActionHandler('navigate-to-install')} title="Install" diff --git a/packages/web/components/templates/SettingsLayout.tsx b/packages/web/components/templates/SettingsLayout.tsx index 71f8f44c6..a6716af9d 100644 --- a/packages/web/components/templates/SettingsLayout.tsx +++ b/packages/web/components/templates/SettingsLayout.tsx @@ -13,7 +13,7 @@ import { SettingsMenu } from './navMenu/SettingsMenu' import { SettingsDropdown } from './navMenu/SettingsDropdown' import { useVerifyAuth } from '../../lib/hooks/useVerifyAuth' import Link from 'next/link' -import { CaretLeft } from 'phosphor-react' +import { CaretLeft } from '@phosphor-icons/react' type SettingsLayoutProps = { title?: string diff --git a/packages/web/components/templates/UploadModal.tsx b/packages/web/components/templates/UploadModal.tsx index e19c74e04..a3dc64937 100644 --- a/packages/web/components/templates/UploadModal.tsx +++ b/packages/web/components/templates/UploadModal.tsx @@ -1,7 +1,7 @@ import * as Progress from '@radix-ui/react-progress' import { styled } from '@stitches/react' import axios from 'axios' -import { File } from 'phosphor-react' +import { File } from '@phosphor-icons/react' import { useCallback, useRef, useState } from 'react' import Dropzone, { DropEvent, DropzoneRef, FileRejection } from 'react-dropzone' import { v4 as uuidv4 } from 'uuid' diff --git a/packages/web/components/templates/article/Article.tsx b/packages/web/components/templates/article/Article.tsx index 5d58274de..25de5385c 100644 --- a/packages/web/components/templates/article/Article.tsx +++ b/packages/web/components/templates/article/Article.tsx @@ -67,8 +67,9 @@ export function Article(props: ArticleProps): JSX.Element { const [lightboxOpen, setLightboxOpen] = useState(false) const [imageSrcs, setImageSrcs] = useState([]) const [lightboxIndex, setlightBoxIndex] = useState(0) - const [linkHoverData, setlinkHoverData] = - useState() + const [linkHoverData, setlinkHoverData] = useState< + LinkHoverData | undefined + >() useEffect(() => { ;(async () => { @@ -97,7 +98,10 @@ export function Article(props: ArticleProps): JSX.Element { // Post message to webkit so apple app embeds get progress updates // TODO: verify if ios still needs this code...seeems to be duplicated useEffect(() => { - if (typeof window?.webkit != 'undefined') { + if ( + typeof window?.webkit != 'undefined' && + 'messageHandlers' in window.webkit + ) { window.webkit.messageHandlers.readingProgressUpdate?.postMessage({ progress: readingProgress, }) diff --git a/packages/web/components/templates/article/HighlightsLayer.tsx b/packages/web/components/templates/article/HighlightsLayer.tsx index a117e0ede..24e96ec9e 100644 --- a/packages/web/components/templates/article/HighlightsLayer.tsx +++ b/packages/web/components/templates/article/HighlightsLayer.tsx @@ -80,13 +80,15 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element { const focusedHighlightMousePos = useRef({ pageX: 0, pageY: 0 }) const [currentHighlightIdx, setCurrentHighlightIdx] = useState(0) - const [focusedHighlight, setFocusedHighlight] = - useState(undefined) + const [focusedHighlight, setFocusedHighlight] = useState< + Highlight | undefined + >(undefined) const [selectionData, setSelectionData] = useSelection(highlightLocations) - const [labelsTarget, setLabelsTarget] = - useState(undefined) + const [labelsTarget, setLabelsTarget] = useState( + undefined + ) const [ confirmDeleteHighlightWithNoteId, @@ -363,13 +365,15 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element { // highlight, so the app can display a native menu const rect = (target as Element).getBoundingClientRect() - window?.webkit?.messageHandlers.viewerAction?.postMessage({ - actionID: 'showMenu', - rectX: rect.x, - rectY: rect.y, - rectWidth: rect.width, - rectHeight: rect.height, - }) + if (window?.webkit?.messageHandlers) { + window?.webkit?.messageHandlers.viewerAction?.postMessage({ + actionID: 'showMenu', + rectX: rect.x, + rectY: rect.y, + rectWidth: rect.width, + rectHeight: rect.height, + }) + } window?.AndroidWebKitMessenger?.handleIdentifiableMessage( 'existingHighlightTap', @@ -394,7 +398,7 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element { const highlight = highlights.find(($0) => $0.id === id) setFocusedHighlight(highlight) setLabelsTarget(highlight) - } else { + } else if (window?.webkit?.messageHandlers) { window?.webkit?.messageHandlers.viewerAction?.postMessage({ actionID: 'pageTapped', }) diff --git a/packages/web/components/templates/article/NotebookHeader.tsx b/packages/web/components/templates/article/NotebookHeader.tsx index e0dbaf3c3..1ff0b98f0 100644 --- a/packages/web/components/templates/article/NotebookHeader.tsx +++ b/packages/web/components/templates/article/NotebookHeader.tsx @@ -1,6 +1,6 @@ import { HStack } from '../../elements/LayoutPrimitives' import { StyledText } from '../../elements/StyledText' -import { Sidebar } from 'phosphor-react' +import { Sidebar } from '@phosphor-icons/react' import { theme } from '../../tokens/stitches.config' import { Button } from '../../elements/Button' import { ExportIcon } from '../../elements/icons/ExportIcon' diff --git a/packages/web/components/templates/article/NotebookModal.tsx b/packages/web/components/templates/article/NotebookModal.tsx index 0250a2b44..4b865af66 100644 --- a/packages/web/components/templates/article/NotebookModal.tsx +++ b/packages/web/components/templates/article/NotebookModal.tsx @@ -9,7 +9,7 @@ import { StyledText } from '../../elements/StyledText' import { theme } from '../../tokens/stitches.config' import type { Highlight } from '../../../lib/networking/fragments/highlightFragment' import { useCallback, useState } from 'react' -import { X } from 'phosphor-react' +import { X } from '@phosphor-icons/react' import { Dropdown, DropdownOption } from '../../elements/DropdownElements' import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers' import { diff_match_patch } from 'diff-match-patch' @@ -37,11 +37,13 @@ export const getHighlightLocation = (patch: string): number | undefined => { export function NotebookModal(props: NotebookModalProps): JSX.Element { const [showConfirmDeleteNote, setShowConfirmDeleteNote] = useState(false) - const [allAnnotations, setAllAnnotations] = - useState(undefined) + const [allAnnotations, setAllAnnotations] = useState( + undefined + ) - const [deletedHighlights, setDeletedAnnotations] = - useState(undefined) + const [deletedHighlights, setDeletedAnnotations] = useState< + Highlight[] | undefined + >(undefined) const handleClose = useCallback(() => { props.onClose(allAnnotations ?? [], deletedHighlights ?? []) diff --git a/packages/web/components/templates/article/ReaderSettingsControl.tsx b/packages/web/components/templates/article/ReaderSettingsControl.tsx index 279605c87..ec956daad 100644 --- a/packages/web/components/templates/article/ReaderSettingsControl.tsx +++ b/packages/web/components/templates/article/ReaderSettingsControl.tsx @@ -8,7 +8,7 @@ import { CaretLeft, CaretRight, Check, -} from 'phosphor-react' +} from '@phosphor-icons/react' import { TickedRangeSlider } from '../../elements/TickedRangeSlider' import { showSuccessToast } from '../../../lib/toastHelpers' import { ReaderSettings } from '../../../lib/hooks/useReaderSettings' diff --git a/packages/web/components/templates/article/SetLabelsControl.tsx b/packages/web/components/templates/article/SetLabelsControl.tsx index e7ad24f86..b8ae855a0 100644 --- a/packages/web/components/templates/article/SetLabelsControl.tsx +++ b/packages/web/components/templates/article/SetLabelsControl.tsx @@ -5,7 +5,7 @@ import { StyledText } from '../../elements/StyledText' import { styled, theme } from '../../tokens/stitches.config' import { Label } from '../../../lib/networking/fragments/labelFragment' import { useGetLabelsQuery } from '../../../lib/networking/queries/useGetLabelsQuery' -import { Check, Circle, Plus, WarningCircle } from 'phosphor-react' +import { Check, Circle, Plus, WarningCircle } from '@phosphor-icons/react' import { createLabelMutation } from '../../../lib/networking/mutations/createLabelMutation' import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers' import { randomLabelColorHex } from '../../../utils/settings-page/labels/labelColorObjects' diff --git a/packages/web/components/templates/article/ShareModal.tsx b/packages/web/components/templates/article/ShareModal.tsx index 9706bd95a..488002ced 100644 --- a/packages/web/components/templates/article/ShareModal.tsx +++ b/packages/web/components/templates/article/ShareModal.tsx @@ -16,7 +16,7 @@ import { useCopyLink } from '../../../lib/hooks/useCopyLink' import { CloseIcon } from '../../elements/images/CloseIcon' import { OmnivoreLogoIcon } from '../../elements/images/OmnivoreNameLogo' import { useState } from 'react' -import { TwitterLogo, FacebookLogo } from 'phosphor-react' +import { TwitterLogo, FacebookLogo } from '@phosphor-icons/react' type ShareType = 'link' | 'highlight' diff --git a/packages/web/components/templates/discoverFeed/DiscoverContainer.tsx b/packages/web/components/templates/discoverFeed/DiscoverContainer.tsx index e3ab2dafd..76174e877 100644 --- a/packages/web/components/templates/discoverFeed/DiscoverContainer.tsx +++ b/packages/web/components/templates/discoverFeed/DiscoverContainer.tsx @@ -2,7 +2,7 @@ import { Box, HStack, VStack } from '../../elements/LayoutPrimitives' import { LibraryFilterMenu } from '../navMenu/LibraryMenu' import { DiscoverHeader } from './DiscoverHeader/DiscoverHeader' import { useRouter } from 'next/router' -import React, { useCallback, useEffect, useState } from "react" +import React, { useCallback, useEffect, useState } from 'react' import { DiscoverItemFeed } from './DiscoverFeed/DiscoverFeed' import { useGetViewerQuery } from '../../../lib/networking/queries/useGetViewerQuery' import toast from 'react-hot-toast' @@ -10,13 +10,13 @@ import { Button } from '../../elements/Button' import { showErrorToast } from '../../../lib/toastHelpers' import { saveDiscoverArticleMutation, - SaveDiscoverArticleOutput -} from "../../../lib/networking/mutations/saveDiscoverArticle" -import { saveUrlMutation } from "../../../lib/networking/mutations/saveUrlMutation" -import { useFetchMore } from "../../../lib/hooks/useFetchMoreScroll" -import { AddLinkModal } from "../homeFeed/AddLinkModal" -import { useGetDiscoverFeedItems } from "../../../lib/networking/queries/useGetDiscoverFeedItems" -import { useGetDiscoverFeeds } from "../../../lib/networking/queries/useGetDiscoverFeeds" + SaveDiscoverArticleOutput, +} from '../../../lib/networking/mutations/saveDiscoverArticle' +import { saveUrlMutation } from '../../../lib/networking/mutations/saveUrlMutation' +import { useFetchMore } from '../../../lib/hooks/useFetchMoreScroll' +import { AddLinkModal } from '../AddLinkModal' +import { useGetDiscoverFeedItems } from '../../../lib/networking/queries/useGetDiscoverFeedItems' +import { useGetDiscoverFeeds } from '../../../lib/networking/queries/useGetDiscoverFeeds' export type LayoutType = 'LIST_LAYOUT' | 'GRID_LAYOUT' @@ -27,8 +27,8 @@ export function DiscoverContainer(): JSX.Element { const viewer = useGetViewerQuery() const [showFilterMenu, setShowFilterMenu] = useState(false) const [layoutType, setLayoutType] = useState('GRID_LAYOUT') - const [showAddLinkModal, setShowAddLinkModal] = useState(false); - const {feeds, revalidate, isValidating} = useGetDiscoverFeeds() + const [showAddLinkModal, setShowAddLinkModal] = useState(false) + const { feeds, revalidate, isValidating } = useGetDiscoverFeeds() const topics = [ { title: 'Popular', @@ -73,8 +73,16 @@ export function DiscoverContainer(): JSX.Element { }, ] - const [selectedFeed, setSelectedFeed] = useState("All Feeds"); - const { discoverItems, setTopic, activeTopic, isLoading, hasMore, setPage, page } = useGetDiscoverFeedItems(topics[1], selectedFeed) + const [selectedFeed, setSelectedFeed] = useState('All Feeds') + const { + discoverItems, + setTopic, + activeTopic, + isLoading, + hasMore, + setPage, + page, + } = useGetDiscoverFeedItems(topics[1], selectedFeed) const handleFetchMore = useCallback(() => { if (isLoading || !hasMore) { return @@ -88,7 +96,11 @@ export function DiscoverContainer(): JSX.Element { timezone: string, locale: string ): Promise => { - const result = await saveDiscoverArticleMutation({discoverArticleId, timezone, locale}) + const result = await saveDiscoverArticleMutation({ + discoverArticleId, + timezone, + locale, + }) if (result?.saveDiscoverArticle) { toast( () => ( @@ -160,8 +172,8 @@ export function DiscoverContainer(): JSX.Element { }, []) const setTopicAndReturnToTop = (topic: TopicTabData) => { - window.scroll(0,0); - setTopic(topic); + window.scroll(0, 0) + setTopic(topic) } return ( @@ -204,12 +216,12 @@ export function DiscoverContainer(): JSX.Element { items={discoverItems ?? []} viewer={viewer.viewerData?.me} /> - { showAddLinkModal && + {showAddLinkModal && ( setShowAddLinkModal(false)} /> - } + )} ) diff --git a/packages/web/components/templates/discoverFeed/DiscoverHeader/SmallTopicBar.tsx b/packages/web/components/templates/discoverFeed/DiscoverHeader/SmallTopicBar.tsx index 70617d593..7e7eefb34 100644 --- a/packages/web/components/templates/discoverFeed/DiscoverHeader/SmallTopicBar.tsx +++ b/packages/web/components/templates/discoverFeed/DiscoverHeader/SmallTopicBar.tsx @@ -1,6 +1,6 @@ import { HStack } from '../../../elements/LayoutPrimitives' import { TopicTab } from './TopicTab' -import { CaretLeft, CaretRight } from 'phosphor-react' +import { CaretLeft, CaretRight } from '@phosphor-icons/react' import React, { useEffect, useRef, useState } from 'react' import { TopicTabData } from '../DiscoverContainer' diff --git a/packages/web/components/templates/discoverFeed/DiscoverHeader/SmallerHeaderLayout.tsx b/packages/web/components/templates/discoverFeed/DiscoverHeader/SmallerHeaderLayout.tsx index 4dad58221..c389a2a00 100644 --- a/packages/web/components/templates/discoverFeed/DiscoverHeader/SmallerHeaderLayout.tsx +++ b/packages/web/components/templates/discoverFeed/DiscoverHeader/SmallerHeaderLayout.tsx @@ -2,7 +2,7 @@ import React from 'react' import { HStack } from '../../../elements/LayoutPrimitives' import { OmnivoreSmallLogo } from '../../../elements/images/OmnivoreNameLogo' import { theme } from '../../../tokens/stitches.config' -import { FunnelSimple } from 'phosphor-react' +import { FunnelSimple } from '@phosphor-icons/react' import { DiscoverHeaderProps } from './DiscoverHeader' import { SmallTopicBar } from './SmallTopicBar' import { PrimaryDropdown } from '../../PrimaryDropdown' diff --git a/packages/web/components/templates/discoverFeed/DiscoverHeader/TopicBar.tsx b/packages/web/components/templates/discoverFeed/DiscoverHeader/TopicBar.tsx index 814e09048..f22d9f5b2 100644 --- a/packages/web/components/templates/discoverFeed/DiscoverHeader/TopicBar.tsx +++ b/packages/web/components/templates/discoverFeed/DiscoverHeader/TopicBar.tsx @@ -1,6 +1,6 @@ import { Box, HStack } from '../../../elements/LayoutPrimitives' import { TopicTab } from './TopicTab' -import { CaretLeft, CaretRight } from 'phosphor-react' +import { CaretLeft, CaretRight } from '@phosphor-icons/react' import React, { useEffect, useRef, useState } from 'react' import { TopicTabData } from '../DiscoverContainer' diff --git a/packages/web/components/templates/discoverFeed/DiscoverItems/DiscoverHoverActions.tsx b/packages/web/components/templates/discoverFeed/DiscoverItems/DiscoverHoverActions.tsx index 465283e22..9ee8d6d6d 100644 --- a/packages/web/components/templates/discoverFeed/DiscoverItems/DiscoverHoverActions.tsx +++ b/packages/web/components/templates/discoverFeed/DiscoverItems/DiscoverHoverActions.tsx @@ -7,12 +7,12 @@ import { Browsers, MinusCircle, PlusCircle, -} from 'phosphor-react' +} from '@phosphor-icons/react' import { timeZone, locale } from '../../../../lib/dateFormatting' import React from 'react' -import { SaveDiscoverArticleOutput } from "../../../../lib/networking/mutations/saveDiscoverArticle" -import { DiscoverFeedItem } from "../../../../lib/networking/queries/useGetDiscoverFeedItems" -import { BrowserIcon } from "../../../elements/icons/BrowserIcon" +import { SaveDiscoverArticleOutput } from '../../../../lib/networking/mutations/saveDiscoverArticle' +import { DiscoverFeedItem } from '../../../../lib/networking/queries/useGetDiscoverFeedItems' +import { BrowserIcon } from '../../../elements/icons/BrowserIcon' type DiscoverHoverActionsProps = { viewer?: UserBasicData @@ -29,7 +29,7 @@ type DiscoverHoverActionsProps = { setSavedUrl: (url: string) => void savedUrl?: string - deleteDiscoverItem: (item: DiscoverFeedItem) => Promise, + deleteDiscoverItem: (item: DiscoverFeedItem) => Promise } export const DiscoverHoverActions = (props: DiscoverHoverActionsProps) => { @@ -60,14 +60,14 @@ export const DiscoverHoverActions = (props: DiscoverHoverActionsProps) => { > ) diff --git a/packages/web/components/templates/discoverFeed/DiscoverItems/DiscoverItemGridCard.tsx b/packages/web/components/templates/discoverFeed/DiscoverItems/DiscoverItemGridCard.tsx index ffe3d585c..68c3dd2b9 100644 --- a/packages/web/components/templates/discoverFeed/DiscoverItems/DiscoverItemGridCard.tsx +++ b/packages/web/components/templates/discoverFeed/DiscoverItems/DiscoverItemGridCard.tsx @@ -22,10 +22,13 @@ import { siteName, TitleStyle, } from '../../../patterns/LibraryCards/LibraryCardStyles' -import { DiscoverItemCardProps, DiscoverItemSubCardProps } from "./DiscoverItemCard" +import { + DiscoverItemCardProps, + DiscoverItemSubCardProps, +} from './DiscoverItemCard' import { DiscoverItemMetadata } from './DiscoverItemMetadata' import { DiscoverHoverActions } from './DiscoverHoverActions' -import { CheckCircle, Circle } from 'phosphor-react' +import { CheckCircle, Circle } from '@phosphor-icons/react' export function DiscoverGridCard(props: DiscoverItemSubCardProps): JSX.Element { const [isHovered, setIsHovered] = useState(false) @@ -100,7 +103,12 @@ export function DiscoverGridCard(props: DiscoverItemSubCardProps): JSX.Element { /> )} - + ) } @@ -122,7 +130,15 @@ const DiscoverGridCardContent = ( } return ( - + <> { setDisplayFallback(true) diff --git a/packages/web/components/templates/discoverFeed/DiscoverItems/DiscoverItemListCard.tsx b/packages/web/components/templates/discoverFeed/DiscoverItems/DiscoverItemListCard.tsx index f4819d6b5..14756f17a 100644 --- a/packages/web/components/templates/discoverFeed/DiscoverItems/DiscoverItemListCard.tsx +++ b/packages/web/components/templates/discoverFeed/DiscoverItems/DiscoverItemListCard.tsx @@ -23,8 +23,11 @@ import { siteName, TitleStyle, } from '../../../patterns/LibraryCards/LibraryCardStyles' -import { CheckCircle, Circle } from 'phosphor-react' -import { DiscoverItemCardProps, DiscoverItemSubCardProps } from "./DiscoverItemCard" +import { CheckCircle, Circle } from '@phosphor-icons/react' +import { + DiscoverItemCardProps, + DiscoverItemSubCardProps, +} from './DiscoverItemCard' import { DiscoverItemMetadata } from './DiscoverItemMetadata' import { DiscoverHoverActions } from './DiscoverHoverActions' @@ -103,13 +106,17 @@ export function DiscoverItemListCard( /> )} - + ) } export function DiscoverListCardContent( - props: DiscoverItemCardProps & { savedId?: string; savedUrl? : string } + props: DiscoverItemCardProps & { savedId?: string; savedUrl?: string } ): JSX.Element { const originText = siteName(props.item.url, props.item.url) const [displayFallback, setDisplayFallback] = useState( @@ -123,7 +130,14 @@ export function DiscoverListCardContent( } return ( - + + legacyBehavior + > { console.log('event.ctrlKey: ', event.ctrlKey, event.metaKey) @@ -129,7 +130,7 @@ export function HighlightsMenu(props: HighlightsMenuProps): JSX.Element { - ); + ) } const sortHighlights = (highlights: Highlight[]) => { diff --git a/packages/web/components/templates/homeFeed/HighlightsLayout.tsx b/packages/web/components/templates/homeFeed/HighlightsLayout.tsx index 1b66e1f91..390805326 100644 --- a/packages/web/components/templates/homeFeed/HighlightsLayout.tsx +++ b/packages/web/components/templates/homeFeed/HighlightsLayout.tsx @@ -1,5 +1,5 @@ import { useRouter } from 'next/router' -import { HighlighterCircle } from 'phosphor-react' +import { HighlighterCircle } from '@phosphor-icons/react' import { useCallback, useEffect, useReducer, useState } from 'react' import { Toaster } from 'react-hot-toast' import { Highlight } from '../../../lib/networking/fragments/highlightFragment' @@ -36,8 +36,9 @@ type HighlightItemsLayoutProps = { export function HighlightItemsLayout( props: HighlightItemsLayoutProps ): JSX.Element { - const [currentItem, setCurrentItem] = - useState(undefined) + const [currentItem, setCurrentItem] = useState( + undefined + ) const listReducer = ( state: LibraryItem[], diff --git a/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx b/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx index cc6014c41..86d458ccf 100644 --- a/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx +++ b/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx @@ -32,13 +32,13 @@ import { ConfirmationModal } from '../../patterns/ConfirmationModal' import { LinkedItemCardAction } from '../../patterns/LibraryCards/CardTypes' import { LinkedItemCard } from '../../patterns/LibraryCards/LinkedItemCard' import { Box, HStack, SpanBox, VStack } from './../../elements/LayoutPrimitives' -import { AddLinkModal } from './AddLinkModal' +import { AddLinkModal } from '../AddLinkModal' import { EditLibraryItemModal } from './EditItemModals' import { EmptyLibrary } from './EmptyLibrary' import { HighlightItemsLayout } from './HighlightsLayout' import { LibraryFilterMenu } from '../navMenu/LibraryMenu' import { LibraryLegacyMenu } from '../navMenu/LibraryLegacyMenu' -import { LibraryHeader, MultiSelectMode } from './LibraryHeader' +import { LegacyLibraryHeader, MultiSelectMode } from './LibraryHeader' import { UploadModal } from '../UploadModal' import { BulkAction } from '../../../lib/networking/mutations/bulkActionMutation' import { bulkActionMutation } from '../../../lib/networking/mutations/bulkActionMutation' @@ -116,7 +116,7 @@ export function HomeFeedContainer(): JSX.Element { performActionOnItem, mutate, error: fetchItemsError, - } = useGetLibraryItemsQuery(queryInputs) + } = useGetLibraryItemsQuery('inbox', queryInputs) useEffect(() => { const handleRevalidate = () => { @@ -975,7 +975,7 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element { }} > {props.mode != 'highlights' && ( - {props.viewer && ( { @@ -88,7 +88,6 @@ export function LibraryHeader(props: LibraryHeaderProps): JSX.Element { right: '0', zIndex: 5, px: '70px', - bg: '$thLibraryBackground', position: 'fixed', left: LIBRARY_LEFT_MENU_WIDTH, height: small ? '60px' : DEFAULT_HEADER_HEIGHT, diff --git a/packages/web/components/templates/homeFeed/MultiSelectControls.tsx b/packages/web/components/templates/homeFeed/MultiSelectControls.tsx index e1cbb2113..a2d48fd05 100644 --- a/packages/web/components/templates/homeFeed/MultiSelectControls.tsx +++ b/packages/web/components/templates/homeFeed/MultiSelectControls.tsx @@ -8,13 +8,30 @@ import { LabelIcon } from '../../elements/icons/LabelIcon' import { TrashIcon } from '../../elements/icons/TrashIcon' import { ConfirmationModal } from '../../patterns/ConfirmationModal' import { AddBulkLabelsModal } from '../article/AddBulkLabelsModal' -import { X } from 'phosphor-react' -import { LibraryHeaderProps } from './LibraryHeader' +import { X } from '@phosphor-icons/react' +import { MultiSelectMode } from './LibraryHeader' import { HeaderCheckboxIcon } from '../../elements/icons/HeaderCheckboxIcon' import { Label } from '../../../lib/networking/fragments/labelFragment' import { MarkAsReadIcon } from '../../elements/icons/MarkAsReadIcon' +import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery' -export const MultiSelectControls = (props: LibraryHeaderProps): JSX.Element => { +export type MultiSelectProps = { + viewer: UserBasicData | undefined + + searchTerm: string | undefined + applySearchQuery: (searchQuery: string) => void + + showFilterMenu: boolean + setShowFilterMenu: (show: boolean) => void + + numItemsSelected: number + multiSelectMode: MultiSelectMode + setMultiSelectMode: (mode: MultiSelectMode) => void + + performMultiSelectAction: (action: BulkAction, labelIds?: string[]) => void +} + +export const MultiSelectControls = (props: MultiSelectProps): JSX.Element => { const [showConfirmDelete, setShowConfirmDelete] = useState(false) const [showLabelsModal, setShowLabelsModal] = useState(false) // Don't change on immediate hover, the button has to be blurred at least once @@ -146,7 +163,7 @@ export const MultiSelectControls = (props: LibraryHeaderProps): JSX.Element => { ) } -export const CheckBoxButton = (props: LibraryHeaderProps): JSX.Element => { +export const CheckBoxButton = (props: MultiSelectProps): JSX.Element => { return (
{ + if ( + event.dataTransfer.types.find((t) => t.toLowerCase() == 'files') + ) { + setShowUploadModal(true) + } + }} + style={{ height: '100%', width: '100%' }} + > + + + {props.hasMore ? ( + + ) : ( + + )} + +
+ + {props.showEditTitleModal && ( + + props.actionHandler('update-item', item) + } + onOpenChange={() => { + props.setShowEditTitleModal(false) + props.setLinkToEdit(undefined) + }} + item={props.linkToEdit as LibraryItem} + /> + )} + {showUnsubscribeConfirmation && ( + setShowUnsubscribeConfirmation(false)} + /> + )} + {props.labelsTarget?.node.id && ( + { + if (props.labelsTarget) { + const activate = props.labelsTarget + props.setActiveItem(activate) + props.setLabelsTarget(undefined) + } + }} + /> + )} + {props.viewer && props.notebookTarget?.node.id && ( + { + // onClose={(highlights: Highlight[]) => { + // if (props.notebookTarget?.node.highlights) { + // props.notebookTarget.node.highlights = highlights + // } + props.setNotebookTarget(open ? props.notebookTarget : undefined) + }} + /> + )} + {showUploadModal && ( + setShowUploadModal(false)} /> + )} + + ) +} + +type LibraryItemsProps = { + items: LibraryItem[] + layout: LayoutType + viewer: UserBasicData | undefined + + gridContainerRef: React.RefObject + + setShowEditTitleModal: (show: boolean) => void + setLinkToEdit: (set: LibraryItem | undefined) => void + setShowUnsubscribeConfirmation: (show: true) => void + setLinkToUnsubscribe: (set: LibraryItem | undefined) => void + + isChecked: (itemId: string) => boolean + setIsChecked: (itemId: string, set: boolean) => void + multiSelectMode: MultiSelectMode + + actionHandler: ( + action: LinkedItemCardAction, + item: LibraryItem | undefined + ) => Promise +} + +function LibraryItems(props: LibraryItemsProps): JSX.Element { + return ( + + {props.items.map((linkedItem) => ( + div': { + bg: '$thLeftMenuBackground', + // bg: '$thLibraryBackground', + }, + '&:focus': { + outline: 'none', + '> div': { + outline: 'none', + bg: '$thBackgroundActive', + }, + }, + '&:hover': { + '> div': { + bg: '$thBackgroundActive', + boxShadow: '$cardBoxShadow', + }, + '> a': { + bg: '$thBackgroundActive', + }, + }, + }} + > + {props.viewer && ( + { + if (action === 'editTitle') { + props.setShowEditTitleModal(true) + props.setLinkToEdit(linkedItem) + } else if (action == 'unsubscribe') { + props.setShowUnsubscribeConfirmation(true) + props.setLinkToUnsubscribe(linkedItem) + } else { + props.actionHandler(action, linkedItem) + } + document.body.style.removeProperty('pointer-events') + }} + /> + )} + + ))} + + ) +} diff --git a/packages/web/components/templates/library/LibraryHeader.tsx b/packages/web/components/templates/library/LibraryHeader.tsx new file mode 100644 index 000000000..aead3f2fe --- /dev/null +++ b/packages/web/components/templates/library/LibraryHeader.tsx @@ -0,0 +1,411 @@ +import { useEffect, useRef, useState } from 'react' +import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives' +import { theme } from '../../tokens/stitches.config' +import { FormInput } from '../../elements/FormElements' +import { searchBarCommands } from '../../../lib/keyboardShortcuts/navigationShortcuts' +import { useKeyboardShortcuts } from '../../../lib/keyboardShortcuts/useKeyboardShortcuts' +import { Button, IconButton } from '../../elements/Button' +import { FunnelSimple, X } from '@phosphor-icons/react' +import { LayoutType, LibraryMode } from '../homeFeed/HomeFeedContainer' +import { OmnivoreSmallLogo } from '../../elements/images/OmnivoreNameLogo' +import { DEFAULT_HEADER_HEIGHT, HeaderSpacer } from '../homeFeed/HeaderSpacer' +import { LIBRARY_LEFT_MENU_WIDTH } from '../navMenu/LibraryMenu' +import { BulkAction } from '../../../lib/networking/mutations/bulkActionMutation' +import { HeaderToggleGridIcon } from '../../elements/icons/HeaderToggleGridIcon' +import { HeaderToggleListIcon } from '../../elements/icons/HeaderToggleListIcon' +import { HeaderToggleTLDRIcon } from '../../elements/icons/HeaderToggleTLDRIcon' +import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery' +import { userHasFeature } from '../../../lib/featureFlag' +import { + MultiSelectControls, + CheckBoxButton, +} from '../homeFeed/MultiSelectControls' + +export type MultiSelectMode = 'off' | 'none' | 'some' | 'visible' | 'search' + +export type LibraryHeaderProps = { + viewer: UserBasicData | undefined + + layout: LayoutType + updateLayout: (layout: LayoutType) => void + + searchTerm: string | undefined + applySearchQuery: (searchQuery: string) => void + + showFilterMenu: boolean + setShowFilterMenu: (show: boolean) => void + + numItemsSelected: number + multiSelectMode: MultiSelectMode + setMultiSelectMode: (mode: MultiSelectMode) => void + + performMultiSelectAction: (action: BulkAction, labelIds?: string[]) => void +} + +export const headerControlWidths = ( + layout: LayoutType, + multiSelectMode: MultiSelectMode +) => { + return { + width: '95%', + '@mdDown': { + width: '100%', + }, + '@media (min-width: 930px)': { + width: '620px', + }, + '@media (min-width: 1280px)': { + width: '940px', + }, + '@media (min-width: 1600px)': { + width: '1232px', + }, + } +} + +export function LibraryHeader(props: LibraryHeaderProps): JSX.Element { + const [small, setSmall] = useState(false) + + useEffect(() => { + const handleScroll = () => { + setSmall(window.scrollY > 40) + } + if (typeof window !== 'undefined') { + window.addEventListener('scroll', handleScroll) + } + return () => { + window.removeEventListener('scroll', handleScroll) + } + }, []) + + return ( + <> + + + + + ) +} + +function LargeHeaderLayout(props: LibraryHeaderProps): JSX.Element { + return ( + + {props.multiSelectMode !== 'off' ? ( + <> + + + ) : ( + + )} + + ) +} + +const HeaderControls = (props: LibraryHeaderProps): JSX.Element => { + const [searchBoxFocused, setSearchBoxFocused] = useState(false) + + return ( + <> + {!searchBoxFocused && ( + + + + )} + + + + + {/* {userHasFeature(props.viewer, 'ai-summaries') && ( + + )} */} + + + + + ) +} + +type MenuHeaderButtonProps = { + showFilterMenu: boolean + setShowFilterMenu: (show: boolean) => void +} + +export function MenuHeaderButton(props: MenuHeaderButtonProps): JSX.Element { + return ( + { + props.setShowFilterMenu(!props.showFilterMenu) + }} + > + + + + ) +} + +type SearchBoxProps = LibraryHeaderProps & { + searchBoxFocused: boolean + setSearchBoxFocused: (show: boolean) => void +} + +export function SearchBox(props: SearchBoxProps): JSX.Element { + const inputRef = useRef(null) + const [searchTerm, setSearchTerm] = useState(props.searchTerm ?? '') + + useEffect(() => { + setSearchTerm(props.searchTerm ?? '') + }, [props.searchTerm]) + + useKeyboardShortcuts( + searchBarCommands((action) => { + if (action === 'focusSearchBar' && inputRef.current) { + inputRef.current.select() + } + if (action == 'clearSearch' && inputRef.current) { + setSearchTerm('') + props.applySearchQuery('') + } + }) + ) + + return ( + + + + + + +
{ + event.preventDefault() + props.applySearchQuery(searchTerm || '') + inputRef.current?.blur() + }} + style={{ width: '100%' }} + > + { + event.target.select() + props.setSearchBoxFocused(true) + }} + onBlur={() => { + props.setSearchBoxFocused(false) + }} + onChange={(event) => { + setSearchTerm(event.target.value) + }} + onKeyDown={(event) => { + const key = event.key.toLowerCase() + if (key == 'escape') { + event.currentTarget.blur() + } + }} + /> + + + { + setSearchTerm('in:inbox') + props.applySearchQuery('') + inputRef.current?.blur() + }} + /> + +
+
+
+ ) +} + +type CancelSearchButtonProps = { + onClick: () => void +} + +const CancelSearchButton = (props: CancelSearchButtonProps): JSX.Element => { + const [color, setColor] = useState( + theme.colors.thTextContrast2.toString() + ) + return ( + + ) +} diff --git a/packages/web/components/templates/library/LibraryItemsContainer.tsx b/packages/web/components/templates/library/LibraryItemsContainer.tsx new file mode 100644 index 000000000..a6b48ccbc --- /dev/null +++ b/packages/web/components/templates/library/LibraryItemsContainer.tsx @@ -0,0 +1,16 @@ +import { Allotment } from 'allotment' +import 'allotment/dist/style.css' +import { LibraryContainer } from './LibraryContainer' + +export function LibraryItemsContainer(): JSX.Element { + return ( + + + + + {/* + + */} + + ) +} diff --git a/packages/web/components/templates/library/LibrarySideBar.tsx b/packages/web/components/templates/library/LibrarySideBar.tsx new file mode 100644 index 000000000..d59b5a1f1 --- /dev/null +++ b/packages/web/components/templates/library/LibrarySideBar.tsx @@ -0,0 +1,62 @@ +import { Action, createAction, useKBar, useRegisterActions } from 'kbar' +import debounce from 'lodash/debounce' +import { useRouter } from 'next/router' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { Toaster } from 'react-hot-toast' +import TopBarProgress from 'react-topbar-progress-indicator' +import { useFetchMore } from '../../../lib/hooks/useFetchMoreScroll' +import { usePersistedState } from '../../../lib/hooks/usePersistedState' +import { libraryListCommands } from '../../../lib/keyboardShortcuts/navigationShortcuts' +import { useKeyboardShortcuts } from '../../../lib/keyboardShortcuts/useKeyboardShortcuts' +import { + PageType, + State, +} from '../../../lib/networking/fragments/articleFragment' +import { + SearchItem, + TypeaheadSearchItemsData, + typeaheadSearchQuery, +} from '../../../lib/networking/queries/typeaheadSearch' +import type { + LibraryItem, + LibraryItemsQueryInput, +} from '../../../lib/networking/queries/useGetLibraryItemsQuery' +import { useGetLibraryItemsQuery } from '../../../lib/networking/queries/useGetLibraryItemsQuery' +import { + useGetViewerQuery, + UserBasicData, +} from '../../../lib/networking/queries/useGetViewerQuery' +import { Button } from '../../elements/Button' +import { StyledText } from '../../elements/StyledText' +import { ConfirmationModal } from '../../patterns/ConfirmationModal' +import { LinkedItemCardAction } from '../../patterns/LibraryCards/CardTypes' +import { LinkedItemCard } from '../../patterns/LibraryCards/LinkedItemCard' +import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives' +import { AddLinkModal } from '../AddLinkModal' +import { EditLibraryItemModal } from '../homeFeed/EditItemModals' +import { EmptyLibrary } from '../homeFeed/EmptyLibrary' +import { LegacyLibraryHeader, MultiSelectMode } from '../homeFeed/LibraryHeader' +import { UploadModal } from '../UploadModal' +import { BulkAction } from '../../../lib/networking/mutations/bulkActionMutation' +import { bulkActionMutation } from '../../../lib/networking/mutations/bulkActionMutation' +import { + showErrorToast, + showSuccessToast, + showSuccessToastWithAction, +} from '../../../lib/toastHelpers' +import { SetPageLabelsModalPresenter } from '../article/SetLabelsModalPresenter' +import { NotebookPresenter } from '../article/NotebookPresenter' +import { saveUrlMutation } from '../../../lib/networking/mutations/saveUrlMutation' +import { articleQuery } from '../../../lib/networking/queries/useGetArticleQuery' +import { PinnedButtons } from '../homeFeed/PinnedButtons' +import { PinnedSearch } from '../../../pages/settings/pinned-searches' +import { FetchItemsError } from '../homeFeed/FetchItemsError' +import { LibraryHeader } from './LibraryHeader' + +type LibrarySideBarProps = { + text: string +} + +export function LibrarySideBar(props: LibrarySideBarProps): JSX.Element { + return {props.text} +} diff --git a/packages/web/components/templates/navMenu/Footer.tsx b/packages/web/components/templates/navMenu/Footer.tsx index fa1e5b611..4a1c4f754 100644 --- a/packages/web/components/templates/navMenu/Footer.tsx +++ b/packages/web/components/templates/navMenu/Footer.tsx @@ -17,7 +17,7 @@ export const NavMenuFooter = (props: NavMenuFooterProps): JSX.Element => { position: 'fixed', bottom: '0%', alignItems: 'center', - backgroundColor: '$thBackground2', + backgroundColor: '$thNavMenuFooter', width: LIBRARY_LEFT_MENU_WIDTH, overflowY: 'auto', overflowX: 'hidden', @@ -29,10 +29,7 @@ export const NavMenuFooter = (props: NavMenuFooterProps): JSX.Element => { }, }} > - + ({ - key: 'menu-labels', - isSessionStorage: false, - initialValue: [], - }) - const [savedSearches, setSavedSearches] = usePersistedState({ - key: 'menu-searches', - isSessionStorage: false, - initialValue: [], - }) - const [subscriptions, setSubscriptions] = usePersistedState({ - key: 'menu-subscriptions', - isSessionStorage: false, - initialValue: [], - }) - const labelsResponse = useGetLabelsQuery() - const searchesResponse = useGetSavedSearchQuery() - const subscriptionsResponse = useGetSubscriptionsQuery() - - useEffect(() => { - if ( - !labelsResponse.error && - !labelsResponse.isLoading && - labelsResponse.labels - ) { - setLabels(labelsResponse.labels) - } - }, [setLabels, labelsResponse]) - - useEffect(() => { - if ( - !subscriptionsResponse.error && - !subscriptionsResponse.isLoading && - subscriptionsResponse.subscriptions - ) { - setSubscriptions(subscriptionsResponse.subscriptions) - } - }, [setSubscriptions, subscriptionsResponse]) - - useEffect(() => { - if ( - !searchesResponse.error && - !searchesResponse.isLoading && - searchesResponse.savedSearches - ) { - setSavedSearches(searchesResponse.savedSearches) - } - }, [setSavedSearches, searchesResponse]) - return ( <> void +export type ShortcutType = 'search' | 'label' | 'newsletter' | 'feed' | 'folder' - searchTerm: string | undefined - applySearchQuery: (searchTerm: string) => void +export type Shortcut = { + type: ShortcutType - showFilterMenu: boolean - setShowFilterMenu: (show: boolean) => void + id: string + name: string + section: string + filter: string + + icon?: string + label?: Label + + join?: string } -export function NavigationMenu(props: LibraryFilterMenuProps): JSX.Element { - const [labels, setLabels] = usePersistedState({ - key: 'menu-labels', - isSessionStorage: false, - initialValue: [], - }) - const [savedSearches, setSavedSearches] = usePersistedState({ - key: 'menu-searches', - isSessionStorage: false, - initialValue: [], - }) - const [subscriptions, setSubscriptions] = usePersistedState({ - key: 'menu-subscriptions', - isSessionStorage: false, - initialValue: [], - }) - const labelsResponse = useGetLabelsQuery() - const searchesResponse = useGetSavedSearchQuery() - const subscriptionsResponse = useGetSubscriptionsQuery() +type NavigationMenuProps = { + section: NavigationSection - useEffect(() => { - if ( - !labelsResponse.error && - !labelsResponse.isLoading && - labelsResponse.labels - ) { - setLabels(labelsResponse.labels) - } - }, [setLabels, labelsResponse]) + setShowAddLinkModal: (show: boolean) => void - useEffect(() => { - if ( - !subscriptionsResponse.error && - !subscriptionsResponse.isLoading && - subscriptionsResponse.subscriptions - ) { - setSubscriptions(subscriptionsResponse.subscriptions) - } - }, [setSubscriptions, subscriptionsResponse]) - - useEffect(() => { - if ( - !searchesResponse.error && - !searchesResponse.isLoading && - searchesResponse.savedSearches - ) { - setSavedSearches(searchesResponse.savedSearches) - } - }, [setSavedSearches, searchesResponse]) + showMenu: boolean + setShowMenu: (show: boolean) => void +} +export function NavigationMenu(props: NavigationMenuProps): JSX.Element { return ( <> { - props.setShowFilterMenu(false) + props.setShowMenu(false) event.preventDefault() }} > @@ -160,7 +133,7 @@ export function NavigationMenu(props: LibraryFilterMenuProps): JSX.Element { }, }} onClick={(event) => { - props.setShowFilterMenu(false) + props.setShowMenu(false) event.preventDefault() }} > @@ -188,7 +161,13 @@ export function NavigationMenu(props: LibraryFilterMenuProps): JSX.Element { ) } -const LibraryNav = (props: LibraryFilterMenuProps): JSX.Element => { +const LibraryNav = (props: NavigationMenuProps): JSX.Element => { + const [moreFolderSectionOpen, setMoreFolderSectionOpen] = + usePersistedState({ + key: 'nav-more-folder-open', + isSessionStorage: false, + initialValue: true, + }) return ( { gap: '5px', width: '100%', borderBottom: '1px solid $thBorderColor', - px: '15px', + px: '0px', pb: '25px', }} alignment="start" @@ -206,95 +185,128 @@ const LibraryNav = (props: LibraryFilterMenuProps): JSX.Element => { } /> - } - /> } /> + } + /> } /> - } - /> + + {moreFolderSectionOpen && ( + + + } + /> + + } + /> + + )} ) } -const Shortcuts = (props: LibraryFilterMenuProps): JSX.Element => { - const router = useRouter() - const [shortcuts] = usePersistedState({ - key: 'library-shortcuts', - isSessionStorage: false, - initialValue: [], - }) +const Shortcuts = (props: NavigationMenuProps): JSX.Element => { + const treeRef = useRef | undefined>(undefined) + const { trigger: resetShortcutsTrigger } = useSWRMutation( + '/api/shortcuts', + resetShortcuts + ) - // const shortcuts: Shortcut[] = [ - // { - // id: '12asdfasdf', - // name: 'Omnivore Blog', - // icon: 'https://substackcdn.com/image/fetch/w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F052c15c4-ecfd-4d32-87db-13bcac9afad5_512x512.png', - // filter: 'subscription:"Money Talk"', - // type: 'feed', - // }, - // { - // id: 'sdfsdfgdsfg', - // name: 'Follow the Money | Arne & Harr', - // filter: 'subscription:"Money Talk"', - // type: 'feed', - // }, - // { - // id: 'sdfasdfasdfsdfsdfsgasdfg', - // name: 'Andrew Kenneson from Center for the Study of Partisanship and Ideology', - // // icon: 'https://substackcdn.com/image/fetch/w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F052c15c4-ecfd-4d32-87db-13bcac9afad5_512x512.png', - // filter: 'in:all label:"Hockey"', - // type: 'newsletter', - // }, - // { - // id: 'sdfasdfasdfsdfsdfsgasdfg', - // name: 'Robert的博客', - // // icon: 'https://substackcdn.com/image/fetch/w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F052c15c4-ecfd-4d32-87db-13bcac9afad5_512x512.png', - // filter: 'in:all label:"Hockey"', - // type: 'feed', - // }, - // { - // id: 'sdfasdfasdfasdfasf', - // name: 'Oldest First', - // // icon: 'https://substackcdn.com/image/fetch/w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F052c15c4-ecfd-4d32-87db-13bcac9afad5_512x512.png', - // filter: 'in:all label:"Hockey"', - // type: 'search', - // }, - // { - // id: 'sdfasdfasdfgasdfg', - // name: 'Hockey', - // // icon: 'https://substackcdn.com/image/fetch/w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F052c15c4-ecfd-4d32-87db-13bcac9afad5_512x512.png', - // filter: 'in:all label:"Hockey"', - // type: 'label', - // label: { - // id: 'sdfsdfsdf', - // name: 'Hockey', - // color: '#E98B8B', - // createdAt: new Date(), - // }, - // }, - // ] - // + const createNewFolder = useCallback(async () => { + if (treeRef.current) { + const result = await treeRef.current.create({ + type: 'internal', + index: 0, + }) + console.log('create leaf: ', result) + } + }, [treeRef]) + + const resetShortcutsToDefault = useCallback(async () => { + resetShortcutsTrigger(null, { + revalidate: true, + }) + }, []) return ( { m: '0px', gap: '8px', width: '100%', - px: '15px', + px: '0px', pb: '25px', }} alignment="start" @@ -319,81 +331,454 @@ const Shortcuts = (props: LibraryFilterMenuProps): JSX.Element => { fontSize: '14px', lineHeight: '125%', color: '$thLibraryMenuPrimary', - pl: '10px', - // mt: '20px', mb: '10px', + px: '15px', }} > - SHORTCUTS + Shortcuts - + } css={{ ml: 'auto' }} > { - router.push(`/settings/shortcuts`) - }} - title="Edit shortcuts" + onSelect={resetShortcutsToDefault} + title="Reset to default" + /> + - {shortcuts.map((shortcut) => { - const selected = props.searchTerm === shortcut.filter - return ( - { - props.applySearchQuery(shortcut.filter) - props.setShowFilterMenu(false) - e.preventDefault() - }} - > - {(shortcut.type == 'feed' || shortcut.type == 'newsletter') && ( - - )} - {shortcut.type == 'search' && ( - - )} - {shortcut.type == 'label' && } - - ) - })} + + + ) } +type ShortcutsTreeProps = { + treeRef: React.MutableRefObject | undefined> +} + +async function getShortcuts(path: string): Promise { + const url = new URL(path, fetchEndpoint) + try { + const response = await fetch(url.toString(), { + method: 'GET', + headers: requestHeaders(), + credentials: 'include', + mode: 'cors', + }) + const payload = await response.json() + if ('shortcuts' in payload) { + return payload['shortcuts'] as Shortcut[] + } + return [] + } catch (err) { + console.log('error getting shortcuts: ', err) + throw err + } +} + +async function setShortcuts( + path: string, + { arg }: { arg: { shortcuts: Shortcut[] } } +): Promise { + const url = new URL(path, fetchEndpoint) + try { + const response = await fetch(url.toString(), { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + ...requestHeaders(), + }, + credentials: 'include', + mode: 'cors', + body: JSON.stringify(arg), + }) + const payload = await response.json() + if (!('shortcuts' in payload)) { + throw new Error('Error syncing shortcuts') + } + return payload['shortcuts'] as Shortcut[] + } catch (err) { + showErrorToast('Error syncing shortcut changes.') + } + return arg.shortcuts +} + +async function resetShortcuts(path: string): Promise { + const url = new URL(path, fetchEndpoint) + try { + const response = await fetch(url.toString(), { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + ...requestHeaders(), + }, + credentials: 'include', + mode: 'cors', + }) + const payload = await response.json() + if (!('shortcuts' in payload)) { + throw new Error('Error syncing shortcuts') + } + return payload['shortcuts'] as Shortcut[] + } catch (err) { + showErrorToast('Error syncing shortcut changes.') + } + return [] +} + +const cachedShortcutsData = (): Shortcut[] | undefined => { + if (typeof localStorage !== 'undefined') { + const str = localStorage.getItem('/api/shortcuts') + if (str) { + return JSON.parse(str) as Shortcut[] + } + } + return undefined +} + +const ShortcutsTree = (props: ShortcutsTreeProps): JSX.Element => { + const router = useRouter() + + const { isValidating, data } = useSWR('/api/shortcuts', getShortcuts, { + fallbackData: cachedShortcutsData(), + onSuccess(data) { + localStorage.setItem('/api/shortcuts', JSON.stringify(data)) + }, + }) + const { trigger, isMutating } = useSWRMutation('/api/shortcuts', setShortcuts) + const [folderOpenState, setFolderOpenState] = usePersistedState< + Record + >({ + key: 'nav-menu-open-state', + isSessionStorage: false, + initialValue: {}, + }) + const tree = useMemo(() => { + const result = new SimpleTree((data ?? []) as Shortcut[]) + return result + }, [data]) + + const syncTreeData = (data: Shortcut[]) => { + trigger( + { shortcuts: data }, + { + optimisticData: data, + rollbackOnError: true, + populateCache: (updatedShortcuts) => { + return updatedShortcuts + }, + revalidate: false, + } + ) + } + + const onMove = useCallback( + (args: { dragIds: string[]; parentId: null | string; index: number }) => { + for (const id of args.dragIds) { + tree?.move({ id, parentId: args.parentId, index: args.index }) + } + syncTreeData(tree.data) + }, + [tree, data] + ) + + const onCreate = useCallback( + (args: { parentId: string | null; index: number; type: string }) => { + const data = { id: uuidv4(), name: '', type: 'folder' } as any + if (args.type === 'internal') { + data.children = [] + } + tree.create({ parentId: args.parentId, index: args.index, data }) + syncTreeData(tree.data) + return data + }, + [tree, data] + ) + + const onDelete = useCallback( + (args: { ids: string[] }) => { + args.ids.forEach((id) => tree.drop({ id })) + syncTreeData(tree.data) + }, + [tree, data] + ) + + const onRename = useCallback( + (args: { name: string; id: string }) => { + tree.update({ id: args.id, changes: { name: args.name } as any }) + syncTreeData(tree.data) + }, + [tree, data] + ) + + const onToggle = useCallback( + (id: string) => { + if (id && props.treeRef.current) { + const isOpen = props.treeRef.current?.isOpen(id) + const newItem: OpenMap = {} + newItem[id] = isOpen + setFolderOpenState({ ...folderOpenState, ...newItem }) + } + }, + [props, folderOpenState, setFolderOpenState] + ) + + const onActivate = useCallback( + (node: NodeApi) => { + console.log('onActivate: ', node) + if (node.data.type == 'folder') { + const join = node.data.join + if (join == 'or') { + const query = node.children + ?.map((child) => { + return `(${child.data.filter})` + }) + .join(' OR ') + console.log('query: ', query) + } + } else if (node.data.section != null && node.data.filter != null) { + router.push(`/l/${node.data.section}?q=${node.data.filter}`) + } + }, + [tree, router] + ) + + return ( + <> + {!isValidating && ( + + {NodeRenderer} + + )} + + ) +} + +function NodeRenderer(args: { + style: CSSProperties + node: NodeApi + tree: TreeApi + dragHandle?: (el: HTMLDivElement | null) => void + preview?: boolean +}) { + const isSelected = false + const [menuVisible, setMenuVisible] = useState(false) + const [menuOpened, setMenuOpened] = useState(false) + + const router = useRouter() + + return ( + { + setMenuVisible(true) + }} + onMouseLeave={() => { + setMenuVisible(false) + }} + title={args.node.data.name} + onClick={(e) => { + // router.push(`/` + props.section) + }} + > + + + + } + css={{ ml: 'auto' }} + onOpenChange={(open) => { + setMenuOpened(open) + }} + > + { + args.tree.delete(args.node) + }} + title="Remove" + /> + {/* {args.node.data.type == 'folder' && ( + { + args.node.data.join = 'or' + }} + title="Folder query: OR" + /> + )} */} + + + + + ) +} + +type NodeItemContentsProps = { + node: NodeApi +} + +const NodeItemContents = (props: NodeItemContentsProps): JSX.Element => { + if (props.node.isEditing) { + return ( + e.currentTarget.select()} + onBlur={() => props.node.reset()} + onKeyDown={(e) => { + if (e.key === 'Escape') { + props.node.reset() + } + if (e.key === 'Enter') { + // props.node.data = { + // id: 'new-folder', + // type: 'folder', + // name: e.currentTarget.value, + // } + props.node.submit(e.currentTarget.value) + props.node.activate() + } + }} + /> + ) + } + if (props.node.isLeaf) { + const shortcut = props.node.data + if (shortcut) { + switch (shortcut.type) { + case 'feed': + case 'newsletter': + return ( + + + + ) + case 'label': + return ( + + + + ) + case 'search': + return ( + + + + ) + } + } + } else { + return ( + { + props.node.toggle() + event.preventDefault() + }} + > + {props.node.isClosed ? ( + + ) : ( + + )} + {props.node.data.name} + + ) + } + return <> +} + type ShortcutItemProps = { shortcut: Shortcut } @@ -434,7 +819,7 @@ const SearchShortcut = (props: ShortcutItemProps): JSX.Element => { { alignment="center" css={{ minWidth: '20px' }} > - + {props.shortcut.name} @@ -450,368 +835,39 @@ const SearchShortcut = (props: ShortcutItemProps): JSX.Element => { } const LabelShortcut = (props: ShortcutItemProps): JSX.Element => { + // return ( - + + {props.shortcut.name} + ) } -function SavedSearches( - props: LibraryFilterMenuProps & { savedSearches: SavedSearch[] | undefined } -): JSX.Element { - const sortedSearches = useMemo(() => { - return props.savedSearches - ?.filter((it) => it.visible) - ?.sort( - (left: SavedSearch, right: SavedSearch) => - left.position - right.position - ) - }, [props.savedSearches]) - - useRegisterActions( - (sortedSearches ?? []).map((item, idx) => { - const key = String(idx + 1) - return { - id: `saved_search_${key}`, - name: item.name, - shortcut: [key], - section: 'Saved Searches', - keywords: '?' + item.name, - perform: () => { - props.applySearchQuery(item.filter) - }, - } - }), - [props.savedSearches] - ) - - const [collapsed, setCollapsed] = usePersistedState({ - key: `--saved-searches-collapsed`, - initialValue: false, - }) - - return ( - - {!collapsed && - sortedSearches && - sortedSearches?.map((item) => ( - - ))} - {!collapsed && sortedSearches !== undefined && ( - - )} - - - - ) -} - -function Subscriptions( - props: LibraryFilterMenuProps & { subscriptions: Subscription[] | undefined } -): JSX.Element { - const [collapsed, setCollapsed] = usePersistedState({ - key: `--subscriptions-collapsed`, - initialValue: false, - }) - - const sortedSubscriptions = useMemo(() => { - if (!props.subscriptions) { - return [] - } - return props.subscriptions - .filter((s) => s.status == 'ACTIVE') - .sort((a, b) => a.name.localeCompare(b.name)) - }, [props.subscriptions]) - - useRegisterActions( - (sortedSubscriptions ?? []).map((subscription, idx) => { - const key = String(idx + 1) - const name = subscription.name - return { - id: `subscription_${key}`, - section: 'Subscriptions', - name: name, - keywords: '*' + name, - perform: () => { - props.applySearchQuery(`subscription:\"${escapeQuotes(name)}\"`) - }, - } - }), - [sortedSubscriptions] - ) - - return ( - - {!collapsed ? ( - <> - - - - {(sortedSubscriptions ?? []).map((item) => { - switch (item.type) { - case SubscriptionType.NEWSLETTER: - return ( - - ) - case SubscriptionType.RSS: - return ( - - ) - } - })} - - - ) : ( - - )} - - ) -} - -function Labels( - props: LibraryFilterMenuProps & { labels: Label[] } -): JSX.Element { - const [collapsed, setCollapsed] = usePersistedState({ - key: `--labels-collapsed`, - initialValue: false, - }) - - const sortedLabels = useMemo(() => { - return props.labels.sort((left: Label, right: Label) => - left.name.localeCompare(right.name) - ) - }, [props.labels]) - - return ( - - {!collapsed && ( - <> - {sortedLabels.map((item) => { - return - })} - - - )} - - ) -} - -type MenuPanelProps = { - title: string - children: ReactNode - editFunc?: () => void - editTitle?: string - hideBottomBorder?: boolean - collapsed: boolean - setCollapsed: (collapsed: boolean) => void -} - -function MenuPanel(props: MenuPanelProps): JSX.Element { - return ( - - - - {props.title} - - - - - - {props.children} - - ) -} - type NavButtonProps = { text: string icon: ReactNode - filterTerm: string - searchTerm: string | undefined - - applySearchQuery: (searchTerm: string) => void - setShowFilterMenu: (show: boolean) => void -} - -type NavButtonRedirectProps = { - text: string - icon: ReactNode - - redirectLocation: string -} - -function NavRedirectButton(props: NavButtonRedirectProps): JSX.Element { - const [selected, setSelected] = useState(false) - const router = useRouter() - - useEffect(() => { - setSelected(window.location.pathname.includes(props.redirectLocation)) - }, []) - - return ( - { - router.push(props.redirectLocation) - e.preventDefault() - }} - > - {props.icon} - {props.text} - - ) + isSelected: boolean + section: NavigationSection } function NavButton(props: NavButtonProps): JSX.Element { - const isInboxFilter = (filter: string) => { - return filter === '' || filter === 'in:inbox' - } - const selected = useMemo(() => { - if (isInboxFilter(props.filterTerm) && !props.searchTerm) { - return true - } - return props.searchTerm === props.filterTerm - }, [props.searchTerm, props.filterTerm]) + const router = useRouter() return ( { - props.applySearchQuery(props.filterTerm) - props.setShowFilterMenu(false) - e.preventDefault() + router.push(`/l/` + props.section) }} > {props.icon} @@ -862,213 +919,3 @@ function NavButton(props: NavButtonProps): JSX.Element { ) } - -type FilterButtonProps = { - text: string - - filterTerm: string - searchTerm: string | undefined - - applySearchQuery: (searchTerm: string) => void - - setShowFilterMenu: (show: boolean) => void -} - -function FilterButton(props: FilterButtonProps): JSX.Element { - const isInboxFilter = (filter: string) => { - return filter === '' || filter === 'in:inbox' - } - const selected = useMemo(() => { - if (isInboxFilter(props.filterTerm) && !props.searchTerm) { - return true - } - return props.searchTerm === props.filterTerm - }, [props.searchTerm, props.filterTerm]) - - return ( - { - props.applySearchQuery(props.filterTerm) - props.setShowFilterMenu(false) - e.preventDefault() - }} - > - {props.text} - - ) -} - -type LabelButtonProps = { - label: Label - searchTerm: string | undefined - applySearchQuery: (searchTerm: string) => void -} - -function LabelButton(props: LabelButtonProps): JSX.Element { - const labelId = `checkbox-label-${props.label.id}` - const checkboxRef = useRef(null) - const state = useMemo(() => { - const term = props.searchTerm ?? '' - if (term.indexOf(`label:\"${escapeQuotes(props.label.name)}\"`) >= 0) { - return 'on' - } - return 'off' - }, [props.searchTerm, props.label]) - - return ( - - - - { - const escapedLabelName = escapeQuotes(props.label.name) - if (e.target.checked) { - props.applySearchQuery( - `${props.searchTerm ?? ''} label:\"${escapedLabelName}\"` - ) - } else { - const query = - props.searchTerm?.replace( - `label:\"${escapedLabelName}\"`, - '' - ) ?? '' - props.applySearchQuery(query) - } - }} - /> - - - ) -} - -type EditButtonProps = { - title: string - destination: string -} - -function EditButton(props: EditButtonProps): JSX.Element { - return ( - - - {props.title} - - - ) -} diff --git a/packages/web/components/templates/navMenu/SettingsDropdown.tsx b/packages/web/components/templates/navMenu/SettingsDropdown.tsx index a54da2f9e..4bab09491 100644 --- a/packages/web/components/templates/navMenu/SettingsDropdown.tsx +++ b/packages/web/components/templates/navMenu/SettingsDropdown.tsx @@ -5,7 +5,7 @@ import { DropdownSeparator, } from '../../elements/DropdownElements' import { useRouter } from 'next/router' -import { List } from 'phosphor-react' +import { List } from '@phosphor-icons/react' export const SettingsDropdown = (): JSX.Element => { const router = useRouter() diff --git a/packages/web/components/templates/navMenu/SettingsMenu.tsx b/packages/web/components/templates/navMenu/SettingsMenu.tsx index 3a06506ce..e60c5008b 100644 --- a/packages/web/components/templates/navMenu/SettingsMenu.tsx +++ b/packages/web/components/templates/navMenu/SettingsMenu.tsx @@ -5,7 +5,7 @@ import { LogoBox } from '../../elements/LogoBox' import Link from 'next/link' import { styled, theme } from '../../tokens/stitches.config' import { Button } from '../../elements/Button' -import { ArrowSquareUpRight } from 'phosphor-react' +import { ArrowSquareUpRight } from '@phosphor-icons/react' import { useRouter } from 'next/router' import { NavMenuFooter } from './Footer' diff --git a/packages/web/components/templates/reader/ReaderHeader.tsx b/packages/web/components/templates/reader/ReaderHeader.tsx index 75c776241..99a00d533 100644 --- a/packages/web/components/templates/reader/ReaderHeader.tsx +++ b/packages/web/components/templates/reader/ReaderHeader.tsx @@ -144,12 +144,6 @@ function ControlButtonBox(props: ReaderHeaderProps): JSX.Element { color={theme.colors.thHighContrast.toString()} /> - - - ) diff --git a/packages/web/components/templates/settings/SettingsTable.tsx b/packages/web/components/templates/settings/SettingsTable.tsx index 1db5872a6..5f81689fd 100644 --- a/packages/web/components/templates/settings/SettingsTable.tsx +++ b/packages/web/components/templates/settings/SettingsTable.tsx @@ -1,4 +1,4 @@ -import { Pencil, Trash } from 'phosphor-react' +import { Pencil, Trash } from '@phosphor-icons/react' import { Toaster } from 'react-hot-toast' import { Button } from '../../elements/Button' import { Dropdown, DropdownOption } from '../../elements/DropdownElements' diff --git a/packages/web/components/tokens/stitches.config.ts b/packages/web/components/tokens/stitches.config.ts index 97edb8660..6ed792011 100644 --- a/packages/web/components/tokens/stitches.config.ts +++ b/packages/web/components/tokens/stitches.config.ts @@ -151,7 +151,7 @@ export const { styled, css, theme, getCssText, globalCss, keyframes, config } = searchActiveOutline: 'rgb(255, 210, 52)', // Reader Colors - readerBg: 'white', + readerBg: '#FAFAFA', readerFont: '#3D3D3D', readerFontHighContrast: 'black', readerTableHeader: '#FFFFFF', @@ -180,7 +180,8 @@ export const { styled, css, theme, getCssText, globalCss, keyframes, config } = thBackground5: '#F5F5F5', thBackgroundActive: '#FFEA9F', thBackgroundContrast: '#FFFFFF', - thLeftMenuBackground: '#FCFCFC', + thLeftMenuBackground: '#F2F2F2', + thNavMenuFooter: '#DFDFDF', thLibraryBackground: '#FFFFFF', thLibrarySearchbox: '#FCFCFC', thLibraryMenuPrimary: '#3D3D3D', @@ -229,6 +230,14 @@ export const { styled, css, theme, getCssText, globalCss, keyframes, config } = thHighContrast: '#3D3D3D', thHighlightBar: '#D9D9D9', + homeCardHover: '#FFFFFF', + homeTextTitle: '#2A2A2A', + homeTextSource: '#3D3D3D', + homeTextBody: '#3D3D3D', + homeTextSubtle: '#898989', + homeActionIcons: '#898989', + homeDivider: '#D9D9D9', + thLibraryAISummaryBorder: '#6A6968', thLibraryAISummaryBackground: '#343434', @@ -324,12 +333,13 @@ const darkThemeSpec = { thBackgroundActive: '#3D3D3D', thBackgroundContrast: '#000000', thLeftMenuBackground: '#343434', + thNavMenuFooter: '#515151', thLibraryBackground: '#2A2A2A', thLibrarySearchbox: '#3D3D3D', thLibraryMenuPrimary: '#EBEBEB', thLibraryMenuSecondary: '#EBEBEB', thLibraryMenuUnselected: 'white', - thLibrarySelectionColor: '#6A6968', + thLibrarySelectionColor: '#515151', thLibraryNavigationMenuFooter: '#3D3D3D', thLibraryMenuFooterHover: '#6A6968', thLibraryMultiselectHover: '#6A6968', @@ -374,6 +384,14 @@ const darkThemeSpec = { thHighlightBar: '#6A6968', + homeCardHover: '#323232', + homeTextTitle: '#FFFFFF', + homeTextSource: '#D9D9D9', + homeTextBody: '#D9D9D9', + homeTextSubtle: '#898989', + homeActionIcons: '#898989', + homeDivider: '#3D3D3D', + thLibraryAISummaryBorder: '#6A6968', thLibraryAISummaryBackground: '#343434', @@ -392,19 +410,6 @@ const blackThemeSpec = { }, } -const sepiaThemeSpec = { - colorScheme: { - colorScheme: 'light', - }, - colors: { - readerBg: '#FBF0D9', - readerFont: '#5F4B32', - readerMargin: '#F3F3F3', - readerFontHighContrast: '#0A0806', - readerTableHeader: '#FFFFFF', - }, -} - const apolloThemeSpec = { colors: { readerBg: '#6A6968', @@ -412,6 +417,47 @@ const apolloThemeSpec = { readerMargin: '#474747', readerFontHighContrast: 'white', readerTableHeader: '#FFFFFF', + + thLeftMenuBackground: '#3D3D3D', + thNavMenuFooter: '#515151', + + thLibrarySelectionColor: '#515151', + thBackground4: '#51515166', // used on hover of nav menu items + thBorderColor: '#6A6968', + + homeCardHover: '#525252', + homeDivider: '#6A6968', + + thBackground: '#474747', + thBackground2: '#515151', + thLibraryMultiselectHover: '#EEE8D5', + }, +} + +const sepiaThemeSpec = { + colorScheme: { + colorScheme: 'light', + }, + colors: { + readerBg: '#FDF6E3', + readerFont: '#5F4B32', + readerMargin: '#F3F3F3', + readerFontHighContrast: '#0A0806', + readerTableHeader: '#FFFFFF', + + thLeftMenuBackground: '#EEE8D5', + thNavMenuFooter: '#DDD6C1', + + thLibrarySelectionColor: '#DDD6C1', + thBackground4: '#DDD6C166', // used on hover of menu items + thBorderColor: '#DDD6C1', + + thBackground: '#FDF6E3', + + homeCardHover: '#EEE8D5', + homeDivider: '#DDD6C1', + + thLibraryMultiselectHover: '#EEE8D5', }, } diff --git a/packages/web/lib/networking/networkHelpers.ts b/packages/web/lib/networking/networkHelpers.ts index d26733d6b..2b810fcee 100644 --- a/packages/web/lib/networking/networkHelpers.ts +++ b/packages/web/lib/networking/networkHelpers.ts @@ -12,7 +12,7 @@ export type RequestContext = { } } -function requestHeaders(): Record { +export function requestHeaders(): Record { const authToken = window?.localStorage.getItem('authToken') || undefined const pendingAuthToken = window?.localStorage.getItem('pendingUserAuth') || undefined @@ -54,6 +54,7 @@ export function gqlFetcher( credentials: 'include', mode: 'cors', }) + return graphQLClient.request(query, variables, requestHeaders()) } @@ -68,10 +69,14 @@ export function apiFetcher(path: string): Promise { }) } -export function apiPoster(path: string, body: any): Promise { +export function apiPoster( + path: string, + body: any, + method = 'POST' +): Promise { const url = new URL(path, fetchEndpoint) return fetch(url.toString(), { - method: 'POST', + method: method, credentials: 'include', mode: 'cors', headers: { @@ -83,17 +88,19 @@ export function apiPoster(path: string, body: any): Promise { } export function makePublicGqlFetcher( + gql: string, variables?: unknown ): (query: string) => Promise { - return (query: string) => gqlFetcher(query, variables, false) + return (query: string) => gqlFetcher(gql, variables, false) } // Partially apply gql variables to the request // This avoids using an object for the swr cache key export function makeGqlFetcher( + gql: string, variables?: unknown ): (query: string) => Promise { - return (query: string) => gqlFetcher(query, variables, true) + return (query: string) => gqlFetcher(gql, variables, true) } export function ssrFetcher( diff --git a/packages/web/lib/networking/queries/useGetArticleOriginalHtmlQuery.tsx b/packages/web/lib/networking/queries/useGetArticleOriginalHtmlQuery.tsx index edee9e662..94870fd94 100644 --- a/packages/web/lib/networking/queries/useGetArticleOriginalHtmlQuery.tsx +++ b/packages/web/lib/networking/queries/useGetArticleOriginalHtmlQuery.tsx @@ -49,7 +49,8 @@ export function useGetArticleOriginalHtmlQuery({ const { data } = useSWRImmutable( slug ? [query, username, slug] : null, - makeGqlFetcher(variables) + makeGqlFetcher(query, variables), + {} ) const resultData: ArticleData | undefined = data as ArticleData diff --git a/packages/web/lib/networking/queries/useGetArticleQuery.tsx b/packages/web/lib/networking/queries/useGetArticleQuery.tsx index f62cc3530..282cff9e1 100644 --- a/packages/web/lib/networking/queries/useGetArticleQuery.tsx +++ b/packages/web/lib/networking/queries/useGetArticleQuery.tsx @@ -7,7 +7,7 @@ import { State, } from '../fragments/articleFragment' import { Highlight, highlightFragment } from '../fragments/highlightFragment' -import { ScopedMutator } from 'swr/dist/types' +import { ScopedMutator } from 'swr/dist/_internal' import { Label, labelFragment } from '../fragments/labelFragment' import { LibraryItems, @@ -116,7 +116,8 @@ export function useGetArticleQuery({ const { data, error, mutate } = useSWR( slug ? [query, username, slug, includeFriendsHighlights] : null, - makeGqlFetcher(variables) + makeGqlFetcher(query, variables), + {} ) let resultData: ArticleData | undefined = data as ArticleData diff --git a/packages/web/lib/networking/queries/useGetArticleSavingStatus.tsx b/packages/web/lib/networking/queries/useGetArticleSavingStatus.tsx index f75101914..8a87f10ac 100644 --- a/packages/web/lib/networking/queries/useGetArticleSavingStatus.tsx +++ b/packages/web/lib/networking/queries/useGetArticleSavingStatus.tsx @@ -87,7 +87,7 @@ export function useGetArticleSavingStatus({ ` const key = id ? [query, id] : [query, url] // poll twice a second - const { data, error } = useSWR(key, makeGqlFetcher({ id, url }), { + const { data, error } = useSWR(key, makeGqlFetcher(query, { id, url }), { refreshInterval: 500, }) diff --git a/packages/web/lib/networking/queries/useGetDiscoverFeeds.tsx b/packages/web/lib/networking/queries/useGetDiscoverFeeds.tsx index 517de97e6..3423847b3 100644 --- a/packages/web/lib/networking/queries/useGetDiscoverFeeds.tsx +++ b/packages/web/lib/networking/queries/useGetDiscoverFeeds.tsx @@ -1,6 +1,6 @@ -import { gql } from "graphql-request" -import useSWR from "swr" -import { makeGqlFetcher } from "../networkHelpers" +import { gql } from 'graphql-request' +import useSWR from 'swr' +import { makeGqlFetcher } from '../networkHelpers' type DiscoverFeedsQueryResponse = { error: any @@ -15,9 +15,9 @@ export type DiscoverFeed = { visibleName: string title: string link: string - description?: string, - image? : string, - type: "rss" | "atom" + description?: string + image?: string + type: 'rss' | 'atom' } export function useGetDiscoverFeeds(): DiscoverFeedsQueryResponse { @@ -26,16 +26,16 @@ export function useGetDiscoverFeeds(): DiscoverFeedsQueryResponse { discoverFeeds { ... on DiscoverFeedSuccess { feeds { - visibleName, - id, - title, - link, - description, - image, + visibleName + id + title + link + description + image type } } - ... on DiscoverFeedError{ + ... on DiscoverFeedError { errorCodes } } @@ -44,12 +44,13 @@ export function useGetDiscoverFeeds(): DiscoverFeedsQueryResponse { const { data, error, mutate, isValidating } = useSWR( [query], - makeGqlFetcher() + makeGqlFetcher(query), + {} ) try { if (data) { - const result = data as { discoverFeeds: { feeds: DiscoverFeed[] }} + const result = data as { discoverFeeds: { feeds: DiscoverFeed[] } } const feeds = result.discoverFeeds.feeds as DiscoverFeed[] return { error, diff --git a/packages/web/lib/networking/queries/useGetHome.tsx b/packages/web/lib/networking/queries/useGetHome.tsx index 05f0bf51e..1c4d32839 100644 --- a/packages/web/lib/networking/queries/useGetHome.tsx +++ b/packages/web/lib/networking/queries/useGetHome.tsx @@ -134,7 +134,8 @@ export function useGetHomeItems(): HomeItemResponse { const { data, error, isValidating, mutate } = useSWR( [query, variables.first, variables.after], - makeGqlFetcher(variables) + makeGqlFetcher(query, variables), + {} ) if (error) { diff --git a/packages/web/lib/networking/queries/useGetIntegrationQuery.tsx b/packages/web/lib/networking/queries/useGetIntegrationQuery.tsx index e1a8f2e7e..07cfc0c7c 100644 --- a/packages/web/lib/networking/queries/useGetIntegrationQuery.tsx +++ b/packages/web/lib/networking/queries/useGetIntegrationQuery.tsx @@ -40,7 +40,11 @@ export function useGetIntegrationQuery(name: string): IntegrationQueryResponse { } ` - const { data, mutate, isValidating } = useSWR(query, makeGqlFetcher({ name })) + const { data, mutate, isValidating } = useSWR( + query, + makeGqlFetcher(query, { name }), + {} + ) if (!data) { return { isValidating, @@ -50,7 +54,7 @@ export function useGetIntegrationQuery(name: string): IntegrationQueryResponse { }, } } - + const result = data as IntegrationQueryResponseData const error = result.integration.errorCodes?.find(() => true) if (error) { diff --git a/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx b/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx index a8699993d..f7e0c6fd5 100644 --- a/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx +++ b/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx @@ -142,13 +142,11 @@ export const recommendationFragment = gql` } ` -export function useGetLibraryItemsQuery({ - limit, - sortDescending, - searchQuery, - cursor, - includeContent = false, -}: LibraryItemsQueryInput): LibraryItemsQueryResponse { +export function useGetLibraryItemsQuery( + folder: string, + { limit, searchQuery, cursor, includeContent = false }: LibraryItemsQueryInput +): LibraryItemsQueryResponse { + const fullQuery = (`in:${folder} use:folders ` + (searchQuery ?? '')).trim() const query = gql` query Search( $after: String @@ -236,28 +234,27 @@ export function useGetLibraryItemsQuery({ const variables = { after: cursor, first: limit, - query: searchQuery, + query: fullQuery, includeContent, } const { data, error, mutate, size, setSize, isValidating } = useSWRInfinite( (pageIndex, previousPageData) => { - const key = [query, limit, sortDescending, searchQuery, undefined] + const key = [query, variables.first, variables.query, undefined] const previousResult = previousPageData as LibraryItemsData - if (pageIndex === 0) { return key } return [ query, limit, - sortDescending, searchQuery, pageIndex === 0 ? undefined : previousResult.search.pageInfo.endCursor, ] }, - (_query, _l, _s, _sq, cursor) => { - return gqlFetcher(query, { ...variables, after: cursor }, true) + (args: any[]) => { + const pageIndex = args[4] as number + return gqlFetcher(query, { ...variables, after: pageIndex }, true) }, { revalidateFirstPage: false } ) diff --git a/packages/web/lib/networking/queries/useGetPublicArticleQuery.tsx b/packages/web/lib/networking/queries/useGetPublicArticleQuery.tsx deleted file mode 100644 index 81a0e60e8..000000000 --- a/packages/web/lib/networking/queries/useGetPublicArticleQuery.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import { gql } from 'graphql-request' -import useSWR from 'swr' -import { makePublicGqlFetcher, RequestContext, ssrFetcher } from '../networkHelpers' -import { Highlight } from '../fragments/highlightFragment' - -type PublicArticleQueryInput = { - username: string - slug: string - selectedHighlightId?: string -} - -export type PublicArticleQueryOutput = { - publicArticle?: PublicArticleAttributes - fetchError: unknown - isLoading: boolean - isValidating: boolean -} - -type PublicArticleData = { - sharedArticle: NestedPublicArticleData -} - -type NestedPublicArticleData = { - article: PublicArticleAttributes -} - -export type PublicArticleAttributes = { - id: string - title: string - slug: string - url: string - author?: string - image?: string - description?: string - hasContent?: boolean - highlights: Highlight[] -} - -export const PublicArticleGQLFragment = gql` - fragment PublicArticle on Article { - id - title - slug - url - author - image - description - savedByViewer - postedByViewer - hasContent - highlights { - id - shortId - quote - prefix - suffix - patch - annotation - sharedAt - user { - id - name - profile { - id - username - pictureUrl - } - } - } - } -` - -const query = gql` - query GetPublicArticle( - $username: String! - $slug: String! - $selectedHighlightId: String - ) { - sharedArticle( - username: $username - slug: $slug - selectedHighlightId: $selectedHighlightId - ) { - ... on SharedArticleSuccess { - article { - ...PublicArticle - } - } - - ... on SharedArticleError { - errorCodes - } - } - } - ${PublicArticleGQLFragment} -` - -export function useGetPublicArticleQuery({ - username, - slug, - selectedHighlightId, -}: PublicArticleQueryInput): PublicArticleQueryOutput { - const variables = { - username, - slug, - selectedHighlightId, - } - - const { data, error, isValidating } = useSWR( - // Only make request if username is defined - !!username ? [query, username, slug, selectedHighlightId] : null, - makePublicGqlFetcher(variables) - ) - const publicArticle = (data as PublicArticleData)?.sharedArticle?.article - - return { - publicArticle, - fetchError: error as unknown, - isLoading: !error && !publicArticle, - isValidating, - } -} - -export async function publicArticleQuery( - context: RequestContext, - input: PublicArticleQueryInput -): Promise { - const result = (await ssrFetcher(context, query, input, false)) as PublicArticleData - if (result.sharedArticle.article) { - return result.sharedArticle.article - } - - return Promise.reject() -} diff --git a/packages/web/lib/networking/queries/useGetSubscriptionsQuery.tsx b/packages/web/lib/networking/queries/useGetSubscriptionsQuery.tsx index 58ec302dd..d4afc71aa 100644 --- a/packages/web/lib/networking/queries/useGetSubscriptionsQuery.tsx +++ b/packages/web/lib/networking/queries/useGetSubscriptionsQuery.tsx @@ -93,7 +93,8 @@ export function useGetSubscriptionsQuery( } const { data, error, mutate, isValidating } = useSWR( [query, variables], - makeGqlFetcher(variables) + makeGqlFetcher(query, variables), + {} ) try { diff --git a/packages/web/lib/networking/queries/useGetWebhookQuery.tsx b/packages/web/lib/networking/queries/useGetWebhookQuery.tsx index e5e05a6d2..bdd29d4c6 100644 --- a/packages/web/lib/networking/queries/useGetWebhookQuery.tsx +++ b/packages/web/lib/networking/queries/useGetWebhookQuery.tsx @@ -40,8 +40,11 @@ export function useGetWebhookQuery(id: string): WebhookQueryResponse { } ` - const { data, mutate, isValidating } = useSWR(query, makeGqlFetcher({ id })) - console.log('webhook data', data) + const { data, mutate, isValidating } = useSWR( + query, + makeGqlFetcher(query, { id }), + {} + ) try { if (data) { diff --git a/packages/web/lib/networking/queries/useValidateUsernameQuery.tsx b/packages/web/lib/networking/queries/useValidateUsernameQuery.tsx index 467d737fc..12fda73f7 100644 --- a/packages/web/lib/networking/queries/useValidateUsernameQuery.tsx +++ b/packages/web/lib/networking/queries/useValidateUsernameQuery.tsx @@ -24,7 +24,8 @@ export function useValidateUsernameQuery({ // Don't fetch if username is empty const { data, error, isValidating } = useSWR( username ? [query, username] : null, - makePublicGqlFetcher({ username }) + makePublicGqlFetcher(query, { username }), + {} ) // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/web/lib/toastHelpers.tsx b/packages/web/lib/toastHelpers.tsx index 18c92a75c..5f0157cb2 100644 --- a/packages/web/lib/toastHelpers.tsx +++ b/packages/web/lib/toastHelpers.tsx @@ -1,5 +1,5 @@ import { toast, ToastOptions } from 'react-hot-toast' -import { CheckCircle, WarningCircle, X } from 'phosphor-react' +import { CheckCircle, WarningCircle, X } from '@phosphor-icons/react' import { Box, HStack } from '../components/elements/LayoutPrimitives' import { styled } from '@stitches/react' import { Button } from '../components/elements/Button' diff --git a/packages/web/package.json b/packages/web/package.json index 2afe9b389..bb1d23f49 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -18,6 +18,7 @@ "dependencies": { "@floating-ui/react": "^0.26.9", "@google-recaptcha/react": "^1.0.3", + "@phosphor-icons/react": "^2.1.5", "@radix-ui/react-avatar": "^0.1.1", "@radix-ui/react-checkbox": "^0.1.5", "@radix-ui/react-dialog": "1.0.5", @@ -31,6 +32,7 @@ "@radix-ui/react-switch": "^1.0.1", "@sentry/nextjs": "^7.42.0", "@stitches/react": "^1.2.5", + "allotment": "^1.20.2", "antd": "4.24.3", "axios": "^1.2.0", "cookie": "^0.5.0", @@ -42,32 +44,29 @@ "kbar": "^0.1.0-beta.35", "loadjs": "^4.3.0-rc1", "markdown-it": "^13.0.1", - "match-sorter": "^6.3.1", "nanoid": "^3.1.29", "next": "^13.5.6", "node-html-markdown": "^1.3.0", "papaparse": "^5.4.1", - "phosphor-react": "^1.4.0", "posthog-js": "^1.78.2", "pspdfkit": "^2023.4.6", "re-resizable": "^6.9.11", "react": "^18.2.0", + "react-arborist": "^3.4.0", "react-color": "^2.19.3", - "react-colorful": "^5.5.1", "react-dom": "^18.2.0", "react-dropzone": "^14.2.3", "react-hot-toast": "^2.1.1", "react-input-autosize": "^3.0.0", "react-markdown": "^8.0.6", "react-markdown-editor-lite": "^1.3.4", - "react-masonry-css": "^1.0.16", "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", "remark-gfm": "^3.0.1", "sharp": "^0.32.6", - "swr": "^1.0.1", + "swr": "^2.2.5", "uuid": "^8.3.2", "yet-another-react-lightbox": "^3.12.0" }, @@ -109,4 +108,4 @@ "volta": { "extends": "../../package.json" } -} +} \ No newline at end of file diff --git a/packages/web/pages/_app.tsx b/packages/web/pages/_app.tsx index 17c2f1364..c9bbbb95a 100644 --- a/packages/web/pages/_app.tsx +++ b/packages/web/pages/_app.tsx @@ -23,6 +23,7 @@ import { updateTheme } from '../lib/themeUpdater' import { ThemeId } from '../components/tokens/stitches.config' import { posthog } from 'posthog-js' import { GoogleReCaptchaProvider } from '@google-recaptcha/react' +import { SWRConfig } from 'swr' TopBarProgress.config({ barColors: { diff --git a/packages/web/pages/highlights.tsx b/packages/web/pages/highlights-old.tsx similarity index 100% rename from packages/web/pages/highlights.tsx rename to packages/web/pages/highlights-old.tsx diff --git a/packages/web/pages/home-old.tsx b/packages/web/pages/home-old.tsx new file mode 100644 index 000000000..1935d846b --- /dev/null +++ b/packages/web/pages/home-old.tsx @@ -0,0 +1,32 @@ +import { PrimaryLayout } from '../components/templates/PrimaryLayout' +import { HomeFeedContainer } from '../components/templates/homeFeed/HomeFeedContainer' +import { VStack } from './../components/elements/LayoutPrimitives' + +export default function Home(): JSX.Element { + return +} + +function LoadedContent(): JSX.Element { + return ( + + + + + + ) +} diff --git a/packages/web/pages/justread/debug.tsx b/packages/web/pages/home/debug.tsx similarity index 100% rename from packages/web/pages/justread/debug.tsx rename to packages/web/pages/home/debug.tsx diff --git a/packages/web/pages/l/[section].tsx b/packages/web/pages/l/[section].tsx new file mode 100644 index 000000000..fc943e05b --- /dev/null +++ b/packages/web/pages/l/[section].tsx @@ -0,0 +1,55 @@ +import { useRouter } from 'next/router' +import { useApplyLocalTheme } from '../../lib/hooks/useApplyLocalTheme' +import { + NavigationLayout, + NavigationSection, +} from '../../components/templates/NavigationLayout' +import { HomeContainer } from '../../components/nav-containers/home' +import { LibraryContainer } from '../../components/templates/library/LibraryContainer' +import { useMemo } from 'react' +import { HighlightsContainer } from '../../components/nav-containers/highlights' + +export default function Home(): JSX.Element { + const router = useRouter() + useApplyLocalTheme() + + const section: NavigationSection | undefined = useMemo(() => { + if (!router.isReady) { + return undefined + } + const res = router.query.section + if (typeof res !== 'string') { + return undefined + } + return res as NavigationSection + }, [router]) + + const sectionView = (name: string | string[] | undefined) => { + if (typeof name !== 'string') { + return <> + } + switch (name) { + case 'home': + return + case 'highlights': + return + case 'library': + return + case 'subscriptions': + return + case 'archive': + return + case 'trash': + return + + default: + return <> + } + } + + return ( + + {sectionView(section)} + + ) +} diff --git a/packages/web/pages/settings/account.tsx b/packages/web/pages/settings/account.tsx index 3cf0f6a84..e9ef3838b 100644 --- a/packages/web/pages/settings/account.tsx +++ b/packages/web/pages/settings/account.tsx @@ -99,7 +99,7 @@ export default function Account(): JSX.Element { isUsernameValidationLoading, ]) - const { itemsPages, isValidating } = useGetLibraryItemsQuery({ + const { itemsPages, isValidating } = useGetLibraryItemsQuery('', { limit: 0, searchQuery: 'in:all', sortDescending: false, diff --git a/packages/web/pages/settings/discover-feeds/index.tsx b/packages/web/pages/settings/discover-feeds/index.tsx index e75bd76ac..c33f344df 100644 --- a/packages/web/pages/settings/discover-feeds/index.tsx +++ b/packages/web/pages/settings/discover-feeds/index.tsx @@ -1,5 +1,5 @@ import { useRouter } from 'next/router' -import { FloppyDisk, Pencil, XCircle } from 'phosphor-react' +import { FloppyDisk, Pencil, XCircle } from '@phosphor-icons/react' import { useMemo, useState } from 'react' import { FormInput } from '../../../components/elements/FormElements' import { HStack, SpanBox } from '../../../components/elements/LayoutPrimitives' diff --git a/packages/web/pages/settings/emails/index.tsx b/packages/web/pages/settings/emails/index.tsx index 28b98c05e..f33d6d7ee 100644 --- a/packages/web/pages/settings/emails/index.tsx +++ b/packages/web/pages/settings/emails/index.tsx @@ -2,7 +2,7 @@ import { Button } from '../../../components/elements/Button' import { useGetNewsletterEmailsQuery } from '../../../lib/networking/queries/useGetNewsletterEmailsQuery' import { createNewsletterEmailMutation } from '../../../lib/networking/mutations/createNewsletterEmailMutation' import { deleteNewsletterEmailMutation } from '../../../lib/networking/mutations/deleteNewsletterEmailMutation' -import { Copy } from 'phosphor-react' +import { Copy } from '@phosphor-icons/react' import { theme, styled } from '../../../components/tokens/stitches.config' import { Box, @@ -80,8 +80,9 @@ function CopyTextButton(props: CopyTextButtonProps): JSX.Element { export default function EmailsPage(): JSX.Element { const { emailAddresses, revalidate, isValidating } = useGetNewsletterEmailsQuery() - const [confirmDeleteEmailId, setConfirmDeleteEmailId] = - useState(undefined) + const [confirmDeleteEmailId, setConfirmDeleteEmailId] = useState< + undefined | string + >(undefined) applyStoredTheme() diff --git a/packages/web/pages/settings/features/beta.tsx b/packages/web/pages/settings/features/beta.tsx index c88fec98a..738d85f75 100644 --- a/packages/web/pages/settings/features/beta.tsx +++ b/packages/web/pages/settings/features/beta.tsx @@ -1,4 +1,4 @@ -import { Spinner } from 'phosphor-react' +import { Spinner } from '@phosphor-icons/react' import { useCallback, useMemo, useState } from 'react' import { Toaster } from 'react-hot-toast' import { Button } from '../../../components/elements/Button' diff --git a/packages/web/pages/settings/feeds/index.tsx b/packages/web/pages/settings/feeds/index.tsx index 6084ac145..0876d7631 100644 --- a/packages/web/pages/settings/feeds/index.tsx +++ b/packages/web/pages/settings/feeds/index.tsx @@ -1,5 +1,5 @@ import { useRouter } from 'next/router' -import { FloppyDisk, Pencil, XCircle } from 'phosphor-react' +import { FloppyDisk, Pencil, XCircle } from '@phosphor-icons/react' import { useMemo, useState } from 'react' import { FormInput } from '../../../components/elements/FormElements' import { diff --git a/packages/web/pages/settings/integrations.tsx b/packages/web/pages/settings/integrations.tsx index 77896cf47..071cd002f 100644 --- a/packages/web/pages/settings/integrations.tsx +++ b/packages/web/pages/settings/integrations.tsx @@ -1,19 +1,19 @@ import { styled } from '@stitches/react' import Image from 'next/image' import { useRouter } from 'next/router' -import { DownloadSimple, Link, Spinner } from 'phosphor-react' +import { DownloadSimple, Link, Spinner } from '@phosphor-icons/react' import { useCallback, useEffect, useState } from 'react' import { Toaster } from 'react-hot-toast' import { Button } from '../../components/elements/Button' import { Dropdown, - DropdownOption + DropdownOption, } from '../../components/elements/DropdownElements' import { Box, HStack, SpanBox, - VStack + VStack, } from '../../components/elements/LayoutPrimitives' import { SettingsLayout } from '../../components/templates/SettingsLayout' import { fetchEndpoint } from '../../lib/appConfig' diff --git a/packages/web/pages/settings/labels.tsx b/packages/web/pages/settings/labels.tsx index df780b045..4124a4b70 100644 --- a/packages/web/pages/settings/labels.tsx +++ b/packages/web/pages/settings/labels.tsx @@ -17,7 +17,12 @@ import { applyStoredTheme, isDarkTheme } from '../../lib/themeUpdater' import { showErrorToast, showSuccessToast } from '../../lib/toastHelpers' import { Label, LabelColor } from '../../lib/networking/fragments/labelFragment' import { StyledText } from '../../components/elements/StyledText' -import { ArrowClockwise, DotsThree, PencilSimple, Trash } from 'phosphor-react' +import { + ArrowClockwise, + DotsThree, + PencilSimple, + Trash, +} from '@phosphor-icons/react' import { GenericTableCardProps } from '../../utils/settings-page/labels/types' import { labelColorObjects } from '../../utils/settings-page/labels/labelColorObjects' import { LabelColorDropdown } from '../../components/elements/LabelColorDropdown' diff --git a/packages/web/pages/settings/pinned-searches.tsx b/packages/web/pages/settings/pinned-searches.tsx index 784062d14..2ee3df74a 100644 --- a/packages/web/pages/settings/pinned-searches.tsx +++ b/packages/web/pages/settings/pinned-searches.tsx @@ -18,7 +18,7 @@ import { applyStoredTheme } from '../../lib/themeUpdater' import { useGetLabelsQuery } from '../../lib/networking/queries/useGetLabelsQuery' import { useGetSavedSearchQuery } from '../../lib/networking/queries/useGetSavedSearchQuery' import { Label } from '../../lib/networking/fragments/labelFragment' -import { CheckSquare, Circle, Square } from 'phosphor-react' +import { CheckSquare, Circle, Square } from '@phosphor-icons/react' import { SavedSearch } from '../../lib/networking/fragments/savedSearchFragment' import { usePersistedState } from '../../lib/hooks/usePersistedState' import { escapeQuotes } from '../../utils/helper' diff --git a/packages/web/pages/settings/saved-searches.tsx b/packages/web/pages/settings/saved-searches.tsx index 7c4de1e26..8c73fd1be 100644 --- a/packages/web/pages/settings/saved-searches.tsx +++ b/packages/web/pages/settings/saved-searches.tsx @@ -25,7 +25,7 @@ import { Trash, Plus, ArrowsDownUp, -} from 'phosphor-react' +} from '@phosphor-icons/react' import { Dropdown, DropdownOption, @@ -156,14 +156,14 @@ export default function SavedSearchesPage(): JSX.Element { const [editingId, setEditingId] = useState(null) const [isCreateMode, setIsCreateMode] = useState(false) const [windowWidth, setWindowWidth] = useState(0) - const [confirmRemoveSavedSearchId, setConfirmRemoveSavedSearchId] = - useState(null) + const [confirmRemoveSavedSearchId, setConfirmRemoveSavedSearchId] = useState< + string | null + >(null) const [draggedElementId, setDraggedElementId] = useState(null) - const [draggedElementPosition, setDraggedElementPosition] = - useState<{ - x: number - y: number - } | null>(null) + const [draggedElementPosition, setDraggedElementPosition] = useState<{ + x: number + y: number + } | null>(null) const [sortedSavedSearch, setSortedSavedSearch] = useState([]) // Some theming stuff here. @@ -579,15 +579,14 @@ function GenericTableCard( editingId === savedSearch?.id || (isCreateMode && !savedSearch) const iconColor = isDarkTheme() ? '#D8D7D5' : '#5F5E58' const DEFAULT_STYLE = { position: null } - const [style, setStyle] = - useState< - Partial<{ - position: string | null - top: string - left: string - maxWidth: string - }> - >(DEFAULT_STYLE) + const [style, setStyle] = useState< + Partial<{ + position: string | null + top: string + left: string + maxWidth: string + }> + >(DEFAULT_STYLE) const handleEdit = () => { editingId && updateSavedSearch(editingId) setEditingId(null) diff --git a/packages/web/pages/settings/shortcuts.tsx b/packages/web/pages/settings/shortcuts.tsx index f9fddfec2..94d2838bf 100644 --- a/packages/web/pages/settings/shortcuts.tsx +++ b/packages/web/pages/settings/shortcuts.tsx @@ -30,11 +30,12 @@ import { DragIcon } from '../../components/elements/icons/DragIcon' import { CoverImage } from '../../components/elements/CoverImage' import { Label } from '../../lib/networking/fragments/labelFragment' import { usePersistedState } from '../../lib/hooks/usePersistedState' -import { CheckSquare, Square } from 'phosphor-react' +import { CheckSquare, Square } from '@phosphor-icons/react' import { Button } from '../../components/elements/Button' import { styled } from '@stitches/react' import { SavedSearch } from '../../lib/networking/fragments/savedSearchFragment' import { escapeQuotes } from '../../utils/helper' +import { Shortcut } from '../../components/templates/navMenu/NavigationMenu' type ListAction = 'RESET' | 'ADD_ITEM' | 'REMOVE_ITEM' const SHORTCUTS_KEY = 'library-shortcuts' @@ -318,6 +319,7 @@ const AvailableItems = (props: ListProps): JSX.Element => { id: search.id, name: search.name, type: 'label', + section: 'library', filter: search.filter, } props.dispatchList({ @@ -364,6 +366,7 @@ const AvailableItems = (props: ListProps): JSX.Element => { id: label.id, type: 'label', label: label, + section: 'library', name: label.name, filter: `label:\"${escapeQuotes(label.name)}\"`, } @@ -408,6 +411,7 @@ const AvailableItems = (props: ListProps): JSX.Element => { onClick={(event) => { const item: Shortcut = { id: subscription.id, + section: 'subscriptions', name: subscription.name, icon: subscription.icon, type: @@ -465,17 +469,6 @@ const AvailableItemButton = (props: AvailableItemButtonProps): JSX.Element => { ) } -export type Shortcut = { - type: 'search' | 'label' | 'newsletter' | 'feed' - - id: string - name: string - filter: string - - icon?: string - label?: Label -} - const SelectedItems = (props: ListProps): JSX.Element => { return ( + + + ) +} diff --git a/packages/web/pages/tools/bulk.tsx b/packages/web/pages/tools/bulk.tsx index 1229aa4bd..24efa499f 100644 --- a/packages/web/pages/tools/bulk.tsx +++ b/packages/web/pages/tools/bulk.tsx @@ -33,7 +33,7 @@ export default function BulkPerformer(): JSX.Element { const [errorMessage, setErrorMessage] = useState() const [runningState, setRunningState] = useState('none') - const { itemsPages, isValidating } = useGetLibraryItemsQuery({ + const { itemsPages, isValidating } = useGetLibraryItemsQuery('', { searchQuery: query, limit: 1, sortDescending: false, diff --git a/packages/web/pages/tools/import/matter-archive.tsx b/packages/web/pages/tools/import/matter-archive.tsx index fdd687cae..a1cec1770 100644 --- a/packages/web/pages/tools/import/matter-archive.tsx +++ b/packages/web/pages/tools/import/matter-archive.tsx @@ -15,7 +15,7 @@ import Dropzone from 'react-dropzone' import { SyncLoader } from 'react-spinners' import { theme } from '../../../components/tokens/stitches.config' -import { Tray } from 'phosphor-react' +import { Tray } from '@phosphor-icons/react' type UploadState = 'none' | 'uploading' | 'completed' diff --git a/packages/web/styles/globals.css b/packages/web/styles/globals.css index 49dd25852..8f5c4c620 100644 --- a/packages/web/styles/globals.css +++ b/packages/web/styles/globals.css @@ -6,7 +6,6 @@ html, body { padding: 0; margin: 0; - overflow: auto; font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; -webkit-font-smoothing: antialiased; diff --git a/yarn.lock b/yarn.lock index 76355e48b..aad75218f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3793,6 +3793,11 @@ dependencies: lodash "^4.17.21" +"@juggle/resize-observer@^3.3.1": + version "3.4.0" + resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz#08d6c5e20cf7e4cc02fd181c4b0c225cd31dbb60" + integrity sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA== + "@langchain/anthropic@^0.1.16": version "0.1.16" resolved "https://registry.yarnpkg.com/@langchain/anthropic/-/anthropic-0.1.16.tgz#c2a9d3dd4e02df7118dd97cf2503c9bd1a4de5ad" @@ -4873,6 +4878,11 @@ node-addon-api "^3.2.1" node-gyp-build "^4.3.0" +"@phosphor-icons/react@^2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@phosphor-icons/react/-/react-2.1.5.tgz#762c368778a4040d52c5532b8af1692b66e16783" + integrity sha512-B7vRm/w+P/+eavWZP5CB5Ul0ffK4Y7fpd/auWKuGvm+8pVgAJzbOK8O0s+DqzR+TwWkh5pHtJTuoAtaSvgCPzg== + "@pkgjs/parseargs@^0.11.0": version "0.11.0" resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" @@ -5595,6 +5605,21 @@ resolved "https://registry.yarnpkg.com/@reach/observe-rect/-/observe-rect-1.2.0.tgz#d7a6013b8aafcc64c778a0ccb83355a11204d3b2" integrity sha512-Ba7HmkFgfQxZqqaeIWWkNK0rEhpxVQHIoVyW1YDSkGsGIXzcaW4deC8B0pZrNSSyLTdIk7y+5olKt5+g0GmFIQ== +"@react-dnd/asap@^4.0.0": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@react-dnd/asap/-/asap-4.0.1.tgz#5291850a6b58ce6f2da25352a64f1b0674871aab" + integrity sha512-kLy0PJDDwvwwTXxqTFNAAllPHD73AycE9ypWeln/IguoGBEbvFcPDbCV03G52bEcC5E+YgupBE0VzHGdC8SIXg== + +"@react-dnd/invariant@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@react-dnd/invariant/-/invariant-2.0.0.tgz#09d2e81cd39e0e767d7da62df9325860f24e517e" + integrity sha512-xL4RCQBCBDJ+GRwKTFhGUW8GXa4yoDfJrPbLblc3U09ciS+9ZJXJ3Qrcs/x2IODOdIE5kQxvMmE2UKyqUictUw== + +"@react-dnd/shallowequal@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz#a3031eb54129f2c66b2753f8404266ec7bf67f0a" + integrity sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg== + "@redis/bloom@1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@redis/bloom/-/bloom-1.2.0.tgz#d3fd6d3c0af3ef92f26767b56414a370c7b63b71" @@ -9649,6 +9674,18 @@ ajv@^8.11.0: require-from-string "^2.0.2" uri-js "^4.2.2" +allotment@^1.20.2: + version "1.20.2" + resolved "https://registry.yarnpkg.com/allotment/-/allotment-1.20.2.tgz#5ea3a630b3265479debb69156658244711f83843" + integrity sha512-TaCuHfYNcsJS9EPk04M7TlG5Rl3vbAdHeAyrTE9D5vbpzV+wxnRoUrulDbfnzaQcPIZKpHJNixDOoZNuzliKEA== + dependencies: + classnames "^2.3.0" + eventemitter3 "^5.0.0" + lodash.clamp "^4.0.0" + lodash.debounce "^4.0.0" + lodash.isequal "^4.5.0" + use-resize-observer "^9.0.0" + ansi-align@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.0.tgz#b536b371cf687caaef236c18d3e21fe3797467cb" @@ -12147,6 +12184,11 @@ classnames@^2.2.6: resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e" integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA== +classnames@^2.3.0: + version "2.5.1" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b" + integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow== + cld@^2.9.1: version "2.9.1" resolved "https://registry.yarnpkg.com/cld/-/cld-2.9.1.tgz#0c6685672d9f4612dfeb75eabfdd17bf282a87a6" @@ -12283,7 +12325,7 @@ cli-width@^3.0.0: resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== -client-only@0.0.1: +client-only@0.0.1, client-only@^0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1" integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== @@ -13980,6 +14022,15 @@ discontinuous-range@1.0.0: resolved "https://registry.yarnpkg.com/discontinuous-range/-/discontinuous-range-1.0.0.tgz#e38331f0844bba49b9a9cb71c771585aab1bc65a" integrity sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ== +dnd-core@14.0.1: + version "14.0.1" + resolved "https://registry.yarnpkg.com/dnd-core/-/dnd-core-14.0.1.tgz#76d000e41c494983210fb20a48b835f81a203c2e" + integrity sha512-+PVS2VPTgKFPYWo3vAFEA8WPbTf7/xo43TifH9G8S1KqnrQu0o77A3unrF5yOugy4mIz7K5wAVFHUcha7wsz6A== + dependencies: + "@react-dnd/asap" "^4.0.0" + "@react-dnd/invariant" "^2.0.0" + redux "^4.1.1" + dns-equal@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d" @@ -15330,6 +15381,11 @@ eventemitter3@^4.0.0, eventemitter3@^4.0.4: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== +eventemitter3@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4" + integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== + eventid@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/eventid/-/eventid-2.0.1.tgz#574e860149457a79a2efe788c459f0c3062d02ec" @@ -21076,12 +21132,17 @@ lodash.capitalize@^4.2.1: resolved "https://registry.yarnpkg.com/lodash.capitalize/-/lodash.capitalize-4.2.1.tgz#f826c9b4e2a8511d84e3aca29db05e1a4f3b72a9" integrity sha512-kZzYOKspf8XVX5AvmQF94gQW0lejFVgb80G85bU4ZWzoJ6C03PQg3coYAUpSTpQWelrZELd3XWgHzw4Ck5kaIw== +lodash.clamp@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/lodash.clamp/-/lodash.clamp-4.0.3.tgz#5c24bedeeeef0753560dc2b4cb4671f90a6ddfaa" + integrity sha512-HvzRFWjtcguTW7yd8NJBshuNaCa8aqNFtnswdT7f/cMd/1YKy5Zzoq4W/Oxvnx9l7aeY258uSdDfM793+eLsVg== + lodash.clonedeep@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= -lodash.debounce@^4.0.8: +lodash.debounce@^4.0.0, lodash.debounce@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= @@ -21126,6 +21187,11 @@ lodash.isboolean@^3.0.3: resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" integrity sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY= +lodash.isequal@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" + integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ== + lodash.isfunction@^3.0.9: version "3.0.9" resolved "https://registry.yarnpkg.com/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz#06de25df4db327ac931981d1bdb067e5af68d051" @@ -21644,14 +21710,6 @@ marks-pane@^1.0.9: resolved "https://registry.yarnpkg.com/marks-pane/-/marks-pane-1.0.9.tgz#c0b5ab813384d8cd81faaeb3bbf3397dc809c1b3" integrity sha512-Ahs4oeG90tbdPWwAJkAAoHg2lRR8lAs9mZXETNPO9hYg3AkjUJBKi1NQ4aaIQZVGrig7c/3NUV1jANl8rFTeMg== -match-sorter@^6.3.1: - version "6.3.1" - resolved "https://registry.yarnpkg.com/match-sorter/-/match-sorter-6.3.1.tgz#98cc37fda756093424ddf3cbc62bfe9c75b92bda" - integrity sha512-mxybbo3pPNuA+ZuCUhm5bwNkXrJTbsk5VWbR5wiwz/GC6LIiegBGn2w3O08UG/jdbYLinw51fSQ5xNU1U3MgBw== - dependencies: - "@babel/runtime" "^7.12.5" - remove-accents "0.4.2" - matcher@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/matcher/-/matcher-5.0.0.tgz#cd82f1c7ae7ee472a9eeaf8ec7cac45e0fe0da62" @@ -21900,6 +21958,11 @@ memfs@^3.1.2, memfs@^3.2.2, memfs@^3.4.1: dependencies: fs-monkey "1.0.3" +"memoize-one@>=3.1.1 <6": + version "5.2.1" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" + integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== + memoizerific@^1.11.3: version "1.11.3" resolved "https://registry.yarnpkg.com/memoizerific/-/memoizerific-1.11.3.tgz#7c87a4646444c32d75438570905f2dbd1b1a805a" @@ -25162,11 +25225,6 @@ pgvector@^0.1.5: resolved "https://registry.yarnpkg.com/pgvector/-/pgvector-0.1.8.tgz#50c44c7a26b1cb17cf0a001b0772fc0eb5c38762" integrity sha512-mD6aw+XYJrsuLl3Y8s8gHDDfOZQ9ERtfQPdhvjOrC7eOTM7b6sNkxeZxBhHwUdXMfHmyGWIbwU0QbmSnn7pPmg== -phosphor-react@^1.4.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/phosphor-react/-/phosphor-react-1.4.1.tgz#97b0e034d9937db9b97fe53b186e9646464fd4e7" - integrity sha512-gO5j7U0xZrdglTAYDYPACU4xDOFBTJmptrrB/GeR+tHhCZF3nUMyGmV/0hnloKjuTrOmpSFlbfOY78H39rgjUQ== - picocolors@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-0.2.1.tgz#570670f793646851d1ba135996962abad587859f" @@ -26635,6 +26693,17 @@ re-resizable@^6.9.11: resolved "https://registry.yarnpkg.com/re-resizable/-/re-resizable-6.9.11.tgz#f356e27877f12d926d076ab9ad9ff0b95912b475" integrity sha512-a3hiLWck/NkmyLvGWUuvkAmN1VhwAz4yOhS6FdMTaxCUVN9joIWkT11wsO68coG/iEYuwn+p/7qAmfQzRhiPLQ== +react-arborist@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/react-arborist/-/react-arborist-3.4.0.tgz#8ef3de2c81d3b8cea0f4f4575c1971bd80c556c5" + integrity sha512-QI46oRGXJr0oaQfqqVobIiIoqPp5Y5gM69D2A2P7uHVif+X75XWnScR5drC7YDKgJ4CXVaDeFwnYKOWRRfncMg== + dependencies: + react-dnd "^14.0.3" + react-dnd-html5-backend "^14.0.3" + react-window "^1.8.10" + redux "^5.0.0" + use-sync-external-store "^1.2.0" + react-color@^2.19.3: version "2.19.3" resolved "https://registry.yarnpkg.com/react-color/-/react-color-2.19.3.tgz#ec6c6b4568312a3c6a18420ab0472e146aa5683d" @@ -26653,10 +26722,23 @@ react-colorful@^5.1.2: resolved "https://registry.yarnpkg.com/react-colorful/-/react-colorful-5.5.1.tgz#29d9c4e496f2ca784dd2bb5053a3a4340cfaf784" integrity sha512-M1TJH2X3RXEt12sWkpa6hLc/bbYS0H6F4rIqjQZ+RxNBstpY67d9TrFXtqdZwhpmBXcCwEi7stKqFue3ZRkiOg== -react-colorful@^5.5.1: - version "5.6.1" - resolved "https://registry.yarnpkg.com/react-colorful/-/react-colorful-5.6.1.tgz#7dc2aed2d7c72fac89694e834d179e32f3da563b" - integrity sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw== +react-dnd-html5-backend@^14.0.3: + version "14.1.0" + resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-14.1.0.tgz#b35a3a0c16dd3a2bfb5eb7ec62cf0c2cace8b62f" + integrity sha512-6ONeqEC3XKVf4eVmMTe0oPds+c5B9Foyj8p/ZKLb7kL2qh9COYxiBHv3szd6gztqi/efkmriywLUVlPotqoJyw== + dependencies: + dnd-core "14.0.1" + +react-dnd@^14.0.3: + version "14.0.5" + resolved "https://registry.yarnpkg.com/react-dnd/-/react-dnd-14.0.5.tgz#ecf264e220ae62e35634d9b941502f3fca0185ed" + integrity sha512-9i1jSgbyVw0ELlEVt/NkCUkxy1hmhJOkePoCH713u75vzHGyXhPDm28oLfc2NMSBjZRM1Y+wRjHXJT3sPrTy+A== + dependencies: + "@react-dnd/invariant" "^2.0.0" + "@react-dnd/shallowequal" "^2.0.0" + dnd-core "14.0.1" + fast-deep-equal "^3.1.3" + hoist-non-react-statics "^3.3.2" react-docgen-typescript@^2.0.0: version "2.2.2" @@ -26803,11 +26885,6 @@ react-markdown@^8.0.6: unist-util-visit "^4.0.0" vfile "^5.0.0" -react-masonry-css@^1.0.16: - version "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" @@ -26945,6 +27022,14 @@ react-virtual@^2.8.2: dependencies: "@reach/observe-rect" "^1.1.0" +react-window@^1.8.10: + version "1.8.10" + resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.10.tgz#9e6b08548316814b443f7002b1cf8fd3a1bdde03" + integrity sha512-Y0Cx+dnU6NLa5/EvoHukUD0BklJ8qITCtVEPY1C/nL8wwoZ0b5aEw8Ff1dOVHw7fCzMt55XfJDd8S8W8LCaUCg== + dependencies: + "@babel/runtime" "^7.0.0" + memoize-one ">=3.1.1 <6" + react@^18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" @@ -27218,6 +27303,18 @@ redis@^4.6.13: "@redis/search" "1.1.6" "@redis/time-series" "1.0.5" +redux@^4.1.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.1.tgz#c08f4306826c49b5e9dc901dee0452ea8fce6197" + integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w== + dependencies: + "@babel/runtime" "^7.9.2" + +redux@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/redux/-/redux-5.0.1.tgz#97fa26881ce5746500125585d5642c77b6e9447b" + integrity sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w== + reflect-metadata@^0.1.13: version "0.1.13" resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08" @@ -27516,11 +27613,6 @@ remedial@^1.0.7: resolved "https://registry.yarnpkg.com/remedial/-/remedial-1.0.8.tgz#a5e4fd52a0e4956adbaf62da63a5a46a78c578a0" integrity sha512-/62tYiOe6DzS5BqVsNpH/nkGlX45C/Sp6V+NtiN6JQNS1Viay7cWkazmRkrQrdFj2eshDe96SIQNIoMxqhzBOg== -remove-accents@0.4.2: - version "0.4.2" - resolved "https://registry.yarnpkg.com/remove-accents/-/remove-accents-0.4.2.tgz#0a43d3aaae1e80db919e07ae254b285d9e1c7bb5" - integrity sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA== - remove-trailing-separator@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" @@ -29543,10 +29635,13 @@ swap-case@^2.0.2: dependencies: tslib "^2.0.3" -swr@^1.0.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/swr/-/swr-1.3.0.tgz#c6531866a35b4db37b38b72c45a63171faf9f4e8" - integrity sha512-dkghQrOl2ORX9HYrMDtPa7LTVHJjCTeZoB1dqTbnnEDlSvN8JEKpYIYurDfvbQFUUS8Cg8PceFVZNkW0KNNYPw== +swr@^2.2.5: + version "2.2.5" + resolved "https://registry.yarnpkg.com/swr/-/swr-2.2.5.tgz#063eea0e9939f947227d5ca760cc53696f46446b" + integrity sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg== + dependencies: + client-only "^0.0.1" + use-sync-external-store "^1.2.0" "sylvester@>= 0.0.8": version "0.0.21" @@ -31088,6 +31183,13 @@ use-latest@^1.0.0: dependencies: use-isomorphic-layout-effect "^1.0.0" +use-resize-observer@^9.0.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/use-resize-observer/-/use-resize-observer-9.1.0.tgz#14735235cf3268569c1ea468f8a90c5789fc5c6c" + integrity sha512-R25VqO9Wb3asSD4eqtcxk8sJalvIOYBqS8MNZlpDSQ4l4xMQxC/J7Id9HoTqPq8FwULIn0PVW+OAqF2dyYbjow== + dependencies: + "@juggle/resize-observer" "^3.3.1" + use-sidecar@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.1.2.tgz#2f43126ba2d7d7e117aa5855e5d8f0276dfe73c2" @@ -31096,6 +31198,11 @@ use-sidecar@^1.1.2: detect-node-es "^1.1.0" tslib "^2.0.0" +use-sync-external-store@^1.2.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz#c3b6390f3a30eba13200d2302dcdf1e7b57b2ef9" + integrity sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw== + use@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"