From 4602f962602a4316ddf357e551edeb28afc4326f Mon Sep 17 00:00:00 2001 From: Jixun Wu Date: Sat, 3 Jun 2023 14:09:11 +0100 Subject: [PATCH 01/23] feat: setup redux store for settings --- package.json | 2 ++ pnpm-lock.yaml | 17 +++++++++ src/Loader.tsx | 24 +++++++++++++ src/features/settings/persistSettings.ts | 45 ++++++++++++++++++++++++ src/features/settings/settingsSlice.ts | 31 ++++++++++++++++ src/main.tsx | 16 ++------- src/store.ts | 2 ++ src/util/logUtils.ts | 20 +++++++++++ src/util/objects.ts | 7 ++++ 9 files changed, 150 insertions(+), 14 deletions(-) create mode 100644 src/Loader.tsx create mode 100644 src/features/settings/persistSettings.ts create mode 100644 src/features/settings/settingsSlice.ts create mode 100644 src/util/objects.ts diff --git a/package.json b/package.json index 819a2b8..af15912 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,9 @@ "@jixun/libparakeet": "0.1.1", "@reduxjs/toolkit": "^1.9.5", "framer-motion": "^10.12.16", + "immer": "^10.0.2", "nanoid": "^4.0.2", + "radash": "^10.8.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-dropzone": "^14.2.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0fa7b36..29dfd3c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,9 +22,15 @@ dependencies: framer-motion: specifier: ^10.12.16 version: 10.12.16(react-dom@18.2.0)(react@18.2.0) + immer: + specifier: ^10.0.2 + version: 10.0.2 nanoid: specifier: ^4.0.2 version: 4.0.2 + radash: + specifier: ^10.8.1 + version: 10.8.1 react: specifier: ^18.2.0 version: 18.2.0 @@ -5483,6 +5489,11 @@ packages: engines: { node: '>= 4' } dev: true + /immer@10.0.2: + resolution: + { integrity: sha512-Rx3CqeqQ19sxUtYV9CU911Vhy8/721wRFnJv3REVGWUmoAcIwzifTsdmJte/MV+0/XpM35LZdQMBGkRIoLPwQA== } + dev: false + /immer@9.0.21: resolution: { integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA== } @@ -6608,6 +6619,12 @@ packages: { integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== } dev: true + /radash@10.8.1: + resolution: + { integrity: sha512-NzYo3XgM9Tzjf5iFPIMG2l5+LSOCi2H7Axe3Ry/1PrhlvuqxUoiLsmcTBtw4CfKtzy5Fzo79STiEj9JZWMfDQg== } + engines: { node: '>=14.18.0' } + dev: false + /randombytes@2.1.0: resolution: { integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== } diff --git a/src/Loader.tsx b/src/Loader.tsx new file mode 100644 index 0000000..a728531 --- /dev/null +++ b/src/Loader.tsx @@ -0,0 +1,24 @@ +import React, { useEffect } from 'react'; +import App from './App'; + +import { ChakraProvider } from '@chakra-ui/react'; +import { Provider } from 'react-redux'; +import { theme } from './theme'; +import { persistSettings } from './features/settings/persistSettings'; +import type { AppStore } from './store'; + +export function Loader({ store }: { store: AppStore }) { + useEffect(() => { + return persistSettings(store); + }, [store]); + + return ( + + + + + + + + ); +} diff --git a/src/features/settings/persistSettings.ts b/src/features/settings/persistSettings.ts new file mode 100644 index 0000000..420266b --- /dev/null +++ b/src/features/settings/persistSettings.ts @@ -0,0 +1,45 @@ +import { debounce } from 'radash'; +import { produce } from 'immer'; + +import type { AppStore } from '~/store'; +import { SettingsState, settingsSlice, updateSettings } from './settingsSlice'; +import { enumObject } from '~/util/objects'; +import { getLogger } from '~/util/logUtils'; + +const DEFAULT_STORAGE_KEY = 'um-react-settings'; + +function mergeSettings(settings: SettingsState): SettingsState { + return produce(settingsSlice.getInitialState(), (draft) => { + for (const [k, v] of enumObject(settings.qmc2?.keys)) { + if (typeof v === 'string') { + draft.qmc2.keys[k] = v; + } + } + }); +} + +export function persistSettings(store: AppStore, storageKey = DEFAULT_STORAGE_KEY) { + let lastSettings: unknown; + + try { + const loadedSettings: SettingsState = JSON.parse(localStorage.getItem(storageKey) ?? ''); + if (loadedSettings) { + const mergedSettings = mergeSettings(loadedSettings); + store.dispatch(updateSettings(mergedSettings)); + getLogger().debug('settings loaded'); + } + } catch { + // load failed, ignore. + } + + return store.subscribe( + debounce({ delay: 150 }, () => { + const currentSettings = store.getState().settings; + if (lastSettings !== currentSettings) { + lastSettings = currentSettings; + localStorage.setItem(storageKey, JSON.stringify(currentSettings)); + getLogger().debug('settings saved'); + } + }) + ); +} diff --git a/src/features/settings/settingsSlice.ts b/src/features/settings/settingsSlice.ts new file mode 100644 index 0000000..b08a340 --- /dev/null +++ b/src/features/settings/settingsSlice.ts @@ -0,0 +1,31 @@ +import { createSlice } from '@reduxjs/toolkit'; +import type { PayloadAction } from '@reduxjs/toolkit'; + +export interface QMCSettings { + keys: Record; // { [fileName]: ekey } +} + +export interface SettingsState { + qmc2: QMCSettings; +} + +const initialState: SettingsState = { + qmc2: { keys: {} }, +}; + +export const settingsSlice = createSlice({ + name: 'settings', + initialState, + reducers: { + updateSettings: (_state, { payload }: PayloadAction) => { + return payload; + }, + resetConfig: () => { + return initialState; + }, + }, +}); + +export const { updateSettings, resetConfig } = settingsSlice.actions; + +export default settingsSlice.reducer; diff --git a/src/main.tsx b/src/main.tsx index dad48ae..469e6dc 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,21 +1,9 @@ -import React from 'react'; import ReactDOM from 'react-dom/client'; -import App from './App'; -import { ChakraProvider } from '@chakra-ui/react'; -import { Provider } from 'react-redux'; import { setupStore } from './store'; -import { theme } from './theme'; +import { Loader } from './Loader'; // Private to this file only. const store = setupStore(); -ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( - - - - - - - -); +ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(); diff --git a/src/store.ts b/src/store.ts index 542db54..b97f8a6 100644 --- a/src/store.ts +++ b/src/store.ts @@ -1,8 +1,10 @@ import { PreloadedState, combineReducers, configureStore } from '@reduxjs/toolkit'; import fileListingReducer from './features/file-listing/fileListingSlice'; +import settingsReducer from './features/settings/settingsSlice'; const rootReducer = combineReducers({ fileListing: fileListingReducer, + settings: settingsReducer, }); export const setupStore = (preloadedState?: PreloadedState) => diff --git a/src/util/logUtils.ts b/src/util/logUtils.ts index e37609a..29430b5 100644 --- a/src/util/logUtils.ts +++ b/src/util/logUtils.ts @@ -23,3 +23,23 @@ export function withGroupedLogs(label: string, fn: () => R): R { ); } } + +const noop = (..._args: unknown[]) => { + // noop +}; + +const dummyLogger = { + log: noop, + info: noop, + warn: noop, + debug: noop, + trace: noop, +}; + +export function getLogger() { + if (import.meta.env.ENABLE_PERF_LOG === '1') { + return window.console; + } else { + return dummyLogger; + } +} diff --git a/src/util/objects.ts b/src/util/objects.ts new file mode 100644 index 0000000..77f700b --- /dev/null +++ b/src/util/objects.ts @@ -0,0 +1,7 @@ +export function* enumObject(obj: Record | null | void): Generator<[string, T]> { + if (obj && typeof obj === 'object') { + for (const key in obj) { + yield [key, obj[key]]; + } + } +} From 4def7a260ebbc9ec354e4ce368ee6ec4cf32d41a Mon Sep 17 00:00:00 2001 From: Jixun Wu Date: Sat, 3 Jun 2023 14:13:37 +0100 Subject: [PATCH 02/23] refactor: move components to sub dir --- src/Loader.tsx | 24 ----------------------- src/{ => components}/App.tsx | 2 +- src/components/AppRoot.tsx | 23 ++++++++++++++++++++++ src/{ => components}/CurrentYear.tsx | 0 src/{ => components}/Footer.tsx | 0 src/{ => components}/SDKVersion.tsx | 0 src/{ => components}/SelectFile.tsx | 4 ++-- src/features/file-listing/FileListing.tsx | 2 +- src/main.tsx | 13 ++++++------ 9 files changed, 34 insertions(+), 34 deletions(-) delete mode 100644 src/Loader.tsx rename src/{ => components}/App.tsx (87%) create mode 100644 src/components/AppRoot.tsx rename src/{ => components}/CurrentYear.tsx (100%) rename src/{ => components}/Footer.tsx (100%) rename src/{ => components}/SDKVersion.tsx (100%) rename src/{ => components}/SelectFile.tsx (94%) diff --git a/src/Loader.tsx b/src/Loader.tsx deleted file mode 100644 index a728531..0000000 --- a/src/Loader.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React, { useEffect } from 'react'; -import App from './App'; - -import { ChakraProvider } from '@chakra-ui/react'; -import { Provider } from 'react-redux'; -import { theme } from './theme'; -import { persistSettings } from './features/settings/persistSettings'; -import type { AppStore } from './store'; - -export function Loader({ store }: { store: AppStore }) { - useEffect(() => { - return persistSettings(store); - }, [store]); - - return ( - - - - - - - - ); -} diff --git a/src/App.tsx b/src/components/App.tsx similarity index 87% rename from src/App.tsx rename to src/components/App.tsx index cf4640d..5abf092 100644 --- a/src/App.tsx +++ b/src/components/App.tsx @@ -1,7 +1,7 @@ import { Box, Center, Container } from '@chakra-ui/react'; import { SelectFile } from './SelectFile'; -import { FileListing } from './features/file-listing/FileListing'; +import { FileListing } from '~/features/file-listing/FileListing'; import { Footer } from './Footer'; function App() { diff --git a/src/components/AppRoot.tsx b/src/components/AppRoot.tsx new file mode 100644 index 0000000..70d1115 --- /dev/null +++ b/src/components/AppRoot.tsx @@ -0,0 +1,23 @@ +import { useEffect } from 'react'; +import App from './App'; + +import { ChakraProvider } from '@chakra-ui/react'; +import { Provider } from 'react-redux'; +import { theme } from '~/theme'; +import { persistSettings } from '~/features/settings/persistSettings'; +import { setupStore } from '~/store'; + +// Private to this file only. +const store = setupStore(); + +export function AppRoot() { + useEffect(() => persistSettings(store), []); + + return ( + + + + + + ); +} diff --git a/src/CurrentYear.tsx b/src/components/CurrentYear.tsx similarity index 100% rename from src/CurrentYear.tsx rename to src/components/CurrentYear.tsx diff --git a/src/Footer.tsx b/src/components/Footer.tsx similarity index 100% rename from src/Footer.tsx rename to src/components/Footer.tsx diff --git a/src/SDKVersion.tsx b/src/components/SDKVersion.tsx similarity index 100% rename from src/SDKVersion.tsx rename to src/components/SDKVersion.tsx diff --git a/src/SelectFile.tsx b/src/components/SelectFile.tsx similarity index 94% rename from src/SelectFile.tsx rename to src/components/SelectFile.tsx index 181c6fe..fc0fd78 100644 --- a/src/SelectFile.tsx +++ b/src/components/SelectFile.tsx @@ -2,8 +2,8 @@ import { useDropzone } from 'react-dropzone'; import { Box, Text } from '@chakra-ui/react'; import { UnlockIcon } from '@chakra-ui/icons'; -import { useAppDispatch } from './hooks'; -import { addNewFile, processFile } from './features/file-listing/fileListingSlice'; +import { useAppDispatch } from '~/hooks'; +import { addNewFile, processFile } from '~/features/file-listing/fileListingSlice'; import { nanoid } from 'nanoid'; export function SelectFile() { diff --git a/src/features/file-listing/FileListing.tsx b/src/features/file-listing/FileListing.tsx index 5211cbf..09bec14 100644 --- a/src/features/file-listing/FileListing.tsx +++ b/src/features/file-listing/FileListing.tsx @@ -1,7 +1,7 @@ import { VStack } from '@chakra-ui/react'; import { selectFiles } from './fileListingSlice'; -import { useAppSelector } from '../../hooks'; +import { useAppSelector } from '~/hooks'; import { FileRow } from './FileRow'; export function FileListing() { diff --git a/src/main.tsx b/src/main.tsx index 469e6dc..d72bd43 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,9 +1,10 @@ +import React from 'react'; import ReactDOM from 'react-dom/client'; -import { setupStore } from './store'; -import { Loader } from './Loader'; +import { AppRoot } from './components/AppRoot'; -// Private to this file only. -const store = setupStore(); - -ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(); +ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( + + + +); From d91b71e0d32d9304c3befbabac2ef74ca4a7c76a Mon Sep 17 00:00:00 2001 From: Jixun Wu Date: Sat, 3 Jun 2023 14:15:21 +0100 Subject: [PATCH 03/23] chore: fix typo --- src/util/__tests__/ConcurrentQueue.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/util/__tests__/ConcurrentQueue.test.ts b/src/util/__tests__/ConcurrentQueue.test.ts index 67db525..5074953 100644 --- a/src/util/__tests__/ConcurrentQueue.test.ts +++ b/src/util/__tests__/ConcurrentQueue.test.ts @@ -3,7 +3,7 @@ import { nextTickAsync } from '../nextTick'; class SimpleQueue extends ConcurrentQueue { handler(_item: T): Promise { - throw new Error('Method not overriden'); + throw new Error('Method not overridden'); } } @@ -39,7 +39,7 @@ test('should be able to process the queue within limit', async () => { await promises[i]; } - // Wait till all fullfilled + // Wait till all fulfilled while (queuedResolver.length !== 5) { await nextTickAsync(); } @@ -85,13 +85,13 @@ test('it should move on to the next item in the queue once failed', async () => promises.push(queue.add(4)); promises.push(queue.add(5)); - // Let first 2 be fullfilled + // Let first 2 be fulfilled for (let i = 0; i < 2; i++) { queuedResolver[i](); await promises[i]; } - // Wait till all fullfilled + // Wait till all fulfilled while (queuedResolver.length !== 4) { await nextTickAsync(); } From 3a2a31f3722dacfad426b2d07bd9edda90e16a16 Mon Sep 17 00:00:00 2001 From: Jixun Wu Date: Sat, 3 Jun 2023 14:15:53 +0100 Subject: [PATCH 04/23] test: fix sanity check test --- src/__tests__/sanity-check.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/__tests__/sanity-check.test.tsx b/src/__tests__/sanity-check.test.tsx index 49d3587..e6f25a0 100644 --- a/src/__tests__/sanity-check.test.tsx +++ b/src/__tests__/sanity-check.test.tsx @@ -1,5 +1,5 @@ import { renderWithProviders, screen, waitFor } from '~/test-utils/test-helper'; -import App from '~/App'; +import App from '~/components/App'; vi.mock('../decrypt-worker/client', () => { return { From bb74c6e2b94360fc1d4ce298fe35da308934202c Mon Sep 17 00:00:00 2001 From: Jixun Wu Date: Sat, 3 Jun 2023 14:58:17 +0100 Subject: [PATCH 05/23] feat: added dummy settings modal --- package.json | 1 + pnpm-lock.yaml | 12 +++++++++++ src/components/App.tsx | 6 +++--- src/components/Toolbar.tsx | 23 ++++++++++++++++++++ src/modals/SettingsModal.tsx | 42 ++++++++++++++++++++++++++++++++++++ src/theme.ts | 18 ++++++++++++++++ 6 files changed, 99 insertions(+), 3 deletions(-) create mode 100644 src/components/Toolbar.tsx create mode 100644 src/modals/SettingsModal.tsx diff --git a/package.json b/package.json index af15912..907d0c6 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-dropzone": "^14.2.3", + "react-icons": "^4.9.0", "react-promise-suspense": "^0.3.4", "react-redux": "^8.0.5" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 29dfd3c..f4c38fc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -40,6 +40,9 @@ dependencies: react-dropzone: specifier: ^14.2.3 version: 14.2.3(react@18.2.0) + react-icons: + specifier: ^4.9.0 + version: 4.9.0(react@18.2.0) react-promise-suspense: specifier: ^0.3.4 version: 0.3.4 @@ -6690,6 +6693,15 @@ packages: use-sidecar: 1.1.2(@types/react@18.2.7)(react@18.2.0) dev: false + /react-icons@4.9.0(react@18.2.0): + resolution: + { integrity: sha512-ijUnFr//ycebOqujtqtV9PFS7JjhWg0QU6ykURVHuL4cbofvRCf3f6GMn9+fBktEFQOIVZnuAYLZdiyadRQRFg== } + peerDependencies: + react: '*' + dependencies: + react: 18.2.0 + dev: false + /react-is@16.13.1: resolution: { integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== } diff --git a/src/components/App.tsx b/src/components/App.tsx index 5abf092..6505f44 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -3,6 +3,7 @@ import { SelectFile } from './SelectFile'; import { FileListing } from '~/features/file-listing/FileListing'; import { Footer } from './Footer'; +import { Toolbar } from './Toolbar'; function App() { return ( @@ -11,9 +12,8 @@ function App() {
- - - + +