chore: Migrate from Create React App to Vite (#231)
* chore(vite): use vite * fix(vite): alias lib directory * chore(vite): set type: module * chore: update vite and MUI * fix(vite): make MUI components load * fix: use node path resolution * chore(vite): add svg support * fix(vite): polyfill global * fix(vite): use import.meta * fix(vite): use correct svg module resolution * chore(vite): migrate to vitest * fix(vite): remove PUBLIC_URL * fix(tests): mock audio service * chore(deps): upgrade to react test library 14 * refactor(tests): simplify room test setup * refactor(tests): make Date.now() mockable * refactor(vite): remove bootstrap shim * chore(deps): drop react-scripts * chore(deps): remove source-map-explorer Source maps do not currently work with MUI and Vite: https://github.com/vitejs/vite/issues/15012 Because of this, source map utilities are currently removed. * refactor(vite): use TypeScript for Vite config * chore(actions): update actions config for new paths * fix(service-worker): use VITE_HOMEPAGE for service worker resolution * fix(vercel): use quotes for build command * fix(vite): use import.meta.env.MODE * fix(service-worker): use correct definition for publicUrl * feat(vite): use vite-plugin-pwa * fix(pwa): make update prompt work * fix(types): use vite/client types * docs(readme): update building instructions * refactor(vite): simplify theme loading workaround * refactor(vite): use manifest object * docs(readme): update tool references * chore(deps): run `npm audit fix` * fix(vite): make syntax highlighter work consistently See: https://github.com/react-syntax-highlighter/react-syntax-highlighter/issues/513 * fix(pwa): remove manifest.json references * refactor(deps): remove jest references * refactor(types): remove react-scripts reference * chore(deps): use TypeScript 5 * refactor(tests): improve persisted storage mocking
This commit is contained in:
parent
72bc66a340
commit
ea34058fa7
3
.env
3
.env
@ -1,2 +1 @@
|
||||
REACT_APP_NAME=$npm_package_name
|
||||
REACT_APP_GITHUB_REPO="https://github.com/jeremyckahn/chitchatter"
|
||||
VITE_GITHUB_REPO="https://github.com/jeremyckahn/chitchatter"
|
||||
|
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@ -4,7 +4,6 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
# - '!main'
|
||||
|
||||
jobs:
|
||||
test_and_build:
|
||||
@ -23,4 +22,4 @@ jobs:
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: build-output
|
||||
path: build
|
||||
path: dist
|
||||
|
3
.github/workflows/deploy.yml
vendored
3
.github/workflows/deploy.yml
vendored
@ -24,11 +24,10 @@ jobs:
|
||||
run: |
|
||||
npm ci
|
||||
npm run build
|
||||
npm run analyze -- --html build/bundle-info.html
|
||||
|
||||
- name: Deploy
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
deploy_key: ${{ secrets.DEPLOY_KEY }}
|
||||
publish_dir: ./build
|
||||
publish_dir: ./dist
|
||||
force_orphan: true
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -9,7 +9,8 @@
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
/dist
|
||||
/dev-dist
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
|
@ -17,7 +17,7 @@ Chitchatter is a free (as in both price and freedom) communication tool. Designe
|
||||
- Embeddable
|
||||
- [Self-hostable](#self-hosting)
|
||||
|
||||
Chitchatter uses the [Create React App](https://github.com/facebook/create-react-app) toolchain. The secure networking and streaming magic would not be possible without [Trystero](https://github.com/dmotz/trystero). File transfer functionality is powered by [`secure-file-transfer`](https://github.com/jeremyckahn/secure-file-transfer).
|
||||
Chitchatter uses [Vite](https://vitejs.dev/). The secure networking and streaming magic would not be possible without [Trystero](https://github.com/dmotz/trystero). File transfer functionality is powered by [`secure-file-transfer`](https://github.com/jeremyckahn/secure-file-transfer).
|
||||
|
||||
## Status
|
||||
|
||||
@ -173,11 +173,11 @@ The page will reload when you make changes. You may also see any lint errors in
|
||||
|
||||
### `npm test`
|
||||
|
||||
Launches the test runner in the interactive watch mode. See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
Launches the test runner in the interactive watch mode.
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `build` folder. It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
Builds the app for production to the `dist` folder. It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.
|
||||
|
||||
@ -204,7 +204,7 @@ When hosted on GitHub Pages and the configuration above has been done, the Produ
|
||||
|
||||
##### On non-GitHub hosts
|
||||
|
||||
Build the app with `PUBLIC_URL="https://your-domain-here.com" npm run build`, and then serve the `build` directory. Any static file serving solution should work provided it is using a [secure context](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts).
|
||||
Build the app with `npm pkg set homepage="https://your-domain-here.com" && npm run build` (with `https://your-domain-here.com` substituted for the root URL that Chitchatter will be served from), and then serve the `dist` directory. Any static file serving solution should work provided it is using a [secure context](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts).
|
||||
|
||||
#### Runtime configuration
|
||||
|
||||
|
@ -2,25 +2,20 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="A peer-to-peer chat app that is serverless, decentralized, and ephemeral"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<link rel="apple-touch-icon" href="/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
Notice the use of in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
Unlike "/favicon.ico" or "favicon.ico", "/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
@ -69,5 +64,6 @@
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
91
manifest.ts
Normal file
91
manifest.ts
Normal file
@ -0,0 +1,91 @@
|
||||
import { ManifestOptions } from 'vite-plugin-pwa'
|
||||
|
||||
export const manifest: Partial<ManifestOptions> = {
|
||||
short_name: 'Chitchatter',
|
||||
name: 'Chitchatter',
|
||||
description:
|
||||
'This is a communication tool that is free, open source, and designed for simplicity and security. All communication between you and your online peers is encrypted. There is no trace of your conversation once you leave.',
|
||||
icons: [
|
||||
{
|
||||
src: 'favicon.ico',
|
||||
sizes: '64x64 32x32 24x24 16x16',
|
||||
type: 'image/x-icon',
|
||||
},
|
||||
{
|
||||
src: 'logo192.png',
|
||||
type: 'image/png',
|
||||
sizes: '192x192',
|
||||
},
|
||||
{
|
||||
src: 'logo512.png',
|
||||
type: 'image/png',
|
||||
sizes: '512x512',
|
||||
},
|
||||
],
|
||||
start_url: './',
|
||||
display: 'fullscreen',
|
||||
theme_color: '#000000',
|
||||
background_color: '#222222',
|
||||
screenshots: [
|
||||
{
|
||||
src: 'screenshots/home-desktop.png',
|
||||
sizes: '2160x1620',
|
||||
type: 'image/png',
|
||||
},
|
||||
{
|
||||
src: 'screenshots/public-room-desktop.png',
|
||||
sizes: '2160x1620',
|
||||
type: 'image/png',
|
||||
},
|
||||
{
|
||||
src: 'screenshots/public-room-desktop-with-video.png',
|
||||
sizes: '2160x1620',
|
||||
type: 'image/png',
|
||||
},
|
||||
{
|
||||
src: 'screenshots/home-mobile-dark.png',
|
||||
sizes: '750x1334',
|
||||
type: 'image/png',
|
||||
form_factor: 'narrow',
|
||||
},
|
||||
{
|
||||
src: 'screenshots/home-mobile-light.png',
|
||||
sizes: '750x1334',
|
||||
type: 'image/png',
|
||||
form_factor: 'narrow',
|
||||
},
|
||||
{
|
||||
src: 'screenshots/public-room-mobile.png',
|
||||
sizes: '750x1334',
|
||||
type: 'image/png',
|
||||
form_factor: 'narrow',
|
||||
},
|
||||
],
|
||||
|
||||
shortcuts: [
|
||||
{
|
||||
name: 'About',
|
||||
url: './about',
|
||||
icons: [
|
||||
{
|
||||
src: 'logo512.png',
|
||||
sizes: '512x512',
|
||||
type: 'image/png',
|
||||
purpose: 'any',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Disclaimer',
|
||||
url: './disclaimer',
|
||||
icons: [
|
||||
{
|
||||
src: 'logo512.png',
|
||||
sizes: '512x512',
|
||||
type: 'image/png',
|
||||
purpose: 'any',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
40996
package-lock.json
generated
40996
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
47
package.json
47
package.json
@ -4,17 +4,17 @@
|
||||
"homepage": "https://chitchatter.im/",
|
||||
"author": "Jeremy Kahn <jeremyckahn@gmail.com>",
|
||||
"license": "GPL-2.0-or-later",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.10.0",
|
||||
"@emotion/styled": "^11.10.0",
|
||||
"@mui/icons-material": "^5.8.4",
|
||||
"@mui/material": "^5.14.12",
|
||||
"@mui/icons-material": "^5.15.6",
|
||||
"@mui/material": "^5.15.6",
|
||||
"@react-hook/debounce": "^4.0.0",
|
||||
"@react-hook/window-size": "^3.1.1",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.3.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/jest": "^28.1.6",
|
||||
"@testing-library/jest-dom": "^6.4.2",
|
||||
"@testing-library/react": "^14.2.1",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"@types/node": "^18.18.4",
|
||||
"@types/react": "^18.2.25",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
@ -35,42 +35,39 @@
|
||||
"react-markdown": "^8.0.3",
|
||||
"react-qrcode-logo": "^2.8.0",
|
||||
"react-router-dom": "^6.16.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"react-youtube": "^10.1.0",
|
||||
"readable-web-to-node-stream": "^3.0.2",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"sass": "^1.54.3",
|
||||
"sdp": "^3.2.0",
|
||||
"secure-file-transfer": "^0.0.7",
|
||||
"streamsaver": "^2.0.6",
|
||||
"trystero": "^0.15.0",
|
||||
"typeface-public-sans": "^1.1.13",
|
||||
"typeface-roboto": "^1.1.13",
|
||||
"typescript": "^4.9.5",
|
||||
"typescript": "^5.4.2",
|
||||
"uuid": "^8.3.2",
|
||||
"vite-plugin-babel-macros": "^1.0.6",
|
||||
"web-vitals": "^2.1.4",
|
||||
"webrtc-adapter": "^8.2.2"
|
||||
},
|
||||
"scripts": {
|
||||
"analyze": "source-map-explorer 'build/static/js/*.js'",
|
||||
"start": "cross-env REACT_APP_HOMEPAGE=$(npm pkg get homepage) react-scripts start",
|
||||
"start": "cross-env VITE_HOMEPAGE=$(npm pkg get homepage) vite --port 3000",
|
||||
"start:tracker": "bittorrent-tracker",
|
||||
"start:streamsaver": "serve -p 3015 node_modules/streamsaver",
|
||||
"dev": "mprocs \"npx cross-env REACT_APP_TRACKER_URL=\"ws://localhost:8000\" REACT_APP_STREAMSAVER_URL=\"http://localhost:3015/mitm.html\" npm run start\" \"npm run start:tracker\" \"npm run start:streamsaver\"",
|
||||
"dev": "mprocs \"npx cross-env VITE_TRACKER_URL=\"ws://localhost:8000\" VITE_STREAMSAVER_URL=\"http://localhost:3015/mitm.html\" npm run start\" \"npm run start:tracker\" \"npm run start:streamsaver\"",
|
||||
"build": "npm run build:app && npm run build:sdk",
|
||||
"build:app": "cross-env REACT_APP_HOMEPAGE=$(npm pkg get homepage) react-scripts build",
|
||||
"build:sdk": "parcel build sdk/sdk.ts --dist-dir build --no-content-hash",
|
||||
"build:app": "cross-env VITE_HOMEPAGE=$(npm pkg get homepage) vite build",
|
||||
"build:sdk": "parcel build sdk/sdk.ts --no-content-hash",
|
||||
"build:sdk:watch": "nodemon --exec \"npm run build:sdk\"",
|
||||
"test": "react-scripts test",
|
||||
"test": "vitest",
|
||||
"prepare": "husky install",
|
||||
"prettier": "prettier 'src/**/*.js' --write",
|
||||
"lint": "eslint src --max-warnings=0"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
"react-app"
|
||||
]
|
||||
},
|
||||
"engines": {
|
||||
@ -99,6 +96,7 @@
|
||||
"@types/webtorrent": "^0.109.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.33.0",
|
||||
"@typescript-eslint/parser": "^5.33.0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"autoprefixer": "^10.4.8",
|
||||
"bittorrent-tracker": "^9.19.0",
|
||||
"cross-env": "^7.0.3",
|
||||
@ -109,6 +107,7 @@
|
||||
"eslint-plugin-react": "^7.30.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"husky": "^8.0.1",
|
||||
"jsdom": "^24.0.0",
|
||||
"mprocs": "^0.6.4",
|
||||
"nodemon": "^3.0.1",
|
||||
"parcel": "^2.10.0",
|
||||
@ -116,11 +115,16 @@
|
||||
"prettier": "^2.7.1",
|
||||
"pretty-quick": "^3.1.3",
|
||||
"process": "^0.11.10",
|
||||
"sass": "^1.69.5",
|
||||
"serve": "^14.1.2",
|
||||
"source-map-explorer": "^2.5.3",
|
||||
"tailwindcss": "^3.1.8",
|
||||
"url": "^0.11.0",
|
||||
"util": "^0.12.5"
|
||||
"util": "^0.12.5",
|
||||
"vite": "^5.0.12",
|
||||
"vite-plugin-node-polyfills": "^0.19.0",
|
||||
"vite-plugin-pwa": "^0.19.2",
|
||||
"vite-plugin-svgr": "^4.2.0",
|
||||
"vitest": "^1.3.1"
|
||||
},
|
||||
"overrides": {
|
||||
"ipfs-core": "npm:dry-uninstall",
|
||||
@ -134,10 +138,5 @@
|
||||
"resolve-url-loader": {
|
||||
"postcss": "8.4.31"
|
||||
}
|
||||
},
|
||||
"jest": {
|
||||
"transformIgnorePatterns": [
|
||||
"node_modules/(?!trystero)/"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -1,88 +0,0 @@
|
||||
{
|
||||
"short_name": "Chitchatter",
|
||||
"name": "Chitchatter",
|
||||
"description": "This is a communication tool that is free, open source, and designed for simplicity and security. All communication between you and your online peers is encrypted. There is no trace of your conversation once you leave.",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": "./",
|
||||
"display": "fullscreen",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#222222",
|
||||
"screenshots": [
|
||||
{
|
||||
"src": "screenshots/home-desktop.png",
|
||||
"sizes": "2160x1620",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "screenshots/public-room-desktop.png",
|
||||
"sizes": "2160x1620",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "screenshots/public-room-desktop-with-video.png",
|
||||
"sizes": "2160x1620",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "screenshots/home-mobile-dark.png",
|
||||
"sizes": "750x1334",
|
||||
"type": "image/png",
|
||||
"form_factor": "narrow"
|
||||
},
|
||||
{
|
||||
"src": "screenshots/home-mobile-light.png",
|
||||
"sizes": "750x1334",
|
||||
"type": "image/png",
|
||||
"form_factor": "narrow"
|
||||
},
|
||||
{
|
||||
"src": "screenshots/public-room-mobile.png",
|
||||
"sizes": "750x1334",
|
||||
"type": "image/png",
|
||||
"form_factor": "narrow"
|
||||
}
|
||||
],
|
||||
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "About",
|
||||
"url": "./about",
|
||||
"icons": [
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Disclaimer",
|
||||
"url": "./disclaimer",
|
||||
"icons": [
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
// NOTE: This file is a shim to enable the Bootstrap component to be
|
||||
// lazy-loaded.
|
||||
import Bootstrap from './Bootstrap.tsx'
|
||||
export default Bootstrap
|
@ -1,5 +1,6 @@
|
||||
import { vi } from 'vitest'
|
||||
import { act, render } from '@testing-library/react'
|
||||
import localforage from 'localforage'
|
||||
import persistedStorage from 'localforage'
|
||||
|
||||
import { PersistedStorageKeys } from 'models/storage'
|
||||
import {
|
||||
@ -11,28 +12,14 @@ import { userSettingsStubFactory } from 'test-utils/stubs/userSettings'
|
||||
|
||||
import Bootstrap, { BootstrapProps } from './Bootstrap'
|
||||
|
||||
const mockPersistedStorage =
|
||||
jest.createMockFromModule<jest.Mock<typeof localforage>>('localforage')
|
||||
|
||||
const mockGetItem = jest.fn()
|
||||
const mockSetItem = jest.fn()
|
||||
vi.mock('localforage')
|
||||
|
||||
const userSettingsStub = userSettingsStubFactory()
|
||||
|
||||
beforeEach(() => {
|
||||
mockGetItem.mockImplementation(() => Promise.resolve(null))
|
||||
mockSetItem.mockImplementation((data: any) => Promise.resolve(data))
|
||||
})
|
||||
|
||||
const renderBootstrap = async (overrides: Partial<BootstrapProps> = {}) => {
|
||||
Object.assign(mockPersistedStorage, {
|
||||
getItem: mockGetItem,
|
||||
setItem: mockSetItem,
|
||||
})
|
||||
|
||||
render(
|
||||
<Bootstrap
|
||||
persistedStorage={mockPersistedStorage as any as typeof localforage}
|
||||
persistedStorage={persistedStorage}
|
||||
initialUserSettings={userSettingsStub}
|
||||
serializationService={mockSerialization}
|
||||
{...overrides}
|
||||
@ -51,7 +38,9 @@ test('renders', async () => {
|
||||
|
||||
test('checks persistedStorage for user settings', async () => {
|
||||
await renderBootstrap()
|
||||
expect(mockGetItem).toHaveBeenCalledWith(PersistedStorageKeys.USER_SETTINGS)
|
||||
expect(persistedStorage.getItem).toHaveBeenCalledWith(
|
||||
PersistedStorageKeys.USER_SETTINGS
|
||||
)
|
||||
})
|
||||
|
||||
test('updates persisted user settings', async () => {
|
||||
@ -59,7 +48,9 @@ test('updates persisted user settings', async () => {
|
||||
initialUserSettings: { ...userSettingsStub, userId: 'abc123' },
|
||||
})
|
||||
|
||||
expect(mockSetItem).toHaveBeenCalledWith(PersistedStorageKeys.USER_SETTINGS, {
|
||||
expect(persistedStorage.setItem).toHaveBeenCalledWith(
|
||||
PersistedStorageKeys.USER_SETTINGS,
|
||||
{
|
||||
colorMode: 'dark',
|
||||
userId: 'abc123',
|
||||
customUsername: '',
|
||||
@ -68,5 +59,6 @@ test('updates persisted user settings', async () => {
|
||||
showActiveTypingStatus: true,
|
||||
publicKey: mockSerializedPublicKey,
|
||||
privateKey: mockSerializedPrivateKey,
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
|
@ -6,8 +6,8 @@ import {
|
||||
Navigate,
|
||||
} from 'react-router-dom'
|
||||
import localforage from 'localforage'
|
||||
import { useRegisterSW } from 'virtual:pwa-register/react'
|
||||
|
||||
import * as serviceWorkerRegistration from 'serviceWorkerRegistration'
|
||||
import { StorageContext } from 'contexts/StorageContext'
|
||||
import { SettingsContext } from 'contexts/SettingsContext'
|
||||
import { homepageUrl, routes } from 'config/routes'
|
||||
@ -87,16 +87,11 @@ const Bootstrap = ({
|
||||
)
|
||||
|
||||
const [persistedStorage] = useState(persistedStorageProp)
|
||||
const [appNeedsUpdate, setAppNeedsUpdate] = useState(false)
|
||||
const [hasLoadedSettings, setHasLoadedSettings] = useState(false)
|
||||
const [userSettings, setUserSettings] =
|
||||
useState<UserSettings>(initialUserSettings)
|
||||
const { userId } = userSettings
|
||||
|
||||
const handleServiceWorkerUpdate = () => {
|
||||
setAppNeedsUpdate(true)
|
||||
}
|
||||
|
||||
const persistUserSettings = useCallback(
|
||||
async (newUserSettings: UserSettings) => {
|
||||
if (queryParams.has(QueryParamKeys.IS_EMBEDDED)) {
|
||||
@ -114,9 +109,9 @@ const Bootstrap = ({
|
||||
[persistedStorageProp, queryParams, serializationService, userSettings]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
serviceWorkerRegistration.register({ onUpdate: handleServiceWorkerUpdate })
|
||||
}, [])
|
||||
const {
|
||||
needRefresh: [appNeedsUpdate],
|
||||
} = useRegisterSW()
|
||||
|
||||
useEffect(() => {
|
||||
;(async () => {
|
||||
|
@ -13,8 +13,7 @@ import { ColorMode, UserSettings } from 'models/settings'
|
||||
|
||||
import type { BootstrapProps } from './Bootstrap'
|
||||
|
||||
// @ts-expect-error
|
||||
const Bootstrap = lazy(() => import('./Bootstrap.js'))
|
||||
const Bootstrap = lazy(() => import('./Bootstrap'))
|
||||
|
||||
export interface InitProps extends Omit<BootstrapProps, 'initialUserSettings'> {
|
||||
getUuid?: typeof uuid
|
||||
|
@ -1,12 +1,13 @@
|
||||
import { vi } from 'vitest'
|
||||
import { PropsWithChildren } from 'react'
|
||||
import { waitFor, render, screen } from '@testing-library/react'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { MemoryRouter as Router, Route, Routes } from 'react-router-dom'
|
||||
|
||||
import { userSettingsContextStubFactory } from 'test-utils/stubs/settingsContext'
|
||||
import { mockEncryptionService } from 'test-utils/mocks/mockEncryptionService'
|
||||
|
||||
import { SettingsContext } from 'contexts/SettingsContext'
|
||||
import { Time } from 'lib/Time'
|
||||
|
||||
import { Room, RoomProps } from './'
|
||||
|
||||
@ -17,13 +18,17 @@ const userSettingsStub = userSettingsContextStubFactory({
|
||||
userId: mockUserId,
|
||||
})
|
||||
|
||||
window.AudioContext = jest.fn().mockImplementation()
|
||||
const mockGetUuid = jest.fn()
|
||||
const mockMessagedSender = jest
|
||||
.fn()
|
||||
.mockImplementation(() => Promise.resolve([]))
|
||||
window.AudioContext = vi.fn().mockImplementation(() => {})
|
||||
const mockGetUuid = vi.fn()
|
||||
const mockMessagedSender = vi.fn().mockImplementation(() => Promise.resolve([]))
|
||||
|
||||
jest.mock('trystero', () => ({
|
||||
const mockTimeService = new Time()
|
||||
const mockNowTime = 1234
|
||||
mockTimeService.now = () => mockNowTime
|
||||
|
||||
vi.mock('../../lib/Audio')
|
||||
|
||||
vi.mock('trystero', () => ({
|
||||
joinRoom: () => ({
|
||||
makeAction: () => [mockMessagedSender, () => {}, () => {}],
|
||||
ping: () => Promise.resolve(0),
|
||||
@ -53,10 +58,14 @@ const RouteStub = ({ children }: PropsWithChildren) => {
|
||||
)
|
||||
}
|
||||
|
||||
jest.useFakeTimers().setSystemTime(100)
|
||||
|
||||
const RoomStub = (props: RoomProps) => {
|
||||
return <Room encryptionService={mockEncryptionService} {...props} />
|
||||
return (
|
||||
<Room
|
||||
encryptionService={mockEncryptionService}
|
||||
timeService={mockTimeService}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
describe('Room', () => {
|
||||
@ -89,9 +98,7 @@ describe('Room', () => {
|
||||
const sendButton = screen.getByLabelText('Send')
|
||||
const textInput = screen.getByPlaceholderText('Your message')
|
||||
|
||||
await waitFor(() => {
|
||||
userEvent.type(textInput, 'hello')
|
||||
})
|
||||
await userEvent.type(textInput, 'hello')
|
||||
|
||||
expect(sendButton).not.toBeDisabled()
|
||||
})
|
||||
@ -106,13 +113,8 @@ describe('Room', () => {
|
||||
const sendButton = screen.getByLabelText('Send')
|
||||
const textInput = screen.getByPlaceholderText('Your message')
|
||||
|
||||
await waitFor(() => {
|
||||
userEvent.type(textInput, 'hello')
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
userEvent.click(sendButton)
|
||||
})
|
||||
await userEvent.type(textInput, 'hello')
|
||||
await userEvent.click(sendButton)
|
||||
|
||||
expect(textInput).toHaveValue('')
|
||||
})
|
||||
@ -131,18 +133,13 @@ describe('Room', () => {
|
||||
const sendButton = screen.getByLabelText('Send')
|
||||
const textInput = screen.getByPlaceholderText('Your message')
|
||||
|
||||
await waitFor(() => {
|
||||
userEvent.type(textInput, 'hello')
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
userEvent.click(sendButton)
|
||||
})
|
||||
await userEvent.type(textInput, 'hello')
|
||||
await userEvent.click(sendButton)
|
||||
|
||||
expect(mockMessagedSender).toHaveBeenCalledWith({
|
||||
authorId: mockUserId,
|
||||
text: 'hello',
|
||||
timeSent: 100,
|
||||
timeSent: mockNowTime,
|
||||
id: 'abc123',
|
||||
})
|
||||
})
|
||||
|
@ -8,6 +8,7 @@ import { v4 as uuid } from 'uuid'
|
||||
|
||||
import { rtcConfig } from 'config/rtcConfig'
|
||||
import { trackerUrls } from 'config/trackerUrls'
|
||||
import { time } from 'lib/Time'
|
||||
import { RoomContext } from 'contexts/RoomContext'
|
||||
import { ShellContext } from 'contexts/ShellContext'
|
||||
import { MessageForm } from 'components/MessageForm'
|
||||
@ -31,12 +32,14 @@ export interface RoomProps {
|
||||
roomId: string
|
||||
userId: string
|
||||
encryptionService?: typeof encryption
|
||||
timeService?: typeof time
|
||||
}
|
||||
|
||||
export function Room({
|
||||
appId = `${encodeURI(window.location.origin)}_${process.env.REACT_APP_NAME}`,
|
||||
appId = `${encodeURI(window.location.origin)}_${process.env.VITE_NAME}`,
|
||||
getUuid = uuid,
|
||||
encryptionService = encryption,
|
||||
timeService = time,
|
||||
roomId,
|
||||
password,
|
||||
userId,
|
||||
@ -68,6 +71,7 @@ export function Room({
|
||||
getUuid,
|
||||
publicKey,
|
||||
encryptionService,
|
||||
timeService,
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -26,8 +26,9 @@ import {
|
||||
} from 'models/chat'
|
||||
import { getPeerName, usePeerNameDisplay } from 'components/PeerNameDisplay'
|
||||
import { Audio } from 'lib/Audio'
|
||||
import { notification } from 'services/Notification'
|
||||
import { time } from 'lib/Time'
|
||||
import { PeerRoom, PeerHookType } from 'lib/PeerRoom'
|
||||
import { notification } from 'services/Notification'
|
||||
import { fileTransfer } from 'lib/FileTransfer'
|
||||
import { AllowedKeyType, encryption } from 'services/Encryption'
|
||||
|
||||
@ -41,6 +42,7 @@ interface UseRoomConfig {
|
||||
publicKey: CryptoKey
|
||||
getUuid?: typeof uuid
|
||||
encryptionService?: typeof encryption
|
||||
timeService?: typeof time
|
||||
}
|
||||
|
||||
interface UserMetadata {
|
||||
@ -57,6 +59,7 @@ export function useRoom(
|
||||
publicKey,
|
||||
getUuid = uuid,
|
||||
encryptionService = encryption,
|
||||
timeService = time,
|
||||
}: UseRoomConfig
|
||||
) {
|
||||
const isPrivate = password !== undefined
|
||||
@ -83,9 +86,7 @@ export function useRoom(
|
||||
const [messageLog, _setMessageLog] = useState<Array<Message | InlineMedia>>(
|
||||
[]
|
||||
)
|
||||
const [newMessageAudio] = useState(
|
||||
() => new Audio(process.env.PUBLIC_URL + '/sounds/new-message.aac')
|
||||
)
|
||||
const [newMessageAudio] = useState(() => new Audio('/sounds/new-message.aac'))
|
||||
|
||||
const { getDisplayUsername } = usePeerNameDisplay()
|
||||
|
||||
@ -237,7 +238,7 @@ export function useRoom(
|
||||
const unsentMessage: UnsentMessage = {
|
||||
authorId: userId,
|
||||
text: message,
|
||||
timeSent: Date.now(),
|
||||
timeSent: timeService.now(),
|
||||
id: getUuid(),
|
||||
}
|
||||
|
||||
@ -248,7 +249,7 @@ export function useRoom(
|
||||
|
||||
setMessageLog([
|
||||
...messageLog,
|
||||
{ ...unsentMessage, timeReceived: Date.now() },
|
||||
{ ...unsentMessage, timeReceived: timeService.now() },
|
||||
])
|
||||
setIsMessageSending(false)
|
||||
}
|
||||
@ -324,7 +325,10 @@ export function useRoom(
|
||||
}
|
||||
}
|
||||
|
||||
setMessageLog([...messageLog, { ...message, timeReceived: Date.now() }])
|
||||
setMessageLog([
|
||||
...messageLog,
|
||||
{ ...message, timeReceived: timeService.now() },
|
||||
])
|
||||
updatePeer(peerId, { isTyping: false })
|
||||
})
|
||||
|
||||
@ -391,7 +395,7 @@ export function useRoom(
|
||||
const unsentInlineMedia: UnsentInlineMedia = {
|
||||
authorId: userId,
|
||||
magnetURI: fileOfferId,
|
||||
timeSent: Date.now(),
|
||||
timeSent: timeService.now(),
|
||||
id: getUuid(),
|
||||
}
|
||||
|
||||
@ -402,7 +406,7 @@ export function useRoom(
|
||||
|
||||
setMessageLog([
|
||||
...messageLog,
|
||||
{ ...unsentInlineMedia, timeReceived: Date.now() },
|
||||
{ ...unsentInlineMedia, timeReceived: timeService.now() },
|
||||
])
|
||||
setIsMessageSending(false)
|
||||
}
|
||||
@ -434,7 +438,10 @@ export function useRoom(
|
||||
}
|
||||
}
|
||||
|
||||
setMessageLog([...messageLog, { ...inlineMedia, timeReceived: Date.now() }])
|
||||
setMessageLog([
|
||||
...messageLog,
|
||||
{ ...inlineMedia, timeReceived: timeService.now() },
|
||||
])
|
||||
})
|
||||
|
||||
receiveTypingStatusChange((typingStatus, peerId) => {
|
||||
|
@ -145,7 +145,9 @@ export const Drawer = ({ isDrawerOpen, onDrawerClose, theme }: DrawerProps) => {
|
||||
<MuiLink
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
href={`${process.env.REACT_APP_GITHUB_REPO}/commit/${commit.hash}`}
|
||||
href={`${import.meta.env.VITE_GITHUB_REPO}/commit/${
|
||||
commit.hash
|
||||
}`}
|
||||
>
|
||||
{commit.shortHash}
|
||||
</MuiLink>
|
||||
|
@ -12,7 +12,7 @@ const { isSecureContext, RTCDataChannel } = window
|
||||
const doesSupportWebRtc = RTCDataChannel !== undefined
|
||||
|
||||
export const isEnvironmentSupported =
|
||||
(isSecureContext && doesSupportWebRtc) || process.env.NODE_ENV === 'test'
|
||||
(isSecureContext && doesSupportWebRtc) || import.meta.env.MODE === 'test'
|
||||
|
||||
export const EnvironmentUnsupportedDialog = () => {
|
||||
const theme = useTheme()
|
||||
|
@ -35,7 +35,7 @@ describe('Shell', () => {
|
||||
userEvent.click(menuButton)
|
||||
})
|
||||
|
||||
const navigation = screen.getByRole('navigation')
|
||||
const navigation = screen.getByLabelText('Navigation menu')
|
||||
|
||||
await waitFor(() => {
|
||||
expect(navigation).toBeVisible()
|
||||
@ -56,7 +56,7 @@ describe('Shell', () => {
|
||||
userEvent.click(closeMenu)
|
||||
})
|
||||
|
||||
const navigation = screen.getByRole('navigation')
|
||||
const navigation = screen.getByLabelText('Navigation menu')
|
||||
|
||||
await waitFor(() => {
|
||||
expect(navigation).not.toBeVisible()
|
||||
|
@ -6,14 +6,17 @@ import DialogContent from '@mui/material/DialogContent'
|
||||
import DialogContentText from '@mui/material/DialogContentText'
|
||||
import DialogTitle from '@mui/material/DialogTitle'
|
||||
import WarningIcon from '@mui/icons-material/Warning'
|
||||
import { useRegisterSW } from 'virtual:pwa-register/react'
|
||||
|
||||
interface UpgradeDialogProps {
|
||||
appNeedsUpdate: boolean
|
||||
}
|
||||
|
||||
export const UpgradeDialog = ({ appNeedsUpdate }: UpgradeDialogProps) => {
|
||||
const { updateServiceWorker } = useRegisterSW()
|
||||
|
||||
const handleRestartClick = () => {
|
||||
window.location.reload()
|
||||
updateServiceWorker(true)
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -9,5 +9,5 @@ export enum routes {
|
||||
}
|
||||
|
||||
export const homepageUrl = new URL(
|
||||
process.env.REACT_APP_HOMEPAGE ?? 'https://chitchatter.im/'
|
||||
import.meta.env.VITE_HOMEPAGE ?? 'https://chitchatter.im/'
|
||||
)
|
||||
|
@ -1,5 +1,5 @@
|
||||
export const streamSaverUrl =
|
||||
process.env.REACT_APP_STREAMSAVER_URL ??
|
||||
import.meta.env.VITE_STREAMSAVER_URL ??
|
||||
// If you would like to host your own Chitchatter instance with an
|
||||
// alternative StreamSaver fork to facilitate file sharing, change this
|
||||
// string to its respective .mitm.html URL.
|
||||
|
@ -8,12 +8,12 @@ let trackerUrls: string[] | undefined = [
|
||||
// https://github.com/dmotz/trystero/blob/694f49974974cc9df8b621db09215d6df10fad09/src/torrent.js#L27-L33
|
||||
]
|
||||
|
||||
// If a tracker URL has been provided via the REACT_APP_TRACKER_URL environment
|
||||
// If a tracker URL has been provided via the VITE_TRACKER_URL environment
|
||||
// variable, prioritize using it. This is mainly relevant for local development
|
||||
// when using the `npm run dev` script. If you are hosting your own Chitchatter
|
||||
// instance, consider populating the trackerUrls above instead.
|
||||
if (process.env.REACT_APP_TRACKER_URL) {
|
||||
trackerUrls.unshift(process.env.REACT_APP_TRACKER_URL)
|
||||
if (import.meta.env.VITE_TRACKER_URL) {
|
||||
trackerUrls.unshift(import.meta.env.VITE_TRACKER_URL)
|
||||
}
|
||||
|
||||
// If no tracker URL overrides have been provided, set trackerUrls to undefined
|
||||
|
@ -3,9 +3,27 @@ import ReactDOM from 'react-dom/client'
|
||||
import 'typeface-roboto'
|
||||
|
||||
import './index.sass'
|
||||
|
||||
import { ThemeProvider, createTheme } from '@mui/material/styles'
|
||||
import { PrismAsyncLight as SyntaxHighlighter } from 'react-syntax-highlighter'
|
||||
|
||||
import Init from './Init'
|
||||
import reportWebVitals from './reportWebVitals'
|
||||
|
||||
// NOTE: This is a workaround for MUI components attempting to access theme code
|
||||
// before it has loaded.
|
||||
// See: https://stackoverflow.com/a/76017295/470685
|
||||
;<ThemeProvider theme={createTheme()} />
|
||||
|
||||
// NOTE: This is a workaround for SyntaxHighlighter not working reliably in the
|
||||
// EmbedCodeDialog component. It seems to have the effect of warming some
|
||||
// sort of internal cache that avoids a race condition within
|
||||
// SyntaxHighlighter.
|
||||
// See: https://github.com/react-syntax-highlighter/react-syntax-highlighter/issues/513
|
||||
ReactDOM.createRoot(document.createElement('div')).render(
|
||||
<SyntaxHighlighter language="" children={''} />
|
||||
)
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
|
||||
root.render(<Init />)
|
||||
|
||||
|
7
src/lib/Time/Time.ts
Normal file
7
src/lib/Time/Time.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export class Time {
|
||||
now = () => {
|
||||
return Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
export const time = new Time()
|
1
src/lib/Time/index.ts
Normal file
1
src/lib/Time/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './Time'
|
@ -16,7 +16,7 @@ import { v4 as uuid } from 'uuid'
|
||||
import { routes } from 'config/routes'
|
||||
import { ShellContext } from 'contexts/ShellContext'
|
||||
import { PeerNameDisplay } from 'components/PeerNameDisplay'
|
||||
import { ReactComponent as Logo } from 'img/logo.svg'
|
||||
import Logo from 'img/logo.svg?react'
|
||||
|
||||
import { EmbedCodeDialog } from './EmbedCodeDialog'
|
||||
|
||||
|
@ -1,9 +1 @@
|
||||
import 'webrtc-adapter'
|
||||
import { Buffer } from 'buffer'
|
||||
|
||||
// @ts-ignore
|
||||
import process from 'process/browser'
|
||||
|
||||
// Polyfill
|
||||
window.Buffer = Buffer
|
||||
window.process = process
|
||||
|
2
src/react-app-env.d.ts
vendored
2
src/react-app-env.d.ts
vendored
@ -1 +1 @@
|
||||
/// <reference types="react-scripts" />
|
||||
/// <reference types="vite-plugin-svgr/client" />
|
||||
|
@ -1,73 +0,0 @@
|
||||
/* eslint-disable no-restricted-globals */
|
||||
|
||||
// This service worker can be customized!
|
||||
// See https://developers.google.com/web/tools/workbox/modules
|
||||
// for the list of available Workbox modules, or add any other
|
||||
// code you'd like.
|
||||
// You can also remove this file if you'd prefer not to use a
|
||||
// service worker, and the Workbox build step will be skipped.
|
||||
|
||||
import { clientsClaim } from 'workbox-core'
|
||||
import { ExpirationPlugin } from 'workbox-expiration'
|
||||
import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching'
|
||||
import { registerRoute } from 'workbox-routing'
|
||||
import { StaleWhileRevalidate } from 'workbox-strategies'
|
||||
|
||||
clientsClaim()
|
||||
|
||||
// Precache all of the assets generated by your build process.
|
||||
// Their URLs are injected into the manifest variable below.
|
||||
// This variable must be present somewhere in your service worker file,
|
||||
// even if you decide not to use precaching. See https://cra.link/PWA
|
||||
precacheAndRoute(self.__WB_MANIFEST)
|
||||
|
||||
// Set up App Shell-style routing, so that all navigation requests
|
||||
// are fulfilled with your index.html shell. Learn more at
|
||||
// https://developers.google.com/web/fundamentals/architecture/app-shell
|
||||
const fileExtensionRegexp = new RegExp('/[^/?]+\\.[^/]+$')
|
||||
registerRoute(
|
||||
// Return false to exempt requests from being fulfilled by index.html.
|
||||
({ request, url }) => {
|
||||
// If this isn't a navigation, skip.
|
||||
if (request.mode !== 'navigate') {
|
||||
return false
|
||||
} // If this is a URL that starts with /_, skip.
|
||||
|
||||
if (url.pathname.startsWith('/_')) {
|
||||
return false
|
||||
} // If this looks like a URL for a resource, because it contains // a file extension, skip.
|
||||
|
||||
if (url.pathname.match(fileExtensionRegexp)) {
|
||||
return false
|
||||
} // Return true to signal that we want to use the handler.
|
||||
|
||||
return true
|
||||
},
|
||||
createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html')
|
||||
)
|
||||
|
||||
// An example runtime caching route for requests that aren't handled by the
|
||||
// precache, in this case same-origin .png requests like those from in public/
|
||||
registerRoute(
|
||||
// Add in any other file extensions or routing criteria as needed.
|
||||
({ url }) =>
|
||||
url.origin === self.location.origin && url.pathname.endsWith('.png'), // Customize this strategy as needed, e.g., by changing to CacheFirst.
|
||||
new StaleWhileRevalidate({
|
||||
cacheName: 'images',
|
||||
plugins: [
|
||||
// Ensure that once this runtime cache reaches a maximum size the
|
||||
// least-recently used images are removed.
|
||||
new ExpirationPlugin({ maxEntries: 50 }),
|
||||
],
|
||||
})
|
||||
)
|
||||
|
||||
// This allows the web app to trigger skipWaiting via
|
||||
// registration.waiting.postMessage({type: 'SKIP_WAITING'})
|
||||
self.addEventListener('message', event => {
|
||||
if (event.data && event.data.type === 'SKIP_WAITING') {
|
||||
self.skipWaiting()
|
||||
}
|
||||
})
|
||||
|
||||
// Any other custom service worker logic can go here.
|
@ -1,141 +0,0 @@
|
||||
// This optional code is used to register a service worker.
|
||||
// register() is not called by default.
|
||||
|
||||
// This lets the app load faster on subsequent visits in production, and gives
|
||||
// it offline capabilities. However, it also means that developers (and users)
|
||||
// will only see deployed updates on subsequent visits to a page, after all the
|
||||
// existing tabs open on the page have been closed, since previously cached
|
||||
// resources are updated in the background.
|
||||
|
||||
// To learn more about the benefits of this model and instructions on how to
|
||||
// opt-in, read https://cra.link/PWA
|
||||
|
||||
const isLocalhost = Boolean(
|
||||
window.location.hostname === 'localhost' ||
|
||||
// [::1] is the IPv6 localhost address.
|
||||
window.location.hostname === '[::1]' ||
|
||||
// 127.0.0.0/8 are considered localhost for IPv4.
|
||||
window.location.hostname.match(
|
||||
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
|
||||
)
|
||||
)
|
||||
|
||||
export function register(config) {
|
||||
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||
// The URL constructor is available in all browsers that support SW.
|
||||
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href)
|
||||
if (publicUrl.origin !== window.location.origin) {
|
||||
// Our service worker won't work if PUBLIC_URL is on a different origin
|
||||
// from what our page is served on. This might happen if a CDN is used to
|
||||
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
|
||||
return
|
||||
}
|
||||
|
||||
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`
|
||||
|
||||
if (isLocalhost) {
|
||||
// This is running on localhost. Let's check if a service worker still exists or not.
|
||||
checkValidServiceWorker(swUrl, config)
|
||||
|
||||
// Add some additional logging to localhost, pointing developers to the
|
||||
// service worker/PWA documentation.
|
||||
navigator.serviceWorker.ready.then(() => {
|
||||
console.log(
|
||||
'This web app is being served cache-first by a service ' +
|
||||
'worker. To learn more, visit https://cra.link/PWA'
|
||||
)
|
||||
})
|
||||
} else {
|
||||
// Is not localhost. Just register service worker
|
||||
registerValidSW(swUrl, config)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function registerValidSW(swUrl, config) {
|
||||
navigator.serviceWorker
|
||||
.register(swUrl)
|
||||
.then(registration => {
|
||||
registration.onupdatefound = () => {
|
||||
const installingWorker = registration.installing
|
||||
if (installingWorker == null) {
|
||||
return
|
||||
}
|
||||
installingWorker.onstatechange = () => {
|
||||
if (installingWorker.state === 'installed') {
|
||||
if (navigator.serviceWorker.controller) {
|
||||
// At this point, the updated precached content has been fetched,
|
||||
// but the previous service worker will still serve the older
|
||||
// content until all client tabs are closed.
|
||||
console.log(
|
||||
'New content is available and will be used when all ' +
|
||||
'tabs for this page are closed. See https://cra.link/PWA.'
|
||||
)
|
||||
|
||||
registration.waiting.postMessage({ type: 'SKIP_WAITING' })
|
||||
|
||||
// Execute callback
|
||||
if (config && config.onUpdate) {
|
||||
config.onUpdate(registration)
|
||||
}
|
||||
} else {
|
||||
// At this point, everything has been precached.
|
||||
// It's the perfect time to display a
|
||||
// "Content is cached for offline use." message.
|
||||
console.log('Content is cached for offline use.')
|
||||
|
||||
// Execute callback
|
||||
if (config && config.onSuccess) {
|
||||
config.onSuccess(registration)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error during service worker registration:', error)
|
||||
})
|
||||
}
|
||||
|
||||
function checkValidServiceWorker(swUrl, config) {
|
||||
// Check if the service worker can be found. If it can't reload the page.
|
||||
fetch(swUrl, {
|
||||
headers: { 'Service-Worker': 'script' },
|
||||
})
|
||||
.then(response => {
|
||||
// Ensure service worker exists, and that we really are getting a JS file.
|
||||
const contentType = response.headers.get('content-type')
|
||||
if (
|
||||
response.status === 404 ||
|
||||
(contentType != null && contentType.indexOf('javascript') === -1)
|
||||
) {
|
||||
// No service worker found. Probably a different app. Reload the page.
|
||||
navigator.serviceWorker.ready.then(registration => {
|
||||
registration.unregister().then(() => {
|
||||
window.location.reload()
|
||||
})
|
||||
})
|
||||
} else {
|
||||
// Service worker found. Proceed as normal.
|
||||
registerValidSW(swUrl, config)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
console.log(
|
||||
'No internet connection found. App is running in offline mode.'
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export function unregister() {
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.ready
|
||||
.then(registration => {
|
||||
registration.unregister()
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error.message)
|
||||
})
|
||||
}
|
||||
}
|
@ -3,12 +3,16 @@
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom'
|
||||
import { vi } from 'vitest'
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks()
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
jest.mock('secure-file-transfer', () => ({
|
||||
vi.mock('trystero')
|
||||
vi.mock('trystero/torrent')
|
||||
|
||||
vi.mock('secure-file-transfer', () => ({
|
||||
__esModule: true,
|
||||
FileTransfer: class FileTransfer {
|
||||
rescindAll() {}
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { vi } from 'vitest'
|
||||
import { encryption } from 'services/Encryption'
|
||||
|
||||
export const mockEncryptionService = encryption
|
||||
|
||||
mockEncryptionService.generateKeyPair = jest.fn(async () => ({
|
||||
mockEncryptionService.generateKeyPair = vi.fn(async () => ({
|
||||
publicKey: encryption.cryptoKeyStub,
|
||||
privateKey: encryption.cryptoKeyStub,
|
||||
}))
|
||||
|
@ -10,12 +10,17 @@
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"baseUrl": "src"
|
||||
"baseUrl": "src",
|
||||
"allowImportingTsExtensions": true,
|
||||
"paths": {
|
||||
"trystero/torrent": ["../node_modules/trystero/src/torrent.d.ts"]
|
||||
},
|
||||
"types": ["vite-plugin-pwa/client", "vite/client"]
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
6
vercel.json
Normal file
6
vercel.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"framework": "vite",
|
||||
"buildCommand": "npm run build",
|
||||
"installCommand": "npm ci && npm pkg set homepage=\"https://$VERCEL_BRANCH_URL\"",
|
||||
"outputDirectory": "dist"
|
||||
}
|
87
vite.config.ts
Normal file
87
vite.config.ts
Normal file
@ -0,0 +1,87 @@
|
||||
/// <reference types="vitest" />
|
||||
import path from 'path'
|
||||
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import svgr from 'vite-plugin-svgr'
|
||||
import { nodePolyfills } from 'vite-plugin-node-polyfills'
|
||||
import macrosPlugin from 'vite-plugin-babel-macros'
|
||||
import { VitePWA } from 'vite-plugin-pwa'
|
||||
|
||||
import { manifest } from './manifest'
|
||||
|
||||
const srcPaths = [
|
||||
'components',
|
||||
'config',
|
||||
'contexts',
|
||||
'lib',
|
||||
'models',
|
||||
'pages',
|
||||
'services',
|
||||
'img',
|
||||
'utils',
|
||||
'test-utils',
|
||||
]
|
||||
|
||||
const srcPathAliases = srcPaths.reduce((acc, dir) => {
|
||||
acc[dir] = path.resolve(__dirname, `./src/${dir}`)
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
const config = () => {
|
||||
return defineConfig({
|
||||
build: {
|
||||
// NOTE: This isn't really working. At the very least, it's still useful
|
||||
// for exposing source code to users.
|
||||
// See: https://github.com/vitejs/vite/issues/15012#issuecomment-1956429165
|
||||
sourcemap: true,
|
||||
},
|
||||
plugins: [
|
||||
svgr({
|
||||
include: '**/*.svg?react',
|
||||
}),
|
||||
react(),
|
||||
macrosPlugin(),
|
||||
nodePolyfills({
|
||||
globals: {
|
||||
Buffer: true,
|
||||
global: true,
|
||||
process: true,
|
||||
},
|
||||
protocolImports: true,
|
||||
}),
|
||||
VitePWA({
|
||||
registerType: 'prompt',
|
||||
devOptions: {
|
||||
enabled: false,
|
||||
},
|
||||
injectRegister: 'auto',
|
||||
manifest,
|
||||
}),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
webtorrent: fileURLToPath(
|
||||
new URL(
|
||||
'./node_modules/webtorrent/webtorrent.min.js',
|
||||
import.meta.url
|
||||
)
|
||||
),
|
||||
...srcPathAliases,
|
||||
},
|
||||
},
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
setupFiles: './src/setupTests.ts',
|
||||
coverage: {
|
||||
reporter: ['text', 'html'],
|
||||
exclude: ['node_modules/', 'src/setupTests.ts'],
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export default config
|
Loading…
Reference in New Issue
Block a user