From 044b2de8c7861504704468ba441d4a6a37eed8f7 Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Sun, 23 Jan 2022 17:12:47 +0100 Subject: refactor: Move runtime state into shared models MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Now the runtime state lives inside the model (instead of being associated to the static settings via a map), which simplifies state management. Static settings are now located inside the runtime models, so we must create tests to make sure that the settings are being persisted correctly. The contents of the config file are now generated as a view of store (instead of a snapshot), which adds flexibility. Signed-off-by: Kristóf Marussy --- .../src/controllers/__tests__/initConfig.spec.ts | 45 +++--- .../controllers/__tests__/initNativeTheme.spec.ts | 10 +- packages/main/src/controllers/initConfig.ts | 30 ++-- packages/main/src/controllers/initNativeTheme.ts | 8 +- packages/main/src/index.ts | 4 +- .../main/src/infrastructure/ConfigPersistence.ts | 4 +- .../impl/FileBasedConfigPersistence.ts | 4 +- packages/main/src/init.ts | 4 +- packages/main/src/stores/Config.ts | 58 -------- packages/main/src/stores/GlobalSettings.ts | 38 +++++ packages/main/src/stores/MainStore.ts | 39 ++--- packages/main/src/stores/Profile.ts | 45 +++--- packages/main/src/stores/RuntimeService.ts | 74 ---------- packages/main/src/stores/Service.ts | 109 ++++++++++---- packages/main/src/stores/ServiceSettings.ts | 37 +++++ packages/main/src/stores/SharedStore.ts | 112 +++++++++++++-- packages/main/src/stores/__tests__/Config.spec.ts | 156 -------------------- .../main/src/stores/__tests__/SharedStore.spec.ts | 160 +++++++++++++++++++++ packages/main/src/utils/SettingsWithId.ts | 25 ++++ packages/main/src/utils/overrideProps.ts | 62 ++++++++ .../renderer/src/components/ServiceSwitcher.tsx | 4 +- packages/renderer/src/env/RendererEnv.ts | 4 +- packages/renderer/src/env/impl/RendererEnvImpl.ts | 55 ------- .../src/env/impl/__tests__/RendererEnvImpl.spec.ts | 87 ----------- packages/renderer/src/stores/Config.ts | 32 ----- packages/renderer/src/stores/RendererStore.ts | 24 ++-- packages/renderer/src/stores/Service.ts | 38 ----- packages/renderer/src/stores/SharedStore.ts | 37 ----- .../renderer/src/stores/__tests__/Service.spec.ts | 63 -------- .../shared/src/contextBridge/SophieRenderer.ts | 2 +- packages/shared/src/index.ts | 41 +++--- packages/shared/src/stores/Config.ts | 38 ----- packages/shared/src/stores/GlobalSettings.ts | 35 +++++ packages/shared/src/stores/Profile.ts | 10 +- packages/shared/src/stores/ProfileSettings.ts | 33 +++++ packages/shared/src/stores/RuntimeService.ts | 47 ------ packages/shared/src/stores/Service.ts | 23 +-- packages/shared/src/stores/ServiceSettings.ts | 38 +++++ packages/shared/src/stores/SharedStore.ts | 9 +- 39 files changed, 759 insertions(+), 885 deletions(-) delete mode 100644 packages/main/src/stores/Config.ts create mode 100644 packages/main/src/stores/GlobalSettings.ts delete mode 100644 packages/main/src/stores/RuntimeService.ts create mode 100644 packages/main/src/stores/ServiceSettings.ts delete mode 100644 packages/main/src/stores/__tests__/Config.spec.ts create mode 100644 packages/main/src/stores/__tests__/SharedStore.spec.ts create mode 100644 packages/main/src/utils/SettingsWithId.ts create mode 100644 packages/main/src/utils/overrideProps.ts delete mode 100644 packages/renderer/src/env/impl/RendererEnvImpl.ts delete mode 100644 packages/renderer/src/env/impl/__tests__/RendererEnvImpl.spec.ts delete mode 100644 packages/renderer/src/stores/Config.ts delete mode 100644 packages/renderer/src/stores/Service.ts delete mode 100644 packages/renderer/src/stores/SharedStore.ts delete mode 100644 packages/renderer/src/stores/__tests__/Service.spec.ts delete mode 100644 packages/shared/src/stores/Config.ts create mode 100644 packages/shared/src/stores/GlobalSettings.ts create mode 100644 packages/shared/src/stores/ProfileSettings.ts delete mode 100644 packages/shared/src/stores/RuntimeService.ts create mode 100644 packages/shared/src/stores/ServiceSettings.ts diff --git a/packages/main/src/controllers/__tests__/initConfig.spec.ts b/packages/main/src/controllers/__tests__/initConfig.spec.ts index dc00b9d..fdd22c9 100644 --- a/packages/main/src/controllers/__tests__/initConfig.spec.ts +++ b/packages/main/src/controllers/__tests__/initConfig.spec.ts @@ -20,16 +20,15 @@ import { jest } from '@jest/globals'; import { mocked } from 'jest-mock'; -import { getSnapshot } from 'mobx-state-tree'; import ms from 'ms'; import type ConfigPersistence from '../../infrastructure/ConfigPersistence'; -import { Config, config as configModel } from '../../stores/Config'; +import { sharedStore, SharedStore } from '../../stores/SharedStore'; import type Disposer from '../../utils/Disposer'; import { silenceLogger } from '../../utils/log'; import initConfig from '../initConfig'; -let config: Config; +let store: SharedStore; const persistenceService: ConfigPersistence = { readConfig: jest.fn(), writeConfig: jest.fn(), @@ -44,7 +43,7 @@ beforeAll(() => { }); beforeEach(() => { - config = configModel.create(); + store = sharedStore.create(); }); describe('when initializing', () => { @@ -56,7 +55,7 @@ describe('when initializing', () => { }); it('should create a new config file', async () => { - await initConfig(config, persistenceService); + await initConfig(store, persistenceService); expect(persistenceService.writeConfig).toHaveBeenCalledTimes(1); }); @@ -65,7 +64,7 @@ describe('when initializing', () => { new Error('boo'), ); await expect(() => - initConfig(config, persistenceService), + initConfig(store, persistenceService), ).rejects.toBeInstanceOf(Error); }); }); @@ -76,16 +75,16 @@ describe('when initializing', () => { found: true, data: { // Use a default empty config file to not trigger config rewrite. - ...getSnapshot(config), + ...store.config, themeSource: 'dark', }, }); }); it('should read the existing config file is there is one', async () => { - await initConfig(config, persistenceService); + await initConfig(store, persistenceService); expect(persistenceService.writeConfig).not.toHaveBeenCalled(); - expect(config.themeSource).toBe('dark'); + expect(store.settings.themeSource).toBe('dark'); }); it('should bail if it cannot set up a watcher', async () => { @@ -93,7 +92,7 @@ describe('when initializing', () => { throw new Error('boo'); }); await expect(() => - initConfig(config, persistenceService), + initConfig(store, persistenceService), ).rejects.toBeInstanceOf(Error); }); }); @@ -108,7 +107,7 @@ describe('when initializing', () => { }, }, }); - await initConfig(config, persistenceService); + await initConfig(store, persistenceService); expect(persistenceService.writeConfig).toHaveBeenCalledTimes(1); }); @@ -119,15 +118,15 @@ describe('when initializing', () => { themeSource: -1, }, }); - await initConfig(config, persistenceService); - expect(config.themeSource).not.toBe(-1); + await initConfig(store, persistenceService); + expect(store.settings.themeSource).not.toBe(-1); expect(persistenceService.writeConfig).not.toHaveBeenCalled(); }); it('should bail if it cannot determine whether there is a config file', async () => { mocked(persistenceService.readConfig).mockRejectedValue(new Error('boo')); await expect(() => - initConfig(config, persistenceService), + initConfig(store, persistenceService), ).rejects.toBeInstanceOf(Error); }); }); @@ -140,10 +139,10 @@ describe('when it has loaded the config', () => { beforeEach(async () => { mocked(persistenceService.readConfig).mockResolvedValueOnce({ found: true, - data: getSnapshot(config), + data: store.config, }); mocked(persistenceService.watchConfig).mockReturnValueOnce(watcherDisposer); - sutDisposer = await initConfig(config, persistenceService, throttleMs); + sutDisposer = await initConfig(store, persistenceService, throttleMs); [[configChangedCallback]] = mocked( persistenceService.watchConfig, ).mock.calls; @@ -152,16 +151,16 @@ describe('when it has loaded the config', () => { it('should throttle saving changes to the config file', () => { mocked(persistenceService.writeConfig).mockResolvedValue(); - config.setThemeSource('dark'); + store.settings.setThemeSource('dark'); jest.advanceTimersByTime(lessThanThrottleMs); - config.setThemeSource('light'); + store.settings.setThemeSource('light'); jest.advanceTimersByTime(throttleMs); expect(persistenceService.writeConfig).toHaveBeenCalledTimes(1); }); it('should handle config writing errors gracefully', () => { mocked(persistenceService.writeConfig).mockRejectedValue(new Error('boo')); - config.setThemeSource('dark'); + store.settings.setThemeSource('dark'); jest.advanceTimersByTime(throttleMs); expect(persistenceService.writeConfig).toHaveBeenCalledTimes(1); }); @@ -171,14 +170,14 @@ describe('when it has loaded the config', () => { found: true, data: { // Use a default empty config file to not trigger config rewrite. - ...getSnapshot(config), + ...store.config, themeSource: 'dark', }, }); await configChangedCallback(); // Do not write back the changes we have just read. expect(persistenceService.writeConfig).not.toHaveBeenCalled(); - expect(config.themeSource).toBe('dark'); + expect(store.settings.themeSource).toBe('dark'); }); it('should update the config file if new details are added', async () => { @@ -203,7 +202,7 @@ describe('when it has loaded the config', () => { }, }); await configChangedCallback(); - expect(config.themeSource).not.toBe(-1); + expect(store.settings.themeSource).not.toBe(-1); expect(persistenceService.writeConfig).not.toHaveBeenCalled(); }); @@ -222,7 +221,7 @@ describe('when it has loaded the config', () => { }); it('should not listen to store changes any more', () => { - config.setThemeSource('dark'); + store.settings.setThemeSource('dark'); jest.advanceTimersByTime(2 * throttleMs); expect(persistenceService.writeConfig).not.toHaveBeenCalled(); }); diff --git a/packages/main/src/controllers/__tests__/initNativeTheme.spec.ts b/packages/main/src/controllers/__tests__/initNativeTheme.spec.ts index 16acca5..9107c78 100644 --- a/packages/main/src/controllers/__tests__/initNativeTheme.spec.ts +++ b/packages/main/src/controllers/__tests__/initNativeTheme.spec.ts @@ -21,7 +21,7 @@ import { jest } from '@jest/globals'; import { mocked } from 'jest-mock'; -import { createMainStore, MainStore } from '../../stores/MainStore'; +import { sharedStore, SharedStore } from '../../stores/SharedStore'; import type Disposer from '../../utils/Disposer'; let shouldUseDarkColors = false; @@ -40,11 +40,11 @@ jest.unstable_mockModule('electron', () => ({ const { nativeTheme } = await import('electron'); const { default: initNativeTheme } = await import('../initNativeTheme'); -let store: MainStore; +let store: SharedStore; let disposeSut: Disposer; beforeEach(() => { - store = createMainStore(); + store = sharedStore.create(); disposeSut = initNativeTheme(store); }); @@ -53,7 +53,7 @@ it('should register a nativeTheme updated listener', () => { }); it('should synchronize themeSource changes to the nativeTheme', () => { - store.config.setThemeSource('dark'); + store.settings.setThemeSource('dark'); expect(nativeTheme.themeSource).toBe('dark'); }); @@ -63,7 +63,7 @@ it('should synchronize shouldUseDarkColors changes to the store', () => { )![1]; shouldUseDarkColors = true; listener(); - expect(store.shared.shouldUseDarkColors).toBe(true); + expect(store.shouldUseDarkColors).toBe(true); }); it('should remove the listener on dispose', () => { diff --git a/packages/main/src/controllers/initConfig.ts b/packages/main/src/controllers/initConfig.ts index c8cd335..55bf6df 100644 --- a/packages/main/src/controllers/initConfig.ts +++ b/packages/main/src/controllers/initConfig.ts @@ -20,11 +20,11 @@ import deepEqual from 'deep-equal'; import { debounce } from 'lodash-es'; -import { getSnapshot, onSnapshot } from 'mobx-state-tree'; +import { reaction } from 'mobx'; import ms from 'ms'; import type ConfigPersistence from '../infrastructure/ConfigPersistence'; -import { Config, ConfigFileIn, ConfigSnapshotOut } from '../stores/Config'; +import { Config, SharedStore } from '../stores/SharedStore'; import type Disposer from '../utils/Disposer'; import { getLogger } from '../utils/log'; @@ -33,18 +33,18 @@ const DEFAULT_CONFIG_DEBOUNCE_TIME = ms('1s'); const log = getLogger('config'); export default async function initConfig( - config: Config, + sharedStore: SharedStore, persistenceService: ConfigPersistence, debounceTime: number = DEFAULT_CONFIG_DEBOUNCE_TIME, ): Promise { log.trace('Initializing config controller'); - let lastSnapshotOnDisk: ConfigSnapshotOut | undefined; + let lastConfigOnDisk: Config | undefined; async function writeConfig(): Promise { - const snapshot = getSnapshot(config); - await persistenceService.writeConfig(snapshot); - lastSnapshotOnDisk = snapshot; + const { config } = sharedStore; + await persistenceService.writeConfig(config); + lastConfigOnDisk = config; } async function readConfig(): Promise { @@ -53,13 +53,13 @@ export default async function initConfig( try { // This cast is unsound if the config file is invalid, // but we'll throw an error in the end anyways. - config.loadFromConfigFile(result.data as ConfigFileIn); + sharedStore.loadConfig(result.data as Config); } catch (error) { log.error('Failed to apply config snapshot', result.data, error); return true; } - lastSnapshotOnDisk = getSnapshot(config); - if (!deepEqual(result.data, lastSnapshotOnDisk, { strict: true })) { + lastConfigOnDisk = sharedStore.config; + if (!deepEqual(result.data, lastConfigOnDisk, { strict: true })) { await writeConfig(); } } @@ -72,11 +72,11 @@ export default async function initConfig( log.info('Created config file'); } - const disposeOnSnapshot = onSnapshot( - config, - debounce((snapshot) => { + const disposeReaction = reaction( + () => sharedStore.config, + debounce((config) => { // We can compare snapshots by reference, since it is only recreated on store changes. - if (lastSnapshotOnDisk !== snapshot) { + if (lastConfigOnDisk !== config) { writeConfig().catch((error) => { log.error('Failed to write config on config change', error); }); @@ -95,6 +95,6 @@ export default async function initConfig( return () => { log.trace('Disposing config controller'); disposeWatcher(); - disposeOnSnapshot(); + disposeReaction(); }; } diff --git a/packages/main/src/controllers/initNativeTheme.ts b/packages/main/src/controllers/initNativeTheme.ts index d2074ab..ce7972a 100644 --- a/packages/main/src/controllers/initNativeTheme.ts +++ b/packages/main/src/controllers/initNativeTheme.ts @@ -21,18 +21,18 @@ import { nativeTheme } from 'electron'; import { autorun } from 'mobx'; -import type { MainStore } from '../stores/MainStore'; +import type { SharedStore } from '../stores/SharedStore'; import type Disposer from '../utils/Disposer'; import { getLogger } from '../utils/log'; const log = getLogger('nativeTheme'); -export default function initNativeTheme(store: MainStore): Disposer { +export default function initNativeTheme(store: SharedStore): Disposer { log.trace('Initializing nativeTheme controller'); const disposeThemeSourceReaction = autorun(() => { - nativeTheme.themeSource = store.config.themeSource; - log.debug('Set theme source:', store.config.themeSource); + nativeTheme.themeSource = store.settings.themeSource; + log.debug('Set theme source:', store.settings.themeSource); }); store.setShouldUseDarkColors(nativeTheme.shouldUseDarkColors); diff --git a/packages/main/src/index.ts b/packages/main/src/index.ts index 66405ea..0978cd9 100644 --- a/packages/main/src/index.ts +++ b/packages/main/src/index.ts @@ -270,13 +270,13 @@ async function createWindow(): Promise { const actionToDispatch = action.parse(rawAction); switch (actionToDispatch.action) { case 'set-selected-service-id': - store.setSelectedServiceId(actionToDispatch.serviceId); + store.shared.setSelectedServiceId(actionToDispatch.serviceId); break; case 'set-browser-view-bounds': store.setBrowserViewBounds(actionToDispatch.browserViewBounds); break; case 'set-theme-source': - store.config.setThemeSource(actionToDispatch.themeSource); + store.settings.setThemeSource(actionToDispatch.themeSource); break; case 'reload-all-services': reloadServiceInject().catch((error) => { diff --git a/packages/main/src/infrastructure/ConfigPersistence.ts b/packages/main/src/infrastructure/ConfigPersistence.ts index 4b96f01..184fa8d 100644 --- a/packages/main/src/infrastructure/ConfigPersistence.ts +++ b/packages/main/src/infrastructure/ConfigPersistence.ts @@ -18,7 +18,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import type { ConfigSnapshotOut } from '../stores/Config'; +import type { Config } from '../stores/SharedStore'; import type Disposer from '../utils/Disposer'; export type ReadConfigResult = @@ -28,7 +28,7 @@ export type ReadConfigResult = export default interface ConfigPersistence { readConfig(): Promise; - writeConfig(configSnapshot: ConfigSnapshotOut): Promise; + writeConfig(config: Config): Promise; watchConfig(callback: () => Promise, throttleMs: number): Disposer; } diff --git a/packages/main/src/infrastructure/impl/FileBasedConfigPersistence.ts b/packages/main/src/infrastructure/impl/FileBasedConfigPersistence.ts index 06e3fab..88d8bf8 100644 --- a/packages/main/src/infrastructure/impl/FileBasedConfigPersistence.ts +++ b/packages/main/src/infrastructure/impl/FileBasedConfigPersistence.ts @@ -24,7 +24,7 @@ import path from 'node:path'; import JSON5 from 'json5'; import throttle from 'lodash-es/throttle'; -import type { ConfigSnapshotOut } from '../../stores/Config'; +import type { Config } from '../../stores/SharedStore'; import type Disposer from '../../utils/Disposer'; import { getLogger } from '../../utils/log'; import type ConfigPersistence from '../ConfigPersistence'; @@ -65,7 +65,7 @@ export default class FileBasedConfigPersistence implements ConfigPersistence { }; } - async writeConfig(configSnapshot: ConfigSnapshotOut): Promise { + async writeConfig(configSnapshot: Config): Promise { const configJson = JSON5.stringify(configSnapshot, { space: 2, }); diff --git a/packages/main/src/init.ts b/packages/main/src/init.ts index 236a075..fd8dd94 100644 --- a/packages/main/src/init.ts +++ b/packages/main/src/init.ts @@ -31,10 +31,10 @@ export default async function init(store: MainStore): Promise { app.getPath('userData'), ); const disposeConfigController = await initConfig( - store.config, + store.shared, configPersistenceService, ); - const disposeNativeThemeController = initNativeTheme(store); + const disposeNativeThemeController = initNativeTheme(store.shared); return () => { disposeNativeThemeController(); diff --git a/packages/main/src/stores/Config.ts b/packages/main/src/stores/Config.ts deleted file mode 100644 index e7fc360..0000000 --- a/packages/main/src/stores/Config.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright (C) 2021-2022 Kristóf Marussy - * - * This file is part of Sophie. - * - * Sophie is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { config as originalConfig, ThemeSource } from '@sophie/shared'; -import { applySnapshot, Instance, SnapshotIn } from 'mobx-state-tree'; - -import { addMissingProfileIds, PartialProfileSnapshotIn } from './Profile'; -import { - addMissingServiceIdsAndProfiles, - PartialServiceSnapshotIn, -} from './Service'; - -export const config = originalConfig.actions((self) => ({ - loadFromConfigFile(snapshot: ConfigFileIn): void { - const profiles = addMissingProfileIds(snapshot.profiles); - const services = addMissingServiceIdsAndProfiles( - snapshot.services, - profiles, - ); - applySnapshot(self, { - ...snapshot, - profiles, - services, - }); - }, - setThemeSource(mode: ThemeSource): void { - self.themeSource = mode; - }, -})); - -export interface Config extends Instance {} - -export interface ConfigSnapshotIn extends SnapshotIn {} - -export interface ConfigFileIn - extends Omit { - profiles?: PartialProfileSnapshotIn[] | undefined; - services?: PartialServiceSnapshotIn[] | undefined; -} - -export type { ConfigSnapshotOut } from '@sophie/shared'; diff --git a/packages/main/src/stores/GlobalSettings.ts b/packages/main/src/stores/GlobalSettings.ts new file mode 100644 index 0000000..1eb13b3 --- /dev/null +++ b/packages/main/src/stores/GlobalSettings.ts @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2022 Kristóf Marussy + * + * This file is part of Sophie. + * + * Sophie is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { + globalSettings as originalGlobalSettings, + ThemeSource, +} from '@sophie/shared'; +import { Instance } from 'mobx-state-tree'; + +export const globalSettings = originalGlobalSettings.actions((self) => ({ + setThemeSource(mode: ThemeSource): void { + self.themeSource = mode; + }, +})); + +export interface GlobalSettings extends Instance {} + +export type { + GlobalSettingsSnapshotIn, + GlobalSettingsSnapshotOut, +} from '@sophie/shared'; diff --git a/packages/main/src/stores/MainStore.ts b/packages/main/src/stores/MainStore.ts index f0d6472..18f5bf9 100644 --- a/packages/main/src/stores/MainStore.ts +++ b/packages/main/src/stores/MainStore.ts @@ -18,21 +18,14 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { BrowserViewBounds, service } from '@sophie/shared'; -import { - applySnapshot, - Instance, - resolveIdentifier, - types, -} from 'mobx-state-tree'; +import type { BrowserViewBounds } from '@sophie/shared'; +import { applySnapshot, Instance, types } from 'mobx-state-tree'; -import { getLogger } from '../utils/log'; - -import type { Config } from './Config.js'; +import { GlobalSettings } from './GlobalSettings'; +import { Profile } from './Profile'; +import { Service } from './Service'; import { sharedStore } from './SharedStore'; -const log = getLogger('mainStore'); - export const mainStore = types .model('MainStore', { browserViewBounds: types.optional( @@ -47,26 +40,20 @@ export const mainStore = types shared: types.optional(sharedStore, {}), }) .views((self) => ({ - get config(): Config { - return self.shared.config; + get settings(): GlobalSettings { + return self.shared.settings; + }, + get profiles(): Profile[] { + return self.shared.profiles; + }, + get services(): Service[] { + return self.shared.services; }, })) .actions((self) => ({ - setSelectedServiceId(serviceId: string): void { - const serviceInstance = resolveIdentifier(service, self, serviceId); - if (serviceInstance === undefined) { - log.warn('Trying to select unknown service', serviceId); - return; - } - self.shared.selectedService = serviceInstance; - log.debug('Selected service', serviceId); - }, setBrowserViewBounds(bounds: BrowserViewBounds): void { applySnapshot(self.browserViewBounds, bounds); }, - setShouldUseDarkColors(shouldUseDarkColors: boolean): void { - self.shared.shouldUseDarkColors = shouldUseDarkColors; - }, })); export interface MainStore extends Instance {} diff --git a/packages/main/src/stores/Profile.ts b/packages/main/src/stores/Profile.ts index 4705862..eaf23c4 100644 --- a/packages/main/src/stores/Profile.ts +++ b/packages/main/src/stores/Profile.ts @@ -18,34 +18,39 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import type { ProfileSnapshotIn } from '@sophie/shared'; +import { + profile as originalProfile, + ProfileSettingsSnapshotIn, +} from '@sophie/shared'; +import { getSnapshot, Instance } from 'mobx-state-tree'; +import SettingsWithId from '../utils/SettingsWithId'; import generateId from '../utils/generateId'; -export interface PartialProfileSnapshotIn - extends Omit { +export interface ProfileConfig extends ProfileSettingsSnapshotIn { id?: string | undefined; } +export const profile = originalProfile.views((self) => ({ + get config(): ProfileConfig { + const { id, settings } = self; + return { ...getSnapshot(settings), id }; + }, +})); + +export interface Profile extends Instance {} + +export type ProfileSettingsSnapshotWithId = + SettingsWithId; + export function addMissingProfileIds( - partialProfiles: PartialProfileSnapshotIn[] | undefined, -): ProfileSnapshotIn[] { - return (partialProfiles ?? []).map((profile) => { - const { name } = profile; - let { id } = profile; - if (typeof id === 'undefined') { - id = generateId(name); - } + profileConfigs: ProfileConfig[] | undefined, +): ProfileSettingsSnapshotWithId[] { + return (profileConfigs ?? []).map((profileConfig) => { + const { id, ...settings } = profileConfig; return { - ...profile, - id, + id: typeof id === 'undefined' ? generateId(settings.name) : id, + settings, }; }); } - -export type { - Profile, - ProfileSnapshotOut, - ProfileSnapshotIn, -} from '@sophie/shared'; -export { profile } from '@sophie/shared'; diff --git a/packages/main/src/stores/RuntimeService.ts b/packages/main/src/stores/RuntimeService.ts deleted file mode 100644 index ecb1942..0000000 --- a/packages/main/src/stores/RuntimeService.ts +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright (C) 2022 Kristóf Marussy - * - * This file is part of Sophie. - * - * Sophie is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { UnreadCount } from '@sophie/service-shared'; -import { runtimeService as originalRuntimeService } from '@sophie/shared'; -import { Instance } from 'mobx-state-tree'; - -export const runtimeService = originalRuntimeService.actions((self) => ({ - setLocation({ - url, - canGoBack, - canGoForward, - }: { - url: string; - canGoBack: boolean; - canGoForward: boolean; - }): void { - self.url = url; - self.canGoBack = canGoBack; - self.canGoForward = canGoForward; - }, - setTitle(title: string): void { - self.title = title; - }, - hibernated(): void { - self.canGoBack = false; - self.canGoForward = false; - self.state = 'hibernated'; - }, - startedLoading(): void { - self.state = 'loading'; - }, - finishedLoading(): void { - if (self.state === 'loading') { - // Do not overwrite crashed state if the service haven't been reloaded yet. - self.state = 'loaded'; - } - }, - crashed(): void { - self.state = 'crashed'; - }, - setUnreadCount({ direct, indirect }: UnreadCount): void { - if (direct !== undefined) { - self.directMessageCount = direct; - } - if (indirect !== undefined) { - self.indirectMessageCount = indirect; - } - }, -})); - -export interface RuntimeService extends Instance {} - -export type { - RuntimeServiceSnapshotIn, - RuntimeServiceSnapshotOut, -} from '@sophie/shared'; diff --git a/packages/main/src/stores/Service.ts b/packages/main/src/stores/Service.ts index 9bc6a43..78c57cb 100644 --- a/packages/main/src/stores/Service.ts +++ b/packages/main/src/stores/Service.ts @@ -18,46 +18,95 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import type { ServiceSnapshotIn } from '@sophie/shared'; +import type { UnreadCount } from '@sophie/service-shared'; +import { + service as originalService, + ServiceSettingsSnapshotIn, +} from '@sophie/shared'; +import { Instance, getSnapshot, ReferenceIdentifier } from 'mobx-state-tree'; +import SettingsWithId from '../utils/SettingsWithId'; import generateId from '../utils/generateId'; +import overrideProps from '../utils/overrideProps'; -import type { ProfileSnapshotIn } from './Profile'; +import { ProfileSettingsSnapshotWithId } from './Profile'; +import { serviceSettings } from './ServiceSettings'; -export interface PartialServiceSnapshotIn - extends Omit { +export interface ServiceConfig + extends Omit { id?: string | undefined; - profile?: string | undefined; + + profile?: ReferenceIdentifier | undefined; } +export const service = overrideProps(originalService, { + settings: serviceSettings, +}) + .views((self) => ({ + get config(): ServiceConfig { + const { id, settings } = self; + return { ...getSnapshot(settings), id }; + }, + })) + .actions((self) => ({ + setLocation({ + url, + canGoBack, + canGoForward, + }: { + url: string; + canGoBack: boolean; + canGoForward: boolean; + }): void { + self.currentUrl = url; + self.canGoBack = canGoBack; + self.canGoForward = canGoForward; + }, + setTitle(title: string): void { + self.title = title; + }, + startedLoading(): void { + self.state = 'loading'; + }, + finishedLoading(): void { + if (self.state === 'loading') { + // Do not overwrite crashed state if the service haven't been reloaded yet. + self.state = 'loaded'; + } + }, + crashed(): void { + self.state = 'crashed'; + }, + setUnreadCount({ direct, indirect }: UnreadCount): void { + if (direct !== undefined) { + self.directMessageCount = direct; + } + if (indirect !== undefined) { + self.indirectMessageCount = indirect; + } + }, + })); + +export interface Service extends Instance {} + +export type ServiceSettingsSnapshotWithId = + SettingsWithId; + export function addMissingServiceIdsAndProfiles( - partialServices: PartialServiceSnapshotIn[] | undefined, - profiles: ProfileSnapshotIn[], -): ServiceSnapshotIn[] { - return (partialServices ?? []).map((service) => { - const { name } = service; - let { id, profile } = service; - if (typeof id === 'undefined') { - id = generateId(name); - } - if (typeof profile === 'undefined') { - profile = generateId(name); - profiles.push({ - id: profile, - name: service.name, - }); + serviceConfigs: ServiceConfig[] | undefined, + profiles: ProfileSettingsSnapshotWithId[], +): ServiceSettingsSnapshotWithId[] { + return (serviceConfigs ?? []).map((serviceConfig) => { + const { id, ...settings } = serviceConfig; + const { name } = settings; + let { profile: profileId } = settings; + if (profileId === undefined) { + profileId = generateId(name); + profiles.push({ id: profileId, settings: { name } }); } return { - ...service, - id, - profile, + id: id === undefined ? generateId(name) : id, + settings: { ...settings, profile: profileId }, }; }); } - -export type { - Service, - ServiceSnapshotOut, - ServiceSnapshotIn, -} from '@sophie/shared'; -export { service } from '@sophie/shared'; diff --git a/packages/main/src/stores/ServiceSettings.ts b/packages/main/src/stores/ServiceSettings.ts new file mode 100644 index 0000000..960de9b --- /dev/null +++ b/packages/main/src/stores/ServiceSettings.ts @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2022 Kristóf Marussy + * + * This file is part of Sophie. + * + * Sophie is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { serviceSettings as originalServiceSettings } from '@sophie/shared'; +import { Instance, types } from 'mobx-state-tree'; + +import overrideProps from '../utils/overrideProps'; + +import { profile } from './Profile'; + +export const serviceSettings = overrideProps(originalServiceSettings, { + profile: types.reference(profile), +}); + +export interface ServiceSettings extends Instance {} + +export type { + ServiceSettingsSnapshotIn, + ServiceSettingsSnapshotOut, +} from '@sophie/shared'; diff --git a/packages/main/src/stores/SharedStore.ts b/packages/main/src/stores/SharedStore.ts index 76ce05c..861c8ce 100644 --- a/packages/main/src/stores/SharedStore.ts +++ b/packages/main/src/stores/SharedStore.ts @@ -19,19 +19,111 @@ */ import { sharedStore as originalSharedStore } from '@sophie/shared'; -import { Instance, types } from 'mobx-state-tree'; +import { + applySnapshot, + getSnapshot, + Instance, + resolveIdentifier, + types, +} from 'mobx-state-tree'; -import { config } from './Config'; -import { runtimeService } from './RuntimeService'; +import SettingsWithId from '../utils/SettingsWithId'; +import { getLogger } from '../utils/log'; +import overrideProps from '../utils/overrideProps'; + +import { globalSettings, GlobalSettingsSnapshotIn } from './GlobalSettings'; +import { addMissingProfileIds, profile, ProfileConfig } from './Profile'; +import { + addMissingServiceIdsAndProfiles, + service, + ServiceConfig, +} from './Service'; + +const log = getLogger('sharedStore'); + +export interface Config extends GlobalSettingsSnapshotIn { + profiles?: ProfileConfig[] | undefined; + + services?: ServiceConfig[] | undefined; +} + +function getConfigs(models: { config: T }[]): T[] | undefined { + return models.length === 0 ? undefined : models.map((model) => model.config); +} + +function reconcileSettings( + originalSnapshots: SettingsWithId[], + settingsSnapshotsWithId: SettingsWithId[], +): SettingsWithId[] { + const idToOriginalSnapshots = new Map( + originalSnapshots.map((originalSnapshot) => [ + originalSnapshot.id, + originalSnapshot, + ]), + ); + return settingsSnapshotsWithId.map(({ id, settings }) => ({ + ...idToOriginalSnapshots.get(id), + id, + settings, + })); +} + +export const sharedStore = overrideProps(originalSharedStore, { + settings: types.optional(globalSettings, {}), + profiles: types.array(profile), + services: types.array(service), + selectedService: types.safeReference(service), +}) + .views((self) => ({ + get config(): Config { + const { settings, profiles, services } = self; + const globalSettingsConfig = getSnapshot(settings); + return { + ...globalSettingsConfig, + profiles: getConfigs(profiles), + services: getConfigs(services), + }; + }, + })) + .actions((self) => ({ + loadConfig(config: Config): void { + const snapshot = getSnapshot(self); + const { profiles, services, ...settings } = config; + const profileSettingsSnapshots = addMissingProfileIds(profiles); + const serviceSettingsSnapshots = addMissingServiceIdsAndProfiles( + services, + profileSettingsSnapshots, + ); + applySnapshot(self, { + ...snapshot, + settings, + profiles: reconcileSettings( + snapshot.profiles, + profileSettingsSnapshots, + ), + services: reconcileSettings( + snapshot.services, + serviceSettingsSnapshots, + ), + }); + }, + setShouldUseDarkColors(shouldUseDarkColors: boolean): void { + self.shouldUseDarkColors = shouldUseDarkColors; + }, + setSelectedServiceId(serviceId: string): void { + const serviceInstance = resolveIdentifier(service, self, serviceId); + if (serviceInstance === undefined) { + log.warn('Trying to select unknown service', serviceId); + return; + } + self.selectedService = serviceInstance; + log.debug('Selected service', serviceId); + }, + })); + +export interface SharedStore extends Instance {} export type { SharedStoreSnapshotIn, SharedStoreSnapshotOut, } from '@sophie/shared'; - -export const sharedStore = originalSharedStore.props({ - config: types.optional(config, {}), - runtimeServices: types.map(runtimeService), -}); - -export interface SharedStore extends Instance {} diff --git a/packages/main/src/stores/__tests__/Config.spec.ts b/packages/main/src/stores/__tests__/Config.spec.ts deleted file mode 100644 index 22ccbc7..0000000 --- a/packages/main/src/stores/__tests__/Config.spec.ts +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Copyright (C) 2021-2022 Kristóf Marussy - * - * This file is part of Sophie. - * - * Sophie is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { config, Config, ConfigFileIn } from '../Config'; -import type { PartialProfileSnapshotIn } from '../Profile'; -import type { PartialServiceSnapshotIn } from '../Service'; - -const profileProps: PartialProfileSnapshotIn = { - name: 'Test profile', -}; - -const serviceProps: PartialServiceSnapshotIn = { - name: 'Test service', - url: 'https://example.com', -}; - -let sut: Config; - -beforeEach(() => { - sut = config.create(); -}); - -describe('preprocessConfigFile', () => { - it('should load profiles with an ID', () => { - sut.loadFromConfigFile({ - profiles: [ - { - id: 'someId', - ...profileProps, - }, - ], - }); - expect(sut.profiles[0].id).toBe('someId'); - }); - - it('should generate an ID for profiles without and ID', () => { - sut.loadFromConfigFile({ - profiles: [profileProps], - }); - expect(sut.profiles[0].id).toBeDefined(); - }); - - it('should load services with an ID and a profile', () => { - sut.loadFromConfigFile({ - profiles: [ - { - id: 'someProfileId', - ...profileProps, - }, - ], - services: [ - { - id: 'someServiceId', - profile: 'someProfileId', - ...serviceProps, - }, - ], - }); - expect(sut.services[0].id).toBe('someServiceId'); - expect(sut.services[0].profile).toBe(sut.profiles[0]); - }); - - it('should refuse to load a profile without a name', () => { - expect(() => { - sut.loadFromConfigFile({ - profiles: [ - { - id: 'someProfileId', - ...profileProps, - name: undefined, - }, - ], - } as unknown as ConfigFileIn); - }).toThrow(); - expect(sut.profiles).toHaveLength(0); - }); - - it('should load services without an ID but with a profile', () => { - sut.loadFromConfigFile({ - profiles: [ - { - id: 'someProfileId', - ...profileProps, - }, - ], - services: [ - { - profile: 'someProfileId', - ...serviceProps, - }, - ], - }); - expect(sut.services[0].id).toBeDefined(); - expect(sut.services[0].profile).toBe(sut.profiles[0]); - }); - - it('should create a profile for a service with an ID but no profile', () => { - sut.loadFromConfigFile({ - services: [ - { - id: 'someServiceId', - ...serviceProps, - }, - ], - }); - expect(sut.services[0].id).toBe('someServiceId'); - expect(sut.services[0].profile).toBeDefined(); - expect(sut.services[0].profile.name).toBe(serviceProps.name); - }); - - it('should create a profile for a service without an ID or profile', () => { - sut.loadFromConfigFile({ - services: [ - { - ...serviceProps, - }, - ], - }); - expect(sut.services[0].id).toBeDefined(); - expect(sut.services[0].profile).toBeDefined(); - expect(sut.services[0].profile.name).toBe(serviceProps.name); - }); - - it('should refuse to load a service without a name', () => { - expect(() => { - sut.loadFromConfigFile({ - services: [ - { - id: 'someServiceId', - ...serviceProps, - name: undefined, - }, - ], - } as unknown as ConfigFileIn); - }).toThrow(); - expect(sut.profiles).toHaveLength(0); - expect(sut.services).toHaveLength(0); - }); -}); diff --git a/packages/main/src/stores/__tests__/SharedStore.spec.ts b/packages/main/src/stores/__tests__/SharedStore.spec.ts new file mode 100644 index 0000000..3ea187c --- /dev/null +++ b/packages/main/src/stores/__tests__/SharedStore.spec.ts @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2021-2022 Kristóf Marussy + * + * This file is part of Sophie. + * + * Sophie is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import type { ProfileConfig } from '../Profile'; +import type { ServiceConfig } from '../Service'; +import { Config, sharedStore, SharedStore } from '../SharedStore'; + +const profileProps: ProfileConfig = { + name: 'Test profile', +}; + +const serviceProps: ServiceConfig = { + name: 'Test service', + url: 'https://example.com', +}; + +let sut: SharedStore; + +beforeEach(() => { + sut = sharedStore.create(); +}); + +describe('loadConfig', () => { + it('should load profiles with an ID', () => { + sut.loadConfig({ + profiles: [ + { + id: 'someId', + ...profileProps, + }, + ], + }); + expect(sut.profiles[0].id).toBe('someId'); + }); + + it('should generate an ID for profiles without and ID', () => { + sut.loadConfig({ + profiles: [profileProps], + }); + expect(sut.profiles[0].id).toBeDefined(); + }); + + it('should load services with an ID and a profile', () => { + sut.loadConfig({ + profiles: [ + { + id: 'someProfileId', + ...profileProps, + }, + ], + services: [ + { + id: 'someServiceId', + profile: 'someProfileId', + ...serviceProps, + }, + ], + }); + expect(sut.services[0].id).toBe('someServiceId'); + expect(sut.services[0].settings.profile).toBe(sut.profiles[0]); + }); + + it('should refuse to load a profile without a name', () => { + expect(() => { + sut.loadConfig({ + profiles: [ + { + id: 'someProfileId', + ...profileProps, + name: undefined, + }, + ], + } as unknown as Config); + }).toThrow(); + expect(sut.profiles).toHaveLength(0); + }); + + it('should load services without an ID but with a profile', () => { + sut.loadConfig({ + profiles: [ + { + id: 'someProfileId', + ...profileProps, + }, + ], + services: [ + { + profile: 'someProfileId', + ...serviceProps, + }, + ], + }); + expect(sut.services[0].id).toBeDefined(); + expect(sut.services[0].settings.profile).toBe(sut.profiles[0]); + }); + + it('should create a profile for a service with an ID but no profile', () => { + sut.loadConfig({ + services: [ + { + id: 'someServiceId', + ...serviceProps, + }, + ], + }); + expect(sut.services[0].id).toBe('someServiceId'); + expect(sut.services[0].settings.profile).toBeDefined(); + expect(sut.services[0].settings.profile.settings.name).toBe( + serviceProps.name, + ); + }); + + it('should create a profile for a service without an ID or profile', () => { + sut.loadConfig({ + services: [ + { + ...serviceProps, + }, + ], + }); + expect(sut.services[0].id).toBeDefined(); + expect(sut.services[0].settings.profile).toBeDefined(); + expect(sut.services[0].settings.profile.settings.name).toBe( + serviceProps.name, + ); + }); + + it('should refuse to load a service without a name', () => { + expect(() => { + sut.loadConfig({ + services: [ + { + id: 'someServiceId', + ...serviceProps, + name: undefined, + }, + ], + } as unknown as Config); + }).toThrow(); + expect(sut.profiles).toHaveLength(0); + expect(sut.services).toHaveLength(0); + }); +}); diff --git a/packages/main/src/utils/SettingsWithId.ts b/packages/main/src/utils/SettingsWithId.ts new file mode 100644 index 0000000..fde3e86 --- /dev/null +++ b/packages/main/src/utils/SettingsWithId.ts @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2022 Kristóf Marussy + * + * This file is part of Sophie. + * + * Sophie is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export default interface SettingsWithId { + id: string; + + settings: T; +} diff --git a/packages/main/src/utils/overrideProps.ts b/packages/main/src/utils/overrideProps.ts new file mode 100644 index 0000000..c626408 --- /dev/null +++ b/packages/main/src/utils/overrideProps.ts @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2022 Kristóf Marussy + * + * This file is part of Sophie. + * + * Sophie is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/** + * @file This file implements a technique to force-override properties of a model. + * + * The overridden properties must conform to the SnapshotIt and SnapshotOut format + * of the original model. Essentially, this means that only views and actions can + * be added safely. + * + * @see https://github.com/mobxjs/mobx-state-tree/issues/1403#issuecomment-940843087 + */ + +import { + IAnyModelType, + IModelType, + ModelProperties, + SnapshotIn, + SnapshotOut, +} from 'mobx-state-tree'; + +export type IUnsafeOverriddenModelType< + BASE extends IAnyModelType, + PROPS extends ModelProperties, +> = BASE extends IModelType + ? IModelType & PROPS, O, CC, CS> + : never; + +export type IOverriddenModelType< + BASE extends IAnyModelType, + PROPS extends ModelProperties, +> = SnapshotIn extends SnapshotIn> + ? SnapshotOut< + IUnsafeOverriddenModelType + > extends SnapshotOut + ? IUnsafeOverriddenModelType + : never + : never; + +export default function overrideProps< + BASE extends IAnyModelType, + PROPS extends ModelProperties, +>(base: BASE, props: PROPS): IOverriddenModelType { + return base.props(props) as IOverriddenModelType; +} diff --git a/packages/renderer/src/components/ServiceSwitcher.tsx b/packages/renderer/src/components/ServiceSwitcher.tsx index 0786b71..167153f 100644 --- a/packages/renderer/src/components/ServiceSwitcher.tsx +++ b/packages/renderer/src/components/ServiceSwitcher.tsx @@ -78,8 +78,8 @@ export default observer(() => { } - aria-label={service.name} + icon={} + aria-label={service.settings.name} /> ))} diff --git a/packages/renderer/src/env/RendererEnv.ts b/packages/renderer/src/env/RendererEnv.ts index 5ca2978..ba4c43e 100644 --- a/packages/renderer/src/env/RendererEnv.ts +++ b/packages/renderer/src/env/RendererEnv.ts @@ -18,10 +18,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import type { Action, RuntimeService } from '@sophie/shared'; +import type { Action } from '@sophie/shared'; export default interface RendererEnv { dispatchMainAction(action: Action): void; - - getRuntimeService(serviceId: string): RuntimeService | undefined; } diff --git a/packages/renderer/src/env/impl/RendererEnvImpl.ts b/packages/renderer/src/env/impl/RendererEnvImpl.ts deleted file mode 100644 index 184d31b..0000000 --- a/packages/renderer/src/env/impl/RendererEnvImpl.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (C) 2022 Kristóf Marussy - * - * This file is part of Sophie. - * - * Sophie is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { - Action, - runtimeService, - RuntimeService, - SophieRenderer, -} from '@sophie/shared'; -import type { IMSTMap } from 'mobx-state-tree'; - -import type { RendererStore } from '../../stores/RendererStore'; -import type RendererEnv from '../RendererEnv'; - -export default class RendererEnvImpl implements RendererEnv { - readonly #ipc: SophieRenderer; - - #runtimeServices: IMSTMap | undefined; - - constructor(ipc: SophieRenderer) { - this.#ipc = ipc; - } - - setStore(store: RendererStore): void { - this.#runtimeServices = store.shared.runtimeServices; - } - - dispatchMainAction(action: Action): void { - this.#ipc.dispatchAction(action); - } - - getRuntimeService(serviceId: string): RuntimeService | undefined { - if (this.#runtimeServices === undefined) { - throw new Error('runtime services map is not yet set'); - } - return this.#runtimeServices.get(serviceId); - } -} diff --git a/packages/renderer/src/env/impl/__tests__/RendererEnvImpl.spec.ts b/packages/renderer/src/env/impl/__tests__/RendererEnvImpl.spec.ts deleted file mode 100644 index d36462c..0000000 --- a/packages/renderer/src/env/impl/__tests__/RendererEnvImpl.spec.ts +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (C) 2022 Kristóf Marussy - * - * This file is part of Sophie. - * - * Sophie is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { jest } from '@jest/globals'; -import { - Action, - runtimeService, - RuntimeService, - SophieRenderer, -} from '@sophie/shared'; - -import { rendererStore } from '../../../stores/RendererStore'; -import RendererEnvImpl from '../RendererEnvImpl'; - -const ipc: SophieRenderer = { - dispatchAction: jest.fn(), - onSharedStoreChange: jest.fn(), -}; -let sut: RendererEnvImpl; - -beforeEach(() => { - sut = new RendererEnvImpl(ipc); -}); - -describe('dispatchMainAction', () => { - it('should dispatch actions via the IPC', () => { - const action: Action = { - action: 'set-theme-source', - themeSource: 'dark', - }; - sut.dispatchMainAction(action); - expect(ipc.dispatchAction).toHaveBeenCalledWith(action); - }); -}); - -describe('getRuntimeService', () => { - describe('when no store was set', () => { - it('should throw an error', () => { - expect(() => sut.getRuntimeService('someId')).toThrow(); - }); - }); - - describe('when the store was set', () => { - let runtimeServiceStore: RuntimeService; - - beforeEach(() => { - runtimeServiceStore = runtimeService.create({ - state: 'loaded', - }); - const store = rendererStore.create({ - shared: { - runtimeServices: { - someId: runtimeServiceStore, - }, - }, - }); - sut.setStore(store); - }); - - it('should return the runtime service for the given ID', () => { - const returnedStore = sut.getRuntimeService('someId'); - expect(returnedStore).toBe(runtimeServiceStore); - }); - - it('should return undefined for an unknown ID', () => { - const returnedStore = sut.getRuntimeService('unknownId'); - expect(returnedStore).toBeUndefined(); - }); - }); -}); diff --git a/packages/renderer/src/stores/Config.ts b/packages/renderer/src/stores/Config.ts deleted file mode 100644 index 070c4ec..0000000 --- a/packages/renderer/src/stores/Config.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (C) 2022 Kristóf Marussy - * - * This file is part of Sophie. - * - * Sophie is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { config as originalConfig } from '@sophie/shared'; -import { Instance, types } from 'mobx-state-tree'; - -import { service } from './Service'; - -export const config = originalConfig.props({ - services: types.array(service), -}); - -export interface Config extends Instance {} - -export type { ConfigSnapshotIn, ConfigSnapshotOut } from '@sophie/shared'; diff --git a/packages/renderer/src/stores/RendererStore.ts b/packages/renderer/src/stores/RendererStore.ts index 731ca28..a4e6197 100644 --- a/packages/renderer/src/stores/RendererStore.ts +++ b/packages/renderer/src/stores/RendererStore.ts @@ -18,17 +18,19 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { BrowserViewBounds, SophieRenderer, ThemeSource } from '@sophie/shared'; +import { + BrowserViewBounds, + sharedStore, + Service, + SophieRenderer, + ThemeSource, +} from '@sophie/shared'; import { applySnapshot, applyPatch, Instance, types } from 'mobx-state-tree'; +import RendererEnv from '../env/RendererEnv'; import getEnv from '../env/getEnv'; -import RendererEnvImpl from '../env/impl/RendererEnvImpl'; import { getLogger } from '../utils/log'; -import type { Config } from './Config'; -import type { Service } from './Service'; -import { sharedStore } from './SharedStore'; - const log = getLogger('RendererStore'); export const rendererStore = types @@ -36,11 +38,8 @@ export const rendererStore = types shared: types.optional(sharedStore, {}), }) .views((self) => ({ - get config(): Config { - return self.shared.config; - }, get services(): Service[] { - return this.config.services; + return self.shared.services; }, get selectedService(): Service | undefined { return self.shared.selectedService; @@ -87,9 +86,10 @@ export interface RendererStore extends Instance {} export function createAndConnectRendererStore( ipc: SophieRenderer, ): RendererStore { - const env = new RendererEnvImpl(ipc); + const env: RendererEnv = { + dispatchMainAction: ipc.dispatchAction, + }; const store = rendererStore.create({}, env); - env.setStore(store); ipc .onSharedStoreChange({ diff --git a/packages/renderer/src/stores/Service.ts b/packages/renderer/src/stores/Service.ts deleted file mode 100644 index 2f45106..0000000 --- a/packages/renderer/src/stores/Service.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (C) 2022 Kristóf Marussy - * - * This file is part of Sophie. - * - * Sophie is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { - runtimeService, - RuntimeService, - service as originalService, -} from '@sophie/shared'; -import { Instance } from 'mobx-state-tree'; - -import getEnv from '../env/getEnv'; - -export const service = originalService.views((self) => ({ - get runtime(): RuntimeService { - return getEnv(self).getRuntimeService(self.id) ?? runtimeService.create(); - }, -})); - -export interface Service extends Instance {} - -export type { ServiceSnapshotIn, ServiceSnapshotOut } from '@sophie/shared'; diff --git a/packages/renderer/src/stores/SharedStore.ts b/packages/renderer/src/stores/SharedStore.ts deleted file mode 100644 index 962f7e2..0000000 --- a/packages/renderer/src/stores/SharedStore.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (C) 2022 Kristóf Marussy - * - * This file is part of Sophie. - * - * Sophie is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { sharedStore as originalSharedStore } from '@sophie/shared'; -import { Instance, types } from 'mobx-state-tree'; - -import { config } from './Config'; -import { service } from './Service'; - -export const sharedStore = originalSharedStore.props({ - config: types.optional(config, {}), - selectedService: types.safeReference(service), -}); - -export interface SharedStore extends Instance {} - -export type { - SharedStoreSnapshotIn, - SharedStoreSnapshotOut, -} from '@sophie/shared'; diff --git a/packages/renderer/src/stores/__tests__/Service.spec.ts b/packages/renderer/src/stores/__tests__/Service.spec.ts deleted file mode 100644 index f835d41..0000000 --- a/packages/renderer/src/stores/__tests__/Service.spec.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (C) 2022 Kristóf Marussy - * - * This file is part of Sophie. - * - * Sophie is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { jest } from '@jest/globals'; -import { runtimeService } from '@sophie/shared'; -import { mocked } from 'jest-mock'; - -import type RendererEnv from '../../env/RendererEnv'; -import { service, Service } from '../Service'; - -const env: RendererEnv = { - dispatchMainAction: jest.fn(), - getRuntimeService: jest.fn(), -}; -let sut: Service; - -beforeEach(() => { - sut = service.create( - { - id: 'serviceId', - name: 'Foo', - url: 'https://example.com', - profile: 'profileId', - }, - env, - ); -}); - -describe('runtime', () => { - it('should return the runtime service with for the service ID', () => { - const runtimeServiceStore = runtimeService.create({}, env); - mocked(env.getRuntimeService).mockReturnValueOnce(runtimeServiceStore); - const returnedStore = sut.runtime; - expect(env.getRuntimeService).toHaveBeenCalledWith('serviceId'); - expect(returnedStore).toBe(runtimeServiceStore); - }); - - it('should return a valid runtime service even if none exists in the environment', () => { - /* - eslint-disable-next-line unicorn/no-useless-undefined -- - `mockReturnValueOnce` expects 1 parameter. - */ - mocked(env.getRuntimeService).mockReturnValueOnce(undefined); - expect(sut.runtime).toHaveProperty('state', 'hibernated'); - }); -}); diff --git a/packages/shared/src/contextBridge/SophieRenderer.ts b/packages/shared/src/contextBridge/SophieRenderer.ts index 9858aa9..28dc0b7 100644 --- a/packages/shared/src/contextBridge/SophieRenderer.ts +++ b/packages/shared/src/contextBridge/SophieRenderer.ts @@ -21,7 +21,7 @@ import { Action } from '../schemas'; import { SharedStoreListener } from '../stores/SharedStore'; -export interface SophieRenderer { +export default interface SophieRenderer { onSharedStoreChange(this: void, listener: SharedStoreListener): Promise; dispatchAction(this: void, action: Action): void; diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index df02854..55cf5ce 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -18,7 +18,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -export type { SophieRenderer } from './contextBridge/SophieRenderer'; +export type { default as SophieRenderer } from './contextBridge/SophieRenderer'; export { MainToRendererIpcMessage, RendererToMainIpcMessage } from './ipc'; @@ -26,33 +26,32 @@ export type { Action, BrowserViewBounds, ThemeSource } from './schemas'; export { action, browserViewBounds, themeSource } from './schemas'; export type { - Config, - ConfigSnapshotIn, - ConfigSnapshotOut, -} from './stores/Config'; -export { config } from './stores/Config'; + GlobalSettings, + GlobalSettingsSnapshotIn, + GlobalSettingsSnapshotOut, +} from './stores/GlobalSettings'; +export { globalSettings } from './stores/GlobalSettings'; -export type { - Profile, - ProfileSnapshotIn, - ProfileSnapshotOut, -} from './stores/Profile'; +export type { Profile } from './stores/Profile'; export { profile } from './stores/Profile'; export type { - RuntimeService, - RuntimeServiceSnapshotIn, - RuntimeServiceSnapshotOut, -} from './stores/RuntimeService'; -export { runtimeService } from './stores/RuntimeService'; + ProfileSettings, + ProfileSettingsSnapshotIn, + ProfileSettingsSnapshotOut, +} from './stores/ProfileSettings'; +export { profileSettings } from './stores/ProfileSettings'; -export type { - Service, - ServiceSnapshotIn, - ServiceSnapshotOut, -} from './stores/Service'; +export type { Service } from './stores/Service'; export { service } from './stores/Service'; +export type { + ServiceSettings, + ServiceSettingsSnapshotIn, + ServiceSettingsSnapshotOut, +} from './stores/ServiceSettings'; +export { serviceSettings } from './stores/ServiceSettings'; + export type { SharedStore, SharedStoreListener, diff --git a/packages/shared/src/stores/Config.ts b/packages/shared/src/stores/Config.ts deleted file mode 100644 index 1d97e7d..0000000 --- a/packages/shared/src/stores/Config.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (C) 2021-2022 Kristóf Marussy - * - * This file is part of Sophie. - * - * Sophie is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Instance, types, SnapshotIn, SnapshotOut } from 'mobx-state-tree'; - -import { themeSource } from '../schemas'; - -import { profile } from './Profile'; -import { service } from './Service'; - -export const config = types.model('Config', { - profiles: types.array(profile), - services: types.array(service), - themeSource: types.optional(types.enumeration(themeSource.options), 'system'), -}); - -export interface Config extends Instance {} - -export interface ConfigSnapshotIn extends SnapshotIn {} - -export interface ConfigSnapshotOut extends SnapshotOut {} diff --git a/packages/shared/src/stores/GlobalSettings.ts b/packages/shared/src/stores/GlobalSettings.ts new file mode 100644 index 0000000..bd0155a --- /dev/null +++ b/packages/shared/src/stores/GlobalSettings.ts @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2022 Kristóf Marussy + * + * This file is part of Sophie. + * + * Sophie is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Instance, types, SnapshotIn, SnapshotOut } from 'mobx-state-tree'; + +import { themeSource } from '../schemas'; + +export const globalSettings = types.model('GlobalSettings', { + themeSource: types.optional(types.enumeration(themeSource.options), 'system'), +}); + +export interface GlobalSettings extends Instance {} + +export interface GlobalSettingsSnapshotIn + extends SnapshotIn {} + +export interface GlobalSettingsSnapshotOut + extends SnapshotOut {} diff --git a/packages/shared/src/stores/Profile.ts b/packages/shared/src/stores/Profile.ts index 88a0f4d..bb058f6 100644 --- a/packages/shared/src/stores/Profile.ts +++ b/packages/shared/src/stores/Profile.ts @@ -18,15 +18,13 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Instance, types, SnapshotIn, SnapshotOut } from 'mobx-state-tree'; +import { Instance, types } from 'mobx-state-tree'; + +import { profileSettings } from './ProfileSettings'; export const profile = types.model('Profile', { id: types.identifier, - name: types.string, + settings: profileSettings, }); export interface Profile extends Instance {} - -export interface ProfileSnapshotIn extends SnapshotIn {} - -export interface ProfileSnapshotOut extends SnapshotOut {} diff --git a/packages/shared/src/stores/ProfileSettings.ts b/packages/shared/src/stores/ProfileSettings.ts new file mode 100644 index 0000000..ec8da5f --- /dev/null +++ b/packages/shared/src/stores/ProfileSettings.ts @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2022 Kristóf Marussy + * + * This file is part of Sophie. + * + * Sophie is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Instance, types, SnapshotIn, SnapshotOut } from 'mobx-state-tree'; + +export const profileSettings = types.model('ProfileSettings', { + name: types.string, +}); + +export interface ProfileSettings extends Instance {} + +export interface ProfileSettingsSnapshotIn + extends SnapshotIn {} + +export interface ProfileSettingsSnapshotOut + extends SnapshotOut {} diff --git a/packages/shared/src/stores/RuntimeService.ts b/packages/shared/src/stores/RuntimeService.ts deleted file mode 100644 index c5b9031..0000000 --- a/packages/shared/src/stores/RuntimeService.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (C) 2022 Kristóf Marussy - * - * This file is part of Sophie. - * - * Sophie is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Instance, types, SnapshotIn, SnapshotOut } from 'mobx-state-tree'; - -export const runtimeService = types.model({ - url: types.maybe(types.string), - canGoBack: false, - canGoForward: false, - title: types.maybe(types.string), - state: types.optional( - types.enumeration('ServiceState', [ - 'hibernated', - 'loading', - 'loaded', - 'crashed', - ]), - 'hibernated', - ), - directMessageCount: 0, - indirectMessageCount: 0, -}); - -export interface RuntimeService extends Instance {} - -export interface RuntimeServiceSnapshotIn - extends SnapshotIn {} - -export interface RuntimeServiceSnapshotOut - extends SnapshotOut {} diff --git a/packages/shared/src/stores/Service.ts b/packages/shared/src/stores/Service.ts index ed2cd9a..36acd3d 100644 --- a/packages/shared/src/stores/Service.ts +++ b/packages/shared/src/stores/Service.ts @@ -18,20 +18,23 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Instance, types, SnapshotIn, SnapshotOut } from 'mobx-state-tree'; +import { Instance, types } from 'mobx-state-tree'; -import { profile } from './Profile'; +import { serviceSettings } from './ServiceSettings'; export const service = types.model('Service', { id: types.identifier, - name: types.string, - profile: types.reference(profile), - // TODO: Remove this once recipes are added. - url: types.string, + settings: serviceSettings, + currentUrl: types.maybe(types.string), + canGoBack: false, + canGoForward: false, + title: types.maybe(types.string), + state: types.optional( + types.enumeration('ServiceState', ['loading', 'loaded', 'crashed']), + 'loading', + ), + directMessageCount: 0, + indirectMessageCount: 0, }); export interface Service extends Instance {} - -export interface ServiceSnapshotIn extends SnapshotIn {} - -export interface ServiceSnapshotOut extends SnapshotOut {} diff --git a/packages/shared/src/stores/ServiceSettings.ts b/packages/shared/src/stores/ServiceSettings.ts new file mode 100644 index 0000000..54cd7eb --- /dev/null +++ b/packages/shared/src/stores/ServiceSettings.ts @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2022 Kristóf Marussy + * + * This file is part of Sophie. + * + * Sophie is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Instance, types, SnapshotIn, SnapshotOut } from 'mobx-state-tree'; + +import { profile } from './Profile'; + +export const serviceSettings = types.model('ServiceSettings', { + name: types.string, + profile: types.reference(profile), + // TODO: Remove this once recipes are added. + url: types.string, +}); + +export interface ServiceSettings extends Instance {} + +export interface ServiceSettingsSnapshotIn + extends SnapshotIn {} + +export interface ServiceSettingsSnapshotOut + extends SnapshotOut {} diff --git a/packages/shared/src/stores/SharedStore.ts b/packages/shared/src/stores/SharedStore.ts index e6e2cad..a04f4bf 100644 --- a/packages/shared/src/stores/SharedStore.ts +++ b/packages/shared/src/stores/SharedStore.ts @@ -26,13 +26,14 @@ import { SnapshotOut, } from 'mobx-state-tree'; -import { config } from './Config'; -import { runtimeService } from './RuntimeService'; +import { globalSettings } from './GlobalSettings'; +import { profile } from './Profile'; import { service } from './Service'; export const sharedStore = types.model('SharedStore', { - config: types.optional(config, {}), - runtimeServices: types.map(runtimeService), + settings: types.optional(globalSettings, {}), + profiles: types.array(profile), + services: types.array(service), selectedService: types.safeReference(service), shouldUseDarkColors: false, }); -- cgit v1.2.3-54-g00ecf