diff --git a/packages/api/src/jobs/rss/refreshFeed.ts b/packages/api/src/jobs/rss/refreshFeed.ts index 8cef84a32..121331038 100644 --- a/packages/api/src/jobs/rss/refreshFeed.ts +++ b/packages/api/src/jobs/rss/refreshFeed.ts @@ -532,6 +532,7 @@ const processSubscription = async ( if (itemCount == 100) { logger.info(`Max limit reached for feed ${feedUrl}`) } + itemCount = itemCount + 1 continue } diff --git a/packages/web/components/elements/Button.tsx b/packages/web/components/elements/Button.tsx index eea66f75a..df08e5fbb 100644 --- a/packages/web/components/elements/Button.tsx +++ b/packages/web/components/elements/Button.tsx @@ -18,6 +18,21 @@ export const Button = styled('button', { border: '1px solid $grayBorderHover', }, }, + ctaBlue: { + borderRadius: '5px', + px: '20px', + py: '8px', + fontSize: '14px', + fontWeight: '500', + cursor: 'pointer', + border: '1px solid $yellow3', + bg: '$ctaBlue', + '&:hover': { + opacity: '0.6', + border: '0px solid $ctaBlue', + }, + }, + ctaDarkYellow: { border: '1px solid transparent', fontSize: '14px', @@ -215,6 +230,36 @@ export const Button = styled('button', { color: '$thLibraryMenuUnselected', cursor: 'pointer', }, + tab: { + px: '15px', + py: '6px', + border: 'none', + bg: 'transparent', + fontSize: '12px', + fontWeight: '500', + fontFamily: '$inter', + color: '$thBorderSubtle', + cursor: 'pointer', + borderRadius: '5px', + '&:hover': { + color: '$thTextContrast', + }, + }, + tabSelected: { + px: '15px', + py: '6px', + border: 'none', + bg: '$thBorderSubtle', + fontSize: '12px', + fontWeight: '500', + fontFamily: '$inter', + color: '$textContrast', + cursor: 'pointer', + borderRadius: '5px', + '&:hover': { + color: '$thTextContrast', + }, + }, squareIcon: { mx: '$1', display: 'flex', diff --git a/packages/web/components/elements/CloseButton.tsx b/packages/web/components/elements/CloseButton.tsx index aaf31a852..d4468c629 100644 --- a/packages/web/components/elements/CloseButton.tsx +++ b/packages/web/components/elements/CloseButton.tsx @@ -14,8 +14,8 @@ export function CloseButton(props: CloseButtonProps): JSX.Element { void } const CaretButton = (): JSX.Element => { @@ -41,35 +44,39 @@ const CaretButton = (): JSX.Element => { export const SplitButton = (props: SplitButtonProps): JSX.Element => { return ( - + - }> + {/* }> console.log()} title="Archive (e)" /> - + */} ) } diff --git a/packages/web/components/templates/PrimaryDropdown.tsx b/packages/web/components/templates/PrimaryDropdown.tsx index 49ec705e3..906c44c59 100644 --- a/packages/web/components/templates/PrimaryDropdown.tsx +++ b/packages/web/components/templates/PrimaryDropdown.tsx @@ -49,12 +49,12 @@ const TriggerButton = (props: TriggerButtonProps): JSX.Element => { return ( void @@ -24,9 +49,127 @@ type AddLinkModalProps = { } export function AddLinkModal(props: AddLinkModalProps): JSX.Element { - const [link, setLink] = useState('') + const [selectedTab, setSelectedTab] = useState('link') - const validateLink = useCallback( + return ( + + + { + // remove focus from modal + ;(document.activeElement as HTMLElement).blur() + }} + > + + + + {selectedTab == 'link' && } + {selectedTab == 'feed' && } + {selectedTab == 'opml' && } + {selectedTab == 'pdf' && } + {selectedTab == 'import' && } + + + + + ) +} + +const AddLinkTab = (props: AddLinkModalProps): JSX.Element => { + const [errorMessage, setErrorMessage] = useState( + undefined + ) + + const addLink = useCallback( + async (link: string) => { + await props.handleLinkSubmission(link, timeZone, locale) + props.onOpenChange(false) + }, + [errorMessage, setErrorMessage] + ) + + return ( + + ) +} + +const AddFeedTab = (props: AddLinkModalProps): JSX.Element => { + const [errorMessage, setErrorMessage] = useState( + undefined + ) + + const subscribe = useCallback( + async (feedUrl: string) => { + if (!feedUrl) { + setErrorMessage('Please enter a valid feed URL') + return + } + + let normailizedUrl: string + // normalize the url + try { + normailizedUrl = new URL(feedUrl.trim()).toString() + } catch (e) { + setErrorMessage('Please enter a valid feed URL') + return + } + + const result = await subscribeMutation({ + url: normailizedUrl, + subscriptionType: SubscriptionType.RSS, + }) + + if (result.subscribe.errorCodes) { + const errorMessage = formatMessage({ + id: `error.${result.subscribe.errorCodes[0]}`, + }) + setErrorMessage(`There was an error adding new feed: ${errorMessage}`) + return + } + + showSuccessToast('New feed has been added.') + }, + [errorMessage, setErrorMessage] + ) + + return ( + + ) +} + +type AddFromURLProps = { + placeholder: string + errorMessage: string | undefined + setErrorMessage: (message: string) => void + onSubmit: (url: string) => Promise +} + +const AddFromURL = (props: AddFromURLProps): JSX.Element => { + const [url, setURL] = useState('') + const [errorMessage, setErrorMessage] = useState(props.errorMessage) + + const validateURL = useCallback( (link: string) => { try { const url = new URL(link) @@ -38,66 +181,577 @@ export function AddLinkModal(props: AddLinkModalProps): JSX.Element { } return true }, - [link] + [url] ) return ( - - - { - // remove focus from modal - ;(document.activeElement as HTMLElement).blur() + +
{ + event.preventDefault() + + if (!validateURL(url)) { + setErrorMessage('Invalid URL') + return + } + + props.onSubmit(url) }} > - - - - { - event.preventDefault() - - let submitLink = link - if (!validateLink(link)) { - // If validation fails, attempting adding - // `https` to give the link a protocol. - const newLink = `https://${link}` - if (!validateLink(newLink)) { - showErrorToast('Invalid link', { position: 'bottom-right' }) - return - } - setLink(newLink) - submitLink = newLink - } - await props.handleLinkSubmission(submitLink, timeZone, locale) - props.onOpenChange(false) - }} - > - setLink(event.target.value)} - css={{ - borderRadius: '8px', - border: '1px solid $textNonessential', - width: '100%', - height: '38px', - p: '6px', - mb: '13px', - fontSize: '14px', - }} - /> - - - -
-
-
+ setURL(event.target.value)} + css={{ + borderRadius: '4px', + width: '100%', + height: '38px', + p: '6px', + mb: '13px', + fontSize: '14px', + color: '$thTextContrast', + bg: '$thLibrarySearchbox', + }} + /> + + + + ) +} + +const UploadOPMLTab = (props: AddLinkModalProps): JSX.Element => { + return ( + + + + ) +} + +const UploadPDFTab = (props: AddLinkModalProps): JSX.Element => { + return ( + + + + * PDFs have a maximum size of 8MB + + + ) +} + +const UploadImportTab = (props: AddLinkModalProps): JSX.Element => { + return ( + + + + * Imports must be in a supported format{' '} + + read more + + . + + + ) +} + +const DragnDropContainer = styled('div', { + width: '100%', + height: '100%', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + zIndex: '1', + alignSelf: 'center', + left: 0, + flexDirection: 'column', +}) + +const DragnDropStyle = styled('div', { + border: '1px solid $grayBorder', + borderRadius: '5px', + width: '100%', + height: '100%', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + alignSelf: 'center', + color: '$thTextSubtle2', + padding: '10px', +}) + +const DragnDropIndicator = styled('div', { + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + alignSelf: 'center', + width: '100%', + height: '100%', + borderRadius: '5px', +}) + +const ProgressIndicator = styled(Progress.Indicator, { + backgroundColor: '$omnivoreCtaYellow', + width: '100%', + height: '100%', +}) + +const ProgressRoot = styled(Progress.Root, { + position: 'relative', + overflow: 'hidden', + background: '$omnivoreGray', + borderRadius: '99999px', + width: '100%', + height: '5px', + transform: 'translateZ(0)', +}) + +type UploadingFile = { + id: string + file: any + name: string + progress: number + status: 'inprogress' | 'success' | 'error' + openUrl: string | undefined + contentType: string + message?: string +} + +type UploadInfo = { + uploadSignedUrl?: string + requestId?: string + message?: string +} + +type UploadPadProps = { + description: string + accept: Accept +} + +const UploadPad = (props: UploadPadProps): JSX.Element => { + const [uploadFiles, setUploadFiles] = useState([]) + const [inDragOperation, setInDragOperation] = useState(false) + const dropzoneRef = useRef(null) + + const openDialog = useCallback( + (event: React.MouseEvent) => { + if (dropzoneRef.current) { + dropzoneRef.current.open() + } + event?.preventDefault() + }, + [dropzoneRef] + ) + + const uploadSignedUrlForFile = async ( + file: UploadingFile + ): Promise => { + let { contentType } = file + if ( + contentType == 'application/vnd.ms-excel' && + file.name.endsWith('.csv') + ) { + contentType = 'text/csv' + } + switch (contentType) { + case 'text/csv': { + let urlCount = 0 + try { + const csvData = await validateCsvFile(file.file) + urlCount = csvData.data.length + if (urlCount > 5000) { + return { + message: + 'Due to an increase in traffic we are limiting CSV imports to 5000 items.', + } + } + if (csvData.inValidData.length > 0) { + return { + message: csvData.inValidData[0].message, + } + } + if (urlCount === 0) { + return { + message: 'No URLs found in CSV file.', + } + } + } catch (error) { + return { + message: 'Invalid CSV file.', + } + } + + try { + const result = await uploadImportFileRequestMutation( + UploadImportFileType.URL_LIST, + contentType + ) + return { + uploadSignedUrl: result?.uploadSignedUrl, + message: `Importing ${urlCount} URLs`, + } + } catch (error) { + console.log('caught error', error) + if (error == 'UPLOAD_DAILY_LIMIT_EXCEEDED') { + return { + message: 'You have exceeded your maximum daily upload limit.', + } + } + } + } + case 'application/zip': { + const result = await uploadImportFileRequestMutation( + UploadImportFileType.MATTER, + contentType + ) + return { + uploadSignedUrl: result?.uploadSignedUrl, + } + } + case 'application/pdf': + case 'application/epub+zip': { + const request = await uploadFileRequestMutation({ + // This will tell the backend not to save the URL + // and give it the local filename as the title. + url: `file://local/${file.id}/${file.file.path}`, + contentType: contentType, + createPageEntry: true, + }) + return { + uploadSignedUrl: request?.uploadSignedUrl, + requestId: request?.createdPageId, + } + } + } + return { + message: `Invalid content type: ${contentType}`, + } + } + + const handleAcceptedFiles = useCallback( + (acceptedFiles: any, event: DropEvent) => { + setInDragOperation(false) + + const addedFiles = acceptedFiles.map( + (file: { name: any; type: string }) => { + return { + id: uuidv4(), + file: file, + name: file.name, + progress: 0, + status: 'inprogress', + contentType: file.type, + } + } + ) + + const allFiles = [...uploadFiles, ...addedFiles] + + setUploadFiles(allFiles) + ;(async () => { + for (const file of addedFiles) { + try { + const uploadInfo = await uploadSignedUrlForFile(file) + if (!uploadInfo.uploadSignedUrl) { + const message = uploadInfo.message || 'No upload URL available' + showErrorToast(message, { duration: 10000 }) + file.status = 'error' + setUploadFiles([...allFiles]) + return + } + + const uploadResult = await axios.request({ + method: 'PUT', + url: uploadInfo.uploadSignedUrl, + data: file.file, + withCredentials: false, + headers: { + 'Content-Type': file.file.type, + }, + onUploadProgress: (p) => { + if (!p.total) { + console.warn('No total available for upload progress') + return + } + const progress = (p.loaded / p.total) * 100 + file.progress = progress + + setUploadFiles([...allFiles]) + }, + }) + + file.progress = 100 + file.status = 'success' + file.openUrl = uploadInfo.requestId + ? `/article/sr/${uploadInfo.requestId}` + : undefined + file.message = uploadInfo.message + + setUploadFiles([...allFiles]) + } catch (error) { + file.status = 'error' + setUploadFiles([...allFiles]) + } + } + })() + }, + [uploadFiles] + ) + + return ( + + { + setInDragOperation(true) + }} + onDragLeave={() => { + setInDragOperation(false) + }} + onDropAccepted={handleAcceptedFiles} + onDropRejected={(fileRejections: FileRejection[], event: DropEvent) => { + console.log('onDropRejected: ', fileRejections, event) + alert('You can only upload PDF files to your Omnivore Library.') + setInDragOperation(false) + event.preventDefault() + }} + preventDropOnDocument={true} + noClick={true} + accept={props.accept} + > + {({ getRootProps, getInputProps, acceptedFiles, fileRejections }) => ( +
+ + + + + + {inDragOperation ? ( + <> + + Drop to upload your file + + + ) : ( + <> + + {props.description} +
or{' '} + + choose your files + +
+ + )} +
+
+
+ + {uploadFiles.map((file) => { + return ( + + + {file.name} + + {file.status != 'inprogress' ? ( + + {file.status == 'success' && file.openUrl && ( + Read Now + )} + {file.status == 'success' && !file.openUrl && ( + + {file.message || 'Your import has started'} + + )} + {file.status == 'error' && ( + + Error Uploading + + )} + + ) : ( + + {' '} + + )} + + ) + })} + +
+ +
+ )} +
+
+ ) +} + +type TabBarProps = { + selectedTab: string + setSelectedTab: (selected: TabName) => void + + onOpenChange: (open: boolean) => void +} + +const TabBar = (props: TabBarProps) => { + return ( + + + + + {/* */} + + + + props.onOpenChange(false)} /> + + ) } diff --git a/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx b/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx index 393ecdd11..a7cdde844 100644 --- a/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx +++ b/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx @@ -963,9 +963,6 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element { applySearchQuery={(searchQuery: string) => { props.applySearchQuery(searchQuery) }} - handleLinkSubmission={props.handleLinkSubmission} - allowSelectMultiple={props.mode !== 'highlights'} - alwaysShowHeader={props.mode == 'highlights'} showFilterMenu={showFilterMenu} setShowFilterMenu={setShowFilterMenu} multiSelectMode={props.multiSelectMode} @@ -1234,7 +1231,8 @@ function LibraryItems(props: LibraryItemsProps): JSX.Element { outline: 'none', }, '&> div': { - bg: '$thBackground3', + bg: '$thLeftMenuBackground', + // bg: '$thLibraryBackground', }, '&:focus': { outline: 'none', @@ -1246,6 +1244,7 @@ function LibraryItems(props: LibraryItemsProps): JSX.Element { '&:hover': { '> div': { bg: '$thBackgroundActive', + boxShadow: '$cardBoxShadow', }, '> a': { bg: '$thBackgroundActive', diff --git a/packages/web/components/templates/homeFeed/LibraryFilterMenu.tsx b/packages/web/components/templates/homeFeed/LibraryFilterMenu.tsx index b9a5a8be8..74cc9b3dc 100644 --- a/packages/web/components/templates/homeFeed/LibraryFilterMenu.tsx +++ b/packages/web/components/templates/homeFeed/LibraryFilterMenu.tsx @@ -122,7 +122,7 @@ export function LibraryFilterMenu(props: LibraryFilterMenuProps): JSX.Element { -