From 7726dbe01d82b2e3058ec8a16019da7080ddaf92 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Tue, 15 Mar 2022 17:25:20 -0500 Subject: [PATCH] settings: convert hubName to react hook --- src/firmware/FlashButton.tsx | 11 +++- src/firmware/actions.ts | 4 +- src/firmware/sagas.test.ts | 37 ++++++----- src/firmware/sagas.ts | 15 +++-- src/reducers.ts | 2 - src/settings/SettingsDrawer.test.tsx | 28 +++++++++ src/settings/SettingsDrawer.tsx | 20 +++--- src/settings/actions.ts | 25 +------- src/settings/defaults.ts | 18 ------ src/settings/hooks.ts | 42 ++++++++++++- src/settings/reducers.test.ts | 67 -------------------- src/settings/reducers.ts | 45 ------------- src/settings/sagas.test.ts | 75 +--------------------- src/settings/sagas.ts | 94 +--------------------------- 14 files changed, 125 insertions(+), 358 deletions(-) delete mode 100644 src/settings/defaults.ts delete mode 100644 src/settings/reducers.test.ts delete mode 100644 src/settings/reducers.ts diff --git a/src/firmware/FlashButton.tsx b/src/firmware/FlashButton.tsx index fa04db94..d988727f 100644 --- a/src/firmware/FlashButton.tsx +++ b/src/firmware/FlashButton.tsx @@ -7,7 +7,7 @@ import { BleConnectionState } from '../ble/reducers'; import { BootloaderConnectionState } from '../lwp3-bootloader/reducers'; import * as notificationActions from '../notifications/actions'; import { useSelector } from '../reducers'; -import { useSettingFlashCurrentProgram } from '../settings/hooks'; +import { useSettingFlashCurrentProgram, useSettingHubName } from '../settings/hooks'; import OpenFileButton, { OpenFileButtonProps } from '../toolbar/OpenFileButton'; import { TooltipId } from '../toolbar/i18n'; import { flashFirmware } from './actions'; @@ -21,6 +21,7 @@ const FlashButton: React.VoidFunctionComponent = ({ id }) => { const flashing = useSelector((s) => s.firmware.flashing); const progress = useSelector((s) => s.firmware.progress); const [isSettingFlashCurrentProgramEnabled] = useSettingFlashCurrentProgram(); + const { hubName } = useSettingHubName(); const dispatch = useDispatch(); @@ -37,7 +38,9 @@ const FlashButton: React.VoidFunctionComponent = ({ id }) => { showProgress={flashing} progress={progress === null ? undefined : progress} onFile={(data) => - dispatch(flashFirmware(data, isSettingFlashCurrentProgramEnabled)) + dispatch( + flashFirmware(data, isSettingFlashCurrentProgramEnabled, hubName), + ) } onReject={(file) => dispatch( @@ -48,7 +51,9 @@ const FlashButton: React.VoidFunctionComponent = ({ id }) => { ) } onClick={() => - dispatch(flashFirmware(null, isSettingFlashCurrentProgramEnabled)) + dispatch( + flashFirmware(null, isSettingFlashCurrentProgramEnabled, hubName), + ) } /> ); diff --git a/src/firmware/actions.ts b/src/firmware/actions.ts index f12ac9b3..e441c44c 100644 --- a/src/firmware/actions.ts +++ b/src/firmware/actions.ts @@ -122,12 +122,14 @@ export type FailToFinishReason = * @param data The firmware zip file data or `null` to get firmware later. * @param flashCurrentProgram If true, flash the current program from the editor, * otherwise use the program from firmware.zip. + * @param hubName A custom hub name or an empty string to use the default name. */ export const flashFirmware = createAction( - (data: ArrayBuffer | null, flashCurrentProgram: boolean) => ({ + (data: ArrayBuffer | null, flashCurrentProgram: boolean, hubName: string) => ({ type: 'flashFirmware.action.flashFirmware', data, flashCurrentProgram, + hubName, }), ); diff --git a/src/firmware/sagas.test.ts b/src/firmware/sagas.test.ts index e2de32bc..aaaf19fc 100644 --- a/src/firmware/sagas.test.ts +++ b/src/firmware/sagas.test.ts @@ -82,11 +82,9 @@ describe('flashFirmware', () => { nextMessageId: createCountFunc(), }); - saga.updateState({ settings: { hubName: 'test name' } }); - // saga is triggered by this action - saga.put(flashFirmwareAction(null, false)); + saga.put(flashFirmwareAction(null, false, 'test name')); // first step is to connect to the hub bootloader @@ -231,7 +229,7 @@ describe('flashFirmware', () => { // saga is triggered by this action - saga.put(flashFirmwareAction(null, false)); + saga.put(flashFirmwareAction(null, false, '')); // first step is to connect to the hub bootloader @@ -280,7 +278,7 @@ describe('flashFirmware', () => { // saga is triggered by this action - saga.put(flashFirmwareAction(null, false)); + saga.put(flashFirmwareAction(null, false, '')); // first step is to connect to the hub bootloader @@ -347,7 +345,7 @@ describe('flashFirmware', () => { // saga is triggered by this action - saga.put(flashFirmwareAction(null, false)); + saga.put(flashFirmwareAction(null, false, '')); // first step is to connect to the hub bootloader @@ -410,7 +408,7 @@ describe('flashFirmware', () => { // saga is triggered by this action - saga.put(flashFirmwareAction(null, false)); + saga.put(flashFirmwareAction(null, false, '')); // first step is to connect to the hub bootloader @@ -476,7 +474,7 @@ describe('flashFirmware', () => { // saga is triggered by this action - saga.put(flashFirmwareAction(null, false)); + saga.put(flashFirmwareAction(null, false, '')); // first step is to connect to the hub bootloader @@ -535,7 +533,7 @@ describe('flashFirmware', () => { // saga is triggered by this action - saga.put(flashFirmwareAction(null, false)); + saga.put(flashFirmwareAction(null, false, '')); // first step is to connect to the hub bootloader @@ -600,7 +598,7 @@ describe('flashFirmware', () => { // saga is triggered by this action - saga.put(flashFirmwareAction(null, false)); + saga.put(flashFirmwareAction(null, false, '')); // first step is to connect to the hub bootloader @@ -681,7 +679,7 @@ describe('flashFirmware', () => { // saga is triggered by this action - saga.put(flashFirmwareAction(null, false)); + saga.put(flashFirmwareAction(null, false, '')); // first step is to connect to the hub bootloader @@ -746,7 +744,7 @@ describe('flashFirmware', () => { // saga is triggered by this action - saga.put(flashFirmwareAction(null, false)); + saga.put(flashFirmwareAction(null, false, '')); // first step is to connect to the hub bootloader @@ -840,7 +838,7 @@ describe('flashFirmware', () => { // saga is triggered by this action - saga.put(flashFirmwareAction(null, false)); + saga.put(flashFirmwareAction(null, false, '')); // first step is to connect to the hub bootloader @@ -943,7 +941,7 @@ describe('flashFirmware', () => { // saga is triggered by this action - saga.put(flashFirmwareAction(null, false)); + saga.put(flashFirmwareAction(null, false, '')); // first step is to connect to the hub bootloader @@ -1086,7 +1084,7 @@ describe('flashFirmware', () => { // saga is triggered by this action - saga.put(flashFirmwareAction(null, false)); + saga.put(flashFirmwareAction(null, false, '')); // first step is to connect to the hub bootloader @@ -1231,6 +1229,7 @@ describe('flashFirmware', () => { flashFirmwareAction( await zip.generateAsync({ type: 'arraybuffer' }), false, + '', ), ); @@ -1378,6 +1377,7 @@ describe('flashFirmware', () => { flashFirmwareAction( await zip.generateAsync({ type: 'arraybuffer' }), false, + '', ), ); @@ -1424,6 +1424,7 @@ describe('flashFirmware', () => { flashFirmwareAction( await zip.generateAsync({ type: 'arraybuffer' }), false, + '', ), ); @@ -1469,6 +1470,7 @@ describe('flashFirmware', () => { flashFirmwareAction( await zip.generateAsync({ type: 'arraybuffer' }), false, + '', ), ); @@ -1527,6 +1529,7 @@ describe('flashFirmware', () => { flashFirmwareAction( await zip.generateAsync({ type: 'arraybuffer' }), false, + '', ), ); @@ -1586,6 +1589,7 @@ describe('flashFirmware', () => { flashFirmwareAction( await zip.generateAsync({ type: 'arraybuffer' }), false, + '', ), ); @@ -1648,6 +1652,7 @@ describe('flashFirmware', () => { flashFirmwareAction( await zip.generateAsync({ type: 'arraybuffer' }), false, + '', ), ); @@ -1736,7 +1741,7 @@ describe('flashFirmware', () => { // saga is triggered by this action - saga.put(flashFirmwareAction(null, false)); + saga.put(flashFirmwareAction(null, false, '')); // first step is to connect to the hub bootloader diff --git a/src/firmware/sagas.ts b/src/firmware/sagas.ts index d786757c..e06df85a 100644 --- a/src/firmware/sagas.ts +++ b/src/firmware/sagas.ts @@ -182,6 +182,7 @@ function* firmwareIterator(data: DataView, maxSize: number): Generator { function* loadFirmware( data: ArrayBuffer, program: string | undefined, + hubName: string, ): SagaGenerator<{ firmware: Uint8Array; deviceId: HubType }> { const [reader, readerErr] = yield* call(() => maybe(FirmwareReader.load(data))); @@ -247,8 +248,6 @@ function* loadFirmware( // if the firmware supports it, we can set a custom hub name if (metadata['max-hub-name-size']) { - const hubName = yield* select((s: RootState) => s.settings.hubName); - // empty string means use default name (don't write over firmware) if (hubName) { firmware.set(encodeHubName(hubName, metadata), metadata['hub-name-offset']); @@ -299,7 +298,11 @@ function* handleFlashFirmware(action: ReturnType): Generat } if (action.data !== null) { - ({ firmware, deviceId } = yield* loadFirmware(action.data, program)); + ({ firmware, deviceId } = yield* loadFirmware( + action.data, + program, + action.hubName, + )); } yield* put(connect()); @@ -341,7 +344,11 @@ function* handleFlashFirmware(action: ReturnType): Generat } const data = yield* call(() => response.arrayBuffer()); - ({ firmware, deviceId } = yield* loadFirmware(data, program)); + ({ firmware, deviceId } = yield* loadFirmware( + data, + program, + action.hubName, + )); if (deviceId !== undefined && info.hubType !== deviceId) { yield* put(didFailToFinish(FailToFinishReasonType.DeviceMismatch)); diff --git a/src/reducers.ts b/src/reducers.ts index 124de3d7..e6d8ffa3 100644 --- a/src/reducers.ts +++ b/src/reducers.ts @@ -10,7 +10,6 @@ import firmware from './firmware/reducers'; import hub from './hub/reducers'; import licenses from './licenses/reducers'; import bootloader from './lwp3-bootloader/reducers'; -import settings from './settings/reducers'; /** * Root reducer for redux store. @@ -23,7 +22,6 @@ export const rootReducer = combineReducers({ firmware, hub, licenses, - settings, }); /** diff --git a/src/settings/SettingsDrawer.test.tsx b/src/settings/SettingsDrawer.test.tsx index 27921f6f..a28c4c47 100644 --- a/src/settings/SettingsDrawer.test.tsx +++ b/src/settings/SettingsDrawer.test.tsx @@ -55,6 +55,34 @@ describe('flashCurrentProgram setting switch', () => { }); }); +describe('hubName setting', () => { + it('should migrate old settings', () => { + // old settings did not use json format, so lack quotes + localStorage.setItem('setting.hubName', 'old name'); + + const [settings] = testRender( + undefined} />, + ); + + const textBox = settings.getByLabelText('Hub name'); + + expect(textBox).toHaveValue('old name'); + }); + + it('should update the setting', () => { + const [settings] = testRender( + undefined} />, + ); + + expect(localStorage.getItem('setting.hubName')).toBe(null); + + const textBox = settings.getByLabelText('Hub name'); + userEvent.type(textBox, 'test name'); + + expect(localStorage.getItem('setting.hubName')).toBe('"test name"'); + }); +}); + describe('about dialog', () => { it('should open the dialog when the button is clicked', async () => { const [settings] = testRender( diff --git a/src/settings/SettingsDrawer.tsx b/src/settings/SettingsDrawer.tsx index c064c903..84bdb0cc 100644 --- a/src/settings/SettingsDrawer.tsx +++ b/src/settings/SettingsDrawer.tsx @@ -35,9 +35,11 @@ import { pseudolocalize } from '../i18n'; import { useSelector } from '../reducers'; import ExternalLinkIcon from '../utils/ExternalLinkIcon'; import { isMacOS } from '../utils/os'; -import { setString } from './actions'; -import { StringSettingId } from './defaults'; -import { useSettingFlashCurrentProgram, useSettingIsShowDocsEnabled } from './hooks'; +import { + useSettingFlashCurrentProgram, + useSettingHubName, + useSettingIsShowDocsEnabled, +} from './hooks'; import { SettingsStringId } from './i18n'; import en from './i18n.en.json'; import './settings.scss'; @@ -71,8 +73,7 @@ const SettingsDrawer: React.VoidFunctionComponent = ({ ); const promptingInstall = useSelector((s) => s.app.promptingInstall); const readyForOfflineUse = useSelector((s) => s.app.readyForOfflineUse); - const hubName = useSelector((s) => s.settings.hubName); - const isHubNameValid = useSelector((s) => s.settings.isHubNameValid); + const { hubName, isHubNameValid, setHubName } = useSettingHubName(); const dispatch = useDispatch(); @@ -200,14 +201,7 @@ const SettingsDrawer: React.VoidFunctionComponent = ({ - dispatch( - setString( - StringSettingId.HubName, - e.currentTarget.value, - ), - ) - } + onChange={(e) => setHubName(e.currentTarget.value)} onMouseOver={(e) => e.preventDefault()} className="pb-hub-name-input" intent={isHubNameValid ? Intent.NONE : Intent.DANGER} diff --git a/src/settings/actions.ts b/src/settings/actions.ts index bdadd76b..b35fdd35 100644 --- a/src/settings/actions.ts +++ b/src/settings/actions.ts @@ -1,30 +1,7 @@ // SPDX-License-Identifier: MIT -// Copyright (c) 2021-2022 The Pybricks Authors +// Copyright (c) 2022 The Pybricks Authors import { createAction } from '../actions'; -import { StringSettingId } from './defaults'; - -/** Creates an action to set/store a setting. */ -export const setString = createAction((id: StringSettingId, newState: string) => ({ - type: 'settings.action.setString', - id, - newState, -})); - -/** Creates an action indicating that setting/storing a setting failed. */ -export const didFailToSetString = createAction((id: StringSettingId, err: Error) => ({ - type: 'settings.action.didFailToSetString', - id, - err, -})); - -export const didStringChange = createAction( - (id: StringSettingId, newState: string) => ({ - type: 'settings.action.didStringChange', - id, - newState, - }), -); /** Requests to toggle the showDocs setting. */ export const settingsToggleShowDocs = createAction(() => ({ diff --git a/src/settings/defaults.ts b/src/settings/defaults.ts deleted file mode 100644 index cf352d71..00000000 --- a/src/settings/defaults.ts +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2021-2022 The Pybricks Authors - -// Definitions for user selectable settings. - -export enum StringSettingId { - HubName = 'hubName', -} - -export function getDefaultStringValue(id: StringSettingId): string { - switch (id) { - case StringSettingId.HubName: - return ''; // empty string will result in 'Pybricks Hub' - // istanbul ignore next: it is a programmer error if we hit this - default: - throw Error(`Bad StringSettingId: ${id}`); - } -} diff --git a/src/settings/hooks.ts b/src/settings/hooks.ts index 53ef9fc4..81f6e896 100644 --- a/src/settings/hooks.ts +++ b/src/settings/hooks.ts @@ -2,7 +2,9 @@ // Copyright (c) 2022 The Pybricks Authors import { Dispatch, SetStateAction, useCallback } from 'react'; -import { useLocalStorage } from 'usehooks-ts'; +import { useIsFirstRender, useLocalStorage } from 'usehooks-ts'; + +const encoder = new TextEncoder(); // this is private type from usehooks-ts type SetValue = Dispatch>; @@ -34,3 +36,41 @@ export function useSettingIsShowDocsEnabled(): { export function useSettingFlashCurrentProgram(): [boolean, SetValue] { return useLocalStorage('setting.flashCurrentProgram', false); } + +/** + * Validates the hub name. + * @param hubName The hub name. + * @returns True if the name if valid, otherwise false. + */ +function validateHubName(hubName: string): boolean { + const encoded = encoder.encode(hubName); + + // Technically, the max hub name size is determined by each individual + // firmware file, so we can't check until the firmware has been selected. + // However all firmware currently have 16 bytes allocated (including zero- + // termination), so we can hard code the check here to allow notifying the + // user earlier for better UX. + return encoded.length < 16; +} + +/** Hook for "hubName" setting. */ +export function useSettingHubName(): { + hubName: string; + isHubNameValid: boolean; + setHubName: (value: string) => void; +} { + if (useIsFirstRender()) { + // in version 1.x, settings didn't use json format, so we have to migrate + const oldSetting = localStorage.getItem('setting.hubName'); + + if (oldSetting !== null && !oldSetting.startsWith('"')) { + localStorage.setItem('setting.hubName', JSON.stringify(oldSetting)); + } + } + + const [hubName, setHubName] = useLocalStorage('setting.hubName', ''); + + const isHubNameValid = validateHubName(hubName); + + return { hubName, isHubNameValid, setHubName }; +} diff --git a/src/settings/reducers.test.ts b/src/settings/reducers.test.ts deleted file mode 100644 index 2c9fd972..00000000 --- a/src/settings/reducers.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2021-2022 The Pybricks Authors - -import { AnyAction } from 'redux'; -import { didStringChange } from './actions'; -import { StringSettingId } from './defaults'; -import reducers from './reducers'; - -type State = ReturnType; - -test('initial state', () => { - expect(reducers(undefined, {} as AnyAction)).toMatchInlineSnapshot(` - Object { - "hubName": "", - "isHubNameValid": true, - } - `); -}); - -describe('hubName', () => { - const testName = 'test name'; - test('setting changed', () => { - expect( - reducers( - { hubName: '' } as State, - didStringChange(StringSettingId.HubName, testName), - ).hubName, - ).toBe(testName); - }); -}); - -describe('isHubNameValid', () => { - test('default name is ok', () => { - expect( - reducers( - undefined, - didStringChange(StringSettingId.HubName, 'Pybricks Hub'), - ).isHubNameValid, - ).toBe(true); - }); - - test('empty name is ok', () => { - expect( - reducers(undefined, didStringChange(StringSettingId.HubName, '')) - .isHubNameValid, - ).toBe(true); - }); - - test('too long name fails', () => { - expect( - reducers( - undefined, - didStringChange(StringSettingId.HubName, 'this name is way too long'), - ).isHubNameValid, - ).toBe(false); - }); - - test('the number of bytes matter, not the number of characters', () => { - expect( - reducers( - undefined, - // Chinese characters are 3 bytes each. - didStringChange(StringSettingId.HubName, 'Pybricks 枢纽!'), - ).isHubNameValid, - ).toBe(false); - }); -}); diff --git a/src/settings/reducers.ts b/src/settings/reducers.ts deleted file mode 100644 index 46be2ab8..00000000 --- a/src/settings/reducers.ts +++ /dev/null @@ -1,45 +0,0 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2021-2022 The Pybricks Authors - -import { Reducer, combineReducers } from 'redux'; -import { didStringChange } from './actions'; -import { StringSettingId, getDefaultStringValue } from './defaults'; - -const encoder = new TextEncoder(); - -const hubName: Reducer = ( - state = getDefaultStringValue(StringSettingId.HubName), - action, -) => { - if (didStringChange.matches(action)) { - if (action.id === StringSettingId.HubName) { - return action.newState; - } - return state; - } - - return state; -}; - -const isHubNameValid: Reducer = (state = true, action) => { - if (didStringChange.matches(action)) { - if (action.id === StringSettingId.HubName) { - const encoded = encoder.encode(action.newState); - - // Technically, the max hub name size is determined by each individual - // firmware file, so we can't check until the firmware has been selected. - // However all firmware currently has 16 bytes allocated (including zero- - // termination), so we can hard code the check here to allow notifying the - // user earlier for better UX. - return encoded.length < 16; - } - return state; - } - - return state; -}; - -export default combineReducers({ - hubName, - isHubNameValid, -}); diff --git a/src/settings/sagas.test.ts b/src/settings/sagas.test.ts index e39d21de..91e49324 100644 --- a/src/settings/sagas.test.ts +++ b/src/settings/sagas.test.ts @@ -1,83 +1,12 @@ // SPDX-License-Identifier: MIT -// Copyright (c) 2020-2022 The Pybricks Authors +// Copyright (c) 2022 The Pybricks Authors // // Tests for settings sagas. import { AsyncSaga } from '../../test'; -import { - didFailToSetString, - didStringChange, - setString, - settingsToggleShowDocs, -} from './actions'; -import { StringSettingId } from './defaults'; +import { settingsToggleShowDocs } from './actions'; import settings from './sagas'; -afterEach(() => { - jest.restoreAllMocks(); -}); - -describe('store settings to local storage', () => { - test('failed storage', async () => { - const saga = new AsyncSaga(settings); - - const testError = new Error('local storage is disabled'); - - const mockSetItem = jest - .spyOn(Object.getPrototypeOf(window.localStorage), 'setItem') - .mockImplementation(() => { - throw testError; - }); - - saga.put(setString(StringSettingId.HubName, 'test name')); - expect(mockSetItem).toHaveBeenCalled(); - - // raises error that storing setting didn't work - const action3 = await saga.take(); - expect(action3).toEqual(didFailToSetString(StringSettingId.HubName, testError)); - - // but the setting is still applied anyway - const action4 = await saga.take(); - expect(action4).toEqual(didStringChange(StringSettingId.HubName, 'test name')); - - await saga.end(); - }); -}); - -describe('storage monitor', () => { - test('ignores other keys', async () => { - const saga = new AsyncSaga(settings); - - window.dispatchEvent( - new StorageEvent('storage', { - key: 'not a setting', - storageArea: localStorage, - }), - ); - - // nothing happens - - await saga.end(); - }); - - test('ignores session storage', async () => { - const saga = new AsyncSaga(settings); - - window.dispatchEvent( - new StorageEvent('storage', { - key: 'setting.showDocs', - newValue: 'true', - oldValue: 'false', - storageArea: sessionStorage, - }), - ); - - // nothing happens - - await saga.end(); - }); -}); - describe('handleToggleShowDocs', () => { it('should toggle the showDocs setting', async () => { const key = 'setting.showDocs'; diff --git a/src/settings/sagas.ts b/src/settings/sagas.ts index deb5a76c..a9e8b4b6 100644 --- a/src/settings/sagas.ts +++ b/src/settings/sagas.ts @@ -1,97 +1,12 @@ // SPDX-License-Identifier: MIT -// Copyright (c) 2021-2022 The Pybricks Authors +// Copyright (c) 2022 The Pybricks Authors // This manages settings by storing them in local storage whenever the app // request to set a setting. When local storage changes, it triggers a did // change action that can be used by reducers to compute the new state. -import { EventChannel, eventChannel } from 'redux-saga'; -import { call, fork, put, select, take, takeEvery } from 'typed-redux-saga/macro'; -import { didStart } from '../app/actions'; -import { RootState } from '../reducers'; -import { ensureError } from '../utils'; -import { - didFailToSetString, - didStringChange, - setString, - settingsToggleShowDocs, -} from './actions'; -import { StringSettingId, getDefaultStringValue } from './defaults'; - -function createLocalStorageEventChannel(): EventChannel { - return eventChannel((emitter) => { - const handler: (e: StorageEvent) => void = (e) => { - if (e.storageArea !== localStorage) { - return; - } - emitter(e); - }; - window.addEventListener('storage', handler); - // istanbul ignore next: this is not normally called - return () => window.removeEventListener('storage', handler); - }); -} - -function* monitorLocalStorage(): Generator { - const chan = yield* call(createLocalStorageEventChannel); - - while (true) { - const event = yield* take(chan); - - // only care about storage keys 'setting.*' - if (!event.key?.startsWith('setting.')) { - continue; - } - - const id = event.key.replace(/^setting\./, ''); - - if (Object.values(StringSettingId).includes(id as StringSettingId)) { - yield* put(didStringChange(id as StringSettingId, event.newValue || '')); - continue; - } - - // istanbul ignore next: should not happen normally - console.error(`Bad setting id: ${id}`); - } -} - -function* loadSettings(): Generator { - for (const id of Object.values(StringSettingId)) { - const storageValue = localStorage.getItem(`setting.${id}`); - const defaultValue = getDefaultStringValue(id); - const value = storageValue === null ? defaultValue : storageValue; - - if (value !== defaultValue) { - yield* put(didStringChange(id, value)); - } - } -} - -function* storeStringSetting(action: ReturnType): Generator { - const key = `setting.${action.id}`; - const newValue = action.newState; - - try { - localStorage.setItem(key, newValue); - } catch (err) { - yield* put(didFailToSetString(action.id, ensureError(err))); - } - - // storage event is only raised when a value is changed externally, so we - // mimic the event when we call setItem(), whether it actually succeeded - // or not. - const oldValue = yield* select((s: RootState) => s.settings[action.id]); - if (action.newState !== oldValue) { - window.dispatchEvent( - new StorageEvent('storage', { - key, - newValue, - oldValue, - storageArea: localStorage, - }), - ); - } -} +import { call, takeEvery } from 'typed-redux-saga/macro'; +import { settingsToggleShowDocs } from './actions'; /** * Hack to wire editor action to settings hook. @@ -113,8 +28,5 @@ function* handleToggleShowDocs(): Generator { } export default function* (): Generator { - yield* fork(monitorLocalStorage); - yield* takeEvery(didStart, loadSettings); - yield* takeEvery(setString, storeStringSetting); yield* takeEvery(settingsToggleShowDocs, handleToggleShowDocs); }