diff options
Diffstat (limited to 'packages')
31 files changed, 366 insertions, 258 deletions
diff --git a/packages/main/src/index.ts b/packages/main/src/index.ts index bcdc3d7..a886a16 100644 --- a/packages/main/src/index.ts +++ b/packages/main/src/index.ts | |||
@@ -19,22 +19,24 @@ | |||
19 | * SPDX-License-Identifier: AGPL-3.0-only | 19 | * SPDX-License-Identifier: AGPL-3.0-only |
20 | */ | 20 | */ |
21 | 21 | ||
22 | import { readFileSync } from 'node:fs'; | ||
23 | import { readFile } from 'node:fs/promises'; | ||
22 | import { arch } from 'node:os'; | 24 | import { arch } from 'node:os'; |
23 | import path from 'node:path'; | 25 | import path from 'node:path'; |
24 | import { URL } from 'node:url'; | 26 | import { URL } from 'node:url'; |
25 | 27 | ||
26 | import { | 28 | import { |
27 | ServiceToMainIpcMessage, | 29 | ServiceToMainIpcMessage, |
28 | unreadCount, | 30 | UnreadCount, |
29 | WebSource, | 31 | WebSource, |
30 | } from '@sophie/service-shared'; | 32 | } from '@sophie/service-shared'; |
31 | import { | 33 | import { |
32 | action, | 34 | Action, |
33 | MainToRendererIpcMessage, | 35 | MainToRendererIpcMessage, |
34 | RendererToMainIpcMessage, | 36 | RendererToMainIpcMessage, |
35 | } from '@sophie/shared'; | 37 | } from '@sophie/shared'; |
36 | import { app, BrowserView, BrowserWindow, ipcMain } from 'electron'; | 38 | import { app, BrowserView, BrowserWindow, ipcMain } from 'electron'; |
37 | import { ensureDirSync, readFile, readFileSync } from 'fs-extra'; | 39 | import { ensureDirSync } from 'fs-extra'; |
38 | import { autorun } from 'mobx'; | 40 | import { autorun } from 'mobx'; |
39 | import { getSnapshot, onAction, onPatch } from 'mobx-state-tree'; | 41 | import { getSnapshot, onAction, onPatch } from 'mobx-state-tree'; |
40 | import osName from 'os-name'; | 42 | import osName from 'os-name'; |
@@ -45,7 +47,7 @@ import { | |||
45 | installDevToolsExtensions, | 47 | installDevToolsExtensions, |
46 | openDevToolsWhenReady, | 48 | openDevToolsWhenReady, |
47 | } from './devTools'; | 49 | } from './devTools'; |
48 | import init from './init'; | 50 | import initReactions from './initReactions'; |
49 | import { createMainStore } from './stores/MainStore'; | 51 | import { createMainStore } from './stores/MainStore'; |
50 | import { getLogger } from './utils/log'; | 52 | import { getLogger } from './utils/log'; |
51 | 53 | ||
@@ -128,7 +130,7 @@ let mainWindow: BrowserWindow | undefined; | |||
128 | 130 | ||
129 | const store = createMainStore(); | 131 | const store = createMainStore(); |
130 | 132 | ||
131 | init(store) | 133 | initReactions(store) |
132 | // eslint-disable-next-line promise/always-return -- `then` instead of top-level await. | 134 | // eslint-disable-next-line promise/always-return -- `then` instead of top-level await. |
133 | .then((disposeCompositionRoot) => { | 135 | .then((disposeCompositionRoot) => { |
134 | app.on('will-quit', disposeCompositionRoot); | 136 | app.on('will-quit', disposeCompositionRoot); |
@@ -267,7 +269,7 @@ async function createWindow(): Promise<unknown> { | |||
267 | return; | 269 | return; |
268 | } | 270 | } |
269 | try { | 271 | try { |
270 | const actionToDispatch = action.parse(rawAction); | 272 | const actionToDispatch = Action.parse(rawAction); |
271 | switch (actionToDispatch.action) { | 273 | switch (actionToDispatch.action) { |
272 | case 'set-selected-service-id': | 274 | case 'set-selected-service-id': |
273 | store.shared.setSelectedServiceId(actionToDispatch.serviceId); | 275 | store.shared.setSelectedServiceId(actionToDispatch.serviceId); |
@@ -331,7 +333,7 @@ async function createWindow(): Promise<unknown> { | |||
331 | // otherwise electron emits a no handler registered warning. | 333 | // otherwise electron emits a no handler registered warning. |
332 | break; | 334 | break; |
333 | case ServiceToMainIpcMessage.SetUnreadCount: | 335 | case ServiceToMainIpcMessage.SetUnreadCount: |
334 | log.log('Unread count:', unreadCount.parse(args[0])); | 336 | log.log('Unread count:', UnreadCount.parse(args[0])); |
335 | break; | 337 | break; |
336 | default: | 338 | default: |
337 | log.error('Unknown IPC message:', channel, args); | 339 | log.error('Unknown IPC message:', channel, args); |
diff --git a/packages/main/src/infrastructure/impl/FileBasedConfigPersistence.ts b/packages/main/src/infrastructure/config/ConfigFile.ts index 88d8bf8..193a20d 100644 --- a/packages/main/src/infrastructure/impl/FileBasedConfigPersistence.ts +++ b/packages/main/src/infrastructure/config/ConfigFile.ts | |||
@@ -17,48 +17,52 @@ | |||
17 | * | 17 | * |
18 | * SPDX-License-Identifier: AGPL-3.0-only | 18 | * SPDX-License-Identifier: AGPL-3.0-only |
19 | */ | 19 | */ |
20 | |||
20 | import { watch } from 'node:fs'; | 21 | import { watch } from 'node:fs'; |
21 | import { readFile, stat, writeFile } from 'node:fs/promises'; | 22 | import { readFile, stat, writeFile } from 'node:fs/promises'; |
22 | import path from 'node:path'; | 23 | import path from 'node:path'; |
23 | 24 | ||
24 | import JSON5 from 'json5'; | 25 | import JSON5 from 'json5'; |
25 | import throttle from 'lodash-es/throttle'; | 26 | import { throttle } from 'lodash-es'; |
26 | 27 | ||
27 | import type { Config } from '../../stores/SharedStore'; | 28 | import type { Config } from '../../stores/SharedStore'; |
28 | import type Disposer from '../../utils/Disposer'; | 29 | import type Disposer from '../../utils/Disposer'; |
29 | import { getLogger } from '../../utils/log'; | 30 | import { getLogger } from '../../utils/log'; |
30 | import type ConfigPersistence from '../ConfigPersistence'; | ||
31 | import type { ReadConfigResult } from '../ConfigPersistence'; | ||
32 | 31 | ||
33 | const log = getLogger('fileBasedConfigPersistence'); | 32 | import type ConfigRepository from './ConfigRepository'; |
33 | import type ReadConfigResult from './ReadConfigResult'; | ||
34 | |||
35 | const log = getLogger('ConfigFile'); | ||
36 | |||
37 | export default class ConfigFile implements ConfigRepository { | ||
38 | readonly #userDataDir: string; | ||
39 | |||
40 | readonly #configFileName: string; | ||
34 | 41 | ||
35 | export default class FileBasedConfigPersistence implements ConfigPersistence { | 42 | readonly #configFilePath: string; |
36 | private readonly configFilePath: string; | ||
37 | 43 | ||
38 | private writingConfig = false; | 44 | #writingConfig = false; |
39 | 45 | ||
40 | private timeLastWritten: Date | undefined; | 46 | #timeLastWritten: Date | undefined; |
41 | 47 | ||
42 | constructor( | 48 | constructor(userDataDir: string, configFileName = 'config.json5') { |
43 | private readonly userDataDir: string, | 49 | this.#userDataDir = userDataDir; |
44 | private readonly configFileName: string = 'config.json5', | 50 | this.#configFileName = configFileName; |
45 | ) { | 51 | this.#configFilePath = path.join(userDataDir, configFileName); |
46 | this.configFileName = configFileName; | ||
47 | this.configFilePath = path.join(this.userDataDir, this.configFileName); | ||
48 | } | 52 | } |
49 | 53 | ||
50 | async readConfig(): Promise<ReadConfigResult> { | 54 | async readConfig(): Promise<ReadConfigResult> { |
51 | let configStr: string; | 55 | let configStr: string; |
52 | try { | 56 | try { |
53 | configStr = await readFile(this.configFilePath, 'utf8'); | 57 | configStr = await readFile(this.#configFilePath, 'utf8'); |
54 | } catch (error) { | 58 | } catch (error) { |
55 | if ((error as NodeJS.ErrnoException).code === 'ENOENT') { | 59 | if ((error as NodeJS.ErrnoException).code === 'ENOENT') { |
56 | log.debug('Config file', this.configFilePath, 'was not found'); | 60 | log.debug('Config file', this.#configFilePath, 'was not found'); |
57 | return { found: false }; | 61 | return { found: false }; |
58 | } | 62 | } |
59 | throw error; | 63 | throw error; |
60 | } | 64 | } |
61 | log.info('Read config file', this.configFilePath); | 65 | log.info('Read config file', this.#configFilePath); |
62 | return { | 66 | return { |
63 | found: true, | 67 | found: true, |
64 | data: JSON5.parse(configStr), | 68 | data: JSON5.parse(configStr), |
@@ -69,32 +73,32 @@ export default class FileBasedConfigPersistence implements ConfigPersistence { | |||
69 | const configJson = JSON5.stringify(configSnapshot, { | 73 | const configJson = JSON5.stringify(configSnapshot, { |
70 | space: 2, | 74 | space: 2, |
71 | }); | 75 | }); |
72 | this.writingConfig = true; | 76 | this.#writingConfig = true; |
73 | try { | 77 | try { |
74 | await writeFile(this.configFilePath, configJson, 'utf8'); | 78 | await writeFile(this.#configFilePath, configJson, 'utf8'); |
75 | const { mtime } = await stat(this.configFilePath); | 79 | const { mtime } = await stat(this.#configFilePath); |
76 | log.trace('Config file', this.configFilePath, 'last written at', mtime); | 80 | log.trace('Config file', this.#configFilePath, 'last written at', mtime); |
77 | this.timeLastWritten = mtime; | 81 | this.#timeLastWritten = mtime; |
78 | } finally { | 82 | } finally { |
79 | this.writingConfig = false; | 83 | this.#writingConfig = false; |
80 | } | 84 | } |
81 | log.info('Wrote config file', this.configFilePath); | 85 | log.info('Wrote config file', this.#configFilePath); |
82 | } | 86 | } |
83 | 87 | ||
84 | watchConfig(callback: () => Promise<void>, throttleMs: number): Disposer { | 88 | watchConfig(callback: () => Promise<void>, throttleMs: number): Disposer { |
85 | log.debug('Installing watcher for', this.userDataDir); | 89 | log.debug('Installing watcher for', this.#userDataDir); |
86 | 90 | ||
87 | const configChanged = throttle(async () => { | 91 | const configChanged = throttle(async () => { |
88 | let mtime: Date; | 92 | let mtime: Date; |
89 | try { | 93 | try { |
90 | const stats = await stat(this.configFilePath); | 94 | const stats = await stat(this.#configFilePath); |
91 | mtime = stats.mtime; | 95 | mtime = stats.mtime; |
92 | log.trace('Config file last modified at', mtime); | 96 | log.trace('Config file last modified at', mtime); |
93 | } catch (error) { | 97 | } catch (error) { |
94 | if ((error as NodeJS.ErrnoException).code === 'ENOENT') { | 98 | if ((error as NodeJS.ErrnoException).code === 'ENOENT') { |
95 | log.debug( | 99 | log.debug( |
96 | 'Config file', | 100 | 'Config file', |
97 | this.configFilePath, | 101 | this.#configFilePath, |
98 | 'was deleted after being changed', | 102 | 'was deleted after being changed', |
99 | ); | 103 | ); |
100 | return; | 104 | return; |
@@ -102,27 +106,27 @@ export default class FileBasedConfigPersistence implements ConfigPersistence { | |||
102 | throw error; | 106 | throw error; |
103 | } | 107 | } |
104 | if ( | 108 | if ( |
105 | !this.writingConfig && | 109 | !this.#writingConfig && |
106 | (this.timeLastWritten === undefined || mtime > this.timeLastWritten) | 110 | (this.#timeLastWritten === undefined || mtime > this.#timeLastWritten) |
107 | ) { | 111 | ) { |
108 | log.debug( | 112 | log.debug( |
109 | 'Found a config file modified at', | 113 | 'Found a config file modified at', |
110 | mtime, | 114 | mtime, |
111 | 'whish is newer than last written', | 115 | 'whish is newer than last written', |
112 | this.timeLastWritten, | 116 | this.#timeLastWritten, |
113 | ); | 117 | ); |
114 | await callback(); | 118 | await callback(); |
115 | } | 119 | } |
116 | }, throttleMs); | 120 | }, throttleMs); |
117 | 121 | ||
118 | const watcher = watch(this.userDataDir, { | 122 | const watcher = watch(this.#userDataDir, { |
119 | persistent: false, | 123 | persistent: false, |
120 | }); | 124 | }); |
121 | 125 | ||
122 | watcher.on('change', (eventType, filename) => { | 126 | watcher.on('change', (eventType, filename) => { |
123 | if ( | 127 | if ( |
124 | eventType === 'change' && | 128 | eventType === 'change' && |
125 | (filename === this.configFileName || filename === null) | 129 | (filename === this.#configFileName || filename === null) |
126 | ) { | 130 | ) { |
127 | configChanged()?.catch((err) => { | 131 | configChanged()?.catch((err) => { |
128 | log.error('Unhandled error while listening for config changes', err); | 132 | log.error('Unhandled error while listening for config changes', err); |
@@ -131,7 +135,7 @@ export default class FileBasedConfigPersistence implements ConfigPersistence { | |||
131 | }); | 135 | }); |
132 | 136 | ||
133 | return () => { | 137 | return () => { |
134 | log.trace('Removing watcher for', this.configFilePath); | 138 | log.trace('Removing watcher for', this.#configFilePath); |
135 | watcher.close(); | 139 | watcher.close(); |
136 | }; | 140 | }; |
137 | } | 141 | } |
diff --git a/packages/main/src/infrastructure/ConfigPersistence.ts b/packages/main/src/infrastructure/config/ConfigRepository.ts index 184fa8d..0ce7fc1 100644 --- a/packages/main/src/infrastructure/ConfigPersistence.ts +++ b/packages/main/src/infrastructure/config/ConfigRepository.ts | |||
@@ -18,8 +18,8 @@ | |||
18 | * SPDX-License-Identifier: AGPL-3.0-only | 18 | * SPDX-License-Identifier: AGPL-3.0-only |
19 | */ | 19 | */ |
20 | 20 | ||
21 | import type { Config } from '../stores/SharedStore'; | 21 | import type { Config } from '../../stores/SharedStore'; |
22 | import type Disposer from '../utils/Disposer'; | 22 | import type Disposer from '../../utils/Disposer'; |
23 | 23 | ||
24 | export type ReadConfigResult = | 24 | export type ReadConfigResult = |
25 | | { found: true; data: unknown } | 25 | | { found: true; data: unknown } |
diff --git a/packages/main/src/infrastructure/config/ReadConfigResult.ts b/packages/main/src/infrastructure/config/ReadConfigResult.ts new file mode 100644 index 0000000..3b3ee55 --- /dev/null +++ b/packages/main/src/infrastructure/config/ReadConfigResult.ts | |||
@@ -0,0 +1,23 @@ | |||
1 | /* | ||
2 | * Copyright (C) 2022 Kristóf Marussy <kristof@marussy.com> | ||
3 | * | ||
4 | * This file is part of Sophie. | ||
5 | * | ||
6 | * Sophie is free software: you can redistribute it and/or modify | ||
7 | * it under the terms of the GNU Affero General Public License as | ||
8 | * published by the Free Software Foundation, version 3. | ||
9 | * | ||
10 | * This program is distributed in the hope that it will be useful, | ||
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
13 | * GNU Affero General Public License for more details. | ||
14 | * | ||
15 | * You should have received a copy of the GNU Affero General Public License | ||
16 | * along with this program. If not, see <https://www.gnu.org/licenses/>. | ||
17 | * | ||
18 | * SPDX-License-Identifier: AGPL-3.0-only | ||
19 | */ | ||
20 | |||
21 | type ReadConfigResult = { found: true; data: unknown } | { found: false }; | ||
22 | |||
23 | export default ReadConfigResult; | ||
diff --git a/packages/main/src/init.ts b/packages/main/src/initReactions.ts index fd8dd94..50e561d 100644 --- a/packages/main/src/init.ts +++ b/packages/main/src/initReactions.ts | |||
@@ -20,21 +20,21 @@ | |||
20 | 20 | ||
21 | import { app } from 'electron'; | 21 | import { app } from 'electron'; |
22 | 22 | ||
23 | import initConfig from './controllers/initConfig'; | 23 | import ConfigFile from './infrastructure/config/ConfigFile'; |
24 | import initNativeTheme from './controllers/initNativeTheme'; | 24 | import synchronizeConfig from './reactions/synchronizeConfig'; |
25 | import FileBasedConfigPersistence from './infrastructure/impl/FileBasedConfigPersistence'; | 25 | import synchronizeNativeTheme from './reactions/synchronizeNativeTheme'; |
26 | import { MainStore } from './stores/MainStore'; | 26 | import type MainStore from './stores/MainStore'; |
27 | import type Disposer from './utils/Disposer'; | 27 | import type Disposer from './utils/Disposer'; |
28 | 28 | ||
29 | export default async function init(store: MainStore): Promise<Disposer> { | 29 | export default async function initReactions( |
30 | const configPersistenceService = new FileBasedConfigPersistence( | 30 | store: MainStore, |
31 | app.getPath('userData'), | 31 | ): Promise<Disposer> { |
32 | ); | 32 | const configRepository = new ConfigFile(app.getPath('userData')); |
33 | const disposeConfigController = await initConfig( | 33 | const disposeConfigController = await synchronizeConfig( |
34 | store.shared, | 34 | store.shared, |
35 | configPersistenceService, | 35 | configRepository, |
36 | ); | 36 | ); |
37 | const disposeNativeThemeController = initNativeTheme(store.shared); | 37 | const disposeNativeThemeController = synchronizeNativeTheme(store.shared); |
38 | 38 | ||
39 | return () => { | 39 | return () => { |
40 | disposeNativeThemeController(); | 40 | disposeNativeThemeController(); |
diff --git a/packages/main/src/controllers/__tests__/initConfig.spec.ts b/packages/main/src/reactions/__tests__/synchronizeConfig.spec.ts index fdd22c9..c145bf3 100644 --- a/packages/main/src/controllers/__tests__/initConfig.spec.ts +++ b/packages/main/src/reactions/__tests__/synchronizeConfig.spec.ts | |||
@@ -22,14 +22,14 @@ import { jest } from '@jest/globals'; | |||
22 | import { mocked } from 'jest-mock'; | 22 | import { mocked } from 'jest-mock'; |
23 | import ms from 'ms'; | 23 | import ms from 'ms'; |
24 | 24 | ||
25 | import type ConfigPersistence from '../../infrastructure/ConfigPersistence'; | 25 | import type ConfigRepository from '../../infrastructure/config/ConfigRepository'; |
26 | import { sharedStore, SharedStore } from '../../stores/SharedStore'; | 26 | import SharedStore from '../../stores/SharedStore'; |
27 | import type Disposer from '../../utils/Disposer'; | 27 | import type Disposer from '../../utils/Disposer'; |
28 | import { silenceLogger } from '../../utils/log'; | 28 | import { silenceLogger } from '../../utils/log'; |
29 | import initConfig from '../initConfig'; | 29 | import synchronizeConfig from '../synchronizeConfig'; |
30 | 30 | ||
31 | let store: SharedStore; | 31 | let store: SharedStore; |
32 | const persistenceService: ConfigPersistence = { | 32 | const repository: ConfigRepository = { |
33 | readConfig: jest.fn(), | 33 | readConfig: jest.fn(), |
34 | writeConfig: jest.fn(), | 34 | writeConfig: jest.fn(), |
35 | watchConfig: jest.fn(), | 35 | watchConfig: jest.fn(), |
@@ -43,35 +43,33 @@ beforeAll(() => { | |||
43 | }); | 43 | }); |
44 | 44 | ||
45 | beforeEach(() => { | 45 | beforeEach(() => { |
46 | store = sharedStore.create(); | 46 | store = SharedStore.create(); |
47 | }); | 47 | }); |
48 | 48 | ||
49 | describe('when initializing', () => { | 49 | describe('when synchronizeializing', () => { |
50 | describe('when there is no config file', () => { | 50 | describe('when there is no config file', () => { |
51 | beforeEach(() => { | 51 | beforeEach(() => { |
52 | mocked(persistenceService.readConfig).mockResolvedValueOnce({ | 52 | mocked(repository.readConfig).mockResolvedValueOnce({ |
53 | found: false, | 53 | found: false, |
54 | }); | 54 | }); |
55 | }); | 55 | }); |
56 | 56 | ||
57 | it('should create a new config file', async () => { | 57 | it('should create a new config file', async () => { |
58 | await initConfig(store, persistenceService); | 58 | await synchronizeConfig(store, repository); |
59 | expect(persistenceService.writeConfig).toHaveBeenCalledTimes(1); | 59 | expect(repository.writeConfig).toHaveBeenCalledTimes(1); |
60 | }); | 60 | }); |
61 | 61 | ||
62 | it('should bail if there is an an error creating the config file', async () => { | 62 | it('should bail if there is an an error creating the config file', async () => { |
63 | mocked(persistenceService.writeConfig).mockRejectedValue( | 63 | mocked(repository.writeConfig).mockRejectedValue(new Error('boo')); |
64 | new Error('boo'), | ||
65 | ); | ||
66 | await expect(() => | 64 | await expect(() => |
67 | initConfig(store, persistenceService), | 65 | synchronizeConfig(store, repository), |
68 | ).rejects.toBeInstanceOf(Error); | 66 | ).rejects.toBeInstanceOf(Error); |
69 | }); | 67 | }); |
70 | }); | 68 | }); |
71 | 69 | ||
72 | describe('when there is a valid config file', () => { | 70 | describe('when there is a valid config file', () => { |
73 | beforeEach(() => { | 71 | beforeEach(() => { |
74 | mocked(persistenceService.readConfig).mockResolvedValueOnce({ | 72 | mocked(repository.readConfig).mockResolvedValueOnce({ |
75 | found: true, | 73 | found: true, |
76 | data: { | 74 | data: { |
77 | // Use a default empty config file to not trigger config rewrite. | 75 | // Use a default empty config file to not trigger config rewrite. |
@@ -82,23 +80,23 @@ describe('when initializing', () => { | |||
82 | }); | 80 | }); |
83 | 81 | ||
84 | it('should read the existing config file is there is one', async () => { | 82 | it('should read the existing config file is there is one', async () => { |
85 | await initConfig(store, persistenceService); | 83 | await synchronizeConfig(store, repository); |
86 | expect(persistenceService.writeConfig).not.toHaveBeenCalled(); | 84 | expect(repository.writeConfig).not.toHaveBeenCalled(); |
87 | expect(store.settings.themeSource).toBe('dark'); | 85 | expect(store.settings.themeSource).toBe('dark'); |
88 | }); | 86 | }); |
89 | 87 | ||
90 | it('should bail if it cannot set up a watcher', async () => { | 88 | it('should bail if it cannot set up a watcher', async () => { |
91 | mocked(persistenceService.watchConfig).mockImplementationOnce(() => { | 89 | mocked(repository.watchConfig).mockImplementationOnce(() => { |
92 | throw new Error('boo'); | 90 | throw new Error('boo'); |
93 | }); | 91 | }); |
94 | await expect(() => | 92 | await expect(() => |
95 | initConfig(store, persistenceService), | 93 | synchronizeConfig(store, repository), |
96 | ).rejects.toBeInstanceOf(Error); | 94 | ).rejects.toBeInstanceOf(Error); |
97 | }); | 95 | }); |
98 | }); | 96 | }); |
99 | 97 | ||
100 | it('should update the config file if new details are added during read', async () => { | 98 | it('should update the config file if new details are added during read', async () => { |
101 | mocked(persistenceService.readConfig).mockResolvedValueOnce({ | 99 | mocked(repository.readConfig).mockResolvedValueOnce({ |
102 | found: true, | 100 | found: true, |
103 | data: { | 101 | data: { |
104 | themeSource: 'light', | 102 | themeSource: 'light', |
@@ -107,26 +105,26 @@ describe('when initializing', () => { | |||
107 | }, | 105 | }, |
108 | }, | 106 | }, |
109 | }); | 107 | }); |
110 | await initConfig(store, persistenceService); | 108 | await synchronizeConfig(store, repository); |
111 | expect(persistenceService.writeConfig).toHaveBeenCalledTimes(1); | 109 | expect(repository.writeConfig).toHaveBeenCalledTimes(1); |
112 | }); | 110 | }); |
113 | 111 | ||
114 | it('should not apply an invalid config file but should not overwrite it', async () => { | 112 | it('should not apply an invalid config file but should not overwrite it', async () => { |
115 | mocked(persistenceService.readConfig).mockResolvedValueOnce({ | 113 | mocked(repository.readConfig).mockResolvedValueOnce({ |
116 | found: true, | 114 | found: true, |
117 | data: { | 115 | data: { |
118 | themeSource: -1, | 116 | themeSource: -1, |
119 | }, | 117 | }, |
120 | }); | 118 | }); |
121 | await initConfig(store, persistenceService); | 119 | await synchronizeConfig(store, repository); |
122 | expect(store.settings.themeSource).not.toBe(-1); | 120 | expect(store.settings.themeSource).not.toBe(-1); |
123 | expect(persistenceService.writeConfig).not.toHaveBeenCalled(); | 121 | expect(repository.writeConfig).not.toHaveBeenCalled(); |
124 | }); | 122 | }); |
125 | 123 | ||
126 | it('should bail if it cannot determine whether there is a config file', async () => { | 124 | it('should bail if it cannot determine whether there is a config file', async () => { |
127 | mocked(persistenceService.readConfig).mockRejectedValue(new Error('boo')); | 125 | mocked(repository.readConfig).mockRejectedValue(new Error('boo')); |
128 | await expect(() => | 126 | await expect(() => |
129 | initConfig(store, persistenceService), | 127 | synchronizeConfig(store, repository), |
130 | ).rejects.toBeInstanceOf(Error); | 128 | ).rejects.toBeInstanceOf(Error); |
131 | }); | 129 | }); |
132 | }); | 130 | }); |
@@ -137,36 +135,34 @@ describe('when it has loaded the config', () => { | |||
137 | let configChangedCallback: () => Promise<void>; | 135 | let configChangedCallback: () => Promise<void>; |
138 | 136 | ||
139 | beforeEach(async () => { | 137 | beforeEach(async () => { |
140 | mocked(persistenceService.readConfig).mockResolvedValueOnce({ | 138 | mocked(repository.readConfig).mockResolvedValueOnce({ |
141 | found: true, | 139 | found: true, |
142 | data: store.config, | 140 | data: store.config, |
143 | }); | 141 | }); |
144 | mocked(persistenceService.watchConfig).mockReturnValueOnce(watcherDisposer); | 142 | mocked(repository.watchConfig).mockReturnValueOnce(watcherDisposer); |
145 | sutDisposer = await initConfig(store, persistenceService, throttleMs); | 143 | sutDisposer = await synchronizeConfig(store, repository, throttleMs); |
146 | [[configChangedCallback]] = mocked( | 144 | [[configChangedCallback]] = mocked(repository.watchConfig).mock.calls; |
147 | persistenceService.watchConfig, | ||
148 | ).mock.calls; | ||
149 | jest.resetAllMocks(); | 145 | jest.resetAllMocks(); |
150 | }); | 146 | }); |
151 | 147 | ||
152 | it('should throttle saving changes to the config file', () => { | 148 | it('should throttle saving changes to the config file', () => { |
153 | mocked(persistenceService.writeConfig).mockResolvedValue(); | 149 | mocked(repository.writeConfig).mockResolvedValue(); |
154 | store.settings.setThemeSource('dark'); | 150 | store.settings.setThemeSource('dark'); |
155 | jest.advanceTimersByTime(lessThanThrottleMs); | 151 | jest.advanceTimersByTime(lessThanThrottleMs); |
156 | store.settings.setThemeSource('light'); | 152 | store.settings.setThemeSource('light'); |
157 | jest.advanceTimersByTime(throttleMs); | 153 | jest.advanceTimersByTime(throttleMs); |
158 | expect(persistenceService.writeConfig).toHaveBeenCalledTimes(1); | 154 | expect(repository.writeConfig).toHaveBeenCalledTimes(1); |
159 | }); | 155 | }); |
160 | 156 | ||
161 | it('should handle config writing errors gracefully', () => { | 157 | it('should handle config writing errors gracefully', () => { |
162 | mocked(persistenceService.writeConfig).mockRejectedValue(new Error('boo')); | 158 | mocked(repository.writeConfig).mockRejectedValue(new Error('boo')); |
163 | store.settings.setThemeSource('dark'); | 159 | store.settings.setThemeSource('dark'); |
164 | jest.advanceTimersByTime(throttleMs); | 160 | jest.advanceTimersByTime(throttleMs); |
165 | expect(persistenceService.writeConfig).toHaveBeenCalledTimes(1); | 161 | expect(repository.writeConfig).toHaveBeenCalledTimes(1); |
166 | }); | 162 | }); |
167 | 163 | ||
168 | it('should read the config file when it has changed', async () => { | 164 | it('should read the config file when it has changed', async () => { |
169 | mocked(persistenceService.readConfig).mockResolvedValueOnce({ | 165 | mocked(repository.readConfig).mockResolvedValueOnce({ |
170 | found: true, | 166 | found: true, |
171 | data: { | 167 | data: { |
172 | // Use a default empty config file to not trigger config rewrite. | 168 | // Use a default empty config file to not trigger config rewrite. |
@@ -176,12 +172,12 @@ describe('when it has loaded the config', () => { | |||
176 | }); | 172 | }); |
177 | await configChangedCallback(); | 173 | await configChangedCallback(); |
178 | // Do not write back the changes we have just read. | 174 | // Do not write back the changes we have just read. |
179 | expect(persistenceService.writeConfig).not.toHaveBeenCalled(); | 175 | expect(repository.writeConfig).not.toHaveBeenCalled(); |
180 | expect(store.settings.themeSource).toBe('dark'); | 176 | expect(store.settings.themeSource).toBe('dark'); |
181 | }); | 177 | }); |
182 | 178 | ||
183 | it('should update the config file if new details are added', async () => { | 179 | it('should update the config file if new details are added', async () => { |
184 | mocked(persistenceService.readConfig).mockResolvedValueOnce({ | 180 | mocked(repository.readConfig).mockResolvedValueOnce({ |
185 | found: true, | 181 | found: true, |
186 | data: { | 182 | data: { |
187 | themeSource: 'light', | 183 | themeSource: 'light', |
@@ -191,11 +187,11 @@ describe('when it has loaded the config', () => { | |||
191 | }, | 187 | }, |
192 | }); | 188 | }); |
193 | await configChangedCallback(); | 189 | await configChangedCallback(); |
194 | expect(persistenceService.writeConfig).toHaveBeenCalledTimes(1); | 190 | expect(repository.writeConfig).toHaveBeenCalledTimes(1); |
195 | }); | 191 | }); |
196 | 192 | ||
197 | it('should not apply an invalid config file when it has changed but should not overwrite it', async () => { | 193 | it('should not apply an invalid config file when it has changed but should not overwrite it', async () => { |
198 | mocked(persistenceService.readConfig).mockResolvedValueOnce({ | 194 | mocked(repository.readConfig).mockResolvedValueOnce({ |
199 | found: true, | 195 | found: true, |
200 | data: { | 196 | data: { |
201 | themeSource: -1, | 197 | themeSource: -1, |
@@ -203,11 +199,11 @@ describe('when it has loaded the config', () => { | |||
203 | }); | 199 | }); |
204 | await configChangedCallback(); | 200 | await configChangedCallback(); |
205 | expect(store.settings.themeSource).not.toBe(-1); | 201 | expect(store.settings.themeSource).not.toBe(-1); |
206 | expect(persistenceService.writeConfig).not.toHaveBeenCalled(); | 202 | expect(repository.writeConfig).not.toHaveBeenCalled(); |
207 | }); | 203 | }); |
208 | 204 | ||
209 | it('should handle config reading errors gracefully', async () => { | 205 | it('should handle config reading errors gracefully', async () => { |
210 | mocked(persistenceService.readConfig).mockRejectedValue(new Error('boo')); | 206 | mocked(repository.readConfig).mockRejectedValue(new Error('boo')); |
211 | await expect(configChangedCallback()).resolves.not.toThrow(); | 207 | await expect(configChangedCallback()).resolves.not.toThrow(); |
212 | }); | 208 | }); |
213 | 209 | ||
@@ -223,7 +219,7 @@ describe('when it has loaded the config', () => { | |||
223 | it('should not listen to store changes any more', () => { | 219 | it('should not listen to store changes any more', () => { |
224 | store.settings.setThemeSource('dark'); | 220 | store.settings.setThemeSource('dark'); |
225 | jest.advanceTimersByTime(2 * throttleMs); | 221 | jest.advanceTimersByTime(2 * throttleMs); |
226 | expect(persistenceService.writeConfig).not.toHaveBeenCalled(); | 222 | expect(repository.writeConfig).not.toHaveBeenCalled(); |
227 | }); | 223 | }); |
228 | }); | 224 | }); |
229 | }); | 225 | }); |
diff --git a/packages/main/src/controllers/__tests__/initNativeTheme.spec.ts b/packages/main/src/reactions/__tests__/synchronizeNativeTheme.spec.ts index 9107c78..cf37568 100644 --- a/packages/main/src/controllers/__tests__/initNativeTheme.spec.ts +++ b/packages/main/src/reactions/__tests__/synchronizeNativeTheme.spec.ts | |||
@@ -21,7 +21,7 @@ | |||
21 | import { jest } from '@jest/globals'; | 21 | import { jest } from '@jest/globals'; |
22 | import { mocked } from 'jest-mock'; | 22 | import { mocked } from 'jest-mock'; |
23 | 23 | ||
24 | import { sharedStore, SharedStore } from '../../stores/SharedStore'; | 24 | import SharedStore from '../../stores/SharedStore'; |
25 | import type Disposer from '../../utils/Disposer'; | 25 | import type Disposer from '../../utils/Disposer'; |
26 | 26 | ||
27 | let shouldUseDarkColors = false; | 27 | let shouldUseDarkColors = false; |
@@ -38,14 +38,16 @@ jest.unstable_mockModule('electron', () => ({ | |||
38 | })); | 38 | })); |
39 | 39 | ||
40 | const { nativeTheme } = await import('electron'); | 40 | const { nativeTheme } = await import('electron'); |
41 | const { default: initNativeTheme } = await import('../initNativeTheme'); | 41 | const { default: synchronizeNativeTheme } = await import( |
42 | '../synchronizeNativeTheme' | ||
43 | ); | ||
42 | 44 | ||
43 | let store: SharedStore; | 45 | let store: SharedStore; |
44 | let disposeSut: Disposer; | 46 | let disposeSut: Disposer; |
45 | 47 | ||
46 | beforeEach(() => { | 48 | beforeEach(() => { |
47 | store = sharedStore.create(); | 49 | store = SharedStore.create(); |
48 | disposeSut = initNativeTheme(store); | 50 | disposeSut = synchronizeNativeTheme(store); |
49 | }); | 51 | }); |
50 | 52 | ||
51 | it('should register a nativeTheme updated listener', () => { | 53 | it('should register a nativeTheme updated listener', () => { |
diff --git a/packages/main/src/controllers/initConfig.ts b/packages/main/src/reactions/synchronizeConfig.ts index 55bf6df..480cc1a 100644 --- a/packages/main/src/controllers/initConfig.ts +++ b/packages/main/src/reactions/synchronizeConfig.ts | |||
@@ -23,32 +23,31 @@ import { debounce } from 'lodash-es'; | |||
23 | import { reaction } from 'mobx'; | 23 | import { reaction } from 'mobx'; |
24 | import ms from 'ms'; | 24 | import ms from 'ms'; |
25 | 25 | ||
26 | import type ConfigPersistence from '../infrastructure/ConfigPersistence'; | 26 | import type ConfigRepository from '../infrastructure/config/ConfigRepository'; |
27 | import { Config, SharedStore } from '../stores/SharedStore'; | 27 | import type SharedStore from '../stores/SharedStore'; |
28 | import type { Config } from '../stores/SharedStore'; | ||
28 | import type Disposer from '../utils/Disposer'; | 29 | import type Disposer from '../utils/Disposer'; |
29 | import { getLogger } from '../utils/log'; | 30 | import { getLogger } from '../utils/log'; |
30 | 31 | ||
31 | const DEFAULT_CONFIG_DEBOUNCE_TIME = ms('1s'); | 32 | const DEFAULT_CONFIG_DEBOUNCE_TIME = ms('1s'); |
32 | 33 | ||
33 | const log = getLogger('config'); | 34 | const log = getLogger('synchronizeConfig'); |
34 | 35 | ||
35 | export default async function initConfig( | 36 | export default async function synchronizeConfig( |
36 | sharedStore: SharedStore, | 37 | sharedStore: SharedStore, |
37 | persistenceService: ConfigPersistence, | 38 | repository: ConfigRepository, |
38 | debounceTime: number = DEFAULT_CONFIG_DEBOUNCE_TIME, | 39 | debounceTime: number = DEFAULT_CONFIG_DEBOUNCE_TIME, |
39 | ): Promise<Disposer> { | 40 | ): Promise<Disposer> { |
40 | log.trace('Initializing config controller'); | ||
41 | |||
42 | let lastConfigOnDisk: Config | undefined; | 41 | let lastConfigOnDisk: Config | undefined; |
43 | 42 | ||
44 | async function writeConfig(): Promise<void> { | 43 | async function writeConfig(): Promise<void> { |
45 | const { config } = sharedStore; | 44 | const { config } = sharedStore; |
46 | await persistenceService.writeConfig(config); | 45 | await repository.writeConfig(config); |
47 | lastConfigOnDisk = config; | 46 | lastConfigOnDisk = config; |
48 | } | 47 | } |
49 | 48 | ||
50 | async function readConfig(): Promise<boolean> { | 49 | async function readConfig(): Promise<boolean> { |
51 | const result = await persistenceService.readConfig(); | 50 | const result = await repository.readConfig(); |
52 | if (result.found) { | 51 | if (result.found) { |
53 | try { | 52 | try { |
54 | // This cast is unsound if the config file is invalid, | 53 | // This cast is unsound if the config file is invalid, |
@@ -84,7 +83,7 @@ export default async function initConfig( | |||
84 | }, debounceTime), | 83 | }, debounceTime), |
85 | ); | 84 | ); |
86 | 85 | ||
87 | const disposeWatcher = persistenceService.watchConfig(async () => { | 86 | const disposeWatcher = repository.watchConfig(async () => { |
88 | try { | 87 | try { |
89 | await readConfig(); | 88 | await readConfig(); |
90 | } catch (error) { | 89 | } catch (error) { |
@@ -93,7 +92,6 @@ export default async function initConfig( | |||
93 | }, debounceTime); | 92 | }, debounceTime); |
94 | 93 | ||
95 | return () => { | 94 | return () => { |
96 | log.trace('Disposing config controller'); | ||
97 | disposeWatcher(); | 95 | disposeWatcher(); |
98 | disposeReaction(); | 96 | disposeReaction(); |
99 | }; | 97 | }; |
diff --git a/packages/main/src/controllers/initNativeTheme.ts b/packages/main/src/reactions/synchronizeNativeTheme.ts index ce7972a..8c4edb3 100644 --- a/packages/main/src/controllers/initNativeTheme.ts +++ b/packages/main/src/reactions/synchronizeNativeTheme.ts | |||
@@ -21,15 +21,13 @@ | |||
21 | import { nativeTheme } from 'electron'; | 21 | import { nativeTheme } from 'electron'; |
22 | import { autorun } from 'mobx'; | 22 | import { autorun } from 'mobx'; |
23 | 23 | ||
24 | import type { SharedStore } from '../stores/SharedStore'; | 24 | import type SharedStore from '../stores/SharedStore'; |
25 | import type Disposer from '../utils/Disposer'; | 25 | import type Disposer from '../utils/Disposer'; |
26 | import { getLogger } from '../utils/log'; | 26 | import { getLogger } from '../utils/log'; |
27 | 27 | ||
28 | const log = getLogger('nativeTheme'); | 28 | const log = getLogger('synchronizeNativeTheme'); |
29 | 29 | ||
30 | export default function initNativeTheme(store: SharedStore): Disposer { | 30 | export default function initNativeTheme(store: SharedStore): Disposer { |
31 | log.trace('Initializing nativeTheme controller'); | ||
32 | |||
33 | const disposeThemeSourceReaction = autorun(() => { | 31 | const disposeThemeSourceReaction = autorun(() => { |
34 | nativeTheme.themeSource = store.settings.themeSource; | 32 | nativeTheme.themeSource = store.settings.themeSource; |
35 | log.debug('Set theme source:', store.settings.themeSource); | 33 | log.debug('Set theme source:', store.settings.themeSource); |
@@ -43,7 +41,6 @@ export default function initNativeTheme(store: SharedStore): Disposer { | |||
43 | nativeTheme.on('updated', shouldUseDarkColorsListener); | 41 | nativeTheme.on('updated', shouldUseDarkColorsListener); |
44 | 42 | ||
45 | return () => { | 43 | return () => { |
46 | log.trace('Disposing nativeTheme controller'); | ||
47 | nativeTheme.off('updated', shouldUseDarkColorsListener); | 44 | nativeTheme.off('updated', shouldUseDarkColorsListener); |
48 | disposeThemeSourceReaction(); | 45 | disposeThemeSourceReaction(); |
49 | }; | 46 | }; |
diff --git a/packages/main/src/stores/GlobalSettings.ts b/packages/main/src/stores/GlobalSettings.ts index 1eb13b3..0a54aa3 100644 --- a/packages/main/src/stores/GlobalSettings.ts +++ b/packages/main/src/stores/GlobalSettings.ts | |||
@@ -19,18 +19,24 @@ | |||
19 | */ | 19 | */ |
20 | 20 | ||
21 | import { | 21 | import { |
22 | globalSettings as originalGlobalSettings, | 22 | GlobalSettings as GlobalSettingsBase, |
23 | ThemeSource, | 23 | ThemeSource, |
24 | } from '@sophie/shared'; | 24 | } from '@sophie/shared'; |
25 | import { Instance } from 'mobx-state-tree'; | 25 | import { Instance } from 'mobx-state-tree'; |
26 | 26 | ||
27 | export const globalSettings = originalGlobalSettings.actions((self) => ({ | 27 | const GlobalSettings = GlobalSettingsBase.actions((self) => ({ |
28 | setThemeSource(mode: ThemeSource): void { | 28 | setThemeSource(mode: ThemeSource): void { |
29 | self.themeSource = mode; | 29 | self.themeSource = mode; |
30 | }, | 30 | }, |
31 | })); | 31 | })); |
32 | 32 | ||
33 | export interface GlobalSettings extends Instance<typeof globalSettings> {} | 33 | /* |
34 | eslint-disable-next-line @typescript-eslint/no-redeclare -- | ||
35 | Intentionally naming the type the same as the store definition. | ||
36 | */ | ||
37 | interface GlobalSettings extends Instance<typeof GlobalSettings> {} | ||
38 | |||
39 | export default GlobalSettings; | ||
34 | 40 | ||
35 | export type { | 41 | export type { |
36 | GlobalSettingsSnapshotIn, | 42 | GlobalSettingsSnapshotIn, |
diff --git a/packages/main/src/stores/MainStore.ts b/packages/main/src/stores/MainStore.ts index 18f5bf9..ff014c9 100644 --- a/packages/main/src/stores/MainStore.ts +++ b/packages/main/src/stores/MainStore.ts | |||
@@ -21,12 +21,12 @@ | |||
21 | import type { BrowserViewBounds } from '@sophie/shared'; | 21 | import type { BrowserViewBounds } from '@sophie/shared'; |
22 | import { applySnapshot, Instance, types } from 'mobx-state-tree'; | 22 | import { applySnapshot, Instance, types } from 'mobx-state-tree'; |
23 | 23 | ||
24 | import { GlobalSettings } from './GlobalSettings'; | 24 | import GlobalSettings from './GlobalSettings'; |
25 | import { Profile } from './Profile'; | 25 | import Profile from './Profile'; |
26 | import { Service } from './Service'; | 26 | import Service from './Service'; |
27 | import { sharedStore } from './SharedStore'; | 27 | import SharedStore from './SharedStore'; |
28 | 28 | ||
29 | export const mainStore = types | 29 | const MainStore = types |
30 | .model('MainStore', { | 30 | .model('MainStore', { |
31 | browserViewBounds: types.optional( | 31 | browserViewBounds: types.optional( |
32 | types.model('BrowserViewBounds', { | 32 | types.model('BrowserViewBounds', { |
@@ -37,7 +37,7 @@ export const mainStore = types | |||
37 | }), | 37 | }), |
38 | {}, | 38 | {}, |
39 | ), | 39 | ), |
40 | shared: types.optional(sharedStore, {}), | 40 | shared: types.optional(SharedStore, {}), |
41 | }) | 41 | }) |
42 | .views((self) => ({ | 42 | .views((self) => ({ |
43 | get settings(): GlobalSettings { | 43 | get settings(): GlobalSettings { |
@@ -56,8 +56,14 @@ export const mainStore = types | |||
56 | }, | 56 | }, |
57 | })); | 57 | })); |
58 | 58 | ||
59 | export interface MainStore extends Instance<typeof mainStore> {} | 59 | /* |
60 | eslint-disable-next-line @typescript-eslint/no-redeclare -- | ||
61 | Intentionally naming the type the same as the store definition. | ||
62 | */ | ||
63 | interface MainStore extends Instance<typeof MainStore> {} | ||
64 | |||
65 | export default MainStore; | ||
60 | 66 | ||
61 | export function createMainStore(): MainStore { | 67 | export function createMainStore(): MainStore { |
62 | return mainStore.create(); | 68 | return MainStore.create(); |
63 | } | 69 | } |
diff --git a/packages/main/src/stores/Profile.ts b/packages/main/src/stores/Profile.ts index 5f77fe4..ec2a64b 100644 --- a/packages/main/src/stores/Profile.ts +++ b/packages/main/src/stores/Profile.ts | |||
@@ -19,7 +19,7 @@ | |||
19 | */ | 19 | */ |
20 | 20 | ||
21 | import { | 21 | import { |
22 | profile as originalProfile, | 22 | Profile as ProfileBase, |
23 | ProfileSettingsSnapshotIn, | 23 | ProfileSettingsSnapshotIn, |
24 | } from '@sophie/shared'; | 24 | } from '@sophie/shared'; |
25 | import { getSnapshot, Instance } from 'mobx-state-tree'; | 25 | import { getSnapshot, Instance } from 'mobx-state-tree'; |
@@ -30,14 +30,20 @@ export interface ProfileConfig extends ProfileSettingsSnapshotIn { | |||
30 | id?: string | undefined; | 30 | id?: string | undefined; |
31 | } | 31 | } |
32 | 32 | ||
33 | export const profile = originalProfile.views((self) => ({ | 33 | const Profile = ProfileBase.views((self) => ({ |
34 | get config(): ProfileConfig { | 34 | get config(): ProfileConfig { |
35 | const { id, settings } = self; | 35 | const { id, settings } = self; |
36 | return { ...getSnapshot(settings), id }; | 36 | return { ...getSnapshot(settings), id }; |
37 | }, | 37 | }, |
38 | })); | 38 | })); |
39 | 39 | ||
40 | export interface Profile extends Instance<typeof profile> {} | 40 | /* |
41 | eslint-disable-next-line @typescript-eslint/no-redeclare -- | ||
42 | Intentionally naming the type the same as the store definition. | ||
43 | */ | ||
44 | interface Profile extends Instance<typeof Profile> {} | ||
45 | |||
46 | export default Profile; | ||
41 | 47 | ||
42 | export type ProfileSettingsSnapshotWithId = [string, ProfileSettingsSnapshotIn]; | 48 | export type ProfileSettingsSnapshotWithId = [string, ProfileSettingsSnapshotIn]; |
43 | 49 | ||
diff --git a/packages/main/src/stores/Service.ts b/packages/main/src/stores/Service.ts index 331805b..e70caa6 100644 --- a/packages/main/src/stores/Service.ts +++ b/packages/main/src/stores/Service.ts | |||
@@ -19,14 +19,14 @@ | |||
19 | */ | 19 | */ |
20 | 20 | ||
21 | import type { UnreadCount } from '@sophie/service-shared'; | 21 | import type { UnreadCount } from '@sophie/service-shared'; |
22 | import { service as originalService } from '@sophie/shared'; | 22 | import { Service as ServiceBase } from '@sophie/shared'; |
23 | import { Instance, getSnapshot, ReferenceIdentifier } from 'mobx-state-tree'; | 23 | import { Instance, getSnapshot, ReferenceIdentifier } from 'mobx-state-tree'; |
24 | 24 | ||
25 | import generateId from '../utils/generateId'; | 25 | import generateId from '../utils/generateId'; |
26 | import overrideProps from '../utils/overrideProps'; | 26 | import overrideProps from '../utils/overrideProps'; |
27 | 27 | ||
28 | import { ProfileSettingsSnapshotWithId } from './Profile'; | 28 | import { ProfileSettingsSnapshotWithId } from './Profile'; |
29 | import { serviceSettings, ServiceSettingsSnapshotIn } from './ServiceSettings'; | 29 | import ServiceSettings, { ServiceSettingsSnapshotIn } from './ServiceSettings'; |
30 | 30 | ||
31 | export interface ServiceConfig | 31 | export interface ServiceConfig |
32 | extends Omit<ServiceSettingsSnapshotIn, 'profile'> { | 32 | extends Omit<ServiceSettingsSnapshotIn, 'profile'> { |
@@ -35,8 +35,8 @@ export interface ServiceConfig | |||
35 | profile?: ReferenceIdentifier | undefined; | 35 | profile?: ReferenceIdentifier | undefined; |
36 | } | 36 | } |
37 | 37 | ||
38 | export const service = overrideProps(originalService, { | 38 | const Service = overrideProps(ServiceBase, { |
39 | settings: serviceSettings, | 39 | settings: ServiceSettings, |
40 | }) | 40 | }) |
41 | .views((self) => ({ | 41 | .views((self) => ({ |
42 | get config(): ServiceConfig { | 42 | get config(): ServiceConfig { |
@@ -83,7 +83,13 @@ export const service = overrideProps(originalService, { | |||
83 | }, | 83 | }, |
84 | })); | 84 | })); |
85 | 85 | ||
86 | export interface Service extends Instance<typeof service> {} | 86 | /* |
87 | eslint-disable-next-line @typescript-eslint/no-redeclare -- | ||
88 | Intentionally naming the type the same as the store definition. | ||
89 | */ | ||
90 | interface Service extends Instance<typeof Service> {} | ||
91 | |||
92 | export default Service; | ||
87 | 93 | ||
88 | export type ServiceSettingsSnapshotWithId = [string, ServiceSettingsSnapshotIn]; | 94 | export type ServiceSettingsSnapshotWithId = [string, ServiceSettingsSnapshotIn]; |
89 | 95 | ||
diff --git a/packages/main/src/stores/ServiceSettings.ts b/packages/main/src/stores/ServiceSettings.ts index 960de9b..e6f48c6 100644 --- a/packages/main/src/stores/ServiceSettings.ts +++ b/packages/main/src/stores/ServiceSettings.ts | |||
@@ -18,18 +18,24 @@ | |||
18 | * SPDX-License-Identifier: AGPL-3.0-only | 18 | * SPDX-License-Identifier: AGPL-3.0-only |
19 | */ | 19 | */ |
20 | 20 | ||
21 | import { serviceSettings as originalServiceSettings } from '@sophie/shared'; | 21 | import { ServiceSettings as ServiceSettingsBase } from '@sophie/shared'; |
22 | import { Instance, types } from 'mobx-state-tree'; | 22 | import { Instance, types } from 'mobx-state-tree'; |
23 | 23 | ||
24 | import overrideProps from '../utils/overrideProps'; | 24 | import overrideProps from '../utils/overrideProps'; |
25 | 25 | ||
26 | import { profile } from './Profile'; | 26 | import Profile from './Profile'; |
27 | 27 | ||
28 | export const serviceSettings = overrideProps(originalServiceSettings, { | 28 | const ServiceSettings = overrideProps(ServiceSettingsBase, { |
29 | profile: types.reference(profile), | 29 | profile: types.reference(Profile), |
30 | }); | 30 | }); |
31 | 31 | ||
32 | export interface ServiceSettings extends Instance<typeof serviceSettings> {} | 32 | /* |
33 | eslint-disable-next-line @typescript-eslint/no-redeclare -- | ||
34 | Intentionally naming the type the same as the store definition. | ||
35 | */ | ||
36 | interface ServiceSettings extends Instance<typeof ServiceSettings> {} | ||
37 | |||
38 | export default ServiceSettings; | ||
33 | 39 | ||
34 | export type { | 40 | export type { |
35 | ServiceSettingsSnapshotIn, | 41 | ServiceSettingsSnapshotIn, |
diff --git a/packages/main/src/stores/SharedStore.ts b/packages/main/src/stores/SharedStore.ts index 499d1ee..c34af75 100644 --- a/packages/main/src/stores/SharedStore.ts +++ b/packages/main/src/stores/SharedStore.ts | |||
@@ -18,7 +18,7 @@ | |||
18 | * SPDX-License-Identifier: AGPL-3.0-only | 18 | * SPDX-License-Identifier: AGPL-3.0-only |
19 | */ | 19 | */ |
20 | 20 | ||
21 | import { sharedStore as originalSharedStore } from '@sophie/shared'; | 21 | import { SharedStore as SharedStoreBase } from '@sophie/shared'; |
22 | import { | 22 | import { |
23 | applySnapshot, | 23 | applySnapshot, |
24 | getSnapshot, | 24 | getSnapshot, |
@@ -28,18 +28,16 @@ import { | |||
28 | IReferenceType, | 28 | IReferenceType, |
29 | IStateTreeNode, | 29 | IStateTreeNode, |
30 | IType, | 30 | IType, |
31 | resolveIdentifier, | ||
32 | types, | 31 | types, |
33 | } from 'mobx-state-tree'; | 32 | } from 'mobx-state-tree'; |
34 | 33 | ||
35 | import { getLogger } from '../utils/log'; | 34 | import { getLogger } from '../utils/log'; |
36 | import overrideProps from '../utils/overrideProps'; | 35 | import overrideProps from '../utils/overrideProps'; |
37 | 36 | ||
38 | import { globalSettings, GlobalSettingsSnapshotIn } from './GlobalSettings'; | 37 | import GlobalSettings, { GlobalSettingsSnapshotIn } from './GlobalSettings'; |
39 | import { addMissingProfileIds, profile, ProfileConfig } from './Profile'; | 38 | import Profile, { addMissingProfileIds, ProfileConfig } from './Profile'; |
40 | import { | 39 | import Service, { |
41 | addMissingServiceIdsAndProfiles, | 40 | addMissingServiceIdsAndProfiles, |
42 | service, | ||
43 | ServiceConfig, | 41 | ServiceConfig, |
44 | } from './Service'; | 42 | } from './Service'; |
45 | 43 | ||
@@ -86,13 +84,13 @@ function applySettings< | |||
86 | current.push(...toApply.map(([id]) => id)); | 84 | current.push(...toApply.map(([id]) => id)); |
87 | } | 85 | } |
88 | 86 | ||
89 | export const sharedStore = overrideProps(originalSharedStore, { | 87 | const SharedStore = overrideProps(SharedStoreBase, { |
90 | settings: types.optional(globalSettings, {}), | 88 | settings: types.optional(GlobalSettings, {}), |
91 | profilesById: types.map(profile), | 89 | profilesById: types.map(Profile), |
92 | profiles: types.array(types.reference(profile)), | 90 | profiles: types.array(types.reference(Profile)), |
93 | servicesById: types.map(service), | 91 | servicesById: types.map(Service), |
94 | services: types.array(types.reference(service)), | 92 | services: types.array(types.reference(Service)), |
95 | selectedService: types.safeReference(service), | 93 | selectedService: types.safeReference(Service), |
96 | }) | 94 | }) |
97 | .views((self) => ({ | 95 | .views((self) => ({ |
98 | get config(): Config { | 96 | get config(): Config { |
@@ -142,7 +140,7 @@ export const sharedStore = overrideProps(originalSharedStore, { | |||
142 | self.shouldUseDarkColors = shouldUseDarkColors; | 140 | self.shouldUseDarkColors = shouldUseDarkColors; |
143 | }, | 141 | }, |
144 | setSelectedServiceId(serviceId: string): void { | 142 | setSelectedServiceId(serviceId: string): void { |
145 | const serviceInstance = resolveIdentifier(service, self, serviceId); | 143 | const serviceInstance = self.servicesById.get(serviceId); |
146 | if (serviceInstance === undefined) { | 144 | if (serviceInstance === undefined) { |
147 | log.warn('Trying to select unknown service', serviceId); | 145 | log.warn('Trying to select unknown service', serviceId); |
148 | return; | 146 | return; |
@@ -152,7 +150,13 @@ export const sharedStore = overrideProps(originalSharedStore, { | |||
152 | }, | 150 | }, |
153 | })); | 151 | })); |
154 | 152 | ||
155 | export interface SharedStore extends Instance<typeof sharedStore> {} | 153 | /* |
154 | eslint-disable-next-line @typescript-eslint/no-redeclare -- | ||
155 | Intentionally naming the type the same as the store definition. | ||
156 | */ | ||
157 | interface SharedStore extends Instance<typeof SharedStore> {} | ||
158 | |||
159 | export default SharedStore; | ||
156 | 160 | ||
157 | export type { | 161 | export type { |
158 | SharedStoreSnapshotIn, | 162 | SharedStoreSnapshotIn, |
diff --git a/packages/main/src/stores/__tests__/SharedStore.spec.ts b/packages/main/src/stores/__tests__/SharedStore.spec.ts index 3ea187c..dfd59a1 100644 --- a/packages/main/src/stores/__tests__/SharedStore.spec.ts +++ b/packages/main/src/stores/__tests__/SharedStore.spec.ts | |||
@@ -20,7 +20,7 @@ | |||
20 | 20 | ||
21 | import type { ProfileConfig } from '../Profile'; | 21 | import type { ProfileConfig } from '../Profile'; |
22 | import type { ServiceConfig } from '../Service'; | 22 | import type { ServiceConfig } from '../Service'; |
23 | import { Config, sharedStore, SharedStore } from '../SharedStore'; | 23 | import SharedStore, { Config } from '../SharedStore'; |
24 | 24 | ||
25 | const profileProps: ProfileConfig = { | 25 | const profileProps: ProfileConfig = { |
26 | name: 'Test profile', | 26 | name: 'Test profile', |
@@ -34,7 +34,7 @@ const serviceProps: ServiceConfig = { | |||
34 | let sut: SharedStore; | 34 | let sut: SharedStore; |
35 | 35 | ||
36 | beforeEach(() => { | 36 | beforeEach(() => { |
37 | sut = sharedStore.create(); | 37 | sut = SharedStore.create(); |
38 | }); | 38 | }); |
39 | 39 | ||
40 | describe('loadConfig', () => { | 40 | describe('loadConfig', () => { |
diff --git a/packages/preload/src/contextBridge/createSophieRenderer.ts b/packages/preload/src/contextBridge/createSophieRenderer.ts index 6003c8b..8bdf07e 100644 --- a/packages/preload/src/contextBridge/createSophieRenderer.ts +++ b/packages/preload/src/contextBridge/createSophieRenderer.ts | |||
@@ -20,7 +20,6 @@ | |||
20 | 20 | ||
21 | import { | 21 | import { |
22 | Action, | 22 | Action, |
23 | action, | ||
24 | MainToRendererIpcMessage, | 23 | MainToRendererIpcMessage, |
25 | RendererToMainIpcMessage, | 24 | RendererToMainIpcMessage, |
26 | SharedStoreListener, | 25 | SharedStoreListener, |
@@ -32,30 +31,33 @@ import log from 'loglevel'; | |||
32 | import type { IJsonPatch } from 'mobx-state-tree'; | 31 | import type { IJsonPatch } from 'mobx-state-tree'; |
33 | 32 | ||
34 | class SharedStoreConnector { | 33 | class SharedStoreConnector { |
35 | private onSharedStoreChangeCalled = false; | 34 | readonly #allowReplaceListener: boolean; |
36 | 35 | ||
37 | private listener: SharedStoreListener | undefined; | 36 | #onSharedStoreChangeCalled = false; |
38 | 37 | ||
39 | constructor(private readonly allowReplaceListener: boolean) { | 38 | #listener: SharedStoreListener | undefined; |
39 | |||
40 | constructor(allowReplaceListener: boolean) { | ||
41 | this.#allowReplaceListener = allowReplaceListener; | ||
40 | ipcRenderer.on( | 42 | ipcRenderer.on( |
41 | MainToRendererIpcMessage.SharedStorePatch, | 43 | MainToRendererIpcMessage.SharedStorePatch, |
42 | (_event, patch) => { | 44 | (_event, patch) => { |
43 | try { | 45 | try { |
44 | // `mobx-state-tree` will validate the patch, so we can safely cast here. | 46 | // `mobx-state-tree` will validate the patch, so we can safely cast here. |
45 | this.listener?.onPatch(patch as IJsonPatch[]); | 47 | this.#listener?.onPatch(patch as IJsonPatch[]); |
46 | } catch (error) { | 48 | } catch (error) { |
47 | log.error('Shared store listener onPatch failed', error); | 49 | log.error('Shared store listener onPatch failed', error); |
48 | this.listener = undefined; | 50 | this.#listener = undefined; |
49 | } | 51 | } |
50 | }, | 52 | }, |
51 | ); | 53 | ); |
52 | } | 54 | } |
53 | 55 | ||
54 | async onSharedStoreChange(listener: SharedStoreListener): Promise<void> { | 56 | async onSharedStoreChange(listener: SharedStoreListener): Promise<void> { |
55 | if (this.onSharedStoreChangeCalled && !this.allowReplaceListener) { | 57 | if (this.#onSharedStoreChangeCalled && !this.#allowReplaceListener) { |
56 | throw new Error('Shared store change listener was already set'); | 58 | throw new Error('Shared store change listener was already set'); |
57 | } | 59 | } |
58 | this.onSharedStoreChangeCalled = true; | 60 | this.#onSharedStoreChangeCalled = true; |
59 | let success = false; | 61 | let success = false; |
60 | let snapshot: unknown; | 62 | let snapshot: unknown; |
61 | try { | 63 | try { |
@@ -71,14 +73,14 @@ class SharedStoreConnector { | |||
71 | } | 73 | } |
72 | // `mobx-state-tree` will validate the snapshot, so we can safely cast here. | 74 | // `mobx-state-tree` will validate the snapshot, so we can safely cast here. |
73 | listener.onSnapshot(snapshot as SharedStoreSnapshotIn); | 75 | listener.onSnapshot(snapshot as SharedStoreSnapshotIn); |
74 | this.listener = listener; | 76 | this.#listener = listener; |
75 | } | 77 | } |
76 | } | 78 | } |
77 | 79 | ||
78 | function dispatchAction(actionToDispatch: Action): void { | 80 | function dispatchAction(actionToDispatch: Action): void { |
79 | // Let the full zod parse error bubble up to the main world, | 81 | // Let the full zod parse error bubble up to the main world, |
80 | // since all data it may contain was provided from the main world. | 82 | // since all data it may contain was provided from the main world. |
81 | const parsedAction = action.parse(actionToDispatch); | 83 | const parsedAction = Action.parse(actionToDispatch); |
82 | try { | 84 | try { |
83 | ipcRenderer.send(RendererToMainIpcMessage.DispatchAction, parsedAction); | 85 | ipcRenderer.send(RendererToMainIpcMessage.DispatchAction, parsedAction); |
84 | } catch (error) { | 86 | } catch (error) { |
diff --git a/packages/renderer/src/components/StoreProvider.tsx b/packages/renderer/src/components/StoreProvider.tsx index 3360a43..de63083 100644 --- a/packages/renderer/src/components/StoreProvider.tsx +++ b/packages/renderer/src/components/StoreProvider.tsx | |||
@@ -20,9 +20,8 @@ | |||
20 | 20 | ||
21 | import React, { createContext, useContext } from 'react'; | 21 | import React, { createContext, useContext } from 'react'; |
22 | 22 | ||
23 | import type { RendererStore } from '../stores/RendererStore'; | 23 | import type RendererStore from '../stores/RendererStore'; |
24 | 24 | ||
25 | // eslint-disable-next-line unicorn/no-useless-undefined -- `createContext` expects 1 parameter. | ||
26 | const StoreContext = createContext<RendererStore | undefined>(undefined); | 25 | const StoreContext = createContext<RendererStore | undefined>(undefined); |
27 | 26 | ||
28 | export function useStore(): RendererStore { | 27 | export function useStore(): RendererStore { |
diff --git a/packages/renderer/src/stores/RendererStore.ts b/packages/renderer/src/stores/RendererStore.ts index 4cbf6aa..c5a94df 100644 --- a/packages/renderer/src/stores/RendererStore.ts +++ b/packages/renderer/src/stores/RendererStore.ts | |||
@@ -20,18 +20,12 @@ | |||
20 | 20 | ||
21 | import { | 21 | import { |
22 | BrowserViewBounds, | 22 | BrowserViewBounds, |
23 | sharedStore, | 23 | SharedStore, |
24 | Service, | 24 | Service, |
25 | SophieRenderer, | 25 | SophieRenderer, |
26 | ThemeSource, | 26 | ThemeSource, |
27 | } from '@sophie/shared'; | 27 | } from '@sophie/shared'; |
28 | import { | 28 | import { applySnapshot, applyPatch, Instance, types } from 'mobx-state-tree'; |
29 | applySnapshot, | ||
30 | applyPatch, | ||
31 | Instance, | ||
32 | types, | ||
33 | IJsonPatch, | ||
34 | } from 'mobx-state-tree'; | ||
35 | 29 | ||
36 | import RendererEnv from '../env/RendererEnv'; | 30 | import RendererEnv from '../env/RendererEnv'; |
37 | import getEnv from '../env/getEnv'; | 31 | import getEnv from '../env/getEnv'; |
@@ -39,9 +33,9 @@ import { getLogger } from '../utils/log'; | |||
39 | 33 | ||
40 | const log = getLogger('RendererStore'); | 34 | const log = getLogger('RendererStore'); |
41 | 35 | ||
42 | export const rendererStore = types | 36 | const RendererStore = types |
43 | .model('RendererStore', { | 37 | .model('RendererStore', { |
44 | shared: types.optional(sharedStore, {}), | 38 | shared: types.optional(SharedStore, {}), |
45 | }) | 39 | }) |
46 | .views((self) => ({ | 40 | .views((self) => ({ |
47 | get services(): Service[] { | 41 | get services(): Service[] { |
@@ -79,7 +73,13 @@ export const rendererStore = types | |||
79 | }, | 73 | }, |
80 | })); | 74 | })); |
81 | 75 | ||
82 | export interface RendererStore extends Instance<typeof rendererStore> {} | 76 | /* |
77 | eslint-disable-next-line @typescript-eslint/no-redeclare -- | ||
78 | Intentionally naming the type the same as the store definition. | ||
79 | */ | ||
80 | interface RendererStore extends Instance<typeof RendererStore> {} | ||
81 | |||
82 | export default RendererStore; | ||
83 | 83 | ||
84 | /** | 84 | /** |
85 | * Creates a new `RootStore` with a new environment and connects it to `ipc`. | 85 | * Creates a new `RootStore` with a new environment and connects it to `ipc`. |
@@ -95,7 +95,7 @@ export function createAndConnectRendererStore( | |||
95 | const env: RendererEnv = { | 95 | const env: RendererEnv = { |
96 | dispatchMainAction: ipc.dispatchAction, | 96 | dispatchMainAction: ipc.dispatchAction, |
97 | }; | 97 | }; |
98 | const store = rendererStore.create({}, env); | 98 | const store = RendererStore.create({}, env); |
99 | 99 | ||
100 | ipc | 100 | ipc |
101 | .onSharedStoreChange({ | 101 | .onSharedStoreChange({ |
diff --git a/packages/renderer/vite.config.js b/packages/renderer/vite.config.js index cb0203c..63c4f77 100644 --- a/packages/renderer/vite.config.js +++ b/packages/renderer/vite.config.js | |||
@@ -48,6 +48,9 @@ export default { | |||
48 | optimizeDeps: { | 48 | optimizeDeps: { |
49 | exclude: ['@sophie/shared'], | 49 | exclude: ['@sophie/shared'], |
50 | }, | 50 | }, |
51 | define: { | ||
52 | __DEV__: JSON.stringify(isDevelopment), // For mobx | ||
53 | }, | ||
51 | build: { | 54 | build: { |
52 | target: chrome, | 55 | target: chrome, |
53 | assetsDir: '.', | 56 | assetsDir: '.', |
diff --git a/packages/service-preload/src/index.ts b/packages/service-preload/src/index.ts index 8b6630a..5383f42 100644 --- a/packages/service-preload/src/index.ts +++ b/packages/service-preload/src/index.ts | |||
@@ -18,7 +18,7 @@ | |||
18 | * SPDX-License-Identifier: AGPL-3.0-only | 18 | * SPDX-License-Identifier: AGPL-3.0-only |
19 | */ | 19 | */ |
20 | 20 | ||
21 | import { ServiceToMainIpcMessage, webSource } from '@sophie/service-shared'; | 21 | import { ServiceToMainIpcMessage, WebSource } from '@sophie/service-shared'; |
22 | import { ipcRenderer, webFrame } from 'electron'; | 22 | import { ipcRenderer, webFrame } from 'electron'; |
23 | 23 | ||
24 | if (webFrame.parent === null) { | 24 | if (webFrame.parent === null) { |
@@ -52,7 +52,7 @@ async function fetchAndExecuteInjectScript(): Promise<void> { | |||
52 | const apiExposedResponse: unknown = await ipcRenderer.invoke( | 52 | const apiExposedResponse: unknown = await ipcRenderer.invoke( |
53 | ServiceToMainIpcMessage.ApiExposedInMainWorld, | 53 | ServiceToMainIpcMessage.ApiExposedInMainWorld, |
54 | ); | 54 | ); |
55 | const injectSource = webSource.parse(apiExposedResponse); | 55 | const injectSource = WebSource.parse(apiExposedResponse); |
56 | // Isolated world 0 is the main world. | 56 | // Isolated world 0 is the main world. |
57 | await webFrame.executeJavaScriptInIsolatedWorld(0, [injectSource]); | 57 | await webFrame.executeJavaScriptInIsolatedWorld(0, [injectSource]); |
58 | } | 58 | } |
diff --git a/packages/service-shared/src/index.ts b/packages/service-shared/src/index.ts index 94be734..a2e5ee5 100644 --- a/packages/service-shared/src/index.ts +++ b/packages/service-shared/src/index.ts | |||
@@ -20,5 +20,4 @@ | |||
20 | 20 | ||
21 | export { MainToServiceIpcMessage, ServiceToMainIpcMessage } from './ipc'; | 21 | export { MainToServiceIpcMessage, ServiceToMainIpcMessage } from './ipc'; |
22 | 22 | ||
23 | export type { UnreadCount, WebSource } from './schemas'; | 23 | export { UnreadCount, WebSource } from './schemas'; |
24 | export { unreadCount, webSource } from './schemas'; | ||
diff --git a/packages/service-shared/src/schemas.ts b/packages/service-shared/src/schemas.ts index 586750c..0b31eb7 100644 --- a/packages/service-shared/src/schemas.ts +++ b/packages/service-shared/src/schemas.ts | |||
@@ -20,16 +20,24 @@ | |||
20 | 20 | ||
21 | import { z } from 'zod'; | 21 | import { z } from 'zod'; |
22 | 22 | ||
23 | export const unreadCount = z.object({ | 23 | export const UnreadCount = z.object({ |
24 | direct: z.number().nonnegative().optional(), | 24 | direct: z.number().nonnegative().optional(), |
25 | indirect: z.number().nonnegative().optional(), | 25 | indirect: z.number().nonnegative().optional(), |
26 | }); | 26 | }); |
27 | 27 | ||
28 | export type UnreadCount = z.infer<typeof unreadCount>; | 28 | /* |
29 | eslint-disable-next-line @typescript-eslint/no-redeclare -- | ||
30 | Intentionally naming the type the same as the schema definition. | ||
31 | */ | ||
32 | export type UnreadCount = z.infer<typeof UnreadCount>; | ||
29 | 33 | ||
30 | export const webSource = z.object({ | 34 | export const WebSource = z.object({ |
31 | code: z.string(), | 35 | code: z.string(), |
32 | url: z.string().nonempty(), | 36 | url: z.string().nonempty(), |
33 | }); | 37 | }); |
34 | 38 | ||
35 | export type WebSource = z.infer<typeof webSource>; | 39 | /* |
40 | eslint-disable-next-line @typescript-eslint/no-redeclare -- | ||
41 | Intentionally naming the type the same as the schema definition. | ||
42 | */ | ||
43 | export type WebSource = z.infer<typeof WebSource>; | ||
diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 55cf5ce..3d30488 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts | |||
@@ -22,40 +22,33 @@ export type { default as SophieRenderer } from './contextBridge/SophieRenderer'; | |||
22 | 22 | ||
23 | export { MainToRendererIpcMessage, RendererToMainIpcMessage } from './ipc'; | 23 | export { MainToRendererIpcMessage, RendererToMainIpcMessage } from './ipc'; |
24 | 24 | ||
25 | export type { Action, BrowserViewBounds, ThemeSource } from './schemas'; | 25 | export { Action, BrowserViewBounds, ThemeSource } from './schemas'; |
26 | export { action, browserViewBounds, themeSource } from './schemas'; | ||
27 | 26 | ||
28 | export type { | 27 | export type { |
29 | GlobalSettings, | ||
30 | GlobalSettingsSnapshotIn, | 28 | GlobalSettingsSnapshotIn, |
31 | GlobalSettingsSnapshotOut, | 29 | GlobalSettingsSnapshotOut, |
32 | } from './stores/GlobalSettings'; | 30 | } from './stores/GlobalSettings'; |
33 | export { globalSettings } from './stores/GlobalSettings'; | 31 | export { default as GlobalSettings } from './stores/GlobalSettings'; |
34 | 32 | ||
35 | export type { Profile } from './stores/Profile'; | 33 | export { default as Profile } from './stores/Profile'; |
36 | export { profile } from './stores/Profile'; | ||
37 | 34 | ||
38 | export type { | 35 | export type { |
39 | ProfileSettings, | ||
40 | ProfileSettingsSnapshotIn, | 36 | ProfileSettingsSnapshotIn, |
41 | ProfileSettingsSnapshotOut, | 37 | ProfileSettingsSnapshotOut, |
42 | } from './stores/ProfileSettings'; | 38 | } from './stores/ProfileSettings'; |
43 | export { profileSettings } from './stores/ProfileSettings'; | 39 | export { default as ProfileSettings } from './stores/ProfileSettings'; |
44 | 40 | ||
45 | export type { Service } from './stores/Service'; | 41 | export { default as Service } from './stores/Service'; |
46 | export { service } from './stores/Service'; | ||
47 | 42 | ||
48 | export type { | 43 | export type { |
49 | ServiceSettings, | ||
50 | ServiceSettingsSnapshotIn, | 44 | ServiceSettingsSnapshotIn, |
51 | ServiceSettingsSnapshotOut, | 45 | ServiceSettingsSnapshotOut, |
52 | } from './stores/ServiceSettings'; | 46 | } from './stores/ServiceSettings'; |
53 | export { serviceSettings } from './stores/ServiceSettings'; | 47 | export { default as ServiceSettings } from './stores/ServiceSettings'; |
54 | 48 | ||
55 | export type { | 49 | export type { |
56 | SharedStore, | ||
57 | SharedStoreListener, | 50 | SharedStoreListener, |
58 | SharedStoreSnapshotIn, | 51 | SharedStoreSnapshotIn, |
59 | SharedStoreSnapshotOut, | 52 | SharedStoreSnapshotOut, |
60 | } from './stores/SharedStore'; | 53 | } from './stores/SharedStore'; |
61 | export { sharedStore } from './stores/SharedStore'; | 54 | export { default as SharedStore } from './stores/SharedStore'; |
diff --git a/packages/shared/src/schemas.ts b/packages/shared/src/schemas.ts index 7fb9717..edf3741 100644 --- a/packages/shared/src/schemas.ts +++ b/packages/shared/src/schemas.ts | |||
@@ -20,43 +20,55 @@ | |||
20 | 20 | ||
21 | import { z } from 'zod'; | 21 | import { z } from 'zod'; |
22 | 22 | ||
23 | const setSelectedServiceId = z.object({ | 23 | const SetSelectedServiceId = z.object({ |
24 | action: z.literal('set-selected-service-id'), | 24 | action: z.literal('set-selected-service-id'), |
25 | serviceId: z.string(), | 25 | serviceId: z.string(), |
26 | }); | 26 | }); |
27 | 27 | ||
28 | export const browserViewBounds = z.object({ | 28 | export const BrowserViewBounds = z.object({ |
29 | x: z.number().int().nonnegative(), | 29 | x: z.number().int().nonnegative(), |
30 | y: z.number().int().nonnegative(), | 30 | y: z.number().int().nonnegative(), |
31 | width: z.number().int().nonnegative(), | 31 | width: z.number().int().nonnegative(), |
32 | height: z.number().int().nonnegative(), | 32 | height: z.number().int().nonnegative(), |
33 | }); | 33 | }); |
34 | 34 | ||
35 | export type BrowserViewBounds = z.infer<typeof browserViewBounds>; | 35 | /* |
36 | eslint-disable-next-line @typescript-eslint/no-redeclare -- | ||
37 | Intentionally naming the type the same as the schema definition. | ||
38 | */ | ||
39 | export type BrowserViewBounds = z.infer<typeof BrowserViewBounds>; | ||
36 | 40 | ||
37 | const setBrowserViewBoundsAction = z.object({ | 41 | const SetBrowserViewBoundsAction = z.object({ |
38 | action: z.literal('set-browser-view-bounds'), | 42 | action: z.literal('set-browser-view-bounds'), |
39 | browserViewBounds, | 43 | browserViewBounds: BrowserViewBounds, |
40 | }); | 44 | }); |
41 | 45 | ||
42 | export const themeSource = z.enum(['system', 'light', 'dark']); | 46 | export const ThemeSource = z.enum(['system', 'light', 'dark']); |
43 | 47 | ||
44 | export type ThemeSource = z.infer<typeof themeSource>; | 48 | /* |
49 | eslint-disable-next-line @typescript-eslint/no-redeclare -- | ||
50 | Intentionally naming the type the same as the schema definition. | ||
51 | */ | ||
52 | export type ThemeSource = z.infer<typeof ThemeSource>; | ||
45 | 53 | ||
46 | const setThemeSourceAction = z.object({ | 54 | const SetThemeSourceAction = z.object({ |
47 | action: z.literal('set-theme-source'), | 55 | action: z.literal('set-theme-source'), |
48 | themeSource, | 56 | themeSource: ThemeSource, |
49 | }); | 57 | }); |
50 | 58 | ||
51 | const reloadAllServicesAction = z.object({ | 59 | const ReloadAllServicesAction = z.object({ |
52 | action: z.literal('reload-all-services'), | 60 | action: z.literal('reload-all-services'), |
53 | }); | 61 | }); |
54 | 62 | ||
55 | export const action = z.union([ | 63 | export const Action = z.union([ |
56 | setSelectedServiceId, | 64 | SetSelectedServiceId, |
57 | setBrowserViewBoundsAction, | 65 | SetBrowserViewBoundsAction, |
58 | setThemeSourceAction, | 66 | SetThemeSourceAction, |
59 | reloadAllServicesAction, | 67 | ReloadAllServicesAction, |
60 | ]); | 68 | ]); |
61 | 69 | ||
62 | export type Action = z.infer<typeof action>; | 70 | /* |
71 | eslint-disable-next-line @typescript-eslint/no-redeclare -- | ||
72 | Intentionally naming the type the same as the schema definition. | ||
73 | */ | ||
74 | export type Action = z.infer<typeof Action>; | ||
diff --git a/packages/shared/src/stores/GlobalSettings.ts b/packages/shared/src/stores/GlobalSettings.ts index bd0155a..3a813b8 100644 --- a/packages/shared/src/stores/GlobalSettings.ts +++ b/packages/shared/src/stores/GlobalSettings.ts | |||
@@ -20,16 +20,22 @@ | |||
20 | 20 | ||
21 | import { Instance, types, SnapshotIn, SnapshotOut } from 'mobx-state-tree'; | 21 | import { Instance, types, SnapshotIn, SnapshotOut } from 'mobx-state-tree'; |
22 | 22 | ||
23 | import { themeSource } from '../schemas'; | 23 | import { ThemeSource } from '../schemas'; |
24 | 24 | ||
25 | export const globalSettings = types.model('GlobalSettings', { | 25 | const GlobalSettings = types.model('GlobalSettings', { |
26 | themeSource: types.optional(types.enumeration(themeSource.options), 'system'), | 26 | themeSource: types.optional(types.enumeration(ThemeSource.options), 'system'), |
27 | }); | 27 | }); |
28 | 28 | ||
29 | export interface GlobalSettings extends Instance<typeof globalSettings> {} | 29 | /* |
30 | eslint-disable-next-line @typescript-eslint/no-redeclare -- | ||
31 | Intentionally naming the type the same as the store definition. | ||
32 | */ | ||
33 | interface GlobalSettings extends Instance<typeof GlobalSettings> {} | ||
34 | |||
35 | export default GlobalSettings; | ||
30 | 36 | ||
31 | export interface GlobalSettingsSnapshotIn | 37 | export interface GlobalSettingsSnapshotIn |
32 | extends SnapshotIn<typeof globalSettings> {} | 38 | extends SnapshotIn<typeof GlobalSettings> {} |
33 | 39 | ||
34 | export interface GlobalSettingsSnapshotOut | 40 | export interface GlobalSettingsSnapshotOut |
35 | extends SnapshotOut<typeof globalSettings> {} | 41 | extends SnapshotOut<typeof GlobalSettings> {} |
diff --git a/packages/shared/src/stores/Profile.ts b/packages/shared/src/stores/Profile.ts index bb058f6..256c33e 100644 --- a/packages/shared/src/stores/Profile.ts +++ b/packages/shared/src/stores/Profile.ts | |||
@@ -20,11 +20,17 @@ | |||
20 | 20 | ||
21 | import { Instance, types } from 'mobx-state-tree'; | 21 | import { Instance, types } from 'mobx-state-tree'; |
22 | 22 | ||
23 | import { profileSettings } from './ProfileSettings'; | 23 | import ProfileSettings from './ProfileSettings'; |
24 | 24 | ||
25 | export const profile = types.model('Profile', { | 25 | const Profile = types.model('Profile', { |
26 | id: types.identifier, | 26 | id: types.identifier, |
27 | settings: profileSettings, | 27 | settings: ProfileSettings, |
28 | }); | 28 | }); |
29 | 29 | ||
30 | export interface Profile extends Instance<typeof profile> {} | 30 | /* |
31 | eslint-disable-next-line @typescript-eslint/no-redeclare -- | ||
32 | Intentionally naming the type the same as the store definition. | ||
33 | */ | ||
34 | interface Profile extends Instance<typeof Profile> {} | ||
35 | |||
36 | export default Profile; | ||
diff --git a/packages/shared/src/stores/ProfileSettings.ts b/packages/shared/src/stores/ProfileSettings.ts index ec8da5f..9f2b27c 100644 --- a/packages/shared/src/stores/ProfileSettings.ts +++ b/packages/shared/src/stores/ProfileSettings.ts | |||
@@ -20,14 +20,20 @@ | |||
20 | 20 | ||
21 | import { Instance, types, SnapshotIn, SnapshotOut } from 'mobx-state-tree'; | 21 | import { Instance, types, SnapshotIn, SnapshotOut } from 'mobx-state-tree'; |
22 | 22 | ||
23 | export const profileSettings = types.model('ProfileSettings', { | 23 | const ProfileSettings = types.model('ProfileSettings', { |
24 | name: types.string, | 24 | name: types.string, |
25 | }); | 25 | }); |
26 | 26 | ||
27 | export interface ProfileSettings extends Instance<typeof profileSettings> {} | 27 | /* |
28 | eslint-disable-next-line @typescript-eslint/no-redeclare -- | ||
29 | Intentionally naming the type the same as the store definition. | ||
30 | */ | ||
31 | interface ProfileSettings extends Instance<typeof ProfileSettings> {} | ||
32 | |||
33 | export default ProfileSettings; | ||
28 | 34 | ||
29 | export interface ProfileSettingsSnapshotIn | 35 | export interface ProfileSettingsSnapshotIn |
30 | extends SnapshotIn<typeof profileSettings> {} | 36 | extends SnapshotIn<typeof ProfileSettings> {} |
31 | 37 | ||
32 | export interface ProfileSettingsSnapshotOut | 38 | export interface ProfileSettingsSnapshotOut |
33 | extends SnapshotOut<typeof profileSettings> {} | 39 | extends SnapshotOut<typeof ProfileSettings> {} |
diff --git a/packages/shared/src/stores/Service.ts b/packages/shared/src/stores/Service.ts index 36acd3d..4a7334d 100644 --- a/packages/shared/src/stores/Service.ts +++ b/packages/shared/src/stores/Service.ts | |||
@@ -20,11 +20,11 @@ | |||
20 | 20 | ||
21 | import { Instance, types } from 'mobx-state-tree'; | 21 | import { Instance, types } from 'mobx-state-tree'; |
22 | 22 | ||
23 | import { serviceSettings } from './ServiceSettings'; | 23 | import ServiceSettings from './ServiceSettings'; |
24 | 24 | ||
25 | export const service = types.model('Service', { | 25 | const Service = types.model('Service', { |
26 | id: types.identifier, | 26 | id: types.identifier, |
27 | settings: serviceSettings, | 27 | settings: ServiceSettings, |
28 | currentUrl: types.maybe(types.string), | 28 | currentUrl: types.maybe(types.string), |
29 | canGoBack: false, | 29 | canGoBack: false, |
30 | canGoForward: false, | 30 | canGoForward: false, |
@@ -37,4 +37,10 @@ export const service = types.model('Service', { | |||
37 | indirectMessageCount: 0, | 37 | indirectMessageCount: 0, |
38 | }); | 38 | }); |
39 | 39 | ||
40 | export interface Service extends Instance<typeof service> {} | 40 | /* |
41 | eslint-disable-next-line @typescript-eslint/no-redeclare -- | ||
42 | Intentionally naming the type the same as the store definition. | ||
43 | */ | ||
44 | interface Service extends Instance<typeof Service> {} | ||
45 | |||
46 | export default Service; | ||
diff --git a/packages/shared/src/stores/ServiceSettings.ts b/packages/shared/src/stores/ServiceSettings.ts index 54cd7eb..6ba1dfa 100644 --- a/packages/shared/src/stores/ServiceSettings.ts +++ b/packages/shared/src/stores/ServiceSettings.ts | |||
@@ -20,19 +20,25 @@ | |||
20 | 20 | ||
21 | import { Instance, types, SnapshotIn, SnapshotOut } from 'mobx-state-tree'; | 21 | import { Instance, types, SnapshotIn, SnapshotOut } from 'mobx-state-tree'; |
22 | 22 | ||
23 | import { profile } from './Profile'; | 23 | import Profile from './Profile'; |
24 | 24 | ||
25 | export const serviceSettings = types.model('ServiceSettings', { | 25 | const ServiceSettings = types.model('ServiceSettings', { |
26 | name: types.string, | 26 | name: types.string, |
27 | profile: types.reference(profile), | 27 | profile: types.reference(Profile), |
28 | // TODO: Remove this once recipes are added. | 28 | // TODO: Remove this once recipes are added. |
29 | url: types.string, | 29 | url: types.string, |
30 | }); | 30 | }); |
31 | 31 | ||
32 | export interface ServiceSettings extends Instance<typeof serviceSettings> {} | 32 | /* |
33 | eslint-disable-next-line @typescript-eslint/no-redeclare -- | ||
34 | Intentionally naming the type the same as the store definition. | ||
35 | */ | ||
36 | interface ServiceSettings extends Instance<typeof ServiceSettings> {} | ||
37 | |||
38 | export default ServiceSettings; | ||
33 | 39 | ||
34 | export interface ServiceSettingsSnapshotIn | 40 | export interface ServiceSettingsSnapshotIn |
35 | extends SnapshotIn<typeof serviceSettings> {} | 41 | extends SnapshotIn<typeof ServiceSettings> {} |
36 | 42 | ||
37 | export interface ServiceSettingsSnapshotOut | 43 | export interface ServiceSettingsSnapshotOut |
38 | extends SnapshotOut<typeof serviceSettings> {} | 44 | extends SnapshotOut<typeof ServiceSettings> {} |
diff --git a/packages/shared/src/stores/SharedStore.ts b/packages/shared/src/stores/SharedStore.ts index f301b9d..0cac3a5 100644 --- a/packages/shared/src/stores/SharedStore.ts +++ b/packages/shared/src/stores/SharedStore.ts | |||
@@ -26,26 +26,32 @@ import { | |||
26 | SnapshotOut, | 26 | SnapshotOut, |
27 | } from 'mobx-state-tree'; | 27 | } from 'mobx-state-tree'; |
28 | 28 | ||
29 | import { globalSettings } from './GlobalSettings'; | 29 | import GlobalSettings from './GlobalSettings'; |
30 | import { profile } from './Profile'; | 30 | import Profile from './Profile'; |
31 | import { service } from './Service'; | 31 | import Service from './Service'; |
32 | 32 | ||
33 | export const sharedStore = types.model('SharedStore', { | 33 | const SharedStore = types.model('SharedStore', { |
34 | settings: types.optional(globalSettings, {}), | 34 | settings: types.optional(GlobalSettings, {}), |
35 | profilesById: types.map(profile), | 35 | profilesById: types.map(Profile), |
36 | profiles: types.array(types.reference(profile)), | 36 | profiles: types.array(types.reference(Profile)), |
37 | servicesById: types.map(service), | 37 | servicesById: types.map(Service), |
38 | services: types.array(types.reference(service)), | 38 | services: types.array(types.reference(Service)), |
39 | selectedService: types.safeReference(service), | 39 | selectedService: types.safeReference(Service), |
40 | shouldUseDarkColors: false, | 40 | shouldUseDarkColors: false, |
41 | }); | 41 | }); |
42 | 42 | ||
43 | export interface SharedStore extends Instance<typeof sharedStore> {} | 43 | /* |
44 | eslint-disable-next-line @typescript-eslint/no-redeclare -- | ||
45 | Intentionally naming the type the same as the store definition. | ||
46 | */ | ||
47 | interface SharedStore extends Instance<typeof SharedStore> {} | ||
48 | |||
49 | export default SharedStore; | ||
44 | 50 | ||
45 | export interface SharedStoreSnapshotIn extends SnapshotIn<typeof sharedStore> {} | 51 | export interface SharedStoreSnapshotIn extends SnapshotIn<typeof SharedStore> {} |
46 | 52 | ||
47 | export interface SharedStoreSnapshotOut | 53 | export interface SharedStoreSnapshotOut |
48 | extends SnapshotOut<typeof sharedStore> {} | 54 | extends SnapshotOut<typeof SharedStore> {} |
49 | 55 | ||
50 | export interface SharedStoreListener { | 56 | export interface SharedStoreListener { |
51 | onSnapshot(snapshot: SharedStoreSnapshotIn): void; | 57 | onSnapshot(snapshot: SharedStoreSnapshotIn): void; |