aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--packages/main/src/controllers/__tests__/initConfig.spec.ts45
-rw-r--r--packages/main/src/controllers/__tests__/initNativeTheme.spec.ts10
-rw-r--r--packages/main/src/controllers/initConfig.ts30
-rw-r--r--packages/main/src/controllers/initNativeTheme.ts8
-rw-r--r--packages/main/src/index.ts4
-rw-r--r--packages/main/src/infrastructure/ConfigPersistence.ts4
-rw-r--r--packages/main/src/infrastructure/impl/FileBasedConfigPersistence.ts4
-rw-r--r--packages/main/src/init.ts4
-rw-r--r--packages/main/src/stores/Config.ts58
-rw-r--r--packages/main/src/stores/GlobalSettings.ts (renamed from packages/renderer/src/stores/Service.ts)20
-rw-r--r--packages/main/src/stores/MainStore.ts39
-rw-r--r--packages/main/src/stores/Profile.ts45
-rw-r--r--packages/main/src/stores/RuntimeService.ts74
-rw-r--r--packages/main/src/stores/Service.ts109
-rw-r--r--packages/main/src/stores/ServiceSettings.ts (renamed from packages/renderer/src/stores/SharedStore.ts)18
-rw-r--r--packages/main/src/stores/SharedStore.ts112
-rw-r--r--packages/main/src/stores/__tests__/SharedStore.spec.ts (renamed from packages/main/src/stores/__tests__/Config.spec.ts)52
-rw-r--r--packages/main/src/utils/SettingsWithId.ts (renamed from packages/renderer/src/stores/Config.ts)15
-rw-r--r--packages/main/src/utils/overrideProps.ts62
-rw-r--r--packages/renderer/src/components/ServiceSwitcher.tsx4
-rw-r--r--packages/renderer/src/env/RendererEnv.ts4
-rw-r--r--packages/renderer/src/env/impl/RendererEnvImpl.ts55
-rw-r--r--packages/renderer/src/env/impl/__tests__/RendererEnvImpl.spec.ts87
-rw-r--r--packages/renderer/src/stores/RendererStore.ts24
-rw-r--r--packages/renderer/src/stores/__tests__/Service.spec.ts63
-rw-r--r--packages/shared/src/contextBridge/SophieRenderer.ts2
-rw-r--r--packages/shared/src/index.ts41
-rw-r--r--packages/shared/src/stores/GlobalSettings.ts (renamed from packages/shared/src/stores/Config.ts)17
-rw-r--r--packages/shared/src/stores/Profile.ts10
-rw-r--r--packages/shared/src/stores/ProfileSettings.ts33
-rw-r--r--packages/shared/src/stores/Service.ts23
-rw-r--r--packages/shared/src/stores/ServiceSettings.ts (renamed from packages/shared/src/stores/RuntimeService.ts)33
-rw-r--r--packages/shared/src/stores/SharedStore.ts9
33 files changed, 496 insertions, 622 deletions
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 @@
20 20
21import { jest } from '@jest/globals'; 21import { jest } from '@jest/globals';
22import { mocked } from 'jest-mock'; 22import { mocked } from 'jest-mock';
23import { getSnapshot } from 'mobx-state-tree';
24import ms from 'ms'; 23import ms from 'ms';
25 24
26import type ConfigPersistence from '../../infrastructure/ConfigPersistence'; 25import type ConfigPersistence from '../../infrastructure/ConfigPersistence';
27import { Config, config as configModel } from '../../stores/Config'; 26import { sharedStore, SharedStore } from '../../stores/SharedStore';
28import type Disposer from '../../utils/Disposer'; 27import type Disposer from '../../utils/Disposer';
29import { silenceLogger } from '../../utils/log'; 28import { silenceLogger } from '../../utils/log';
30import initConfig from '../initConfig'; 29import initConfig from '../initConfig';
31 30
32let config: Config; 31let store: SharedStore;
33const persistenceService: ConfigPersistence = { 32const persistenceService: ConfigPersistence = {
34 readConfig: jest.fn(), 33 readConfig: jest.fn(),
35 writeConfig: jest.fn(), 34 writeConfig: jest.fn(),
@@ -44,7 +43,7 @@ beforeAll(() => {
44}); 43});
45 44
46beforeEach(() => { 45beforeEach(() => {
47 config = configModel.create(); 46 store = sharedStore.create();
48}); 47});
49 48
50describe('when initializing', () => { 49describe('when initializing', () => {
@@ -56,7 +55,7 @@ describe('when initializing', () => {
56 }); 55 });
57 56
58 it('should create a new config file', async () => { 57 it('should create a new config file', async () => {
59 await initConfig(config, persistenceService); 58 await initConfig(store, persistenceService);
60 expect(persistenceService.writeConfig).toHaveBeenCalledTimes(1); 59 expect(persistenceService.writeConfig).toHaveBeenCalledTimes(1);
61 }); 60 });
62 61
@@ -65,7 +64,7 @@ describe('when initializing', () => {
65 new Error('boo'), 64 new Error('boo'),
66 ); 65 );
67 await expect(() => 66 await expect(() =>
68 initConfig(config, persistenceService), 67 initConfig(store, persistenceService),
69 ).rejects.toBeInstanceOf(Error); 68 ).rejects.toBeInstanceOf(Error);
70 }); 69 });
71 }); 70 });
@@ -76,16 +75,16 @@ describe('when initializing', () => {
76 found: true, 75 found: true,
77 data: { 76 data: {
78 // Use a default empty config file to not trigger config rewrite. 77 // Use a default empty config file to not trigger config rewrite.
79 ...getSnapshot(config), 78 ...store.config,
80 themeSource: 'dark', 79 themeSource: 'dark',
81 }, 80 },
82 }); 81 });
83 }); 82 });
84 83
85 it('should read the existing config file is there is one', async () => { 84 it('should read the existing config file is there is one', async () => {
86 await initConfig(config, persistenceService); 85 await initConfig(store, persistenceService);
87 expect(persistenceService.writeConfig).not.toHaveBeenCalled(); 86 expect(persistenceService.writeConfig).not.toHaveBeenCalled();
88 expect(config.themeSource).toBe('dark'); 87 expect(store.settings.themeSource).toBe('dark');
89 }); 88 });
90 89
91 it('should bail if it cannot set up a watcher', async () => { 90 it('should bail if it cannot set up a watcher', async () => {
@@ -93,7 +92,7 @@ describe('when initializing', () => {
93 throw new Error('boo'); 92 throw new Error('boo');
94 }); 93 });
95 await expect(() => 94 await expect(() =>
96 initConfig(config, persistenceService), 95 initConfig(store, persistenceService),
97 ).rejects.toBeInstanceOf(Error); 96 ).rejects.toBeInstanceOf(Error);
98 }); 97 });
99 }); 98 });
@@ -108,7 +107,7 @@ describe('when initializing', () => {
108 }, 107 },
109 }, 108 },
110 }); 109 });
111 await initConfig(config, persistenceService); 110 await initConfig(store, persistenceService);
112 expect(persistenceService.writeConfig).toHaveBeenCalledTimes(1); 111 expect(persistenceService.writeConfig).toHaveBeenCalledTimes(1);
113 }); 112 });
114 113
@@ -119,15 +118,15 @@ describe('when initializing', () => {
119 themeSource: -1, 118 themeSource: -1,
120 }, 119 },
121 }); 120 });
122 await initConfig(config, persistenceService); 121 await initConfig(store, persistenceService);
123 expect(config.themeSource).not.toBe(-1); 122 expect(store.settings.themeSource).not.toBe(-1);
124 expect(persistenceService.writeConfig).not.toHaveBeenCalled(); 123 expect(persistenceService.writeConfig).not.toHaveBeenCalled();
125 }); 124 });
126 125
127 it('should bail if it cannot determine whether there is a config file', async () => { 126 it('should bail if it cannot determine whether there is a config file', async () => {
128 mocked(persistenceService.readConfig).mockRejectedValue(new Error('boo')); 127 mocked(persistenceService.readConfig).mockRejectedValue(new Error('boo'));
129 await expect(() => 128 await expect(() =>
130 initConfig(config, persistenceService), 129 initConfig(store, persistenceService),
131 ).rejects.toBeInstanceOf(Error); 130 ).rejects.toBeInstanceOf(Error);
132 }); 131 });
133}); 132});
@@ -140,10 +139,10 @@ describe('when it has loaded the config', () => {
140 beforeEach(async () => { 139 beforeEach(async () => {
141 mocked(persistenceService.readConfig).mockResolvedValueOnce({ 140 mocked(persistenceService.readConfig).mockResolvedValueOnce({
142 found: true, 141 found: true,
143 data: getSnapshot(config), 142 data: store.config,
144 }); 143 });
145 mocked(persistenceService.watchConfig).mockReturnValueOnce(watcherDisposer); 144 mocked(persistenceService.watchConfig).mockReturnValueOnce(watcherDisposer);
146 sutDisposer = await initConfig(config, persistenceService, throttleMs); 145 sutDisposer = await initConfig(store, persistenceService, throttleMs);
147 [[configChangedCallback]] = mocked( 146 [[configChangedCallback]] = mocked(
148 persistenceService.watchConfig, 147 persistenceService.watchConfig,
149 ).mock.calls; 148 ).mock.calls;
@@ -152,16 +151,16 @@ describe('when it has loaded the config', () => {
152 151
153 it('should throttle saving changes to the config file', () => { 152 it('should throttle saving changes to the config file', () => {
154 mocked(persistenceService.writeConfig).mockResolvedValue(); 153 mocked(persistenceService.writeConfig).mockResolvedValue();
155 config.setThemeSource('dark'); 154 store.settings.setThemeSource('dark');
156 jest.advanceTimersByTime(lessThanThrottleMs); 155 jest.advanceTimersByTime(lessThanThrottleMs);
157 config.setThemeSource('light'); 156 store.settings.setThemeSource('light');
158 jest.advanceTimersByTime(throttleMs); 157 jest.advanceTimersByTime(throttleMs);
159 expect(persistenceService.writeConfig).toHaveBeenCalledTimes(1); 158 expect(persistenceService.writeConfig).toHaveBeenCalledTimes(1);
160 }); 159 });
161 160
162 it('should handle config writing errors gracefully', () => { 161 it('should handle config writing errors gracefully', () => {
163 mocked(persistenceService.writeConfig).mockRejectedValue(new Error('boo')); 162 mocked(persistenceService.writeConfig).mockRejectedValue(new Error('boo'));
164 config.setThemeSource('dark'); 163 store.settings.setThemeSource('dark');
165 jest.advanceTimersByTime(throttleMs); 164 jest.advanceTimersByTime(throttleMs);
166 expect(persistenceService.writeConfig).toHaveBeenCalledTimes(1); 165 expect(persistenceService.writeConfig).toHaveBeenCalledTimes(1);
167 }); 166 });
@@ -171,14 +170,14 @@ describe('when it has loaded the config', () => {
171 found: true, 170 found: true,
172 data: { 171 data: {
173 // Use a default empty config file to not trigger config rewrite. 172 // Use a default empty config file to not trigger config rewrite.
174 ...getSnapshot(config), 173 ...store.config,
175 themeSource: 'dark', 174 themeSource: 'dark',
176 }, 175 },
177 }); 176 });
178 await configChangedCallback(); 177 await configChangedCallback();
179 // Do not write back the changes we have just read. 178 // Do not write back the changes we have just read.
180 expect(persistenceService.writeConfig).not.toHaveBeenCalled(); 179 expect(persistenceService.writeConfig).not.toHaveBeenCalled();
181 expect(config.themeSource).toBe('dark'); 180 expect(store.settings.themeSource).toBe('dark');
182 }); 181 });
183 182
184 it('should update the config file if new details are added', async () => { 183 it('should update the config file if new details are added', async () => {
@@ -203,7 +202,7 @@ describe('when it has loaded the config', () => {
203 }, 202 },
204 }); 203 });
205 await configChangedCallback(); 204 await configChangedCallback();
206 expect(config.themeSource).not.toBe(-1); 205 expect(store.settings.themeSource).not.toBe(-1);
207 expect(persistenceService.writeConfig).not.toHaveBeenCalled(); 206 expect(persistenceService.writeConfig).not.toHaveBeenCalled();
208 }); 207 });
209 208
@@ -222,7 +221,7 @@ describe('when it has loaded the config', () => {
222 }); 221 });
223 222
224 it('should not listen to store changes any more', () => { 223 it('should not listen to store changes any more', () => {
225 config.setThemeSource('dark'); 224 store.settings.setThemeSource('dark');
226 jest.advanceTimersByTime(2 * throttleMs); 225 jest.advanceTimersByTime(2 * throttleMs);
227 expect(persistenceService.writeConfig).not.toHaveBeenCalled(); 226 expect(persistenceService.writeConfig).not.toHaveBeenCalled();
228 }); 227 });
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 @@
21import { jest } from '@jest/globals'; 21import { jest } from '@jest/globals';
22import { mocked } from 'jest-mock'; 22import { mocked } from 'jest-mock';
23 23
24import { createMainStore, MainStore } from '../../stores/MainStore'; 24import { sharedStore, SharedStore } from '../../stores/SharedStore';
25import type Disposer from '../../utils/Disposer'; 25import type Disposer from '../../utils/Disposer';
26 26
27let shouldUseDarkColors = false; 27let shouldUseDarkColors = false;
@@ -40,11 +40,11 @@ jest.unstable_mockModule('electron', () => ({
40const { nativeTheme } = await import('electron'); 40const { nativeTheme } = await import('electron');
41const { default: initNativeTheme } = await import('../initNativeTheme'); 41const { default: initNativeTheme } = await import('../initNativeTheme');
42 42
43let store: MainStore; 43let store: SharedStore;
44let disposeSut: Disposer; 44let disposeSut: Disposer;
45 45
46beforeEach(() => { 46beforeEach(() => {
47 store = createMainStore(); 47 store = sharedStore.create();
48 disposeSut = initNativeTheme(store); 48 disposeSut = initNativeTheme(store);
49}); 49});
50 50
@@ -53,7 +53,7 @@ it('should register a nativeTheme updated listener', () => {
53}); 53});
54 54
55it('should synchronize themeSource changes to the nativeTheme', () => { 55it('should synchronize themeSource changes to the nativeTheme', () => {
56 store.config.setThemeSource('dark'); 56 store.settings.setThemeSource('dark');
57 expect(nativeTheme.themeSource).toBe('dark'); 57 expect(nativeTheme.themeSource).toBe('dark');
58}); 58});
59 59
@@ -63,7 +63,7 @@ it('should synchronize shouldUseDarkColors changes to the store', () => {
63 )![1]; 63 )![1];
64 shouldUseDarkColors = true; 64 shouldUseDarkColors = true;
65 listener(); 65 listener();
66 expect(store.shared.shouldUseDarkColors).toBe(true); 66 expect(store.shouldUseDarkColors).toBe(true);
67}); 67});
68 68
69it('should remove the listener on dispose', () => { 69it('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 @@
20 20
21import deepEqual from 'deep-equal'; 21import deepEqual from 'deep-equal';
22import { debounce } from 'lodash-es'; 22import { debounce } from 'lodash-es';
23import { getSnapshot, onSnapshot } from 'mobx-state-tree'; 23import { reaction } from 'mobx';
24import ms from 'ms'; 24import ms from 'ms';
25 25
26import type ConfigPersistence from '../infrastructure/ConfigPersistence'; 26import type ConfigPersistence from '../infrastructure/ConfigPersistence';
27import { Config, ConfigFileIn, ConfigSnapshotOut } from '../stores/Config'; 27import { Config, SharedStore } from '../stores/SharedStore';
28import type Disposer from '../utils/Disposer'; 28import type Disposer from '../utils/Disposer';
29import { getLogger } from '../utils/log'; 29import { getLogger } from '../utils/log';
30 30
@@ -33,18 +33,18 @@ const DEFAULT_CONFIG_DEBOUNCE_TIME = ms('1s');
33const log = getLogger('config'); 33const log = getLogger('config');
34 34
35export default async function initConfig( 35export default async function initConfig(
36 config: Config, 36 sharedStore: SharedStore,
37 persistenceService: ConfigPersistence, 37 persistenceService: ConfigPersistence,
38 debounceTime: number = DEFAULT_CONFIG_DEBOUNCE_TIME, 38 debounceTime: number = DEFAULT_CONFIG_DEBOUNCE_TIME,
39): Promise<Disposer> { 39): Promise<Disposer> {
40 log.trace('Initializing config controller'); 40 log.trace('Initializing config controller');
41 41
42 let lastSnapshotOnDisk: ConfigSnapshotOut | undefined; 42 let lastConfigOnDisk: Config | undefined;
43 43
44 async function writeConfig(): Promise<void> { 44 async function writeConfig(): Promise<void> {
45 const snapshot = getSnapshot(config); 45 const { config } = sharedStore;
46 await persistenceService.writeConfig(snapshot); 46 await persistenceService.writeConfig(config);
47 lastSnapshotOnDisk = snapshot; 47 lastConfigOnDisk = config;
48 } 48 }
49 49
50 async function readConfig(): Promise<boolean> { 50 async function readConfig(): Promise<boolean> {
@@ -53,13 +53,13 @@ export default async function initConfig(
53 try { 53 try {
54 // This cast is unsound if the config file is invalid, 54 // This cast is unsound if the config file is invalid,
55 // but we'll throw an error in the end anyways. 55 // but we'll throw an error in the end anyways.
56 config.loadFromConfigFile(result.data as ConfigFileIn); 56 sharedStore.loadConfig(result.data as Config);
57 } catch (error) { 57 } catch (error) {
58 log.error('Failed to apply config snapshot', result.data, error); 58 log.error('Failed to apply config snapshot', result.data, error);
59 return true; 59 return true;
60 } 60 }
61 lastSnapshotOnDisk = getSnapshot(config); 61 lastConfigOnDisk = sharedStore.config;
62 if (!deepEqual(result.data, lastSnapshotOnDisk, { strict: true })) { 62 if (!deepEqual(result.data, lastConfigOnDisk, { strict: true })) {
63 await writeConfig(); 63 await writeConfig();
64 } 64 }
65 } 65 }
@@ -72,11 +72,11 @@ export default async function initConfig(
72 log.info('Created config file'); 72 log.info('Created config file');
73 } 73 }
74 74
75 const disposeOnSnapshot = onSnapshot( 75 const disposeReaction = reaction(
76 config, 76 () => sharedStore.config,
77 debounce((snapshot) => { 77 debounce((config) => {
78 // We can compare snapshots by reference, since it is only recreated on store changes. 78 // We can compare snapshots by reference, since it is only recreated on store changes.
79 if (lastSnapshotOnDisk !== snapshot) { 79 if (lastConfigOnDisk !== config) {
80 writeConfig().catch((error) => { 80 writeConfig().catch((error) => {
81 log.error('Failed to write config on config change', error); 81 log.error('Failed to write config on config change', error);
82 }); 82 });
@@ -95,6 +95,6 @@ export default async function initConfig(
95 return () => { 95 return () => {
96 log.trace('Disposing config controller'); 96 log.trace('Disposing config controller');
97 disposeWatcher(); 97 disposeWatcher();
98 disposeOnSnapshot(); 98 disposeReaction();
99 }; 99 };
100} 100}
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 @@
21import { nativeTheme } from 'electron'; 21import { nativeTheme } from 'electron';
22import { autorun } from 'mobx'; 22import { autorun } from 'mobx';
23 23
24import type { MainStore } from '../stores/MainStore'; 24import type { SharedStore } from '../stores/SharedStore';
25import type Disposer from '../utils/Disposer'; 25import type Disposer from '../utils/Disposer';
26import { getLogger } from '../utils/log'; 26import { getLogger } from '../utils/log';
27 27
28const log = getLogger('nativeTheme'); 28const log = getLogger('nativeTheme');
29 29
30export default function initNativeTheme(store: MainStore): Disposer { 30export default function initNativeTheme(store: SharedStore): Disposer {
31 log.trace('Initializing nativeTheme controller'); 31 log.trace('Initializing nativeTheme controller');
32 32
33 const disposeThemeSourceReaction = autorun(() => { 33 const disposeThemeSourceReaction = autorun(() => {
34 nativeTheme.themeSource = store.config.themeSource; 34 nativeTheme.themeSource = store.settings.themeSource;
35 log.debug('Set theme source:', store.config.themeSource); 35 log.debug('Set theme source:', store.settings.themeSource);
36 }); 36 });
37 37
38 store.setShouldUseDarkColors(nativeTheme.shouldUseDarkColors); 38 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<unknown> {
270 const actionToDispatch = action.parse(rawAction); 270 const actionToDispatch = action.parse(rawAction);
271 switch (actionToDispatch.action) { 271 switch (actionToDispatch.action) {
272 case 'set-selected-service-id': 272 case 'set-selected-service-id':
273 store.setSelectedServiceId(actionToDispatch.serviceId); 273 store.shared.setSelectedServiceId(actionToDispatch.serviceId);
274 break; 274 break;
275 case 'set-browser-view-bounds': 275 case 'set-browser-view-bounds':
276 store.setBrowserViewBounds(actionToDispatch.browserViewBounds); 276 store.setBrowserViewBounds(actionToDispatch.browserViewBounds);
277 break; 277 break;
278 case 'set-theme-source': 278 case 'set-theme-source':
279 store.config.setThemeSource(actionToDispatch.themeSource); 279 store.settings.setThemeSource(actionToDispatch.themeSource);
280 break; 280 break;
281 case 'reload-all-services': 281 case 'reload-all-services':
282 reloadServiceInject().catch((error) => { 282 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 @@
18 * SPDX-License-Identifier: AGPL-3.0-only 18 * SPDX-License-Identifier: AGPL-3.0-only
19 */ 19 */
20 20
21import type { ConfigSnapshotOut } from '../stores/Config'; 21import type { Config } from '../stores/SharedStore';
22import type Disposer from '../utils/Disposer'; 22import type Disposer from '../utils/Disposer';
23 23
24export type ReadConfigResult = 24export type ReadConfigResult =
@@ -28,7 +28,7 @@ export type ReadConfigResult =
28export default interface ConfigPersistence { 28export default interface ConfigPersistence {
29 readConfig(): Promise<ReadConfigResult>; 29 readConfig(): Promise<ReadConfigResult>;
30 30
31 writeConfig(configSnapshot: ConfigSnapshotOut): Promise<void>; 31 writeConfig(config: Config): Promise<void>;
32 32
33 watchConfig(callback: () => Promise<void>, throttleMs: number): Disposer; 33 watchConfig(callback: () => Promise<void>, throttleMs: number): Disposer;
34} 34}
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';
24import JSON5 from 'json5'; 24import JSON5 from 'json5';
25import throttle from 'lodash-es/throttle'; 25import throttle from 'lodash-es/throttle';
26 26
27import type { ConfigSnapshotOut } from '../../stores/Config'; 27import type { Config } from '../../stores/SharedStore';
28import type Disposer from '../../utils/Disposer'; 28import type Disposer from '../../utils/Disposer';
29import { getLogger } from '../../utils/log'; 29import { getLogger } from '../../utils/log';
30import type ConfigPersistence from '../ConfigPersistence'; 30import type ConfigPersistence from '../ConfigPersistence';
@@ -65,7 +65,7 @@ export default class FileBasedConfigPersistence implements ConfigPersistence {
65 }; 65 };
66 } 66 }
67 67
68 async writeConfig(configSnapshot: ConfigSnapshotOut): Promise<void> { 68 async writeConfig(configSnapshot: Config): Promise<void> {
69 const configJson = JSON5.stringify(configSnapshot, { 69 const configJson = JSON5.stringify(configSnapshot, {
70 space: 2, 70 space: 2,
71 }); 71 });
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<Disposer> {
31 app.getPath('userData'), 31 app.getPath('userData'),
32 ); 32 );
33 const disposeConfigController = await initConfig( 33 const disposeConfigController = await initConfig(
34 store.config, 34 store.shared,
35 configPersistenceService, 35 configPersistenceService,
36 ); 36 );
37 const disposeNativeThemeController = initNativeTheme(store); 37 const disposeNativeThemeController = initNativeTheme(store.shared);
38 38
39 return () => { 39 return () => {
40 disposeNativeThemeController(); 40 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 @@
1/*
2 * Copyright (C) 2021-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
21import { config as originalConfig, ThemeSource } from '@sophie/shared';
22import { applySnapshot, Instance, SnapshotIn } from 'mobx-state-tree';
23
24import { addMissingProfileIds, PartialProfileSnapshotIn } from './Profile';
25import {
26 addMissingServiceIdsAndProfiles,
27 PartialServiceSnapshotIn,
28} from './Service';
29
30export const config = originalConfig.actions((self) => ({
31 loadFromConfigFile(snapshot: ConfigFileIn): void {
32 const profiles = addMissingProfileIds(snapshot.profiles);
33 const services = addMissingServiceIdsAndProfiles(
34 snapshot.services,
35 profiles,
36 );
37 applySnapshot(self, {
38 ...snapshot,
39 profiles,
40 services,
41 });
42 },
43 setThemeSource(mode: ThemeSource): void {
44 self.themeSource = mode;
45 },
46}));
47
48export interface Config extends Instance<typeof config> {}
49
50export interface ConfigSnapshotIn extends SnapshotIn<typeof config> {}
51
52export interface ConfigFileIn
53 extends Omit<ConfigSnapshotIn, 'profiles' | 'services'> {
54 profiles?: PartialProfileSnapshotIn[] | undefined;
55 services?: PartialServiceSnapshotIn[] | undefined;
56}
57
58export type { ConfigSnapshotOut } from '@sophie/shared';
diff --git a/packages/renderer/src/stores/Service.ts b/packages/main/src/stores/GlobalSettings.ts
index 2f45106..1eb13b3 100644
--- a/packages/renderer/src/stores/Service.ts
+++ b/packages/main/src/stores/GlobalSettings.ts
@@ -19,20 +19,20 @@
19 */ 19 */
20 20
21import { 21import {
22 runtimeService, 22 globalSettings as originalGlobalSettings,
23 RuntimeService, 23 ThemeSource,
24 service as originalService,
25} from '@sophie/shared'; 24} from '@sophie/shared';
26import { Instance } from 'mobx-state-tree'; 25import { Instance } from 'mobx-state-tree';
27 26
28import getEnv from '../env/getEnv'; 27export const globalSettings = originalGlobalSettings.actions((self) => ({
29 28 setThemeSource(mode: ThemeSource): void {
30export const service = originalService.views((self) => ({ 29 self.themeSource = mode;
31 get runtime(): RuntimeService {
32 return getEnv(self).getRuntimeService(self.id) ?? runtimeService.create();
33 }, 30 },
34})); 31}));
35 32
36export interface Service extends Instance<typeof service> {} 33export interface GlobalSettings extends Instance<typeof globalSettings> {}
37 34
38export type { ServiceSnapshotIn, ServiceSnapshotOut } from '@sophie/shared'; 35export type {
36 GlobalSettingsSnapshotIn,
37 GlobalSettingsSnapshotOut,
38} 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 @@
18 * SPDX-License-Identifier: AGPL-3.0-only 18 * SPDX-License-Identifier: AGPL-3.0-only
19 */ 19 */
20 20
21import { BrowserViewBounds, service } from '@sophie/shared'; 21import type { BrowserViewBounds } from '@sophie/shared';
22import { 22import { applySnapshot, Instance, types } from 'mobx-state-tree';
23 applySnapshot,
24 Instance,
25 resolveIdentifier,
26 types,
27} from 'mobx-state-tree';
28 23
29import { getLogger } from '../utils/log'; 24import { GlobalSettings } from './GlobalSettings';
30 25import { Profile } from './Profile';
31import type { Config } from './Config.js'; 26import { Service } from './Service';
32import { sharedStore } from './SharedStore'; 27import { sharedStore } from './SharedStore';
33 28
34const log = getLogger('mainStore');
35
36export const mainStore = types 29export const mainStore = types
37 .model('MainStore', { 30 .model('MainStore', {
38 browserViewBounds: types.optional( 31 browserViewBounds: types.optional(
@@ -47,26 +40,20 @@ export const mainStore = types
47 shared: types.optional(sharedStore, {}), 40 shared: types.optional(sharedStore, {}),
48 }) 41 })
49 .views((self) => ({ 42 .views((self) => ({
50 get config(): Config { 43 get settings(): GlobalSettings {
51 return self.shared.config; 44 return self.shared.settings;
45 },
46 get profiles(): Profile[] {
47 return self.shared.profiles;
48 },
49 get services(): Service[] {
50 return self.shared.services;
52 }, 51 },
53 })) 52 }))
54 .actions((self) => ({ 53 .actions((self) => ({
55 setSelectedServiceId(serviceId: string): void {
56 const serviceInstance = resolveIdentifier(service, self, serviceId);
57 if (serviceInstance === undefined) {
58 log.warn('Trying to select unknown service', serviceId);
59 return;
60 }
61 self.shared.selectedService = serviceInstance;
62 log.debug('Selected service', serviceId);
63 },
64 setBrowserViewBounds(bounds: BrowserViewBounds): void { 54 setBrowserViewBounds(bounds: BrowserViewBounds): void {
65 applySnapshot(self.browserViewBounds, bounds); 55 applySnapshot(self.browserViewBounds, bounds);
66 }, 56 },
67 setShouldUseDarkColors(shouldUseDarkColors: boolean): void {
68 self.shared.shouldUseDarkColors = shouldUseDarkColors;
69 },
70 })); 57 }));
71 58
72export interface MainStore extends Instance<typeof mainStore> {} 59export interface MainStore extends Instance<typeof mainStore> {}
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 @@
18 * SPDX-License-Identifier: AGPL-3.0-only 18 * SPDX-License-Identifier: AGPL-3.0-only
19 */ 19 */
20 20
21import type { ProfileSnapshotIn } from '@sophie/shared'; 21import {
22 profile as originalProfile,
23 ProfileSettingsSnapshotIn,
24} from '@sophie/shared';
25import { getSnapshot, Instance } from 'mobx-state-tree';
22 26
27import SettingsWithId from '../utils/SettingsWithId';
23import generateId from '../utils/generateId'; 28import generateId from '../utils/generateId';
24 29
25export interface PartialProfileSnapshotIn 30export interface ProfileConfig extends ProfileSettingsSnapshotIn {
26 extends Omit<ProfileSnapshotIn, 'id'> {
27 id?: string | undefined; 31 id?: string | undefined;
28} 32}
29 33
34export const profile = originalProfile.views((self) => ({
35 get config(): ProfileConfig {
36 const { id, settings } = self;
37 return { ...getSnapshot(settings), id };
38 },
39}));
40
41export interface Profile extends Instance<typeof profile> {}
42
43export type ProfileSettingsSnapshotWithId =
44 SettingsWithId<ProfileSettingsSnapshotIn>;
45
30export function addMissingProfileIds( 46export function addMissingProfileIds(
31 partialProfiles: PartialProfileSnapshotIn[] | undefined, 47 profileConfigs: ProfileConfig[] | undefined,
32): ProfileSnapshotIn[] { 48): ProfileSettingsSnapshotWithId[] {
33 return (partialProfiles ?? []).map((profile) => { 49 return (profileConfigs ?? []).map((profileConfig) => {
34 const { name } = profile; 50 const { id, ...settings } = profileConfig;
35 let { id } = profile;
36 if (typeof id === 'undefined') {
37 id = generateId(name);
38 }
39 return { 51 return {
40 ...profile, 52 id: typeof id === 'undefined' ? generateId(settings.name) : id,
41 id, 53 settings,
42 }; 54 };
43 }); 55 });
44} 56}
45
46export type {
47 Profile,
48 ProfileSnapshotOut,
49 ProfileSnapshotIn,
50} from '@sophie/shared';
51export { 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 @@
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
21import { UnreadCount } from '@sophie/service-shared';
22import { runtimeService as originalRuntimeService } from '@sophie/shared';
23import { Instance } from 'mobx-state-tree';
24
25export const runtimeService = originalRuntimeService.actions((self) => ({
26 setLocation({
27 url,
28 canGoBack,
29 canGoForward,
30 }: {
31 url: string;
32 canGoBack: boolean;
33 canGoForward: boolean;
34 }): void {
35 self.url = url;
36 self.canGoBack = canGoBack;
37 self.canGoForward = canGoForward;
38 },
39 setTitle(title: string): void {
40 self.title = title;
41 },
42 hibernated(): void {
43 self.canGoBack = false;
44 self.canGoForward = false;
45 self.state = 'hibernated';
46 },
47 startedLoading(): void {
48 self.state = 'loading';
49 },
50 finishedLoading(): void {
51 if (self.state === 'loading') {
52 // Do not overwrite crashed state if the service haven't been reloaded yet.
53 self.state = 'loaded';
54 }
55 },
56 crashed(): void {
57 self.state = 'crashed';
58 },
59 setUnreadCount({ direct, indirect }: UnreadCount): void {
60 if (direct !== undefined) {
61 self.directMessageCount = direct;
62 }
63 if (indirect !== undefined) {
64 self.indirectMessageCount = indirect;
65 }
66 },
67}));
68
69export interface RuntimeService extends Instance<typeof runtimeService> {}
70
71export type {
72 RuntimeServiceSnapshotIn,
73 RuntimeServiceSnapshotOut,
74} 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 @@
18 * SPDX-License-Identifier: AGPL-3.0-only 18 * SPDX-License-Identifier: AGPL-3.0-only
19 */ 19 */
20 20
21import type { ServiceSnapshotIn } from '@sophie/shared'; 21import type { UnreadCount } from '@sophie/service-shared';
22import {
23 service as originalService,
24 ServiceSettingsSnapshotIn,
25} from '@sophie/shared';
26import { Instance, getSnapshot, ReferenceIdentifier } from 'mobx-state-tree';
22 27
28import SettingsWithId from '../utils/SettingsWithId';
23import generateId from '../utils/generateId'; 29import generateId from '../utils/generateId';
30import overrideProps from '../utils/overrideProps';
24 31
25import type { ProfileSnapshotIn } from './Profile'; 32import { ProfileSettingsSnapshotWithId } from './Profile';
33import { serviceSettings } from './ServiceSettings';
26 34
27export interface PartialServiceSnapshotIn 35export interface ServiceConfig
28 extends Omit<ServiceSnapshotIn, 'id' | 'profile'> { 36 extends Omit<ServiceSettingsSnapshotIn, 'profile'> {
29 id?: string | undefined; 37 id?: string | undefined;
30 profile?: string | undefined; 38
39 profile?: ReferenceIdentifier | undefined;
31} 40}
32 41
42export const service = overrideProps(originalService, {
43 settings: serviceSettings,
44})
45 .views((self) => ({
46 get config(): ServiceConfig {
47 const { id, settings } = self;
48 return { ...getSnapshot(settings), id };
49 },
50 }))
51 .actions((self) => ({
52 setLocation({
53 url,
54 canGoBack,
55 canGoForward,
56 }: {
57 url: string;
58 canGoBack: boolean;
59 canGoForward: boolean;
60 }): void {
61 self.currentUrl = url;
62 self.canGoBack = canGoBack;
63 self.canGoForward = canGoForward;
64 },
65 setTitle(title: string): void {
66 self.title = title;
67 },
68 startedLoading(): void {
69 self.state = 'loading';
70 },
71 finishedLoading(): void {
72 if (self.state === 'loading') {
73 // Do not overwrite crashed state if the service haven't been reloaded yet.
74 self.state = 'loaded';
75 }
76 },
77 crashed(): void {
78 self.state = 'crashed';
79 },
80 setUnreadCount({ direct, indirect }: UnreadCount): void {
81 if (direct !== undefined) {
82 self.directMessageCount = direct;
83 }
84 if (indirect !== undefined) {
85 self.indirectMessageCount = indirect;
86 }
87 },
88 }));
89
90export interface Service extends Instance<typeof service> {}
91
92export type ServiceSettingsSnapshotWithId =
93 SettingsWithId<ServiceSettingsSnapshotIn>;
94
33export function addMissingServiceIdsAndProfiles( 95export function addMissingServiceIdsAndProfiles(
34 partialServices: PartialServiceSnapshotIn[] | undefined, 96 serviceConfigs: ServiceConfig[] | undefined,
35 profiles: ProfileSnapshotIn[], 97 profiles: ProfileSettingsSnapshotWithId[],
36): ServiceSnapshotIn[] { 98): ServiceSettingsSnapshotWithId[] {
37 return (partialServices ?? []).map((service) => { 99 return (serviceConfigs ?? []).map((serviceConfig) => {
38 const { name } = service; 100 const { id, ...settings } = serviceConfig;
39 let { id, profile } = service; 101 const { name } = settings;
40 if (typeof id === 'undefined') { 102 let { profile: profileId } = settings;
41 id = generateId(name); 103 if (profileId === undefined) {
42 } 104 profileId = generateId(name);
43 if (typeof profile === 'undefined') { 105 profiles.push({ id: profileId, settings: { name } });
44 profile = generateId(name);
45 profiles.push({
46 id: profile,
47 name: service.name,
48 });
49 } 106 }
50 return { 107 return {
51 ...service, 108 id: id === undefined ? generateId(name) : id,
52 id, 109 settings: { ...settings, profile: profileId },
53 profile,
54 }; 110 };
55 }); 111 });
56} 112}
57
58export type {
59 Service,
60 ServiceSnapshotOut,
61 ServiceSnapshotIn,
62} from '@sophie/shared';
63export { service } from '@sophie/shared';
diff --git a/packages/renderer/src/stores/SharedStore.ts b/packages/main/src/stores/ServiceSettings.ts
index 962f7e2..960de9b 100644
--- a/packages/renderer/src/stores/SharedStore.ts
+++ b/packages/main/src/stores/ServiceSettings.ts
@@ -18,20 +18,20 @@
18 * SPDX-License-Identifier: AGPL-3.0-only 18 * SPDX-License-Identifier: AGPL-3.0-only
19 */ 19 */
20 20
21import { sharedStore as originalSharedStore } from '@sophie/shared'; 21import { serviceSettings as originalServiceSettings } from '@sophie/shared';
22import { Instance, types } from 'mobx-state-tree'; 22import { Instance, types } from 'mobx-state-tree';
23 23
24import { config } from './Config'; 24import overrideProps from '../utils/overrideProps';
25import { service } from './Service';
26 25
27export const sharedStore = originalSharedStore.props({ 26import { profile } from './Profile';
28 config: types.optional(config, {}), 27
29 selectedService: types.safeReference(service), 28export const serviceSettings = overrideProps(originalServiceSettings, {
29 profile: types.reference(profile),
30}); 30});
31 31
32export interface SharedStore extends Instance<typeof sharedStore> {} 32export interface ServiceSettings extends Instance<typeof serviceSettings> {}
33 33
34export type { 34export type {
35 SharedStoreSnapshotIn, 35 ServiceSettingsSnapshotIn,
36 SharedStoreSnapshotOut, 36 ServiceSettingsSnapshotOut,
37} from '@sophie/shared'; 37} 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 @@
19 */ 19 */
20 20
21import { sharedStore as originalSharedStore } from '@sophie/shared'; 21import { sharedStore as originalSharedStore } from '@sophie/shared';
22import { Instance, types } from 'mobx-state-tree'; 22import {
23 applySnapshot,
24 getSnapshot,
25 Instance,
26 resolveIdentifier,
27 types,
28} from 'mobx-state-tree';
23 29
24import { config } from './Config'; 30import SettingsWithId from '../utils/SettingsWithId';
25import { runtimeService } from './RuntimeService'; 31import { getLogger } from '../utils/log';
32import overrideProps from '../utils/overrideProps';
33
34import { globalSettings, GlobalSettingsSnapshotIn } from './GlobalSettings';
35import { addMissingProfileIds, profile, ProfileConfig } from './Profile';
36import {
37 addMissingServiceIdsAndProfiles,
38 service,
39 ServiceConfig,
40} from './Service';
41
42const log = getLogger('sharedStore');
43
44export interface Config extends GlobalSettingsSnapshotIn {
45 profiles?: ProfileConfig[] | undefined;
46
47 services?: ServiceConfig[] | undefined;
48}
49
50function getConfigs<T>(models: { config: T }[]): T[] | undefined {
51 return models.length === 0 ? undefined : models.map((model) => model.config);
52}
53
54function reconcileSettings<T>(
55 originalSnapshots: SettingsWithId<T>[],
56 settingsSnapshotsWithId: SettingsWithId<T>[],
57): SettingsWithId<T>[] {
58 const idToOriginalSnapshots = new Map(
59 originalSnapshots.map((originalSnapshot) => [
60 originalSnapshot.id,
61 originalSnapshot,
62 ]),
63 );
64 return settingsSnapshotsWithId.map(({ id, settings }) => ({
65 ...idToOriginalSnapshots.get(id),
66 id,
67 settings,
68 }));
69}
70
71export const sharedStore = overrideProps(originalSharedStore, {
72 settings: types.optional(globalSettings, {}),
73 profiles: types.array(profile),
74 services: types.array(service),
75 selectedService: types.safeReference(service),
76})
77 .views((self) => ({
78 get config(): Config {
79 const { settings, profiles, services } = self;
80 const globalSettingsConfig = getSnapshot(settings);
81 return {
82 ...globalSettingsConfig,
83 profiles: getConfigs(profiles),
84 services: getConfigs(services),
85 };
86 },
87 }))
88 .actions((self) => ({
89 loadConfig(config: Config): void {
90 const snapshot = getSnapshot(self);
91 const { profiles, services, ...settings } = config;
92 const profileSettingsSnapshots = addMissingProfileIds(profiles);
93 const serviceSettingsSnapshots = addMissingServiceIdsAndProfiles(
94 services,
95 profileSettingsSnapshots,
96 );
97 applySnapshot(self, {
98 ...snapshot,
99 settings,
100 profiles: reconcileSettings(
101 snapshot.profiles,
102 profileSettingsSnapshots,
103 ),
104 services: reconcileSettings(
105 snapshot.services,
106 serviceSettingsSnapshots,
107 ),
108 });
109 },
110 setShouldUseDarkColors(shouldUseDarkColors: boolean): void {
111 self.shouldUseDarkColors = shouldUseDarkColors;
112 },
113 setSelectedServiceId(serviceId: string): void {
114 const serviceInstance = resolveIdentifier(service, self, serviceId);
115 if (serviceInstance === undefined) {
116 log.warn('Trying to select unknown service', serviceId);
117 return;
118 }
119 self.selectedService = serviceInstance;
120 log.debug('Selected service', serviceId);
121 },
122 }));
123
124export interface SharedStore extends Instance<typeof sharedStore> {}
26 125
27export type { 126export type {
28 SharedStoreSnapshotIn, 127 SharedStoreSnapshotIn,
29 SharedStoreSnapshotOut, 128 SharedStoreSnapshotOut,
30} from '@sophie/shared'; 129} from '@sophie/shared';
31
32export const sharedStore = originalSharedStore.props({
33 config: types.optional(config, {}),
34 runtimeServices: types.map(runtimeService),
35});
36
37export interface SharedStore extends Instance<typeof sharedStore> {}
diff --git a/packages/main/src/stores/__tests__/Config.spec.ts b/packages/main/src/stores/__tests__/SharedStore.spec.ts
index 22ccbc7..3ea187c 100644
--- a/packages/main/src/stores/__tests__/Config.spec.ts
+++ b/packages/main/src/stores/__tests__/SharedStore.spec.ts
@@ -18,28 +18,28 @@
18 * SPDX-License-Identifier: AGPL-3.0-only 18 * SPDX-License-Identifier: AGPL-3.0-only
19 */ 19 */
20 20
21import { config, Config, ConfigFileIn } from '../Config'; 21import type { ProfileConfig } from '../Profile';
22import type { PartialProfileSnapshotIn } from '../Profile'; 22import type { ServiceConfig } from '../Service';
23import type { PartialServiceSnapshotIn } from '../Service'; 23import { Config, sharedStore, SharedStore } from '../SharedStore';
24 24
25const profileProps: PartialProfileSnapshotIn = { 25const profileProps: ProfileConfig = {
26 name: 'Test profile', 26 name: 'Test profile',
27}; 27};
28 28
29const serviceProps: PartialServiceSnapshotIn = { 29const serviceProps: ServiceConfig = {
30 name: 'Test service', 30 name: 'Test service',
31 url: 'https://example.com', 31 url: 'https://example.com',
32}; 32};
33 33
34let sut: Config; 34let sut: SharedStore;
35 35
36beforeEach(() => { 36beforeEach(() => {
37 sut = config.create(); 37 sut = sharedStore.create();
38}); 38});
39 39
40describe('preprocessConfigFile', () => { 40describe('loadConfig', () => {
41 it('should load profiles with an ID', () => { 41 it('should load profiles with an ID', () => {
42 sut.loadFromConfigFile({ 42 sut.loadConfig({
43 profiles: [ 43 profiles: [
44 { 44 {
45 id: 'someId', 45 id: 'someId',
@@ -51,14 +51,14 @@ describe('preprocessConfigFile', () => {
51 }); 51 });
52 52
53 it('should generate an ID for profiles without and ID', () => { 53 it('should generate an ID for profiles without and ID', () => {
54 sut.loadFromConfigFile({ 54 sut.loadConfig({
55 profiles: [profileProps], 55 profiles: [profileProps],
56 }); 56 });
57 expect(sut.profiles[0].id).toBeDefined(); 57 expect(sut.profiles[0].id).toBeDefined();
58 }); 58 });
59 59
60 it('should load services with an ID and a profile', () => { 60 it('should load services with an ID and a profile', () => {
61 sut.loadFromConfigFile({ 61 sut.loadConfig({
62 profiles: [ 62 profiles: [
63 { 63 {
64 id: 'someProfileId', 64 id: 'someProfileId',
@@ -74,12 +74,12 @@ describe('preprocessConfigFile', () => {
74 ], 74 ],
75 }); 75 });
76 expect(sut.services[0].id).toBe('someServiceId'); 76 expect(sut.services[0].id).toBe('someServiceId');
77 expect(sut.services[0].profile).toBe(sut.profiles[0]); 77 expect(sut.services[0].settings.profile).toBe(sut.profiles[0]);
78 }); 78 });
79 79
80 it('should refuse to load a profile without a name', () => { 80 it('should refuse to load a profile without a name', () => {
81 expect(() => { 81 expect(() => {
82 sut.loadFromConfigFile({ 82 sut.loadConfig({
83 profiles: [ 83 profiles: [
84 { 84 {
85 id: 'someProfileId', 85 id: 'someProfileId',
@@ -87,13 +87,13 @@ describe('preprocessConfigFile', () => {
87 name: undefined, 87 name: undefined,
88 }, 88 },
89 ], 89 ],
90 } as unknown as ConfigFileIn); 90 } as unknown as Config);
91 }).toThrow(); 91 }).toThrow();
92 expect(sut.profiles).toHaveLength(0); 92 expect(sut.profiles).toHaveLength(0);
93 }); 93 });
94 94
95 it('should load services without an ID but with a profile', () => { 95 it('should load services without an ID but with a profile', () => {
96 sut.loadFromConfigFile({ 96 sut.loadConfig({
97 profiles: [ 97 profiles: [
98 { 98 {
99 id: 'someProfileId', 99 id: 'someProfileId',
@@ -108,11 +108,11 @@ describe('preprocessConfigFile', () => {
108 ], 108 ],
109 }); 109 });
110 expect(sut.services[0].id).toBeDefined(); 110 expect(sut.services[0].id).toBeDefined();
111 expect(sut.services[0].profile).toBe(sut.profiles[0]); 111 expect(sut.services[0].settings.profile).toBe(sut.profiles[0]);
112 }); 112 });
113 113
114 it('should create a profile for a service with an ID but no profile', () => { 114 it('should create a profile for a service with an ID but no profile', () => {
115 sut.loadFromConfigFile({ 115 sut.loadConfig({
116 services: [ 116 services: [
117 { 117 {
118 id: 'someServiceId', 118 id: 'someServiceId',
@@ -121,12 +121,14 @@ describe('preprocessConfigFile', () => {
121 ], 121 ],
122 }); 122 });
123 expect(sut.services[0].id).toBe('someServiceId'); 123 expect(sut.services[0].id).toBe('someServiceId');
124 expect(sut.services[0].profile).toBeDefined(); 124 expect(sut.services[0].settings.profile).toBeDefined();
125 expect(sut.services[0].profile.name).toBe(serviceProps.name); 125 expect(sut.services[0].settings.profile.settings.name).toBe(
126 serviceProps.name,
127 );
126 }); 128 });
127 129
128 it('should create a profile for a service without an ID or profile', () => { 130 it('should create a profile for a service without an ID or profile', () => {
129 sut.loadFromConfigFile({ 131 sut.loadConfig({
130 services: [ 132 services: [
131 { 133 {
132 ...serviceProps, 134 ...serviceProps,
@@ -134,13 +136,15 @@ describe('preprocessConfigFile', () => {
134 ], 136 ],
135 }); 137 });
136 expect(sut.services[0].id).toBeDefined(); 138 expect(sut.services[0].id).toBeDefined();
137 expect(sut.services[0].profile).toBeDefined(); 139 expect(sut.services[0].settings.profile).toBeDefined();
138 expect(sut.services[0].profile.name).toBe(serviceProps.name); 140 expect(sut.services[0].settings.profile.settings.name).toBe(
141 serviceProps.name,
142 );
139 }); 143 });
140 144
141 it('should refuse to load a service without a name', () => { 145 it('should refuse to load a service without a name', () => {
142 expect(() => { 146 expect(() => {
143 sut.loadFromConfigFile({ 147 sut.loadConfig({
144 services: [ 148 services: [
145 { 149 {
146 id: 'someServiceId', 150 id: 'someServiceId',
@@ -148,7 +152,7 @@ describe('preprocessConfigFile', () => {
148 name: undefined, 152 name: undefined,
149 }, 153 },
150 ], 154 ],
151 } as unknown as ConfigFileIn); 155 } as unknown as Config);
152 }).toThrow(); 156 }).toThrow();
153 expect(sut.profiles).toHaveLength(0); 157 expect(sut.profiles).toHaveLength(0);
154 expect(sut.services).toHaveLength(0); 158 expect(sut.services).toHaveLength(0);
diff --git a/packages/renderer/src/stores/Config.ts b/packages/main/src/utils/SettingsWithId.ts
index 070c4ec..fde3e86 100644
--- a/packages/renderer/src/stores/Config.ts
+++ b/packages/main/src/utils/SettingsWithId.ts
@@ -18,15 +18,8 @@
18 * SPDX-License-Identifier: AGPL-3.0-only 18 * SPDX-License-Identifier: AGPL-3.0-only
19 */ 19 */
20 20
21import { config as originalConfig } from '@sophie/shared'; 21export default interface SettingsWithId<T> {
22import { Instance, types } from 'mobx-state-tree'; 22 id: string;
23 23
24import { service } from './Service'; 24 settings: T;
25 25}
26export const config = originalConfig.props({
27 services: types.array(service),
28});
29
30export interface Config extends Instance<typeof config> {}
31
32export type { ConfigSnapshotIn, ConfigSnapshotOut } from '@sophie/shared';
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 @@
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/**
22 * @file This file implements a technique to force-override properties of a model.
23 *
24 * The overridden properties must conform to the SnapshotIt and SnapshotOut format
25 * of the original model. Essentially, this means that only views and actions can
26 * be added safely.
27 *
28 * @see https://github.com/mobxjs/mobx-state-tree/issues/1403#issuecomment-940843087
29 */
30
31import {
32 IAnyModelType,
33 IModelType,
34 ModelProperties,
35 SnapshotIn,
36 SnapshotOut,
37} from 'mobx-state-tree';
38
39export type IUnsafeOverriddenModelType<
40 BASE extends IAnyModelType,
41 PROPS extends ModelProperties,
42> = BASE extends IModelType<infer P, infer O, infer CC, infer CS>
43 ? IModelType<Omit<P, keyof PROPS> & PROPS, O, CC, CS>
44 : never;
45
46export type IOverriddenModelType<
47 BASE extends IAnyModelType,
48 PROPS extends ModelProperties,
49> = SnapshotIn<BASE> extends SnapshotIn<IUnsafeOverriddenModelType<BASE, PROPS>>
50 ? SnapshotOut<
51 IUnsafeOverriddenModelType<BASE, PROPS>
52 > extends SnapshotOut<BASE>
53 ? IUnsafeOverriddenModelType<BASE, PROPS>
54 : never
55 : never;
56
57export default function overrideProps<
58 BASE extends IAnyModelType,
59 PROPS extends ModelProperties,
60>(base: BASE, props: PROPS): IOverriddenModelType<BASE, PROPS> {
61 return base.props(props) as IOverriddenModelType<BASE, PROPS>;
62}
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(() => {
78 <ServiceSwitcherTab 78 <ServiceSwitcherTab
79 key={service.id} 79 key={service.id}
80 value={service.id} 80 value={service.id}
81 icon={<ServiceIcon name={service.name} />} 81 icon={<ServiceIcon name={service.settings.name} />}
82 aria-label={service.name} 82 aria-label={service.settings.name}
83 /> 83 />
84 ))} 84 ))}
85 </ServiceSwitcherRoot> 85 </ServiceSwitcherRoot>
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 @@
18 * SPDX-License-Identifier: AGPL-3.0-only 18 * SPDX-License-Identifier: AGPL-3.0-only
19 */ 19 */
20 20
21import type { Action, RuntimeService } from '@sophie/shared'; 21import type { Action } from '@sophie/shared';
22 22
23export default interface RendererEnv { 23export default interface RendererEnv {
24 dispatchMainAction(action: Action): void; 24 dispatchMainAction(action: Action): void;
25
26 getRuntimeService(serviceId: string): RuntimeService | undefined;
27} 25}
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 @@
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
21import {
22 Action,
23 runtimeService,
24 RuntimeService,
25 SophieRenderer,
26} from '@sophie/shared';
27import type { IMSTMap } from 'mobx-state-tree';
28
29import type { RendererStore } from '../../stores/RendererStore';
30import type RendererEnv from '../RendererEnv';
31
32export default class RendererEnvImpl implements RendererEnv {
33 readonly #ipc: SophieRenderer;
34
35 #runtimeServices: IMSTMap<typeof runtimeService> | undefined;
36
37 constructor(ipc: SophieRenderer) {
38 this.#ipc = ipc;
39 }
40
41 setStore(store: RendererStore): void {
42 this.#runtimeServices = store.shared.runtimeServices;
43 }
44
45 dispatchMainAction(action: Action): void {
46 this.#ipc.dispatchAction(action);
47 }
48
49 getRuntimeService(serviceId: string): RuntimeService | undefined {
50 if (this.#runtimeServices === undefined) {
51 throw new Error('runtime services map is not yet set');
52 }
53 return this.#runtimeServices.get(serviceId);
54 }
55}
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 @@
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
21import { jest } from '@jest/globals';
22import {
23 Action,
24 runtimeService,
25 RuntimeService,
26 SophieRenderer,
27} from '@sophie/shared';
28
29import { rendererStore } from '../../../stores/RendererStore';
30import RendererEnvImpl from '../RendererEnvImpl';
31
32const ipc: SophieRenderer = {
33 dispatchAction: jest.fn(),
34 onSharedStoreChange: jest.fn(),
35};
36let sut: RendererEnvImpl;
37
38beforeEach(() => {
39 sut = new RendererEnvImpl(ipc);
40});
41
42describe('dispatchMainAction', () => {
43 it('should dispatch actions via the IPC', () => {
44 const action: Action = {
45 action: 'set-theme-source',
46 themeSource: 'dark',
47 };
48 sut.dispatchMainAction(action);
49 expect(ipc.dispatchAction).toHaveBeenCalledWith(action);
50 });
51});
52
53describe('getRuntimeService', () => {
54 describe('when no store was set', () => {
55 it('should throw an error', () => {
56 expect(() => sut.getRuntimeService('someId')).toThrow();
57 });
58 });
59
60 describe('when the store was set', () => {
61 let runtimeServiceStore: RuntimeService;
62
63 beforeEach(() => {
64 runtimeServiceStore = runtimeService.create({
65 state: 'loaded',
66 });
67 const store = rendererStore.create({
68 shared: {
69 runtimeServices: {
70 someId: runtimeServiceStore,
71 },
72 },
73 });
74 sut.setStore(store);
75 });
76
77 it('should return the runtime service for the given ID', () => {
78 const returnedStore = sut.getRuntimeService('someId');
79 expect(returnedStore).toBe(runtimeServiceStore);
80 });
81
82 it('should return undefined for an unknown ID', () => {
83 const returnedStore = sut.getRuntimeService('unknownId');
84 expect(returnedStore).toBeUndefined();
85 });
86 });
87});
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 @@
18 * SPDX-License-Identifier: AGPL-3.0-only 18 * SPDX-License-Identifier: AGPL-3.0-only
19 */ 19 */
20 20
21import { BrowserViewBounds, SophieRenderer, ThemeSource } from '@sophie/shared'; 21import {
22 BrowserViewBounds,
23 sharedStore,
24 Service,
25 SophieRenderer,
26 ThemeSource,
27} from '@sophie/shared';
22import { applySnapshot, applyPatch, Instance, types } from 'mobx-state-tree'; 28import { applySnapshot, applyPatch, Instance, types } from 'mobx-state-tree';
23 29
30import RendererEnv from '../env/RendererEnv';
24import getEnv from '../env/getEnv'; 31import getEnv from '../env/getEnv';
25import RendererEnvImpl from '../env/impl/RendererEnvImpl';
26import { getLogger } from '../utils/log'; 32import { getLogger } from '../utils/log';
27 33
28import type { Config } from './Config';
29import type { Service } from './Service';
30import { sharedStore } from './SharedStore';
31
32const log = getLogger('RendererStore'); 34const log = getLogger('RendererStore');
33 35
34export const rendererStore = types 36export const rendererStore = types
@@ -36,11 +38,8 @@ export const rendererStore = types
36 shared: types.optional(sharedStore, {}), 38 shared: types.optional(sharedStore, {}),
37 }) 39 })
38 .views((self) => ({ 40 .views((self) => ({
39 get config(): Config {
40 return self.shared.config;
41 },
42 get services(): Service[] { 41 get services(): Service[] {
43 return this.config.services; 42 return self.shared.services;
44 }, 43 },
45 get selectedService(): Service | undefined { 44 get selectedService(): Service | undefined {
46 return self.shared.selectedService; 45 return self.shared.selectedService;
@@ -87,9 +86,10 @@ export interface RendererStore extends Instance<typeof rendererStore> {}
87export function createAndConnectRendererStore( 86export function createAndConnectRendererStore(
88 ipc: SophieRenderer, 87 ipc: SophieRenderer,
89): RendererStore { 88): RendererStore {
90 const env = new RendererEnvImpl(ipc); 89 const env: RendererEnv = {
90 dispatchMainAction: ipc.dispatchAction,
91 };
91 const store = rendererStore.create({}, env); 92 const store = rendererStore.create({}, env);
92 env.setStore(store);
93 93
94 ipc 94 ipc
95 .onSharedStoreChange({ 95 .onSharedStoreChange({
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 @@
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
21import { jest } from '@jest/globals';
22import { runtimeService } from '@sophie/shared';
23import { mocked } from 'jest-mock';
24
25import type RendererEnv from '../../env/RendererEnv';
26import { service, Service } from '../Service';
27
28const env: RendererEnv = {
29 dispatchMainAction: jest.fn(),
30 getRuntimeService: jest.fn(),
31};
32let sut: Service;
33
34beforeEach(() => {
35 sut = service.create(
36 {
37 id: 'serviceId',
38 name: 'Foo',
39 url: 'https://example.com',
40 profile: 'profileId',
41 },
42 env,
43 );
44});
45
46describe('runtime', () => {
47 it('should return the runtime service with for the service ID', () => {
48 const runtimeServiceStore = runtimeService.create({}, env);
49 mocked(env.getRuntimeService).mockReturnValueOnce(runtimeServiceStore);
50 const returnedStore = sut.runtime;
51 expect(env.getRuntimeService).toHaveBeenCalledWith('serviceId');
52 expect(returnedStore).toBe(runtimeServiceStore);
53 });
54
55 it('should return a valid runtime service even if none exists in the environment', () => {
56 /*
57 eslint-disable-next-line unicorn/no-useless-undefined --
58 `mockReturnValueOnce` expects 1 parameter.
59 */
60 mocked(env.getRuntimeService).mockReturnValueOnce(undefined);
61 expect(sut.runtime).toHaveProperty('state', 'hibernated');
62 });
63});
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 @@
21import { Action } from '../schemas'; 21import { Action } from '../schemas';
22import { SharedStoreListener } from '../stores/SharedStore'; 22import { SharedStoreListener } from '../stores/SharedStore';
23 23
24export interface SophieRenderer { 24export default interface SophieRenderer {
25 onSharedStoreChange(this: void, listener: SharedStoreListener): Promise<void>; 25 onSharedStoreChange(this: void, listener: SharedStoreListener): Promise<void>;
26 26
27 dispatchAction(this: void, action: Action): void; 27 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 @@
18 * SPDX-License-Identifier: AGPL-3.0-only 18 * SPDX-License-Identifier: AGPL-3.0-only
19 */ 19 */
20 20
21export type { SophieRenderer } from './contextBridge/SophieRenderer'; 21export type { default as SophieRenderer } from './contextBridge/SophieRenderer';
22 22
23export { MainToRendererIpcMessage, RendererToMainIpcMessage } from './ipc'; 23export { MainToRendererIpcMessage, RendererToMainIpcMessage } from './ipc';
24 24
@@ -26,34 +26,33 @@ export type { Action, BrowserViewBounds, ThemeSource } from './schemas';
26export { action, browserViewBounds, themeSource } from './schemas'; 26export { action, browserViewBounds, themeSource } from './schemas';
27 27
28export type { 28export type {
29 Config, 29 GlobalSettings,
30 ConfigSnapshotIn, 30 GlobalSettingsSnapshotIn,
31 ConfigSnapshotOut, 31 GlobalSettingsSnapshotOut,
32} from './stores/Config'; 32} from './stores/GlobalSettings';
33export { config } from './stores/Config'; 33export { globalSettings } from './stores/GlobalSettings';
34 34
35export type { 35export type { Profile } from './stores/Profile';
36 Profile,
37 ProfileSnapshotIn,
38 ProfileSnapshotOut,
39} from './stores/Profile';
40export { profile } from './stores/Profile'; 36export { profile } from './stores/Profile';
41 37
42export type { 38export type {
43 RuntimeService, 39 ProfileSettings,
44 RuntimeServiceSnapshotIn, 40 ProfileSettingsSnapshotIn,
45 RuntimeServiceSnapshotOut, 41 ProfileSettingsSnapshotOut,
46} from './stores/RuntimeService'; 42} from './stores/ProfileSettings';
47export { runtimeService } from './stores/RuntimeService'; 43export { profileSettings } from './stores/ProfileSettings';
48 44
49export type { 45export type { Service } from './stores/Service';
50 Service,
51 ServiceSnapshotIn,
52 ServiceSnapshotOut,
53} from './stores/Service';
54export { service } from './stores/Service'; 46export { service } from './stores/Service';
55 47
56export type { 48export type {
49 ServiceSettings,
50 ServiceSettingsSnapshotIn,
51 ServiceSettingsSnapshotOut,
52} from './stores/ServiceSettings';
53export { serviceSettings } from './stores/ServiceSettings';
54
55export type {
57 SharedStore, 56 SharedStore,
58 SharedStoreListener, 57 SharedStoreListener,
59 SharedStoreSnapshotIn, 58 SharedStoreSnapshotIn,
diff --git a/packages/shared/src/stores/Config.ts b/packages/shared/src/stores/GlobalSettings.ts
index 1d97e7d..bd0155a 100644
--- a/packages/shared/src/stores/Config.ts
+++ b/packages/shared/src/stores/GlobalSettings.ts
@@ -1,5 +1,5 @@
1/* 1/*
2 * Copyright (C) 2021-2022 Kristóf Marussy <kristof@marussy.com> 2 * Copyright (C) 2022 Kristóf Marussy <kristof@marussy.com>
3 * 3 *
4 * This file is part of Sophie. 4 * This file is part of Sophie.
5 * 5 *
@@ -22,17 +22,14 @@ import { Instance, types, SnapshotIn, SnapshotOut } from 'mobx-state-tree';
22 22
23import { themeSource } from '../schemas'; 23import { themeSource } from '../schemas';
24 24
25import { profile } from './Profile'; 25export const globalSettings = types.model('GlobalSettings', {
26import { service } from './Service';
27
28export const config = types.model('Config', {
29 profiles: types.array(profile),
30 services: types.array(service),
31 themeSource: types.optional(types.enumeration(themeSource.options), 'system'), 26 themeSource: types.optional(types.enumeration(themeSource.options), 'system'),
32}); 27});
33 28
34export interface Config extends Instance<typeof config> {} 29export interface GlobalSettings extends Instance<typeof globalSettings> {}
35 30
36export interface ConfigSnapshotIn extends SnapshotIn<typeof config> {} 31export interface GlobalSettingsSnapshotIn
32 extends SnapshotIn<typeof globalSettings> {}
37 33
38export interface ConfigSnapshotOut extends SnapshotOut<typeof config> {} 34export interface GlobalSettingsSnapshotOut
35 extends SnapshotOut<typeof globalSettings> {}
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 @@
18 * SPDX-License-Identifier: AGPL-3.0-only 18 * SPDX-License-Identifier: AGPL-3.0-only
19 */ 19 */
20 20
21import { Instance, types, SnapshotIn, SnapshotOut } from 'mobx-state-tree'; 21import { Instance, types } from 'mobx-state-tree';
22
23import { profileSettings } from './ProfileSettings';
22 24
23export const profile = types.model('Profile', { 25export const profile = types.model('Profile', {
24 id: types.identifier, 26 id: types.identifier,
25 name: types.string, 27 settings: profileSettings,
26}); 28});
27 29
28export interface Profile extends Instance<typeof profile> {} 30export interface Profile extends Instance<typeof profile> {}
29
30export interface ProfileSnapshotIn extends SnapshotIn<typeof profile> {}
31
32export interface ProfileSnapshotOut extends SnapshotOut<typeof profile> {}
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 @@
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
21import { Instance, types, SnapshotIn, SnapshotOut } from 'mobx-state-tree';
22
23export const profileSettings = types.model('ProfileSettings', {
24 name: types.string,
25});
26
27export interface ProfileSettings extends Instance<typeof profileSettings> {}
28
29export interface ProfileSettingsSnapshotIn
30 extends SnapshotIn<typeof profileSettings> {}
31
32export interface ProfileSettingsSnapshotOut
33 extends SnapshotOut<typeof profileSettings> {}
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 @@
18 * SPDX-License-Identifier: AGPL-3.0-only 18 * SPDX-License-Identifier: AGPL-3.0-only
19 */ 19 */
20 20
21import { Instance, types, SnapshotIn, SnapshotOut } from 'mobx-state-tree'; 21import { Instance, types } from 'mobx-state-tree';
22 22
23import { profile } from './Profile'; 23import { serviceSettings } from './ServiceSettings';
24 24
25export const service = types.model('Service', { 25export const service = types.model('Service', {
26 id: types.identifier, 26 id: types.identifier,
27 name: types.string, 27 settings: serviceSettings,
28 profile: types.reference(profile), 28 currentUrl: types.maybe(types.string),
29 // TODO: Remove this once recipes are added. 29 canGoBack: false,
30 url: types.string, 30 canGoForward: false,
31 title: types.maybe(types.string),
32 state: types.optional(
33 types.enumeration('ServiceState', ['loading', 'loaded', 'crashed']),
34 'loading',
35 ),
36 directMessageCount: 0,
37 indirectMessageCount: 0,
31}); 38});
32 39
33export interface Service extends Instance<typeof service> {} 40export interface Service extends Instance<typeof service> {}
34
35export interface ServiceSnapshotIn extends SnapshotIn<typeof service> {}
36
37export interface ServiceSnapshotOut extends SnapshotOut<typeof service> {}
diff --git a/packages/shared/src/stores/RuntimeService.ts b/packages/shared/src/stores/ServiceSettings.ts
index c5b9031..54cd7eb 100644
--- a/packages/shared/src/stores/RuntimeService.ts
+++ b/packages/shared/src/stores/ServiceSettings.ts
@@ -20,28 +20,19 @@
20 20
21import { Instance, types, SnapshotIn, SnapshotOut } from 'mobx-state-tree'; 21import { Instance, types, SnapshotIn, SnapshotOut } from 'mobx-state-tree';
22 22
23export const runtimeService = types.model({ 23import { profile } from './Profile';
24 url: types.maybe(types.string), 24
25 canGoBack: false, 25export const serviceSettings = types.model('ServiceSettings', {
26 canGoForward: false, 26 name: types.string,
27 title: types.maybe(types.string), 27 profile: types.reference(profile),
28 state: types.optional( 28 // TODO: Remove this once recipes are added.
29 types.enumeration('ServiceState', [ 29 url: types.string,
30 'hibernated',
31 'loading',
32 'loaded',
33 'crashed',
34 ]),
35 'hibernated',
36 ),
37 directMessageCount: 0,
38 indirectMessageCount: 0,
39}); 30});
40 31
41export interface RuntimeService extends Instance<typeof runtimeService> {} 32export interface ServiceSettings extends Instance<typeof serviceSettings> {}
42 33
43export interface RuntimeServiceSnapshotIn 34export interface ServiceSettingsSnapshotIn
44 extends SnapshotIn<typeof runtimeService> {} 35 extends SnapshotIn<typeof serviceSettings> {}
45 36
46export interface RuntimeServiceSnapshotOut 37export interface ServiceSettingsSnapshotOut
47 extends SnapshotOut<typeof runtimeService> {} 38 extends SnapshotOut<typeof serviceSettings> {}
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 {
26 SnapshotOut, 26 SnapshotOut,
27} from 'mobx-state-tree'; 27} from 'mobx-state-tree';
28 28
29import { config } from './Config'; 29import { globalSettings } from './GlobalSettings';
30import { runtimeService } from './RuntimeService'; 30import { profile } from './Profile';
31import { service } from './Service'; 31import { service } from './Service';
32 32
33export const sharedStore = types.model('SharedStore', { 33export const sharedStore = types.model('SharedStore', {
34 config: types.optional(config, {}), 34 settings: types.optional(globalSettings, {}),
35 runtimeServices: types.map(runtimeService), 35 profiles: types.array(profile),
36 services: types.array(service),
36 selectedService: types.safeReference(service), 37 selectedService: types.safeReference(service),
37 shouldUseDarkColors: false, 38 shouldUseDarkColors: false,
38}); 39});