From ffd8a28d42f46bc84cf124a98dcbd285072e93e8 Mon Sep 17 00:00:00 2001 From: Cayman Date: Fri, 20 Sep 2024 11:34:58 -0400 Subject: [PATCH] feat: remove react-bundle --- .vscode/settings.json | 3 + index.html | 2 +- package-lock.json | 287 +++------- package.json | 11 +- src/App.jsx | 57 -- src/App.tsx | 27 + src/bundles/app-idle.js | 44 -- src/bundles/files.js | 491 ------------------ src/bundles/index.js | 16 - src/bundles/ipfs-provider.js | 79 --- src/bundles/redirects.js | 16 - src/bundles/routes.js | 27 - .../add-files/{AddFiles.jsx => AddFiles.tsx} | 26 +- src/components/box/Box.jsx | 94 ---- src/components/box/Box.tsx | 89 ++++ src/components/copy-link/CopyLink.jsx | 78 --- src/components/copy-link/CopyLink.tsx | 58 +++ .../download-files/DownloadFiles.jsx | 83 --- .../download-files/DownloadFiles.tsx | 62 +++ src/components/file-tree/FileTree.jsx | 32 -- src/components/file-tree/FileTree.tsx | 26 + src/components/file/File.jsx | 204 -------- src/components/file/File.tsx | 156 ++++++ .../file/utils/{archive.js => archive.ts} | 5 +- ...sync-it-to-file.js => async-it-to-file.ts} | 5 +- .../file/utils/{download.js => download.ts} | 3 +- .../file/utils/{extToType.js => extToType.ts} | 0 .../file/utils/{view.js => view.ts} | 2 +- src/components/headline/Headline.jsx | 11 +- src/components/info/Info.jsx | 221 -------- src/components/info/Info.stories.js | 2 +- src/components/info/Info.tsx | 197 +++++++ src/hooks/useCurrentPage.ts | 16 + src/hooks/useFiles.ts | 61 +++ src/hooks/useHelia.ts | 6 + src/{index.jsx => index.tsx} | 17 +- src/page/Page.jsx | 97 ---- src/page/Page.tsx | 59 +++ src/providers/FilesProvider.tsx | 210 ++++++++ src/providers/HeliaProvider.tsx | 85 +++ src/util.ts | 18 + 41 files changed, 1193 insertions(+), 1790 deletions(-) create mode 100644 .vscode/settings.json delete mode 100644 src/App.jsx create mode 100644 src/App.tsx delete mode 100644 src/bundles/app-idle.js delete mode 100644 src/bundles/files.js delete mode 100644 src/bundles/index.js delete mode 100644 src/bundles/ipfs-provider.js delete mode 100644 src/bundles/redirects.js delete mode 100644 src/bundles/routes.js rename src/components/add-files/{AddFiles.jsx => AddFiles.tsx} (63%) delete mode 100644 src/components/box/Box.jsx create mode 100644 src/components/box/Box.tsx delete mode 100644 src/components/copy-link/CopyLink.jsx create mode 100644 src/components/copy-link/CopyLink.tsx delete mode 100644 src/components/download-files/DownloadFiles.jsx create mode 100644 src/components/download-files/DownloadFiles.tsx delete mode 100644 src/components/file-tree/FileTree.jsx create mode 100644 src/components/file-tree/FileTree.tsx delete mode 100644 src/components/file/File.jsx create mode 100644 src/components/file/File.tsx rename src/components/file/utils/{archive.js => archive.ts} (73%) rename src/components/file/utils/{async-it-to-file.js => async-it-to-file.ts} (75%) rename src/components/file/utils/{download.js => download.ts} (71%) rename src/components/file/utils/{extToType.js => extToType.ts} (100%) rename src/components/file/utils/{view.js => view.ts} (76%) delete mode 100644 src/components/info/Info.jsx create mode 100644 src/components/info/Info.tsx create mode 100644 src/hooks/useCurrentPage.ts create mode 100644 src/hooks/useFiles.ts create mode 100644 src/hooks/useHelia.ts rename src/{index.jsx => index.tsx} (67%) delete mode 100644 src/page/Page.jsx create mode 100644 src/page/Page.tsx create mode 100644 src/providers/FilesProvider.tsx create mode 100644 src/providers/HeliaProvider.tsx create mode 100644 src/util.ts diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..082b194 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "makefile.configureOnOpen": false +} \ No newline at end of file diff --git a/index.html b/index.html index e230482..68561aa 100644 --- a/index.html +++ b/index.html @@ -40,6 +40,6 @@ To begin the development, run `npm start` or `yarn start`. To create a production bundle, use `npm run build` or `yarn build`. --> - + diff --git a/package-lock.json b/package-lock.json index 3a974fd..864fea7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,14 +35,14 @@ "pull-file-reader": "^1.0.2", "qrcode.react": "^1.0.1", "react": "^17.0.1", - "react-circular-progressbar": "^2.0.3", + "react-circular-progressbar": "^2.0.4", "react-copy-to-clipboard": "^5.0.3", "react-dnd": "^13.1.1", "react-dnd-html5-backend": "^12.1.1", "react-dom": "^17.0.1", "react-helmet": "^6.1.0", "react-i18next": "^11.8.8", - "react-intl": "^5.13.1", + "react-intl": "^6.6.8", "react-loader-spinner": "^4.0.0", "react-modal": "^3.12.1", "react-router-dom": "^5.2.0", @@ -52,7 +52,8 @@ "shortid": "^2.2.16", "tachyons": "^4.12.0", "uglifyjs-webpack-plugin": "^2.2.0", - "video-extensions": "^1.1.0" + "video-extensions": "^1.1.0", + "wouter": "^3.3.5" }, "devDependencies": { "@olizilla/lol": "2.0.0", @@ -81,7 +82,7 @@ "ipfsd-ctl": "^7.2.0", "npm-run-all": "^4.1.5", "source-map-explorer": "^2.5.2", - "typescript": "^4.9.5", + "typescript": "^5.6.2", "vite": "^5.4.6" } }, @@ -3410,7 +3411,6 @@ "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.0.0.tgz", "integrity": "sha512-rRqXOqdFmk7RYvj4khklyqzcfQl9vEL/usogncBHRZfZBDOwMGuSRNFl02fu5KGHXdbinju+YXyuR+Nk8xlr/g==", "license": "MIT", - "peer": true, "dependencies": { "@formatjs/intl-localematcher": "0.5.4", "tslib": "^2.4.0" @@ -3421,7 +3421,6 @@ "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.0.tgz", "integrity": "sha512-hnk/nY8FyrL5YxwP9e4r9dqeM6cAbo8PeU9UjyXojZMNvVad2Z06FAVHyR3Ecw6fza+0GH7vdJgiKIVXTMbSBA==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -3431,7 +3430,6 @@ "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.7.8.tgz", "integrity": "sha512-nBZJYmhpcSX0WeJ5SDYUkZ42AgR3xiyhNCsQweFx3cz/ULJjym8bHAzWKvG5e2+1XO98dBYC0fWeeAECAVSwLA==", "license": "MIT", - "peer": true, "dependencies": { "@formatjs/ecma402-abstract": "2.0.0", "@formatjs/icu-skeleton-parser": "1.8.2", @@ -3443,27 +3441,27 @@ "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.2.tgz", "integrity": "sha512-k4ERKgw7aKGWJZgTarIcNEmvyTVD9FYh0mTrrBMHZ1b8hUu6iOJ4SzsZlo3UNAvHYa+PnvntIwRPt1/vy4nA9Q==", "license": "MIT", - "peer": true, "dependencies": { "@formatjs/ecma402-abstract": "2.0.0", "tslib": "^2.4.0" } }, "node_modules/@formatjs/intl": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@formatjs/intl/-/intl-2.2.1.tgz", - "integrity": "sha512-vgvyUOOrzqVaOFYzTf2d3+ToSkH2JpR7x/4U1RyoHQLmvEaTQvXJ7A2qm1Iy3brGNXC/+/7bUlc3lpH+h/LOJA==", + "version": "2.10.4", + "resolved": "https://registry.npmjs.org/@formatjs/intl/-/intl-2.10.4.tgz", + "integrity": "sha512-56483O+HVcL0c7VucAS2tyH020mt9XTozZO67cwtGg0a7KWDukS/FzW3OnvaHmTHDuYsoPIzO+ZHVfU6fT/bJw==", + "license": "MIT", "dependencies": { - "@formatjs/ecma402-abstract": "1.11.4", - "@formatjs/fast-memoize": "1.2.1", - "@formatjs/icu-messageformat-parser": "2.1.0", - "@formatjs/intl-displaynames": "5.4.3", - "@formatjs/intl-listformat": "6.5.3", - "intl-messageformat": "9.13.0", - "tslib": "^2.1.0" + "@formatjs/ecma402-abstract": "2.0.0", + "@formatjs/fast-memoize": "2.2.0", + "@formatjs/icu-messageformat-parser": "2.7.8", + "@formatjs/intl-displaynames": "6.6.8", + "@formatjs/intl-listformat": "7.5.7", + "intl-messageformat": "10.5.14", + "tslib": "^2.4.0" }, "peerDependencies": { - "typescript": "^4.5" + "typescript": "^4.7 || 5" }, "peerDependenciesMeta": { "typescript": { @@ -3472,63 +3470,25 @@ } }, "node_modules/@formatjs/intl-displaynames": { - "version": "5.4.3", - "resolved": "https://registry.npmjs.org/@formatjs/intl-displaynames/-/intl-displaynames-5.4.3.tgz", - "integrity": "sha512-4r12A3mS5dp5hnSaQCWBuBNfi9Amgx2dzhU4lTFfhSxgb5DOAiAbMpg6+7gpWZgl4ahsj3l2r/iHIjdmdXOE2Q==", - "license": "MIT", - "dependencies": { - "@formatjs/ecma402-abstract": "1.11.4", - "@formatjs/intl-localematcher": "0.2.25", - "tslib": "^2.1.0" - } - }, - "node_modules/@formatjs/intl-displaynames/node_modules/@formatjs/ecma402-abstract": { - "version": "1.11.4", - "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.4.tgz", - "integrity": "sha512-EBikYFp2JCdIfGEb5G9dyCkTGDmC57KSHhRQOC3aYxoPWVZvfWCDjZwkGYHN7Lis/fmuWl906bnNTJifDQ3sXw==", + "version": "6.6.8", + "resolved": "https://registry.npmjs.org/@formatjs/intl-displaynames/-/intl-displaynames-6.6.8.tgz", + "integrity": "sha512-Lgx6n5KxN16B3Pb05z3NLEBQkGoXnGjkTBNCZI+Cn17YjHJ3fhCeEJJUqRlIZmJdmaXQhjcQVDp6WIiNeRYT5g==", "license": "MIT", "dependencies": { - "@formatjs/intl-localematcher": "0.2.25", - "tslib": "^2.1.0" - } - }, - "node_modules/@formatjs/intl-displaynames/node_modules/@formatjs/intl-localematcher": { - "version": "0.2.25", - "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.25.tgz", - "integrity": "sha512-YmLcX70BxoSopLFdLr1Ds99NdlTI2oWoLbaUW2M406lxOIPzE1KQhRz2fPUkq34xVZQaihCoU29h0KK7An3bhA==", - "license": "MIT", - "dependencies": { - "tslib": "^2.1.0" + "@formatjs/ecma402-abstract": "2.0.0", + "@formatjs/intl-localematcher": "0.5.4", + "tslib": "^2.4.0" } }, "node_modules/@formatjs/intl-listformat": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/@formatjs/intl-listformat/-/intl-listformat-6.5.3.tgz", - "integrity": "sha512-ozpz515F/+3CU+HnLi5DYPsLa6JoCfBggBSSg/8nOB5LYSFW9+ZgNQJxJ8tdhKYeODT+4qVHX27EeJLoxLGLNg==", + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/@formatjs/intl-listformat/-/intl-listformat-7.5.7.tgz", + "integrity": "sha512-MG2TSChQJQT9f7Rlv+eXwUFiG24mKSzmF144PLb8m8OixyXqn4+YWU+5wZracZGCgVTVmx8viCf7IH3QXoiB2g==", "license": "MIT", "dependencies": { - "@formatjs/ecma402-abstract": "1.11.4", - "@formatjs/intl-localematcher": "0.2.25", - "tslib": "^2.1.0" - } - }, - "node_modules/@formatjs/intl-listformat/node_modules/@formatjs/ecma402-abstract": { - "version": "1.11.4", - "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.4.tgz", - "integrity": "sha512-EBikYFp2JCdIfGEb5G9dyCkTGDmC57KSHhRQOC3aYxoPWVZvfWCDjZwkGYHN7Lis/fmuWl906bnNTJifDQ3sXw==", - "license": "MIT", - "dependencies": { - "@formatjs/intl-localematcher": "0.2.25", - "tslib": "^2.1.0" - } - }, - "node_modules/@formatjs/intl-listformat/node_modules/@formatjs/intl-localematcher": { - "version": "0.2.25", - "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.25.tgz", - "integrity": "sha512-YmLcX70BxoSopLFdLr1Ds99NdlTI2oWoLbaUW2M406lxOIPzE1KQhRz2fPUkq34xVZQaihCoU29h0KK7An3bhA==", - "license": "MIT", - "dependencies": { - "tslib": "^2.1.0" + "@formatjs/ecma402-abstract": "2.0.0", + "@formatjs/intl-localematcher": "0.5.4", + "tslib": "^2.4.0" } }, "node_modules/@formatjs/intl-localematcher": { @@ -3536,66 +3496,10 @@ "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.4.tgz", "integrity": "sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.4.0" } }, - "node_modules/@formatjs/intl/node_modules/@formatjs/ecma402-abstract": { - "version": "1.11.4", - "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.4.tgz", - "integrity": "sha512-EBikYFp2JCdIfGEb5G9dyCkTGDmC57KSHhRQOC3aYxoPWVZvfWCDjZwkGYHN7Lis/fmuWl906bnNTJifDQ3sXw==", - "dependencies": { - "@formatjs/intl-localematcher": "0.2.25", - "tslib": "^2.1.0" - } - }, - "node_modules/@formatjs/intl/node_modules/@formatjs/fast-memoize": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-1.2.1.tgz", - "integrity": "sha512-Rg0e76nomkz3vF9IPlKeV+Qynok0r7YZjL6syLz4/urSg0IbjPZCB/iYUMNsYA643gh4mgrX3T7KEIFIxJBQeg==", - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/@formatjs/intl/node_modules/@formatjs/icu-messageformat-parser": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.1.0.tgz", - "integrity": "sha512-Qxv/lmCN6hKpBSss2uQ8IROVnta2r9jd3ymUEIjm2UyIkUCHVcbUVRGL/KS/wv7876edvsPe+hjHVJ4z8YuVaw==", - "dependencies": { - "@formatjs/ecma402-abstract": "1.11.4", - "@formatjs/icu-skeleton-parser": "1.3.6", - "tslib": "^2.1.0" - } - }, - "node_modules/@formatjs/intl/node_modules/@formatjs/icu-skeleton-parser": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.3.6.tgz", - "integrity": "sha512-I96mOxvml/YLrwU2Txnd4klA7V8fRhb6JG/4hm3VMNmeJo1F03IpV2L3wWt7EweqNLES59SZ4d6hVOPCSf80Bg==", - "dependencies": { - "@formatjs/ecma402-abstract": "1.11.4", - "tslib": "^2.1.0" - } - }, - "node_modules/@formatjs/intl/node_modules/@formatjs/intl-localematcher": { - "version": "0.2.25", - "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.25.tgz", - "integrity": "sha512-YmLcX70BxoSopLFdLr1Ds99NdlTI2oWoLbaUW2M406lxOIPzE1KQhRz2fPUkq34xVZQaihCoU29h0KK7An3bhA==", - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/@formatjs/intl/node_modules/intl-messageformat": { - "version": "9.13.0", - "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-9.13.0.tgz", - "integrity": "sha512-7sGC7QnSQGa5LZP7bXLDhVDtQOeKGeBFGHF2Y8LVBwYZoQZCgWeKoPGTa5GMG8g/TzDgeXuYJQis7Ggiw2xTOw==", - "dependencies": { - "@formatjs/ecma402-abstract": "1.11.4", - "@formatjs/fast-memoize": "1.2.1", - "@formatjs/icu-messageformat-parser": "2.1.0", - "tslib": "^2.1.0" - } - }, "node_modules/@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", @@ -28347,7 +28251,6 @@ "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.5.14.tgz", "integrity": "sha512-IjC6sI0X7YRjjyVH9aUgdftcmZK7WXdHeil4KwbjDnRWjnVitKpAx3rr6t6di1joFp5188VqKcobOPA6mCLG/w==", "license": "BSD-3-Clause", - "peer": true, "dependencies": { "@formatjs/ecma402-abstract": "2.0.0", "@formatjs/fast-memoize": "2.2.0", @@ -36733,6 +36636,12 @@ "node": ">=4.0.0" } }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, "node_modules/mixin-deep": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", @@ -41229,25 +41138,25 @@ } }, "node_modules/react-intl": { - "version": "5.25.1", - "resolved": "https://registry.npmjs.org/react-intl/-/react-intl-5.25.1.tgz", - "integrity": "sha512-pkjdQDvpJROoXLMltkP/5mZb0/XqrqLoPGKUCfbdkP8m6U9xbK40K51Wu+a4aQqTEvEK5lHBk0fWzUV72SJ3Hg==", + "version": "6.6.8", + "resolved": "https://registry.npmjs.org/react-intl/-/react-intl-6.6.8.tgz", + "integrity": "sha512-M0pkhzcgV31h++2901BiRXWl69hp2zPyLxRrSwRjd1ErXbNoubz/f4M6DrRTd4OiSUrT4ajRQzrmtS5plG4FtA==", "license": "BSD-3-Clause", "dependencies": { - "@formatjs/ecma402-abstract": "1.11.4", - "@formatjs/icu-messageformat-parser": "2.1.0", - "@formatjs/intl": "2.2.1", - "@formatjs/intl-displaynames": "5.4.3", - "@formatjs/intl-listformat": "6.5.3", + "@formatjs/ecma402-abstract": "2.0.0", + "@formatjs/icu-messageformat-parser": "2.7.8", + "@formatjs/intl": "2.10.4", + "@formatjs/intl-displaynames": "6.6.8", + "@formatjs/intl-listformat": "7.5.7", "@types/hoist-non-react-statics": "^3.3.1", "@types/react": "16 || 17 || 18", "hoist-non-react-statics": "^3.3.2", - "intl-messageformat": "9.13.0", - "tslib": "^2.1.0" + "intl-messageformat": "10.5.14", + "tslib": "^2.4.0" }, "peerDependencies": { - "react": "^16.3.0 || 17 || 18", - "typescript": "^4.5" + "react": "^16.6.0 || 17 || 18", + "typescript": "^4.7 || 5" }, "peerDependenciesMeta": { "typescript": { @@ -41255,67 +41164,6 @@ } } }, - "node_modules/react-intl/node_modules/@formatjs/ecma402-abstract": { - "version": "1.11.4", - "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.4.tgz", - "integrity": "sha512-EBikYFp2JCdIfGEb5G9dyCkTGDmC57KSHhRQOC3aYxoPWVZvfWCDjZwkGYHN7Lis/fmuWl906bnNTJifDQ3sXw==", - "license": "MIT", - "dependencies": { - "@formatjs/intl-localematcher": "0.2.25", - "tslib": "^2.1.0" - } - }, - "node_modules/react-intl/node_modules/@formatjs/fast-memoize": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-1.2.1.tgz", - "integrity": "sha512-Rg0e76nomkz3vF9IPlKeV+Qynok0r7YZjL6syLz4/urSg0IbjPZCB/iYUMNsYA643gh4mgrX3T7KEIFIxJBQeg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/react-intl/node_modules/@formatjs/icu-messageformat-parser": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.1.0.tgz", - "integrity": "sha512-Qxv/lmCN6hKpBSss2uQ8IROVnta2r9jd3ymUEIjm2UyIkUCHVcbUVRGL/KS/wv7876edvsPe+hjHVJ4z8YuVaw==", - "license": "MIT", - "dependencies": { - "@formatjs/ecma402-abstract": "1.11.4", - "@formatjs/icu-skeleton-parser": "1.3.6", - "tslib": "^2.1.0" - } - }, - "node_modules/react-intl/node_modules/@formatjs/icu-skeleton-parser": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.3.6.tgz", - "integrity": "sha512-I96mOxvml/YLrwU2Txnd4klA7V8fRhb6JG/4hm3VMNmeJo1F03IpV2L3wWt7EweqNLES59SZ4d6hVOPCSf80Bg==", - "license": "MIT", - "dependencies": { - "@formatjs/ecma402-abstract": "1.11.4", - "tslib": "^2.1.0" - } - }, - "node_modules/react-intl/node_modules/@formatjs/intl-localematcher": { - "version": "0.2.25", - "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.25.tgz", - "integrity": "sha512-YmLcX70BxoSopLFdLr1Ds99NdlTI2oWoLbaUW2M406lxOIPzE1KQhRz2fPUkq34xVZQaihCoU29h0KK7An3bhA==", - "license": "MIT", - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/react-intl/node_modules/intl-messageformat": { - "version": "9.13.0", - "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-9.13.0.tgz", - "integrity": "sha512-7sGC7QnSQGa5LZP7bXLDhVDtQOeKGeBFGHF2Y8LVBwYZoQZCgWeKoPGTa5GMG8g/TzDgeXuYJQis7Ggiw2xTOw==", - "license": "BSD-3-Clause", - "dependencies": { - "@formatjs/ecma402-abstract": "1.11.4", - "@formatjs/fast-memoize": "1.2.1", - "@formatjs/icu-messageformat-parser": "2.1.0", - "tslib": "^2.1.0" - } - }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -41977,6 +41825,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regexparam": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/regexparam/-/regexparam-3.0.0.tgz", + "integrity": "sha512-RSYAtP31mvYLkAHrOlh25pCNQ5hWnT106VukGaaFfuJrZFkGRX5GhUAdPqpSDXxOhA2c4akmRuplv1mRqnBn6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/regexpp": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", @@ -46017,16 +45874,17 @@ } }, "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", + "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", "devOptional": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, "node_modules/uglify-js": { @@ -46990,6 +46848,15 @@ "node": ">=0.10.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", + "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/utf8-byte-length": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", @@ -49336,6 +49203,20 @@ "microevent.ts": "~0.1.1" } }, + "node_modules/wouter": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/wouter/-/wouter-3.3.5.tgz", + "integrity": "sha512-bx3fLQAMn+EhYbBdY3W1gw9ZfO/uchudxYMwOIBzF3HVgqNEEIT199vEoh7FLTC0Vz5+rpMO6NdFsOkGX1QQCw==", + "license": "Unlicense", + "dependencies": { + "mitt": "^3.0.1", + "regexparam": "^3.0.0", + "use-sync-external-store": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/package.json b/package.json index a05164a..e224045 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "leadMaintainer": "Diogo Silva ", "private": true, "type": "module", - "main": "src/index.jsx", + "main": "src/index.tsx", "scripts": { "start": "vite", "prebuild": "lol public/locales > src/lib/languages.json", @@ -50,14 +50,14 @@ "pull-file-reader": "^1.0.2", "qrcode.react": "^1.0.1", "react": "^17.0.1", - "react-circular-progressbar": "^2.0.3", + "react-circular-progressbar": "^2.0.4", "react-copy-to-clipboard": "^5.0.3", "react-dnd": "^13.1.1", "react-dnd-html5-backend": "^12.1.1", "react-dom": "^17.0.1", "react-helmet": "^6.1.0", "react-i18next": "^11.8.8", - "react-intl": "^5.13.1", + "react-intl": "^6.6.8", "react-loader-spinner": "^4.0.0", "react-modal": "^3.12.1", "react-router-dom": "^5.2.0", @@ -67,7 +67,8 @@ "shortid": "^2.2.16", "tachyons": "^4.12.0", "uglifyjs-webpack-plugin": "^2.2.0", - "video-extensions": "^1.1.0" + "video-extensions": "^1.1.0", + "wouter": "^3.3.5" }, "devDependencies": { "@olizilla/lol": "2.0.0", @@ -96,7 +97,7 @@ "ipfsd-ctl": "^7.2.0", "npm-run-all": "^4.1.5", "source-map-explorer": "^2.5.2", - "typescript": "^4.9.5", + "typescript": "^5.6.2", "vite": "^5.4.6" }, "homepage": "./", diff --git a/src/App.jsx b/src/App.jsx deleted file mode 100644 index 0b1166f..0000000 --- a/src/App.jsx +++ /dev/null @@ -1,57 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import { connect } from 'redux-bundler-react' - -// Components -import Header from './components/header/Header' -import Footer from './components/footer/Footer' - -// Styles -import './App.css' - -export class App extends Component { - static propTypes = { - doInitIpfs: PropTypes.func.isRequired, - doUpdateUrl: PropTypes.func.isRequired, - route: PropTypes.oneOfType([ - PropTypes.func, - PropTypes.element - ]).isRequired, - ipfsReady: PropTypes.bool.isRequired, - ipfsInitFailed: PropTypes.bool.isRequired - } - - componentDidMount () { - this.props.doInitIpfs() - } - - render () { - const { route: Page, ipfsReady, ipfsInitFailed } = this.props - - // Only shows the page if IPFS is ready or if the initialization has failed. - // This way we can make sure we always use user's own node if available and - // the public gateway otherwise. - const ready = ipfsReady || ipfsInitFailed - - return ( -
-
-
-
- { ready && } -
-
-
-
- ) - } -} - -export default connect( - 'selectRoute', - 'selectIpfsReady', - 'selectIpfsInitFailed', - 'doUpdateUrl', - 'doInitIpfs', - App -) diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..34fb240 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,27 @@ +// Components +import Header from './components/header/Header' +import Footer from './components/footer/Footer' + +// Styles +import './App.css' +import { useHelia } from './hooks/useHelia' +import { Page } from './page/Page' + +export const App = () => { + const { starting, error } = useHelia() + + // Only shows the page if IPFS is ready or if the initialization has failed. + const ready = !starting || error + + return ( +
+
+
+
+ { ready && } +
+
+
+
+ ) +} diff --git a/src/bundles/app-idle.js b/src/bundles/app-idle.js deleted file mode 100644 index cdb4709..0000000 --- a/src/bundles/app-idle.js +++ /dev/null @@ -1,44 +0,0 @@ -// Extracted from https://github.com/HenrikJoreteg/redux-bundler/blob/c1b8ce7629ee6c389f4155b3254e530bd09c868e/src/bundles/create-reactor-bundle.js -import { debounce, ric, raf } from 'redux-bundler' - -const defaults = { - idleTimeout: 2000, - idleAction: 'APP_IDLE', - stopWhenTabInactive: true -} - -const ricOptions = { timeout: 500 } - -export const getIdleDispatcher = (stopWhenInactive, timeout, fn) => - debounce(() => { - // the requestAnimationFrame ensures it doesn't run when tab isn't active - stopWhenInactive ? raf(() => ric(fn, ricOptions)) : ric(fn, ricOptions) - }, timeout) - -const bundle = spec => ({ - name: 'appIdle', - - init: store => { - const opts = Object.assign({}, defaults, spec) - const { idleAction, idleTimeout } = opts - let idleDispatcher - if (idleTimeout) { - idleDispatcher = getIdleDispatcher( - opts.stopWhenTabInactive, - idleTimeout, - () => store.dispatch({ type: idleAction }) - ) - } - - const callback = () => { - if (idleDispatcher) { - idleDispatcher() - } - } - - store.subscribe(callback) - callback() - } -}) - -export default bundle diff --git a/src/bundles/files.js b/src/bundles/files.js deleted file mode 100644 index 94f3605..0000000 --- a/src/bundles/files.js +++ /dev/null @@ -1,491 +0,0 @@ -import { createSelector } from 'redux-bundler' -import shortid from 'shortid' -import toUri from 'multiaddr-to-uri' -// import { makeCIDFromFiles } from '../lib/files' -import ENDPOINTS from '../constants/endpoints' -// import PAGES from '../constants/pages' -import blobToIt from 'blob-to-it' -import { asyncItToFile } from '../components/file/utils/async-it-to-file' - -/** - * @typedef {object} FileState - * @property {string} id - * @property {string} name - * @property {number} size - * @property {number} progress - * @property {boolean} pending - * @property {import('multiformats/cid').CID} [cid] - * @property {true} [published] - * - */ -const initialState = { - files: {}, - limits: { - maxSize: 1073741824, // 1GB - hasExceeded: false, - hasDirs: false - }, - shareLink: { - outdated: false, - link: null, - cid: null - }, - loading: false, - error: null, -} - -const bundle = { - name: 'files', - actionBaseType: 'FILES', - - /* ============================================================ - Reducer - ============================================================ */ - - reducer: (state = initialState, action) => { - switch (action.type) { - case 'FILES_ADD_STARTED': - return { - ...state, - files: { - ...state.files, - ...action.payload.file - } - } - - case 'FILES_ADD_FINISHED': - return { - ...state, - files: { - ...state.files, - [action.payload.id]: { - ...state.files[action.payload.id], - cid: action.payload.cid, - pending: false - } - }, - shareLink: { - ...state.shareLink, - outdated: true - }, - error: null - } - - case 'FILES_ADD_FAILED': - return { - ...state, - files: { - ...state.files, - [action.payload.id]: { - ...state.files[action.payload.id], - pending: false, - error: action.payload.error - } - }, - shareLink: { - ...state.shareLink, - outdated: false - } - } - - case 'FILES_ADD_PUBLISH_FINISHED': - return { - ...state, - files: { - ...state.files, - [action.payload.id]: { - ...state.files[action.payload.id], - published: true, - progress: 100 - } - } - } - case 'FILES_ADD_PUBLISH_ERROR': - return { - ...state, - files: { - ...state.files, - [action.payload.id]: { - ...state.files[action.payload.id], - error: action.payload.error - } - } - } - - case 'FILES_SHARE_LINK': - return { - ...state, - shareLink: { - ...state.shareLink, - link: action.payload.link, - cid: action.payload.cid, - outdated: false - } - } - - case 'FILES_FETCH_STARTED': - return { - ...state, - loading: true - } - - case 'FILES_FETCH_FINISHED': - return { - ...state, - loading: false, - files: { - ...state.files, - ...action.payload.files - }, - shareLink: { - ...state.shareLink, - outdated: true - } - } - - case 'FILES_FETCH_FAILED': - return { - ...state, - loading: false, - files: { - ...state.files, - error: action.payload.error - }, - shareLink: { - ...state.shareLink, - outdated: false - } - } - - case 'FILES_MAX_SIZE_EXCEEDED': - return { - ...state, - limits: { - ...state.limits, - hasExceeded: true - } - } - - case 'FILES_DIR_FOUND': - return { - ...state, - limits: { - ...state.limits, - hasDirs: true - } - } - - case 'FILES_DOWNLOAD_STARTED': - return { - ...state, - files: { - ...state.files, - [action.payload.id]: { - ...state.files[action.payload.id], - progress: 0, - pending: true - } - } - } - - case 'FILES_DOWNLOAD_PROGRESS': - return { - ...state, - files: { - ...state.files, - [action.payload.id]: { - ...state.files[action.payload.id], - progress: action.payload.progress - } - } - } - - case 'FILES_DOWNLOAD_FINISHED': - return { - ...state, - files: { - ...state.files, - [action.payload.id]: { - ...state.files[action.payload.id], - progress: 100, - pending: false - } - }, - error: null - } - - case 'FILES_DOWNLOAD_FAILED': - return { - ...state, - files: { - ...state.files, - [action.payload.id]: { - ...state.files[action.payload.id], - progress: 100, - pending: false - } - } - } - - case 'FILES_RESET': - return initialState - - default: - return state - } - }, - - /* ============================================================ - Selectors - ============================================================ */ - - selectIsLoading: state => state.files.loading, - - selectMaxFileSize: state => state.files.limits.maxSize, - - selectHasExceededMaxSize: state => state.files.limits.hasExceeded, - - selectHasDirs: state => state.files.limits.hasDirs, - - selectFiles: state => state.files.files, - - selectExistFiles: createSelector( - 'selectFiles', - (files) => Object.keys(files).length - ), - - selectPendingFiles: createSelector( - 'selectFiles', - (files) => Object.values(files).filter((file) => file.pending) - ), - - selectExistFilesPending: createSelector( - 'selectPendingFiles', - (pendingFiles) => pendingFiles.length - ), - - selectShareLink: state => state.files.shareLink.link, - - selectShareCID: state => state.files.shareLink.cid, - - selectIsShareLinkOutdated: state => state.files.shareLink.outdated, - - reactGetShareLink: createSelector( - 'selectIsShareLinkOutdated', - 'selectCurrentPage', - 'selectExistFiles', - 'selectExistFilesPending', - (isShareLinkOutdated, currentPage, existFiles, existFilesPending) => { - if (currentPage === 'add' && isShareLinkOutdated && existFiles && !existFilesPending) { - return { actionCreator: 'doShareLink' } - } - } - ), - - /* ============================================================ - Action Creators - ============================================================ */ - - /** - * - * @param {File[]} files - */ - doAddFiles: (files) => async ({ dispatch, getIpfs, getFs }) => { - /** - * @type {import('helia').Helia} - */ - const ipfs = getIpfs() - /** - * @type {import('@helia/mfs').MFS} - */ - const fs = getFs() - - for (const _file of files) { - const fileId = shortid.generate() - const fileName = _file.name - const fileSize = _file.size - - - /** - * @type {Record} - */ - const file = { - [fileId]: { - id: fileId, - name: fileName, - size: fileSize, - progress: 0, - pending: true - } - } - - dispatch({ type: 'FILES_ADD_STARTED', payload: { file } }) - - try { - const content = blobToIt(_file) - - await fs.writeByteStream(content, fileName) - const { cid } = await fs.stat(`/${fileName}`) - - ipfs.routing.provide(cid, { - onProgress: (evt) => { - console.info(`Publish progress "${evt.type}" detail:`, evt.detail) - } - }).then(() => { - dispatch({ type: 'FILES_ADD_PUBLISH_FINISHED', payload: { id: fileId } }) - }) - .catch((/** @type {any} */err) => { - console.error(err) - dispatch({ type: 'FILES_ADD_PUBLISH_ERROR', payload: { id: fileId, error: err.message } }) - }) - dispatch({ type: 'FILES_ADD_FINISHED', payload: { id: fileId, cid } }) - } catch (/** @type {any} */err) { - console.error(err) - dispatch({ type: 'FILES_ADD_FAILED', payload: { id: fileId, error: err.message } }) - } - } - }, - - doShareLink: () => async ({ dispatch, store, getIpfs, getFs }) => { - /** - * @type {import('@helia/mfs').MFS} - */ - const fs = getFs() - const storeShareLink = store.selectShareLink() - - const { cid } = await fs.stat('/') - - const shareLink = `${ENDPOINTS.share}/${cid}` - - if (storeShareLink !== shareLink) { - dispatch({ type: 'FILES_SHARE_LINK', payload: { link: shareLink, cid } }) - } - }, - - /** - * This function fetches the CIDs from the network using Helia and unixfs. - */ - doFetchFileTree: (cid) => async ({ dispatch, store, getIpfs, getUnixFs }) => { - /** - * @type {import('@helia/unixfs').UnixFS} - */ - const fs = getUnixFs() - // const ipfsFiles = [] - const files = {} - - dispatch({ type: 'FILES_SHARE_LINK', payload: { cid } }) - dispatch({ type: 'FILES_FETCH_STARTED' }) - - try { - // determines whether to use the public gateway or the user's node. - if (store.selectIpfsReady()) { - // ipfsFiles = await fs.ls - // for await (const file of fs.ls(cid)) { - // ipfsFiles.push(file) - // } - } else { - // const url = `${ENDPOINTS.api}/v0/ls?arg=${cid}` - // const res = await window.fetch(url) - // const objs = await res.json() - // ipfsFiles = objs.Objects[0].Links - console.error('IPFS not ready') - return - } - - const maxSize = store.selectMaxFileSize() - - for await (const file of fs.ls(cid)) { - const fileId = shortid.generate() - const fileName = file.name - const fileSize = file.size - const fileType = file.type - const fileCid = file.cid - - files[fileId] = { - id: fileId, - name: fileName, - size: fileSize, - type: fileType, - cid: fileCid, - progress: 100, - pending: false - } - - if (fileSize > maxSize) { - dispatch({ type: 'FILES_MAX_SIZE_EXCEEDED' }) - } - - if (fileType === 'directory') { - dispatch({ type: 'FILES_DIR_FOUND' }) - } - } - - dispatch({ type: 'FILES_FETCH_FINISHED', payload: { files: files } }) - } catch (/** @type {any} */err) { - console.error(err) - dispatch({ type: 'FILES_FETCH_FAILED', payload: { error: err.message } }) - } - }, - - doGetFileURL: (filename, cid, opts = { download: true }) => async ({ store }) => { - const url = ENDPOINTS.gateway - - if (!cid) { - const files = Object.values(store.selectFiles()) - cid = files[0].cid - filename = files[0].name - throw Error('No CID provided. fix me') - } - - return { - url: `${url}/${cid}?${opts.download ? 'download=true&' : ''}filename=${encodeURIComponent(filename)}`, - filename - } - }, - - /** - * @deprecated - */ - doGetArchiveURL: (cid) => async ({ store, getIpfs, getFs }) => { - // const ipfs = getIpfs() - const fs = getFs() - const apiAddress = store.selectIpfsApiAddress() - - // Try to use the HTTP API of the local daemon - const url = apiAddress !== null - ? toUri(apiAddress).replace('tcp://', 'http://').concat('/api') - : ENDPOINTS.api - - // If no cid was passed it is to download everything - if (!cid) { - // const files = Object.values(store.selectFiles()) - cid = await fs.stat('/') - } - - return { - url: `${url}/v0/get?arg=${cid}&archive=true&compress=true`, - filename: `shared-via-ipfs_${cid.string.slice(-7)}.tar.gz` - } - }, - - /** - * - * @param {import('multiformats/cid').CID} cid - * @returns - */ - doGetFile: (cid, filename) => async ({ getIpfs, getUnixFs }) => { - /** - * @type {import('@helia/unixfs').UnixFS} - */ - const fs = getUnixFs() - - // const stats = await fs.stat(cid) - const file = asyncItToFile(fs.cat(cid), filename) - - return file - }, - - doResetFiles: () => ({ dispatch }) => dispatch({ type: 'FILES_RESET' }) -} - -export default bundle diff --git a/src/bundles/index.js b/src/bundles/index.js deleted file mode 100644 index fed5683..0000000 --- a/src/bundles/index.js +++ /dev/null @@ -1,16 +0,0 @@ -import { composeBundles, createUrlBundle } from 'redux-bundler' -import ipfsProvider from './ipfs-provider' -import appIdle from './app-idle' -import routesBundle from './routes' -import redirectsBundle from './redirects' -import filesBundle from './files' - -const urlBundle = createUrlBundle - -export default composeBundles( - appIdle({ idleTimeout: 5000 }), - ipfsProvider, - routesBundle, - redirectsBundle, - filesBundle -) diff --git a/src/bundles/ipfs-provider.js b/src/bundles/ipfs-provider.js deleted file mode 100644 index 4861c37..0000000 --- a/src/bundles/ipfs-provider.js +++ /dev/null @@ -1,79 +0,0 @@ -import { createHelia } from 'helia' -import { unixfs } from '@helia/unixfs' -import { mfs } from '@helia/mfs' -import { devToolsMetrics } from '@libp2p/devtools-metrics' - -const initialState = { - failed: false, - ready: false -} - -const reducer = (state = initialState, action) => { - if (action.type === 'IPFS_INIT') { - return { ...state, ready: false, failed: false } - } - if (action.type === 'IPFS_ERRORED') { - return { ...state, ready: false, failed: true } - } - if (action.type === 'IPFS_STARTED') { - return { ...state, ready: true, failed: false } - } - - return state -} - -let ipfs = null -let fs = null - -const extra = { - getIpfs () { - return ipfs - }, - getFs () { - if (fs == null) { - fs = mfs(ipfs) - } - return fs - }, - getUnixFs () { - return unixfs(ipfs) - } -} - -const selectors = { - selectIpfsReady: state => state.ipfs.ready, - selectIpfsProvider: state => state.ipfs.provider, - selectIpfsApiAddress: state => state.ipfs.apiAddress, - selectIpfsInitFailed: state => state.ipfs.failed, -} - -const actions = { - doInitIpfs: () => async ({ getState, dispatch }) => { - dispatch({ type: 'INIT_IPFS' }) - - try { - ipfs = await createHelia({ - libp2p: { - metrics: devToolsMetrics() - } - }) - } catch (err) { - dispatch({ type: 'IPFS_ERRORED' }) - throw Error('Could not connect to JS-IPFS') - } - - dispatch({ type: 'IPFS_STARTED' }) - - return { ipfs } - } -} - -const bundle = { - name: 'ipfs', - reducer, - getExtraArgs: () => extra, - ...selectors, - ...actions -} - -export default bundle diff --git a/src/bundles/redirects.js b/src/bundles/redirects.js deleted file mode 100644 index c340417..0000000 --- a/src/bundles/redirects.js +++ /dev/null @@ -1,16 +0,0 @@ -import { createSelector } from 'redux-bundler' - -const bundle = { - name: 'redirects', - - reactToEmptyHash: createSelector( - 'selectHash', - (hash) => { - if (hash === '') { - return { actionCreator: 'doUpdateHash', args: ['#/'] } - } - } - ) -} - -export default bundle diff --git a/src/bundles/routes.js b/src/bundles/routes.js deleted file mode 100644 index f2a18f5..0000000 --- a/src/bundles/routes.js +++ /dev/null @@ -1,27 +0,0 @@ -import { createRouteBundle, createSelector } from 'redux-bundler' -// import PAGES from '../constants/pages' -import Page from '../page/Page' - - -const bundle = createRouteBundle({ - '/:hash': Page, - '/add/:hash': Page, - '*': Page -}, { routeInfoSelector: 'selectHash' }) - -bundle.selectCurrentPage = createSelector( - 'selectRouteInfo', - ({ url, params }) => (url.startsWith('/add') || !params.hash) ? 'add' : 'download' -) - -bundle.selectFilename = createSelector( - 'selectRouteInfo', - ({ url }) => { - const newUrl = new URL(url, 'http://example.com') - const filename = newUrl.searchParams.get('filename') - - return filename - } -) - -export default bundle diff --git a/src/components/add-files/AddFiles.jsx b/src/components/add-files/AddFiles.tsx similarity index 63% rename from src/components/add-files/AddFiles.jsx rename to src/components/add-files/AddFiles.tsx index 74c294f..e524219 100644 --- a/src/components/add-files/AddFiles.jsx +++ b/src/components/add-files/AddFiles.tsx @@ -1,19 +1,16 @@ -import React from 'react' -import { connect } from 'redux-bundler-react' -import { withTranslation } from 'react-i18next' +import { useTranslation } from 'react-i18next' import { IGNORED_FILES } from '../../constants/files' +import { ChangeEvent } from 'react' -/** - * @returns {File[]} - */ -const parseFiles = (ev) => { +const parseFiles = (ev: ChangeEvent) => { ev.preventDefault() ev.stopPropagation() return Object.values(ev?.target?.files || {}) } -export const AddFiles = ({ t, doAddFiles }) => { +export const AddFiles = ({ doAddFiles }: { doAddFiles: (files: File[]) => void }) => { + const { t } = useTranslation() const onAddFiles = (ev) => { const filesList = parseFiles(ev) doAddFiles(filesList) @@ -29,7 +26,10 @@ export const AddFiles = ({ t, doAddFiles }) => {
) } - -export const TranslatedAddFiles = withTranslation('translation')(AddFiles) - -export default connect('doAddFiles', TranslatedAddFiles) diff --git a/src/components/box/Box.jsx b/src/components/box/Box.jsx deleted file mode 100644 index 67e3a57..0000000 --- a/src/components/box/Box.jsx +++ /dev/null @@ -1,94 +0,0 @@ -import React, { forwardRef } from 'react' -import { withTranslation, Trans } from 'react-i18next' -import { useDrop } from 'react-dnd' -import { NativeTypes } from 'react-dnd-html5-backend' -import { connect } from 'redux-bundler-react' -import classNames from 'classnames' - -// Components -import Loader from '../loader/Loader' -import AddFiles from '../add-files/AddFiles' -import FileTree from '../file-tree/FileTree' -import CopyLink from '../copy-link/CopyLink' -import DownloadFiles from '../download-files/DownloadFiles' - -export const Box = forwardRef(({ children, className }, ref) => ( -
- { children } -
-)) - -export const BoxDownload = ({ files, isLoading, showSizeWarning }) => ( - - { isLoading && } - { showSizeWarning &&
-

- - You may experience issues when downloading files larger than 1GB. Please use IPFS Desktop - or IPFS Companion to do so. - -

-
} - - -
-) - -export const RawBoxAdd = ({ files, isLoading, shareLink, doAddFiles, t }) => { - const [{ isOver }, drop] = useDrop({ - accept: [NativeTypes.FILE], - collect: (monitor) => ({ - isOver: monitor.isOver() - }), - drop: async ({ files }) => { - if (!files) return null - // still can't tell a dir from a file on the web web in 2021 XD https://stackoverflow.com/a/25095250/11518426 - files = files.filter(f => !(!f.type && f.size % 4096 === 0)) - // check: https://react-dnd.github.io/react-dnd/docs/api/use-drop - // this is the handler that lets you call the function `doAddFiles` - doAddFiles(files) - } - }) - - return - - { isLoading && } - - { shareLink && } - -} - -export const BoxAdd = withTranslation('translation')( - connect( - 'doAddFiles', - RawBoxAdd - ) -) - -export const RawBoxNotAvailable = ({ t }) => ( - -

{t('box.missingDaemon')}

-

- - You need a running daemon to add files to IPFS. - -

-

- - Make sure you configure your IPFS API to allow cross-origin (CORS) requests: - -

-
- $ ipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin '["{ window.location.origin }", "https://share.ipfs.io"]' - $ ipfs config --json API.HTTPHeaders.Access-Control-Allow-Methods '["PUT", "GET", "POST"]' -
-

{t('box.runDaemon')}

-
- $ ipfs daemon - Initializing daemon... - API server listening on /ip4/127.0.0.1/tcp/5001 -
-
-) - -export const BoxNotAvailable = withTranslation('translation')(RawBoxNotAvailable) diff --git a/src/components/box/Box.tsx b/src/components/box/Box.tsx new file mode 100644 index 0000000..24992b4 --- /dev/null +++ b/src/components/box/Box.tsx @@ -0,0 +1,89 @@ +import { Trans, useTranslation } from 'react-i18next' +import { useDrop } from 'react-dnd' +import { NativeTypes } from 'react-dnd-html5-backend' +import classNames from 'classnames' + +// Components +import Loader from '../loader/Loader' +import { AddFiles } from '../add-files/AddFiles' +import { FileTree } from '../file-tree/FileTree' +import { CopyLink } from '../copy-link/CopyLink' +import { DownloadFiles } from '../download-files/DownloadFiles' +import { FileState, ShareLinkState } from '../../providers/FilesProvider' +import { useAddFiles, useFilesDispatch } from '../../hooks/useFiles' +import { forwardRef } from 'react' +import { useHelia } from '../../hooks/useHelia' + +export const Box = forwardRef((props, ref) => { + const { children, className } = props + return ( +
+ { children } +
+ ) +}) + +export const BoxDownload = ({ files, isLoading }: {files: Record, isLoading: boolean}) => ( + + { isLoading && } + + + +) + +export const BoxAdd = ({ files, isLoading, shareLink }: { files: Record, isLoading: boolean, shareLink: ShareLinkState }) => { + const dispatch = useFilesDispatch() + const heliaState = useHelia() + const doAddFiles = useAddFiles(dispatch, heliaState) + const [{ isOver }, drop] = useDrop<{files: File[], type: string}, Promise, {isOver: boolean}>({ + accept: [NativeTypes.FILE], + collect: (monitor) => ({ + isOver: monitor.isOver() + }), + drop: async ({ files }) => { + if (!files) return + // still can't tell a dir from a file on the web web in 2021 XD https://stackoverflow.com/a/25095250/11518426 + files = files.filter(f => !(!f.type && f.size % 4096 === 0)) + // check: https://react-dnd.github.io/react-dnd/docs/api/use-drop + // this is the handler that lets you call the function `doAddFiles` + doAddFiles(files) + } + }) + + return + + { isLoading && } + + { shareLink.link && } + +} + +// TODO this text is all outdated +export const BoxNotAvailable = () => { + const { t } = useTranslation('myNamespace'); + return ( + +

{t('box.missingDaemon')}

+

+ + You need a running daemon to add files to IPFS. + +

+

+ + Make sure you configure your IPFS API to allow cross-origin (CORS) requests: + +

+
+ $ ipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin '["{ window.location.origin }", "https://share.ipfs.io"]' + $ ipfs config --json API.HTTPHeaders.Access-Control-Allow-Methods '["PUT", "GET", "POST"]' +
+

{t('box.runDaemon')}

+
+ $ ipfs daemon + Initializing daemon... + API server listening on /ip4/127.0.0.1/tcp/5001 +
+
+ ) +} diff --git a/src/components/copy-link/CopyLink.jsx b/src/components/copy-link/CopyLink.jsx deleted file mode 100644 index 4fcd26d..0000000 --- a/src/components/copy-link/CopyLink.jsx +++ /dev/null @@ -1,78 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' -import { withTranslation } from 'react-i18next' -import classnames from 'classnames' -import { CopyToClipboard } from 'react-copy-to-clipboard' -import QRCode from 'qrcode.react' - -class CopyLink extends React.Component { - static propTypes = { - shareLink: PropTypes.string.isRequired, - withLabel: PropTypes.bool.isRequired - } - - static defaultProps = { - shareLink: '', - withLabel: true - } - - state = { - copied: false - } - - render () { - const { shareLink, t } = this.props - const copyBtnClass = classnames({ - 'o-50 no-pointer-events': this.state.copied, - 'o-80 glow pointer': !this.state.copied - }, ['pa2 w3 flex items-center justify-center br-pill bg-navy f7 white']) - - return ( -
- { this.props.withLabel &&
{t('copyLink.labelAll')}
} -
- {t('copyLink.footNote')} -
-
-
- { shareLink } -
- -
- { this.state.copied ? t('copyLink.copied') : t('copyLink.copy') } -
-
-
-
-
- {t('copyLink.qrLabel')} - -
-
-
- ) - } - - handleOnCopyClick = () => { - this.setState( - { copied: true }, - () => setTimeout(() => this.setState({ copied: false }), 2500) - ) - } -} - -export default withTranslation('translation')(CopyLink) diff --git a/src/components/copy-link/CopyLink.tsx b/src/components/copy-link/CopyLink.tsx new file mode 100644 index 0000000..0097a69 --- /dev/null +++ b/src/components/copy-link/CopyLink.tsx @@ -0,0 +1,58 @@ +import { useCallback, useState } from 'react' +import { useTranslation } from 'react-i18next' +import classnames from 'classnames' +import CopyToClipboard from 'react-copy-to-clipboard' +import QRCode from 'qrcode.react' + +export const CopyLink = ({shareLink, withLabel}: {shareLink: string, withLabel?: boolean}) => { + const [copied, setCopied] = useState(false) + const { t } = useTranslation() + const handleOnCopyClick = useCallback(() => { + setCopied(true) + setTimeout(() => setCopied(false), 2500) + }, []) + const copyBtnClass = classnames({ + 'o-50 no-pointer-events': copied, + 'o-80 glow pointer': !copied + }, ['pa2 w3 flex items-center justify-center br-pill bg-navy f7 white']) + + return ( +
+ { withLabel &&
{t('copyLink.labelAll')}
} +
+ {t('copyLink.footNote')} +
+
+
+ { shareLink } +
+ +
+ { copied ? t('copyLink.copied') : t('copyLink.copy') } +
+
+
+
+
+ {t('copyLink.qrLabel')} + +
+
+
+ ) +} + diff --git a/src/components/download-files/DownloadFiles.jsx b/src/components/download-files/DownloadFiles.jsx deleted file mode 100644 index 4a84b5e..0000000 --- a/src/components/download-files/DownloadFiles.jsx +++ /dev/null @@ -1,83 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' -import { connect } from 'redux-bundler-react' -import { withTranslation } from 'react-i18next' -import classnames from 'classnames' -import { CircularProgressbar } from 'react-circular-progressbar' -import downloadArchive from '../file/utils/archive' -// import downloadFile from '../file/utils/download' - -// Styles -import 'react-circular-progressbar/dist/styles.css' -import './DownloadFiles.css' - -export class DownloadFiles extends React.Component { - static propTypes = { - doGetArchiveURL: PropTypes.func, - doGetFileURL: PropTypes.func, - length: PropTypes.number - } - - state = { - progress: 100 - } - - handleOnClick = async () => { - const { doGetArchiveURL, doGetFileURL, existFiles } = this.props - // const fs = selectUnixFs() - // console.log('download click fs', fs) - - - if (existFiles === 1) { - // just create a file object from Helia unixfs - // const { url, filename } = await doGetFileURL() - // downloadFile(url, filename) - alert('FIX_ME') - } else { - // create a directory - const { url, filename } = await doGetArchiveURL() - const updater = (progress) => this.setState({ progress: progress }) - downloadArchive(url, filename, updater) - } - } - - render () { - const { existFiles, isLoading, t } = this.props - const { progress } = this.state - const btnClass = classnames({ - 'ba b--navy bg-white navy no-pointer-events': progress !== 100, - 'bg-navy white glow pointer': progress === 100, - 'no-pointer-events o-50': isLoading, - 'o-80': !isLoading - }, ['DownloadFilesButton w-100-ns pv2 ph4 mb2 mt3 flex justify-center items-center br-pill f4 montserrat']) - - return ( -
- -
- ) - } -} - -export const TranslatedDownloadFiles = withTranslation('translation')(DownloadFiles) - -export default connect( - 'selectIsLoading', - 'selectExistFiles', - 'doGetArchiveURL', - 'doGetFileURL', - TranslatedDownloadFiles -) diff --git a/src/components/download-files/DownloadFiles.tsx b/src/components/download-files/DownloadFiles.tsx new file mode 100644 index 0000000..9f1d972 --- /dev/null +++ b/src/components/download-files/DownloadFiles.tsx @@ -0,0 +1,62 @@ +import { useCallback, useState } from 'react' +import { useTranslation } from 'react-i18next' +import classnames from 'classnames' +import { CircularProgressbar } from 'react-circular-progressbar' +import downloadArchive from '../file/utils/archive' +import { FileState } from '../../providers/FilesProvider' +// import downloadFile from '../file/utils/download' + +// Styles +import 'react-circular-progressbar/dist/styles.css' +import './DownloadFiles.css' + +export const DownloadFiles = ({files, isLoading}: {files: Record, isLoading: boolean}) => { + const [progress, setProgress] = useState(100) + + const handleOnClick = useCallback(async () => { + // const fs = selectUnixFs() + // console.log('download click fs', fs) + + if (Object.keys(files).length === 1) { + // just create a file object from Helia unixfs + // const { url, filename } = await doGetFileURL() + // downloadFile(url, filename) + alert('FIX_ME') + } else { + console.log({files}) + // create a directory + // TODO + // @ts-expect-error + const { url, filename } = await doGetArchiveURL() + const updater = (progress) => setProgress(progress) + downloadArchive(url, filename, updater) + } + }, [files]) + + const { t } = useTranslation() + const btnClass = classnames({ + 'ba b--navy bg-white navy no-pointer-events': progress !== 100, + 'bg-navy white glow pointer': progress === 100, + 'no-pointer-events o-50': isLoading, + 'o-80': !isLoading + }, ['DownloadFilesButton w-100-ns pv2 ph4 mb2 mt3 flex justify-center items-center br-pill f4 montserrat']) + + return ( +
+ +
+ ) +} diff --git a/src/components/file-tree/FileTree.jsx b/src/components/file-tree/FileTree.jsx deleted file mode 100644 index 2e724b2..0000000 --- a/src/components/file-tree/FileTree.jsx +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react' -import { withTranslation, Trans } from 'react-i18next' - -// Components -import File from '../file/File' - -const FileTree = ({ files, isDownload }) => { - const filesMap = Object.entries(files) - - return ( -
- {!isDownload && filesMap.length > 1 && ( -
- Share individual files: -
)} - { files - ? Object.entries(files).map(([id, file]) => - - ) - : null } -
) -} -export default withTranslation('translation')(FileTree) diff --git a/src/components/file-tree/FileTree.tsx b/src/components/file-tree/FileTree.tsx new file mode 100644 index 0000000..a040654 --- /dev/null +++ b/src/components/file-tree/FileTree.tsx @@ -0,0 +1,26 @@ +import { useTranslation, Trans } from 'react-i18next' + +// Components +import { File } from '../file/File' +import { FileState } from '../../providers/FilesProvider' + +export const FileTree = ({ files, isDownload }: { files: Record, isDownload?: boolean }) => { + const filesMap = Object.entries(files) + const { t } = useTranslation() + + return ( +
+ {!isDownload && filesMap.length > 1 && ( +
+ Share individual files: +
)} + { + filesMap.map(([id, file]) => + + ) + } +
) +} diff --git a/src/components/file/File.jsx b/src/components/file/File.jsx deleted file mode 100644 index 9cde489..0000000 --- a/src/components/file/File.jsx +++ /dev/null @@ -1,204 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' -// import filesize from 'filesize' -import { CircularProgressbar } from 'react-circular-progressbar' -import classnames from 'classnames' -import { connect } from 'redux-bundler-react' -import { withTranslation } from 'react-i18next' -import viewFile from './utils/view' -import downloadFile from './utils/download' -import downloadArchive from './utils/archive' -import ENDPOINTS from '../../constants/endpoints' -import { CopyToClipboard } from 'react-copy-to-clipboard' - -// Components -import FileIcon from './file-icon/FileIcon' - -// Styles -import './File.css' -import 'react-circular-progressbar/dist/styles.css' - -// Static -import GlyphTick from '../../media/icons/GlyphTick' -import GlyphCancel from '../../media/icons/GlyphCancel' -import IconDownload from '../../media/icons/Download' -import IconView from '../../media/icons/View' -import GlyphAttention from '../../media/icons/GlyphAttention' - -export class File extends React.Component { - static propTypes = { - id: PropTypes.string, - cid: PropTypes.object, - name: PropTypes.string, - type: PropTypes.string, - size: PropTypes.number, - progress: PropTypes.number, - error: PropTypes.string, - gatewayURL: PropTypes.string, - isDownload: PropTypes.bool, - doGetFileURL: PropTypes.func, - doGetArchiveURL: PropTypes.func, - filename: PropTypes.string, - } - - state = { - progress: 100 - } - - handleViewClick = async () => { - const { cid, name, doGetFileURL, filename } = this.props - const { url } = await doGetFileURL(name, cid, { download: false }) - viewFile(url, filename) - } - - handleDownloadClick = async () => { - const { cid, name, type, doGetArchiveURL, doGetFile, filename } = this.props - console.log('handleDownloadClick cid, filename', cid, filename) - /** - * @type {import('@helia/unixfs').UnixFS} - */ - // const fs = selectUnixFs() - - if (type === 'dir') { - const { url } = await doGetArchiveURL(cid) - const updater = (progress) => this.setState({ progress: progress }) - return downloadArchive(url, filename, updater) - } - - // const file = new globalThis.File() - // for await (const buf of fs.cat(cid)) { - - // } - // const { url, filename } = await doGetFileURL(name, cid) - // const filename = selectFileName() - const file = await doGetFile(cid, filename) - console.log(file) - const url = URL.createObjectURL(file) - console.log(url, name) - downloadFile(url, name) - alert('FIX_ME') - } - - renderWarningSign = () => { - const { isDownload, size, maxFileSize } = this.props - - if (isDownload && size > maxFileSize) { - return - } - } - - renderFileStatus = () => { - const { isDownload, type, error } = this.props - const progress = isDownload && type === 'dir' ? this.state.progress : this.props.progress - const fillColor = isDownload ? '#3e6175' : '#69c4cd' - const glyphWidth = 25 - - if (isDownload && progress === 100) { - return
- { this.renderWarningSign() } - - -
- } else if (error) { - return - } else if (progress === 100) { - return - } else { - return ( -
- { this.renderWarningSign() } - -
- ) - } - } - - renderCopyButton = ({ url, t }) => { - if (this.props.isDownload) { - return null - } - - const copyBtnClass = classnames({ - 'o-50 no-pointer-events': this.state.copied, - 'o-80 glow pointer': !this.state.copied - }, ['pa2 w3 flex items-center justify-center br-pill bg-aqua f7 fw5']) - - return ( - -
- {this.state.copied ? t('copyLink.copied') : t('copyLink.copy')} -
-
- ) - } - - render () { - const { name, type, cid, error, t } = this.props - console.log('cid', cid) - const size = Number(BigInt(this.props.size ?? 0)) - - const fileNameClass = classnames({ charcoal: !error, gray: error }, ['FileLinkName ph2 f6 b truncate']) - const fileSizeClass = classnames({ 'charcoal-muted': !error, gray: error }, ['f6']) - - const url = cid ? `${ENDPOINTS.gateway}/${cid}?filename=${encodeURI(name)}` : undefined - - return ( -
- -
- -
- {name} - {size && `(~${size})`} -
-
- { this.renderFileStatus() } - { this.renderCopyButton({ url, t }) } -
-
- ) - } - - handleOnCopyClick = () => { - this.setState( - { copied: true }, - () => setTimeout(() => this.setState({ copied: false }), 2500) - ) - } -} - -export const TranslatedFile = withTranslation('translation')(File) - -export default connect( - 'selectMaxFileSize', - 'selectShareCID', - 'doGetFileURL', - 'doGetArchiveURL', - 'doGetFile', - 'selectFilename', - TranslatedFile -) diff --git a/src/components/file/File.tsx b/src/components/file/File.tsx new file mode 100644 index 0000000..68b5e0a --- /dev/null +++ b/src/components/file/File.tsx @@ -0,0 +1,156 @@ +import { useCallback, useState } from 'react' +import { CircularProgressbar } from 'react-circular-progressbar' +import classnames from 'classnames' +import { useTranslation } from 'react-i18next' +import viewFile from './utils/view' +import downloadFile from './utils/download' +import ENDPOINTS from '../../constants/endpoints' +import { CopyToClipboard } from 'react-copy-to-clipboard' + +// Components +import FileIcon from './file-icon/FileIcon' + +// Styles +import './File.css' +import 'react-circular-progressbar/dist/styles.css' + +// Static +import GlyphTick from '../../media/icons/GlyphTick' +import GlyphCancel from '../../media/icons/GlyphCancel' +import IconDownload from '../../media/icons/Download' +import IconView from '../../media/icons/View' +import GlyphAttention from '../../media/icons/GlyphAttention' +import { FileState } from '../../providers/FilesProvider' +import { doGetFile, doGetFileURL } from '../../util' +import { useHelia } from '../../hooks/useHelia' + +export const File = ({file, isDownload}: {file: FileState, isDownload?: boolean}) => { + const [progress, setProgress] = useState(100) + const [copied, setCopied] = useState(false) + const { unixfs } = useHelia() + const { cid, name, size, error } = file + + const handleViewClick = useCallback(async () => { + if (!cid) return + const { url } = doGetFileURL(name, cid, { download: false }) + viewFile(url) + }, [file]) + + const handleDownloadClick = useCallback(async () => { + console.log('handleDownloadClick cid, filename', cid, name) + if (!unixfs || !cid) return + + const file = await doGetFile(unixfs, cid, name) + console.log(file) + const url = URL.createObjectURL(file) + console.log(url, name) + downloadFile(url, name) + alert('FIX_ME') + }, [file]) + + const handleOnCopyClick = useCallback(() => { + setCopied(true) + setTimeout(() => setCopied(false), 2500) + }, [copied]) + + const renderWarningSign = useCallback(() => { + // TODO make more robust + const maxFileSize = 1024 * 1024 * 1024 // 1GB + if (isDownload && file.size > maxFileSize) { + return + } + }, [file, isDownload]) + + const renderFileStatus = useCallback(() => { + const _progress = isDownload ? progress : file.progress + const fillColor = isDownload ? '#3e6175' : '#69c4cd' + const glyphWidth = 25 + + if (isDownload && _progress === 100) { + return
+ { renderWarningSign() } + + +
+ } else if (error) { + return + } else if (progress === 100) { + return + } else { + return ( +
+ { renderWarningSign() } + +
+ ) + } + }, [file, progress, isDownload]) + + const renderCopyButton = useCallback(({ url, t }) => { + if (isDownload) { + return null + } + + const copyBtnClass = classnames({ + 'o-50 no-pointer-events': copied, + 'o-80 glow pointer': !copied + }, ['pa2 w3 flex items-center justify-center br-pill bg-aqua f7 fw5']) + + return ( + +
+ {copied ? t('copyLink.copied') : t('copyLink.copy')} +
+
+ ) + }, [isDownload, copied]) + + const { t } = useTranslation() + console.log('cid', cid) + + const fileNameClass = classnames({ charcoal: !error, gray: error }, ['FileLinkName ph2 f6 b truncate']) + const fileSizeClass = classnames({ 'charcoal-muted': !error, gray: error }, ['f6']) + + const url = cid ? new URL(`/#${cid}?filename=${encodeURI(name)}`, window.location.href).toString() : undefined + + return ( +
+ +
+ {/* TODO figure out how type was passed thru */} + +
+ {name} + {size && `(~${size})`} +
+
+ { renderFileStatus() } + { renderCopyButton({ url, t }) } +
+
+ ) +} diff --git a/src/components/file/utils/archive.js b/src/components/file/utils/archive.ts similarity index 73% rename from src/components/file/utils/archive.js rename to src/components/file/utils/archive.ts index 9861057..95cf94e 100644 --- a/src/components/file/utils/archive.js +++ b/src/components/file/utils/archive.ts @@ -1,4 +1,4 @@ -const archive = (url, filename, progressCallback) => { +const archive = (url: string, filename: string, progressCallback: (n: number | null) => void) => { const xhr = new window.XMLHttpRequest() let total = 0 @@ -21,7 +21,6 @@ const archive = (url, filename, progressCallback) => { progressCallback(100) document.body.appendChild(a) - a.style = 'display:none' a.href = url a.download = filename a.click() @@ -30,7 +29,7 @@ const archive = (url, filename, progressCallback) => { } xhr.onprogress = (e) => { - total = e.lengthComputable ? e.total : (total || xhr.getResponseHeader('X-Content-Length') || xhr.getResponseHeader('Content-Length')) + total = e.lengthComputable ? e.total : (total || Number(xhr.getResponseHeader('X-Content-Length') || xhr.getResponseHeader('Content-Length') || 0)) progressCallback((e.loaded / total) * 100) } diff --git a/src/components/file/utils/async-it-to-file.js b/src/components/file/utils/async-it-to-file.ts similarity index 75% rename from src/components/file/utils/async-it-to-file.js rename to src/components/file/utils/async-it-to-file.ts index c75d4b1..465b74c 100644 --- a/src/components/file/utils/async-it-to-file.js +++ b/src/components/file/utils/async-it-to-file.ts @@ -3,11 +3,8 @@ import toBrowserReadableStream from 'it-to-browser-readablestream' /** * This function takes an async iterable, and creates a file object that * does not load the full content of the AsyncIterable into memory. - * - * @param {AsyncIterable} asyncIt - * @returns {Promise} */ -export async function asyncItToFile (asyncIt, filename) { +export async function asyncItToFile (asyncIt: AsyncIterable, filename: string) { const stream = toBrowserReadableStream(asyncIt) const responseFromStream = new Response(stream) const blob = await responseFromStream.blob() diff --git a/src/components/file/utils/download.js b/src/components/file/utils/download.ts similarity index 71% rename from src/components/file/utils/download.js rename to src/components/file/utils/download.ts index 2481e79..d199621 100644 --- a/src/components/file/utils/download.js +++ b/src/components/file/utils/download.ts @@ -1,9 +1,8 @@ -const download = (url, fileName) => { +const download = (url: string, fileName: string) => { const link = document.createElement('a') link.setAttribute('href', url) link.setAttribute('download', fileName) - // link.setAttribute() link.click() } diff --git a/src/components/file/utils/extToType.js b/src/components/file/utils/extToType.ts similarity index 100% rename from src/components/file/utils/extToType.js rename to src/components/file/utils/extToType.ts diff --git a/src/components/file/utils/view.js b/src/components/file/utils/view.ts similarity index 76% rename from src/components/file/utils/view.js rename to src/components/file/utils/view.ts index a229b85..6d26b0a 100644 --- a/src/components/file/utils/view.js +++ b/src/components/file/utils/view.ts @@ -1,4 +1,4 @@ -const view = (url, fileName) => { +const view = (url: string, fileName?: any) => { const link = document.createElement('a') link.setAttribute('href', url) diff --git a/src/components/headline/Headline.jsx b/src/components/headline/Headline.jsx index 210859c..45b3f1e 100644 --- a/src/components/headline/Headline.jsx +++ b/src/components/headline/Headline.jsx @@ -1,8 +1,6 @@ -import React from 'react' -import { connect } from 'redux-bundler-react' import { withTranslation } from 'react-i18next' -const Headline = ({ t, imgHeight = 70, isDownload, ipfsProvider }) => { +const Headline = ({ t, imgHeight = 70, isDownload }) => { // Download page if (isDownload) { return ( @@ -22,9 +20,4 @@ const Headline = ({ t, imgHeight = 70, isDownload, ipfsProvider }) => { ) } -export const TranslatedHeadline = withTranslation('translation')(Headline) - -export default connect( - 'selectIpfsProvider', - TranslatedHeadline -) +export default withTranslation('translation')(Headline) diff --git a/src/components/info/Info.jsx b/src/components/info/Info.jsx deleted file mode 100644 index db18eb9..0000000 --- a/src/components/info/Info.jsx +++ /dev/null @@ -1,221 +0,0 @@ -import React from 'react' -import { connect } from 'redux-bundler-react' -import { withTranslation, Trans } from 'react-i18next' - -import Modal from '../modal/Modal' - -class Info extends React.Component { - // Modals created by links - constructor () { - super() - this.state = { - showModalReprovide: false, - showModalPrivacy: false, - showModalHow: false, - showModalCid: false - } - - this.handleOpenModalHow = this.handleOpenModalHow.bind(this) - this.handleCloseModalHow = this.handleCloseModalHow.bind(this) - this.handleOpenModalCid = this.handleOpenModalCid.bind(this) - this.handleCloseModalCid = this.handleCloseModalCid.bind(this) - this.handleOpenModalReprovide = this.handleOpenModalReprovide.bind(this) - this.handleCloseModalReprovide = this.handleCloseModalReprovide.bind(this) - this.handleOpenModalPrivacy = this.handleOpenModalPrivacy.bind(this) - this.handleCloseModalPrivacy = this.handleCloseModalPrivacy.bind(this) - } - - handleOpenModalHow () { - this.setState({ showModalHow: true }) - } - - handleCloseModalHow () { - this.setState({ showModalHow: false }) - } - - handleOpenModalCid () { - this.setState({ showModalCid: true }) - } - - handleCloseModalCid () { - this.setState({ showModalCid: false }) - } - - handleCloseModalReprovide () { - this.setState({ showModalReprovide: false }) - } - - handleOpenModalReprovide () { - this.setState({ showModalReprovide: true }) - } - - handleOpenModalPrivacy () { - this.setState({ showModalPrivacy: true }) - } - - handleCloseModalPrivacy () { - this.setState({ showModalPrivacy: false }) - } - - render () { - // imgHeight = 70 was unused - const { t, isDownload, ipfsProvider } = this.props - // Classes and styles - const iconContainerClass = 'mr3 fill-aqua' - const labelClass = 'f4 fw1 mb2 ml1 white montserrat' - const descriptionClass = 'f6 ml1 gray-muted lh-copy' - const anchorClass = 'no-underline underline-hover aqua bg-transparent pointer bn pa0' - const anchorStyle = { outline: 'none' } - // Links - const howLink = - const cidLink = - const reprovideLink = - const privacyLink = - - const isUsingDaemon = ipfsProvider === 'js-ipfs-api' - - // Info for the Download page - if (isDownload) { - return ( -
-
-
-
-
- -
-
-
- {t('info.download.labelHow')} -
-
- {t('info.download.copyHow')} {howLink} -
-
-
- -
-
- -
-
-
- {t('info.download.labelKeep')} -
-
- {isUsingDaemon ? t('info.download.copyKeepDaemon') : t('info.download.copyKeepPage')} {reprovideLink} -
-
-
-
-
- -

{t('modal.how.copy')}

-

- Learn how IPFS is changing the internet by visiting the IPFS website. Curious about how content addressing works? Visit ProtoSchool for tutorials and other resources. -

-
- -

{t('modal.reprovide.copy')}

-

- Curious how this works under the hood? Check out the official IPFS documentation for everything from basic explainers to developer tools. -

-
-
- ) - } - - // Info for the Add page - return ( -
-
-
-
-
- -
-
-
- {t('info.add.labelAdd')} -
-
- {t('info.add.copyAdd')} {privacyLink} -
-
-
- -
-
-
-
-
- {isUsingDaemon ? t('info.add.labelKeepDaemon') : t('info.add.labelKeepPage')} -
-
- {isUsingDaemon ? t('info.add.copyKeepDaemon') : t('info.add.copyKeepPage')} -
-
-
-
-
-
-
-
- {t('info.add.labelSend')} -
-
- {t('info.add.copySend')} {cidLink} -
-
-
-
-
-
- -

{t('modal.privacy.copy')}

-

- Want to learn more about IPFS? Visit the website for the basics, or dig into the details of IPFS and privacy in the official documentation. -

-
- -

{t('modal.cid.copy')}

-

- Want to dig deeper into how content identifiers work? Visit ProtoSchool for tutorials and other resources on the decentralized web tech that makes IPFS tick. -

-
-
-
- ) - } -} - -export const TranslatedInfo = withTranslation('translation')(Info) - -export default connect( - 'selectIpfsProvider', - TranslatedInfo -) diff --git a/src/components/info/Info.stories.js b/src/components/info/Info.stories.js index 0588963..4cb16ec 100644 --- a/src/components/info/Info.stories.js +++ b/src/components/info/Info.stories.js @@ -2,7 +2,7 @@ import React from 'react' import { storiesOf } from '@storybook/react' import { checkA11y } from '@storybook/addon-a11y' import i18n from '../../i18n-decorator' -import { TranslatedInfo as Info } from './Info' +import { Info } from './Info' storiesOf('Info', module) .addDecorator(checkA11y) diff --git a/src/components/info/Info.tsx b/src/components/info/Info.tsx new file mode 100644 index 0000000..85605ac --- /dev/null +++ b/src/components/info/Info.tsx @@ -0,0 +1,197 @@ +import { Trans, useTranslation } from 'react-i18next' + +import Modal from '../modal/Modal' +import { useCallback, useState } from 'react' + +export const Info = ({isDownload}: {isDownload?: boolean}) => { + const { t } = useTranslation() + // Modals created by links + const [showModalReprovide, setShowModalReprovide] = useState(false) + const [showModalPrivacy, setShowModalPrivacy] = useState(false) + const [showModalHow, setShowModalHow] = useState(false) + const [showModalCid, setShowModalCid] = useState(false) + + const handleOpenModalHow = useCallback(() => { + setShowModalHow(true) + }, []) + + const handleCloseModalHow = useCallback(() => { + setShowModalHow(false) + }, []) + + const handleOpenModalCid = useCallback(() => { + setShowModalCid(true) + }, []) + + const handleCloseModalCid = useCallback(() => { + setShowModalCid(false) + }, []) + + const handleCloseModalReprovide = useCallback(() =>{ + setShowModalReprovide(false) + }, []) + + const handleOpenModalReprovide = useCallback(() => { + setShowModalReprovide(true) + }, []) + + const handleOpenModalPrivacy = useCallback(() => { + setShowModalPrivacy(true) + }, []) + + const handleCloseModalPrivacy = useCallback(() => { + setShowModalPrivacy(false) + }, []) + + // imgHeight = 70 was unused + // Classes and styles + const iconContainerClass = 'mr3 fill-aqua' + const labelClass = 'f4 fw1 mb2 ml1 white montserrat' + const descriptionClass = 'f6 ml1 gray-muted lh-copy' + const anchorClass = 'no-underline underline-hover aqua bg-transparent pointer bn pa0' + const anchorStyle = { outline: 'none' } + // Links + const howLink = + const cidLink = + const reprovideLink = + const privacyLink = + + const isUsingDaemon = false + + // Info for the Download page + if (isDownload) { + return ( +
+
+
+
+
+ +
+
+
+ {t('info.download.labelHow')} +
+
+ {t('info.download.copyHow')} {howLink} +
+
+
+ +
+
+ +
+
+
+ {t('info.download.labelKeep')} +
+
+ {isUsingDaemon ? t('info.download.copyKeepDaemon') : t('info.download.copyKeepPage')} {reprovideLink} +
+
+
+
+
+ +

{t('modal.how.copy')}

+

+ Learn how IPFS is changing the internet by visiting the IPFS website. Curious about how content addressing works? Visit ProtoSchool for tutorials and other resources. +

+
+ +

{t('modal.reprovide.copy')}

+

+ Curious how this works under the hood? Check out the official IPFS documentation for everything from basic explainers to developer tools. +

+
+
+ ) + } + + // Info for the Add page + return ( +
+
+
+
+
+ +
+
+
+ {t('info.add.labelAdd')} +
+
+ {t('info.add.copyAdd')} {privacyLink} +
+
+
+ +
+
+
+
+
+ {isUsingDaemon ? t('info.add.labelKeepDaemon') : t('info.add.labelKeepPage')} +
+
+ {isUsingDaemon ? t('info.add.copyKeepDaemon') : t('info.add.copyKeepPage')} +
+
+
+
+
+
+
+
+ {t('info.add.labelSend')} +
+
+ {t('info.add.copySend')} {cidLink} +
+
+
+
+
+
+ +

{t('modal.privacy.copy')}

+

+ Want to learn more about IPFS? Visit the website for the basics, or dig into the details of IPFS and privacy in the official documentation. +

+
+ +

{t('modal.cid.copy')}

+

+ Want to dig deeper into how content identifiers work? Visit ProtoSchool for tutorials and other resources on the decentralized web tech that makes IPFS tick. +

+
+
+
+ ) +} diff --git a/src/hooks/useCurrentPage.ts b/src/hooks/useCurrentPage.ts new file mode 100644 index 0000000..ad2d30e --- /dev/null +++ b/src/hooks/useCurrentPage.ts @@ -0,0 +1,16 @@ +import { cid } from "is-ipfs" +import { useHashLocation } from "wouter/use-hash-location" + +/** + * * `(?<=\/)` — Positive lookbehind to ensure that the match is preceded by / without including it in the result. + * * `[^\/?]+` — Matches one or more characters that are not / or ?. + * * `(?=\?|$)` — Positive lookahead to ensure that the match is followed by either a ? or the end of the string ($). + */ +const cidRegex = /(?<=\/)[^\/?]+(?=\?|$)/ + +export const useCurrentPage = () => { + const [location] = useHashLocation() + const maybeCid = location.match(cidRegex)?.[0] ?? '' + if (location.startsWith("/add") || !cid(maybeCid)) return "add" + return "download" +} diff --git a/src/hooks/useFiles.ts b/src/hooks/useFiles.ts new file mode 100644 index 0000000..753db25 --- /dev/null +++ b/src/hooks/useFiles.ts @@ -0,0 +1,61 @@ +import { Dispatch, useContext } from "react" +import shortid from 'shortid' +import { AddFileState, FilesAction, FilesContext, FilesDispatchContext } from "../providers/FilesProvider" +import blobToIt from "blob-to-it" +import { HeliaContextType } from "../providers/HeliaProvider" + +export function useFiles () { + return useContext(FilesContext) +} + +export function useFilesDispatch () { + return useContext(FilesDispatchContext) +} + +export function useAddFiles (dispatch: Dispatch, heliaState: HeliaContextType) { + if (heliaState.starting) { + throw new Error('Helia not active') + } + const { helia, mfs } = heliaState + + return (files: File[]) => { + for (const _file of files) { + const id: string = shortid.generate() + const name = _file.name + + const file: AddFileState = { + id, + name, + size: _file.size, + progress: 0, + cid: null, + pending: true, + published: false, + } + + Promise.resolve().then(async () => { + dispatch({ type: 'add_start', ...file}) + const content = blobToIt(_file) + await mfs.writeByteStream(content, name) + const { cid } = await mfs.stat(`/${name}`) + dispatch({ type: 'add_success', id: id, cid }) + return cid + }).catch((err: Error) => { + console.error(err) + dispatch({ type: 'add_fail', id: id, error: err }) + throw err + }).then(async (cid) => { + dispatch({ type: 'publish_start', id: id }) + await helia.routing.provide(cid, { + onProgress: (evt) => { + console.info(`Publish progress "${evt.type}" detail:`, evt.detail) + } + }) + dispatch({ type: 'publish_success', id: id, cid }) + }).catch((err: Error) => { + console.error(err) + dispatch({ type: 'publish_fail', id: id, error: err }) + }) + } + } +} \ No newline at end of file diff --git a/src/hooks/useHelia.ts b/src/hooks/useHelia.ts new file mode 100644 index 0000000..e5f63f2 --- /dev/null +++ b/src/hooks/useHelia.ts @@ -0,0 +1,6 @@ +import { useContext } from 'react' +import { HeliaContext } from '../providers/HeliaProvider' + +export const useHelia = () => { + return useContext(HeliaContext) +} diff --git a/src/index.jsx b/src/index.tsx similarity index 67% rename from src/index.jsx rename to src/index.tsx index 217098d..5a41da6 100644 --- a/src/index.jsx +++ b/src/index.tsx @@ -1,22 +1,23 @@ -import React from 'react' import ReactDOM from 'react-dom' -import { Provider } from 'redux-bundler-react' -import App from './App' +import { App } from './App' import registerServiceWorker from './registerServiceWorker' -import getStore from './bundles' import { I18nextProvider } from 'react-i18next' import i18n from './i18n' import { HTML5Backend } from 'react-dnd-html5-backend' import { DndProvider } from 'react-dnd' +import { HeliaProvider } from './providers/HeliaProvider' +import { FilesProvider } from './providers/FilesProvider' ReactDOM.render( - - + + + + + - - , document.getElementById('root')) + , document.getElementById('root')) registerServiceWorker() diff --git a/src/page/Page.jsx b/src/page/Page.jsx deleted file mode 100644 index 36ecc0a..0000000 --- a/src/page/Page.jsx +++ /dev/null @@ -1,97 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' -import { connect } from 'redux-bundler-react' -import { withTranslation } from 'react-i18next' -import { Helmet } from 'react-helmet' -import { cid } from 'is-ipfs' - -// Constants -// import PAGES from '../constants/pages' - -// Components -import { BoxAdd, BoxDownload, BoxNotAvailable } from '../components/box/Box' -import Headline from '../components/headline/Headline' -import Info from '../components/info/Info' - -class Page extends React.Component { - static propTypes = { - routeInfo: PropTypes.object.isRequired, - isLoading: PropTypes.bool.isRequired, - files: PropTypes.object.isRequired, - doFetchFileTree: PropTypes.func.isRequired, - doResetFiles: PropTypes.func.isRequired, - doUpdateHash: PropTypes.func.isRequired, - currentPage: PropTypes.string.isRequired, - ipfsInitFailed: PropTypes.bool.isRequired, - shareLink: PropTypes.string - } - - componentDidUpdate (prevProps) { - this.handleRouting(prevProps) - } - - componentDidMount () { - this.handleRouting() - } - - handleRouting (prevProps) { - const { doUpdateHash, doFetchFileTree, doResetFiles } = this.props - const prevHash = prevProps && prevProps.routeInfo.params.hash - const currentHash = this.props.routeInfo.params.hash - - if (prevHash !== currentHash) { - doResetFiles() - cid(currentHash) ? doFetchFileTree(currentHash) : doUpdateHash('#/') - } - } - - render () { - const { currentPage, ipfsInitFailed, shareLink, files, hasExceededMaxSize, isLoading, t } = this.props - const isDownload = currentPage === 'download' - let headline - let content - let info - - if (isDownload) { - content = - headline = - info = - } else if (ipfsInitFailed) { - content = - headline = isDownload ? : - info = isDownload ? : - } else { - content = - headline = - info = - } - - return ( -
- - {t('pageTitle.ipfs')} | { isDownload ? t('pageTitle.download') : t('pageTitle.add') } - - { headline } - -
- { content } - { info } -
-
- ) - } -} - -export default connect( - 'doFetchFileTree', - 'doResetFiles', - 'doUpdateHash', - 'selectIpfsInitFailed', - 'selectRouteInfo', - 'selectIsLoading', - 'selectHasExceededMaxSize', - 'selectFiles', - 'selectCurrentPage', - 'selectShareLink', - withTranslation('translation')(Page) -) diff --git a/src/page/Page.tsx b/src/page/Page.tsx new file mode 100644 index 0000000..6568944 --- /dev/null +++ b/src/page/Page.tsx @@ -0,0 +1,59 @@ +import { useTranslation } from 'react-i18next' +import { Helmet } from 'react-helmet' + + +// Components +import { BoxAdd, BoxDownload, BoxNotAvailable } from '../components/box/Box' +import Headline from '../components/headline/Headline' +import { Info } from '../components/info/Info' +import { useCurrentPage } from '../hooks/useCurrentPage' +import { useFiles } from '../hooks/useFiles' +import { useHelia } from '../hooks/useHelia' + +export const Page = () => { + const [t] = useTranslation() + const currentPage = useCurrentPage() + const heliaState = useHelia() + const { shareLink, files, fetch } = useFiles() + const isDownload = currentPage === 'download' + let content + if (heliaState.error) { + content = + } else if (isDownload) { + content = + } else { + content = + } + const headline = + const info = + + + // TODO: reimplement this: + // if the hash has changed, reset the files + // and if the hash is a cid, fetch the new file tree + // + // handleRouting (prevProps) { + // const { doUpdateHash, doFetchFileTree, doResetFiles } = this.props + // const prevHash = prevProps && prevProps.routeInfo.params.hash + // const currentHash = this.props.routeInfo.params.hash + + // if (prevHash !== currentHash) { + // doResetFiles() + // cid(currentHash) ? doFetchFileTree(currentHash) : doUpdateHash('#/') + // } + // } + + return ( +
+ + {t('pageTitle.ipfs')} | { isDownload ? t('pageTitle.download') : t('pageTitle.add') } + + { headline } + +
+ { content } + { info } +
+
+ ) +} diff --git a/src/providers/FilesProvider.tsx b/src/providers/FilesProvider.tsx new file mode 100644 index 0000000..361d9c6 --- /dev/null +++ b/src/providers/FilesProvider.tsx @@ -0,0 +1,210 @@ +import { CID } from 'multiformats/cid' +import { createContext, useReducer } from 'react' + +export type AddFileState = { + id: string + name: string + size: number + progress: number + pending: true + cid: null + published: false + error?: undefined +} +export type FileState = AddFileState | { + id: string + name: string + size: number + progress: number + pending: false + cid: CID + published: boolean + error: Error | undefined +} + +export type ShareLinkState = { + outdated: false + link: string + cid: CID +} | { + outdated: true + link: null + cid: null +} + +export type FetchState = { + loading: boolean + error: Error | null +} + +export type FilesState = { + files: Record + shareLink: ShareLinkState + fetch: FetchState +} + +export type FilesAction = + | { type: 'add_start' } & AddFileState + | { type: 'add_success', id: string, cid: CID } + | { type: 'add_fail', id: string, error: Error } + + | { type: 'publish_start', id: string } + | { type: 'publish_success', id: string, cid: CID } + | { type: 'publish_fail', id: string, error: Error } + + | { type: 'share_link', link: string, cid: CID } + + | { type: 'fetch_start' } + | { type: 'fetch_success', files: Record } + | { type: 'fetch_fail', error: Error } + +function filesReducer (state: FilesState, action: FilesAction): FilesState { + switch (action.type) { + case 'add_start': + return { + ...state, + files: { + ...state.files, + [action.id]: { + id: action.id, + name: action.name, + size: action.size, + progress: action.progress, + pending: action.pending, + cid: action.cid, + published: action.published + } + } + } + + case 'add_success': + return { + ...state, + files: { + ...state.files, + [action.id]: { + ...state.files[action.id], + pending: false, + cid: action.cid + } as FileState + } + } + + case 'add_fail': + return { + ...state, + files: { + ...state.files, + [action.id]: { + ...state.files[action.id], + pending: false, + error: action.error + } as FileState + } + } + + case 'publish_start': + return { + ...state, + files: { + ...state.files, + [action.id]: { + ...state.files[action.id], + published: false + } + } + } + + case 'publish_success': + return { + ...state, + files: { + ...state.files, + [action.id]: { + ...state.files[action.id], + published: true + } as FileState + } + } + + case 'publish_fail': + return { + ...state, + files: { + ...state.files, + [action.id]: { + ...state.files[action.id], + error: action.error + } as FileState + } + } + + case 'share_link': + return { + ...state, + shareLink: { + outdated: false, + link: action.link, + cid: action.cid + } + } + + case 'fetch_start': + return { + ...state, + fetch: { + loading: true, + error: null + } + } + + case 'fetch_success': + return { + ...state, + files: { + ...state.files, + ...action.files, + }, + fetch: { + ...state.fetch, + loading: false + } + } + + case 'fetch_fail': + return { + ...state, + fetch: { + loading: false, + error: action.error + } + } + + default: + return state + } +} + +const initialState: FilesState = { + files: {}, + shareLink: { outdated: true, link: null, cid: null }, + fetch: { + loading: false, + error: null + } +} + +export const FilesContext = createContext(initialState) +export const FilesDispatchContext = createContext>(null as any) + +export const FilesProvider = ({ children }) => { + const [state, dispatch] = useReducer(filesReducer, initialState) + + return ( + + + {children} + + + ) +} \ No newline at end of file diff --git a/src/providers/HeliaProvider.tsx b/src/providers/HeliaProvider.tsx new file mode 100644 index 0000000..dfddba1 --- /dev/null +++ b/src/providers/HeliaProvider.tsx @@ -0,0 +1,85 @@ +/* eslint-disable no-console */ + +import { unixfs as _unixfs, UnixFS } from '@helia/unixfs' +import { mfs as _mfs, MFS } from '@helia/mfs' +import { createHelia, HeliaLibp2p } from 'helia' +import PropTypes from 'prop-types' +import { + useEffect, + useState, + useCallback, + createContext +} from 'react' +import { devToolsMetrics } from '@libp2p/devtools-metrics' + +export type HeliaContextType = { + helia: null + unixfs: null + mfs: null + error: boolean + starting: true +} | { + helia: HeliaLibp2p + unixfs: UnixFS + mfs: MFS + error: boolean + starting: false +} + +export const HeliaContext = createContext({ + helia: null, + unixfs: null, + mfs: null, + error: false, + starting: true +}) + +export const HeliaProvider = ({ children }) => { + const [helia, setHelia] = useState(null) + const [unixfs, setUnixfs] = useState(null) + const [mfs, setMfs] = useState(null) + const [starting, setStarting] = useState(true) + const [error, setError] = useState(false) + + const startHelia = useCallback(async () => { + if (helia == null) { + try { + console.info('Starting Helia') + const helia = await createHelia({ + libp2p: { + metrics: devToolsMetrics() + } + }) as HeliaLibp2p + setHelia(helia) + setUnixfs(_unixfs(helia)) + setMfs(_mfs(helia)) + setStarting(false) + } catch (e) { + console.error(e) + setError(true) + } + } + }, []) + + useEffect(() => { + startHelia() + }, []) + + const value = { + helia, + unixfs, + mfs, + error, + starting + } as HeliaContextType + + return ( + + {children} + + ) +} + +HeliaProvider.propTypes = { + children: PropTypes.any +} \ No newline at end of file diff --git a/src/util.ts b/src/util.ts new file mode 100644 index 0000000..9c7e0e6 --- /dev/null +++ b/src/util.ts @@ -0,0 +1,18 @@ +import { CID } from 'multiformats/cid' +import { UnixFS } from '@helia/unixfs' +import { asyncItToFile } from './components/file/utils/async-it-to-file' + +export function doGetFileURL (filename: string, cid: CID, opts = { download: true }) { + return { + url: `#/${cid}?${opts.download ? 'download=true&' : ''}filename=${encodeURIComponent(filename)}`, + filename + } +} + +export function doGetFile (unixfs: UnixFS, cid: CID, filename: string) { + // TODO fix this + // @ts-expect-error types mismatch + const file = asyncItToFile(unixfs.cat(cid), filename) + + return file +}