From 0130c3be5234b5b8433bae1afa5d33ce630da5f1 Mon Sep 17 00:00:00 2001 From: dr-leevsey <74569686+dr-leevsey@users.noreply.github.com> Date: Thu, 11 Jul 2024 16:31:37 +0300 Subject: [PATCH 1/2] feat(epub-view): epub viewer with persisted location (#1164) Co-authored-by: Andrejs --- package.json | 1 + src/components/ContentItem/contentItem.tsx | 8 +- src/components/EPubView/EPubView.tsx | 49 ++++++++++++ src/components/contentIpfs/contentIpfs.tsx | 19 +++-- src/hooks/useEPubLocation.tsx | 47 +++++++++++ src/services/ipfs/types.ts | 1 + src/services/ipfs/utils/content.ts | 14 ++-- yarn.lock | 93 +++++++++++++++++++++- 8 files changed, 215 insertions(+), 17 deletions(-) create mode 100644 src/components/EPubView/EPubView.tsx create mode 100644 src/hooks/useEPubLocation.tsx diff --git a/package.json b/package.json index 3f35876e7..265370f24 100644 --- a/package.json +++ b/package.json @@ -234,6 +234,7 @@ "react-localization": "^1.0.15", "react-markdown": "^7.1.1", "react-number-format": "^5.1.2", + "react-reader": "^2.0.10", "react-redux": "^8.0.5", "react-router-dom": "^6.9.0", "react-textarea-autosize": "^8.2.0", diff --git a/src/components/ContentItem/contentItem.tsx b/src/components/ContentItem/contentItem.tsx index 525c29d81..0d7c837cb 100644 --- a/src/components/ContentItem/contentItem.tsx +++ b/src/components/ContentItem/contentItem.tsx @@ -1,13 +1,13 @@ // TODO: refactor needed -import React, { useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { Link } from 'react-router-dom'; -import { $TsFixMe } from 'src/types/tsfix'; +import { LinksType } from 'src/containers/Search/types'; import useQueueIpfsContent from 'src/hooks/useQueueIpfsContent'; import type { - IpfsContentType, IPFSContentDetails, + IpfsContentType, } from 'src/services/ipfs/types'; -import { LinksType } from 'src/containers/Search/types'; +import { $TsFixMe } from 'src/types/tsfix'; import { parseArrayLikeToDetails } from 'src/services/ipfs/utils/content'; diff --git a/src/components/EPubView/EPubView.tsx b/src/components/EPubView/EPubView.tsx new file mode 100644 index 000000000..1c35fc9ab --- /dev/null +++ b/src/components/EPubView/EPubView.tsx @@ -0,0 +1,49 @@ +import React, { + CSSProperties, + ComponentProps, + useCallback, + useMemo, +} from 'react'; +import { ReactReader } from 'react-reader'; +import useEPubLocation from 'src/hooks/useEPubLocation'; + +interface IProps { + url: string; + search?: boolean; + style?: CSSProperties; +} + +type EpubInitOptions = ComponentProps['epubInitOptions']; +const epubInitOptions: EpubInitOptions = { openAs: 'epub' }; + +function EPubView({ url, search, style }: IProps) { + const [location, setLocation] = useEPubLocation(url); + const currentLocation = location && !search ? location : 0; + + const currentStyle = useMemo( + () => ({ height: search ? '300px' : '60vh', ...style }), + [style, search] + ); + + const onLocationChange = useCallback( + (loc: string) => { + if (!search) { + setLocation(loc); + } + }, + [search, setLocation] + ); + + return ( +
+ +
+ ); +} + +export default React.memo(EPubView); diff --git a/src/components/contentIpfs/contentIpfs.tsx b/src/components/contentIpfs/contentIpfs.tsx index 0f1fc69bd..469f1ef7e 100644 --- a/src/components/contentIpfs/contentIpfs.tsx +++ b/src/components/contentIpfs/contentIpfs.tsx @@ -1,12 +1,14 @@ -import { IPFSContentDetails, IPFSContentMaybe } from 'src/services/ipfs/types'; import { CYBER_GATEWAY } from 'src/constants/config'; +import { CYBER_GATEWAY_URL } from 'src/services/ipfs/config'; +import { IPFSContentDetails, IPFSContentMaybe } from 'src/services/ipfs/types'; +import EPubView from '../EPubView/EPubView'; +import Pdf from '../PDF'; +import TextMarkdown from '../TextMarkdown'; import VideoPlayerGatewayOnly from '../VideoPlayer/VideoPlayerGatewayOnly'; +import Audio from './component/Audio/Audio'; import GatewayContent from './component/gateway'; -import TextMarkdown from '../TextMarkdown'; -import LinkHttp from './component/link'; -import Pdf from '../PDF'; import Img from './component/img'; -import Audio from './component/Audio/Audio'; +import LinkHttp from './component/link'; function OtherItem({ content, @@ -77,6 +79,12 @@ function ContentIpfs({ details, content, cid, search }: ContentTabProps) { {contentType === 'link' && ( )} + {contentType === 'epub' && ( + + )} {contentType === 'other' && ( )} @@ -85,4 +93,5 @@ function ContentIpfs({ details, content, cid, search }: ContentTabProps) { ); } + export default ContentIpfs; diff --git a/src/hooks/useEPubLocation.tsx b/src/hooks/useEPubLocation.tsx new file mode 100644 index 000000000..9fd921d01 --- /dev/null +++ b/src/hooks/useEPubLocation.tsx @@ -0,0 +1,47 @@ +/* eslint-disable import/prefer-default-export */ +import { useCallback } from 'react'; +import type { EpubView } from 'react-reader'; + +const epubKey = 'cyb:epub'; + +const getEPubMap = (): Record => { + try { + const epubMapString = localStorage.getItem(epubKey) || ''; + const epubMap = JSON.parse(epubMapString); + + return epubMap; + } catch (error) { + console.error('Failed to parse epub locations map:', error); + + return {}; + } +}; + +const getLocation = (url: string) => { + const epubMap = getEPubMap(); + + return epubMap[url] ?? null; +}; + +const getSetEPubLocation = + (url: string) => (location: EpubView['location']) => { + const epubMap = getEPubMap(); + + try { + epubMap[url] = location; + localStorage.setItem(epubKey, JSON.stringify(epubMap)); + } catch (error) { + console.error('Failed to save EPub location:', error); + } + }; + +const useEPubLocation = ( + url: string +): [EpubView['location'], (url: string) => void] => { + const currentLocation = getLocation(url); + const setEPubLocation = useCallback(getSetEPubLocation(url), [url]); + + return [currentLocation, setEPubLocation]; +}; + +export default useEPubLocation; diff --git a/src/services/ipfs/types.ts b/src/services/ipfs/types.ts index b025f9357..0e51eb956 100644 --- a/src/services/ipfs/types.ts +++ b/src/services/ipfs/types.ts @@ -91,6 +91,7 @@ export type IpfsContentType = | 'video' | 'audio' | 'html' + | 'epub' | 'other'; export type IPFSContentDetails = diff --git a/src/services/ipfs/utils/content.ts b/src/services/ipfs/utils/content.ts index 98e62db89..4bf6acce1 100644 --- a/src/services/ipfs/utils/content.ts +++ b/src/services/ipfs/utils/content.ts @@ -31,13 +31,18 @@ export const detectContentType = ( if (mime.includes('audio')) { return 'audio'; } + + if (mime.includes('epub')) { + return 'epub'; + } } + return 'other'; }; const basic = /\s?|(]*>|]*>|]+>)+/i; -function isHtml(string) { +function isHtml(string: string) { const newString = string.trim().slice(0, 1000); return basic.test(newString); } @@ -55,13 +60,10 @@ export const chunksToBlob = ( // eslint-disable-next-line import/no-unused-modules, import/prefer-default-export export const parseArrayLikeToDetails = async ( content: IPFSContentMaybe, - // rawDataResponse: Uint8ArrayLike | undefined, - // mime: string | undefined, cid: string, onProgress?: onProgressCallback ): Promise => { try { - // console.log('------parseArrayLikeToDetails', cid, content); const mime = content?.meta?.mime; const response: IPFSContentDetails = { link: `/ipfs/${cid}`, @@ -69,7 +71,7 @@ export const parseArrayLikeToDetails = async ( cid, }; const initialType = detectContentType(mime); - if (['video', 'audio'].indexOf(initialType) > -1) { + if (['video', 'audio', 'epub'].indexOf(initialType) > -1) { return { ...response, type: initialType, gateway: true }; } @@ -77,8 +79,6 @@ export const parseArrayLikeToDetails = async ( ? await getResponseResult(content.result, onProgress) : undefined; - // console.log(rawData); - if (!mime) { response.text = `Can't detect MIME for ${cid.toString()}`; response.gateway = true; // ??? diff --git a/yarn.lock b/yarn.lock index 66f96be16..969d6e67f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9761,6 +9761,13 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== +"@types/localforage@0.0.34": + version "0.0.34" + resolved "https://registry.yarnpkg.com/@types/localforage/-/localforage-0.0.34.tgz#5e31c32dd8791ec4b9ff3ef47c9cb55b2d0d9438" + integrity sha512-tJxahnjm9dEI1X+hQSC5f2BSd/coZaqbIl1m3TCl0q9SVuC52XcXfV0XmoCU1+PmjyucuVITwoTnN8OlTbEXXA== + dependencies: + localforage "*" + "@types/lodash@^4.14.167": version "4.14.194" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.194.tgz#b71eb6f7a0ff11bff59fc987134a093029258a76" @@ -10631,6 +10638,11 @@ dependencies: tslib "^2.3.0" +"@xmldom/xmldom@^0.7.5": + version "0.7.13" + resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.7.13.tgz#ff34942667a4e19a9f4a0996a76814daac364cf3" + integrity sha512-lm2GW5PkosIzccsaZIz7tp8cPADSIlIHWDFTR1N0SzfinhhYgeIQjFMz4rYzanCScr3DqQLeomUDArp6MWKm+g== + "@xtuc/ieee754@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" @@ -13809,6 +13821,11 @@ core-js@^2.4.0, core-js@^2.4.1, core-js@^2.5.3: resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec" integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== +core-js@^3.18.3: + version "3.37.1" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.37.1.tgz#d21751ddb756518ac5a00e4d66499df981a62db9" + integrity sha512-Xn6qmxrQZyB0FFY8E3bgRXei3lWDJHhvI+u0q9TKIYM49G8pAr0FgnnrFRAmsbptZL1yxRADVXn+x5AGsbBfyw== + core-js@^3.21.1, core-js@^3.30.0: version "3.30.1" resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.30.1.tgz#fc9c5adcc541d8e9fa3e381179433cbf795628ba" @@ -15808,6 +15825,21 @@ envinfo@^7.7.3: resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.8.1.tgz#06377e3e5f4d379fea7ac592d5ad8927e0c4d475" integrity sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw== +epubjs@^0.3.93: + version "0.3.93" + resolved "https://registry.yarnpkg.com/epubjs/-/epubjs-0.3.93.tgz#100c4597db152fc07d5246be38acca928b6b0b22" + integrity sha512-c06pNSdBxcXv3dZSbXAVLE1/pmleRhOT6mXNZo6INKmvuKpYB65MwU/lO7830czCtjIiK9i+KR+3S+p0wtljrw== + dependencies: + "@types/localforage" "0.0.34" + "@xmldom/xmldom" "^0.7.5" + core-js "^3.18.3" + event-emitter "^0.3.5" + jszip "^3.7.1" + localforage "^1.10.0" + lodash "^4.17.21" + marks-pane "^1.0.9" + path-webpack "0.0.3" + err-code@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/err-code/-/err-code-3.0.1.tgz#a444c7b992705f2b120ee320b09972eef331c920" @@ -19507,6 +19539,11 @@ image-size@~0.5.0: resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c" integrity sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ== +immediate@~3.0.5: + version "3.0.6" + resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" + integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== + immer@^9.0.21: version "9.0.21" resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.21.tgz#1e025ea31a40f24fb064f1fef23e931496330176" @@ -22484,6 +22521,16 @@ jsprim@^1.2.2: array-includes "^3.1.5" object.assign "^4.1.3" +jszip@^3.7.1: + version "3.10.1" + resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.10.1.tgz#34aee70eb18ea1faec2f589208a157d1feb091c2" + integrity sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g== + dependencies: + lie "~3.3.0" + pako "~1.0.2" + readable-stream "~2.3.6" + setimmediate "^1.0.5" + just-debounce-it@^3.0.1: version "3.2.0" resolved "https://registry.yarnpkg.com/just-debounce-it/-/just-debounce-it-3.2.0.tgz#4352265f4af44188624ce9fdbc6bff4d49c63a80" @@ -22885,6 +22932,20 @@ libsodium@^0.7.11: resolved "https://registry.yarnpkg.com/libsodium/-/libsodium-0.7.11.tgz#cd10aae7bcc34a300cc6ad0ac88fcca674cfbc2e" integrity sha512-WPfJ7sS53I2s4iM58QxY3Inb83/6mjlYgcmZs7DJsvDlnmVUwNinBCi5vBT43P6bHRy01O4zsMU2CoVR6xJ40A== +lie@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e" + integrity sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw== + dependencies: + immediate "~3.0.5" + +lie@~3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a" + integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ== + dependencies: + immediate "~3.0.5" + lilconfig@^2.0.3: version "2.1.0" resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52" @@ -22962,6 +23023,13 @@ loader-utils@^2.0.0, loader-utils@^2.0.4: emojis-list "^3.0.0" json5 "^2.1.2" +localforage@*, localforage@^1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.10.0.tgz#5c465dc5f62b2807c3a84c0c6a1b1b3212781dd4" + integrity sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg== + dependencies: + lie "3.1.1" + localized-strings@^0.2.0: version "0.2.4" resolved "https://registry.yarnpkg.com/localized-strings/-/localized-strings-0.2.4.tgz#9d61c06b60cc7b5edf7c46e6c7f2d1ecb84aeb2c" @@ -23337,6 +23405,11 @@ markdown-to-jsx@^7.1.8: resolved "https://registry.yarnpkg.com/markdown-to-jsx/-/markdown-to-jsx-7.2.0.tgz#e7b46b65955f6a04d48a753acd55874a14bdda4b" integrity sha512-3l4/Bigjm4bEqjCR6Xr+d4DtM1X6vvtGsMGSjJYyep8RjjIvcWtrXBS8Wbfe1/P+atKNMccpsraESIaWVplzVg== +marks-pane@^1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/marks-pane/-/marks-pane-1.0.9.tgz#c0b5ab813384d8cd81faaeb3bbf3397dc809c1b3" + integrity sha512-Ahs4oeG90tbdPWwAJkAAoHg2lRR8lAs9mZXETNPO9hYg3AkjUJBKi1NQ4aaIQZVGrig7c/3NUV1jANl8rFTeMg== + mat4-decompose@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/mat4-decompose/-/mat4-decompose-1.0.4.tgz#65eb4fe39d70878f7a444eb4624d52f7e7eb2faf" @@ -25484,7 +25557,7 @@ pako@~0.2.0: resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75" integrity sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA== -pako@~1.0.5: +pako@~1.0.2, pako@~1.0.5: version "1.0.11" resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== @@ -25714,6 +25787,11 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== +path-webpack@0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/path-webpack/-/path-webpack-0.0.3.tgz#ff6dec749eec5a94605c04d5f63fc55607a03a16" + integrity sha512-AmeDxedoo5svf7aB3FYqSAKqMxys014lVKBzy1o/5vv9CtU7U4wgGWL1dA2o6MOzcD53ScN4Jmiq6VbtLz1vIQ== + pathe@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.0.tgz#e2e13f6c62b31a3289af4ba19886c230f295ec03" @@ -27594,6 +27672,14 @@ react-popper@^2.3.0: react-fast-compare "^3.0.1" warning "^4.0.2" +react-reader@^2.0.10: + version "2.0.10" + resolved "https://registry.yarnpkg.com/react-reader/-/react-reader-2.0.10.tgz#91f855dde41470dc6045eb0648e9e842e019acd9" + integrity sha512-eNd4GEmbsttSq79eCAXFRw0VkGMeGTQh4mCRiFOFhlhZOsVPOqkCnUqWqrhiFpB/7SDaDkJJE8wcZYgIZQ7CoA== + dependencies: + epubjs "^0.3.93" + react-swipeable "^7.0.1" + react-redux@^8.0.5: version "8.0.5" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-8.0.5.tgz#e5fb8331993a019b8aaf2e167a93d10af469c7bd" @@ -27713,6 +27799,11 @@ react-style-singleton@^2.2.1: invariant "^2.2.4" tslib "^2.0.0" +react-swipeable@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/react-swipeable/-/react-swipeable-7.0.1.tgz#cd299f5986c5e4a7ee979839658c228f660e1e0c" + integrity sha512-RKB17JdQzvECfnVj9yDZsiYn3vH0eyva/ZbrCZXZR0qp66PBRhtg4F9yJcJTWYT5Adadi+x4NoG53BxKHwIYLQ== + react-textarea-autosize@^8.2.0, react-textarea-autosize@^8.3.2: version "8.4.1" resolved "https://registry.yarnpkg.com/react-textarea-autosize/-/react-textarea-autosize-8.4.1.tgz#bcfc5462727014b808b14ee916c01e275e8a8335" From b54b808563c040cde8eefa825f3214e31aa3f0f5 Mon Sep 17 00:00:00 2001 From: Cheslav Zhuravsky Date: Wed, 24 Jul 2024 21:53:34 +0700 Subject: [PATCH 2/2] feat(analytics): add analytics (#1207) --- src/index.html | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/index.html b/src/index.html index 287b56252..6ba1d7770 100644 --- a/src/index.html +++ b/src/index.html @@ -61,4 +61,10 @@ + +