diff --git a/packages/api/src/generated/graphql.ts b/packages/api/src/generated/graphql.ts index 3c9449a96..bb36edab4 100644 --- a/packages/api/src/generated/graphql.ts +++ b/packages/api/src/generated/graphql.ts @@ -244,6 +244,7 @@ export enum BulkActionType { } export enum ContentReader { + Epub = 'EPUB', Pdf = 'PDF', Web = 'WEB' } diff --git a/packages/api/src/generated/schema.graphql b/packages/api/src/generated/schema.graphql index 82d0ef913..420960b75 100644 --- a/packages/api/src/generated/schema.graphql +++ b/packages/api/src/generated/schema.graphql @@ -203,6 +203,7 @@ enum BulkActionType { } enum ContentReader { + EPUB PDF WEB } diff --git a/packages/api/src/resolvers/article/index.ts b/packages/api/src/resolvers/article/index.ts index bc9f6603c..c8596909b 100644 --- a/packages/api/src/resolvers/article/index.ts +++ b/packages/api/src/resolvers/article/index.ts @@ -98,10 +98,12 @@ import { } from '../../utils/parser' import { parseSearchQuery, SortBy, SortOrder } from '../../utils/search' import { + contentReaderForPageType, getStorageFileDetails, makeStorageFilePublic, } from '../../utils/uploads' import { WithDataSourcesContext } from '../types' +import { pageTypeForContentType } from '../upload_files' enum ArticleFormat { Markdown = 'markdown', @@ -258,7 +260,7 @@ export const createArticleResolver = authorized< uploadFileHash = uploadFileDetails.md5Hash userArticleUrl = uploadFileDetails.fileUrl canonicalUrl = uploadFile.url - pageType = PageType.File + pageType = pageTypeForContentType(uploadFile.contentType) title = titleForFilePath(uploadFile.url) } else if ( source !== 'puppeteer-parse' && @@ -950,8 +952,7 @@ export const searchResolver = authorized< ...r, image: r.image && createImageProxyUrl(r.image, 260, 260), isArchived: !!r.archivedAt, - contentReader: - r.pageType === PageType.File ? ContentReader.Pdf : ContentReader.Web, + contentReader: contentReaderForPageType(r.pageType), originalArticleUrl: r.url, publishedAt: validatedDate(r.publishedAt), ownedByViewer: r.userId === claims.uid, @@ -1054,10 +1055,7 @@ export const updatesSinceResolver = authorized< ...p, image: p.image && createImageProxyUrl(p.image, 260, 260), isArchived: !!p.archivedAt, - contentReader: - p.pageType === PageType.File - ? ContentReader.Pdf - : ContentReader.Web, + contentReader: contentReaderForPageType(p.pageType), } as SearchItem, cursor: endCursor, itemID: p.id, diff --git a/packages/api/src/resolvers/function_resolvers.ts b/packages/api/src/resolvers/function_resolvers.ts index f5624512c..c7ba7a305 100644 --- a/packages/api/src/resolvers/function_resolvers.ts +++ b/packages/api/src/resolvers/function_resolvers.ts @@ -20,6 +20,7 @@ import { import { userDataToUser, validatedDate, wordsCount } from '../utils/helpers' import { createImageProxyUrl } from '../utils/imageproxy' import { + contentReaderForPageType, generateDownloadSignedUrl, generateUploadFilePathName, } from '../utils/uploads' @@ -375,7 +376,8 @@ export const functionResolvers = { Article: { async url(article: Article, _: unknown, ctx: WithDataSourcesContext) { if ( - article.pageType == PageType.File && + (article.pageType == PageType.File || + article.pageType == PageType.Book) && ctx.claims && article.uploadFileId ) { @@ -468,9 +470,7 @@ export const functionResolvers = { return !!page?.archivedAt || false }, contentReader(article: { pageType: PageType }) { - return article.pageType === PageType.File - ? ContentReader.Pdf - : ContentReader.Web + return contentReaderForPageType(article.pageType) }, highlights( article: { id: string; userId?: string; highlights?: Highlight[] }, @@ -551,7 +551,11 @@ export const functionResolvers = { }, SearchItem: { async url(item: SearchItem, _: unknown, ctx: WithDataSourcesContext) { - if (item.pageType == PageType.File && ctx.claims && item.uploadFileId) { + if ( + (item.pageType == PageType.File || item.pageType == PageType.Book) && + ctx.claims && + item.uploadFileId + ) { const upload = await ctx.models.uploadFile.get(item.uploadFileId) if (!upload || !upload.fileName) { return undefined diff --git a/packages/api/src/resolvers/upload_files/index.ts b/packages/api/src/resolvers/upload_files/index.ts index d6c9684e5..bdc82c9dc 100644 --- a/packages/api/src/resolvers/upload_files/index.ts +++ b/packages/api/src/resolvers/upload_files/index.ts @@ -27,6 +27,13 @@ const isFileUrl = (url: string): boolean => { return parsedUrl.protocol == 'file:' } +export const pageTypeForContentType = (contentType: string): PageType => { + if (contentType == 'application/epub+zip') { + return PageType.Book + } + return PageType.File +} + export const uploadFileRequestResolver: ResolverFn< UploadFileRequestResult, unknown, @@ -145,7 +152,7 @@ export const uploadFileRequestResolver: ResolverFn< title: title, hash: uploadFilePathName, content: '', - pageType: PageType.File, + pageType: pageTypeForContentType(input.contentType), uploadFileId: uploadFileData.id, slug: generateSlug(uploadFilePathName), createdAt: new Date(), diff --git a/packages/api/src/schema.ts b/packages/api/src/schema.ts index afcd9c787..898633232 100755 --- a/packages/api/src/schema.ts +++ b/packages/api/src/schema.ts @@ -36,6 +36,7 @@ const schema = gql` enum ContentReader { WEB PDF + EPUB } input SortParams { diff --git a/packages/api/src/services/speech.ts b/packages/api/src/services/speech.ts index c23dbb784..cb41f0458 100644 --- a/packages/api/src/services/speech.ts +++ b/packages/api/src/services/speech.ts @@ -1,4 +1,6 @@ import { Page, PageType } from '../elastic/types' +import { ContentReader } from '../generated/graphql' +import { contentReaderForPageType } from '../utils/uploads' import { FeatureName, isOptedIn } from './features' /* @@ -8,7 +10,10 @@ export const shouldSynthesize = async ( userId: string, page: Page ): Promise => { - if (page.pageType === PageType.File || !page.content) { + if ( + contentReaderForPageType(page.pageType) === ContentReader.Web || + !page.content + ) { // we don't synthesize files for now return false } diff --git a/packages/api/src/utils/uploads.ts b/packages/api/src/utils/uploads.ts index 4c73c8780..738704662 100644 --- a/packages/api/src/utils/uploads.ts +++ b/packages/api/src/utils/uploads.ts @@ -2,6 +2,19 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { File, GetSignedUrlConfig, Storage } from '@google-cloud/storage' import { env } from '../env' +import { ContentReader, PageType } from '../generated/graphql' + +export const contentReaderForPageType = (pageType: PageType) => { + console.log('getting content reader: ', pageType) + switch (pageType) { + case PageType.Book: + return ContentReader.Epub + case PageType.File: + return ContentReader.Pdf + default: + return ContentReader.Web + } +} /* On GAE/Prod, we shall rely on default app engine service account credentials. * Two changes needed: 1) add default service account to our uploads GCS Bucket @@ -39,6 +52,7 @@ export const generateUploadSignedUrl = async ( expires: Date.now() + 15 * 60 * 1000, // 15 minutes contentType: contentType, } + console.log('signed url for: ', options) // Get a v4 signed URL for uploading file const [url] = await storage @@ -60,6 +74,7 @@ export const generateDownloadSignedUrl = async ( .bucket(bucketName) .file(filePathName) .getSignedUrl(options) + console.log('generating download signed url', url) return url } diff --git a/packages/web/components/templates/UploadModal.tsx b/packages/web/components/templates/UploadModal.tsx index f21507adf..296ba0dbe 100644 --- a/packages/web/components/templates/UploadModal.tsx +++ b/packages/web/components/templates/UploadModal.tsx @@ -126,6 +126,7 @@ export function UploadModal(props: UploadModalProps): JSX.Element { ;(async () => { for (const file of addedFiles) { try { + console.log('using content type: ', file.file.type) const request = await uploadFileRequestMutation({ // This will tell the backend not to save the URL // and give it the local filename as the title. @@ -145,7 +146,7 @@ export function UploadModal(props: UploadModalProps): JSX.Element { data: file.file, withCredentials: false, headers: { - 'Content-Type': 'application/pdf', + 'Content-Type': file.file.type, }, onUploadProgress: (p) => { if (!p.total) { diff --git a/packages/web/lib/networking/fragments/articleFragment.ts b/packages/web/lib/networking/fragments/articleFragment.ts index 3511cfbaf..47e093d50 100644 --- a/packages/web/lib/networking/fragments/articleFragment.ts +++ b/packages/web/lib/networking/fragments/articleFragment.ts @@ -23,7 +23,7 @@ export const articleFragment = gql` } ` -export type ContentReader = 'WEB' | 'PDF' +export type ContentReader = 'WEB' | 'PDF' | 'EPUB' export enum State { SUCCEEDED = 'SUCCEEDED', diff --git a/packages/web/lib/themeUpdater.tsx b/packages/web/lib/themeUpdater.tsx index 54e32559c..6829d0147 100644 --- a/packages/web/lib/themeUpdater.tsx +++ b/packages/web/lib/themeUpdater.tsx @@ -4,6 +4,7 @@ import { sepiaTheme, apolloTheme, blackTheme, + theme, } from '../components/tokens/stitches.config' const themeKey = 'theme' @@ -24,8 +25,8 @@ export function updateTheme(themeId: string): void { updateThemeLocally(themeId) } -function getTheme(themeId: string) { - switch (currentTheme()) { +export function getTheme(themeId: string) { + switch (themeId) { case ThemeId.Dark: return darkTheme case ThemeId.Sepia: @@ -35,7 +36,7 @@ function getTheme(themeId: string) { case ThemeId.Black: return blackTheme } - return ThemeId.Light + return theme } export function updateThemeLocally(themeId: string): void { diff --git a/packages/web/package.json b/packages/web/package.json index 8d06e53ac..92e83aef6 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -39,6 +39,7 @@ "dayjs": "^1.11.7", "diff-match-patch": "^1.0.5", "downshift": "^6.1.9", + "epubjs": "^0.3.93", "graphql-request": "^3.6.1", "kbar": "^0.1.0-beta.35", "markdown-it": "^13.0.1", diff --git a/packages/web/pages/[username]/[slug]/index.tsx b/packages/web/pages/[username]/[slug]/index.tsx index 19c8740e2..133702236 100644 --- a/packages/web/pages/[username]/[slug]/index.tsx +++ b/packages/web/pages/[username]/[slug]/index.tsx @@ -38,12 +38,18 @@ import { ReaderHeader } from '../../../components/templates/reader/ReaderHeader' import { EditArticleModal } from '../../../components/templates/homeFeed/EditItemModals' import { VerticalArticleActionsMenu } from '../../../components/templates/article/VerticalArticleActions' import { PdfHeaderSpacer } from '../../../components/templates/article/PdfHeaderSpacer' +import { EpubContainerProps } from '../../../components/templates/article/EpubContainer' const PdfArticleContainerNoSSR = dynamic( () => import('./../../../components/templates/article/PdfArticleContainer'), { ssr: false } ) +const EpubContainerNoSSR = dynamic( + () => import('./../../../components/templates/article/EpubContainer'), + { ssr: false } +) + export default function Home(): JSX.Element { const router = useRouter() const { cache, mutate } = useSWRConfig() @@ -403,14 +409,15 @@ export default function Home(): JSX.Element { /> ) : null} - {article && viewerData?.me && article.contentReader == 'PDF' ? ( + {article && viewerData?.me && article.contentReader == 'PDF' && ( - ) : ( + )} + {article && viewerData?.me && article.contentReader == 'WEB' && ( )} + {article && viewerData?.me && article.contentReader == 'EPUB' && ( + + {article && viewerData?.me ? ( + + ) : ( + + )} + + )} + {article && readerSettings.showSetLabelsModal && (