aboutsummaryrefslogtreecommitdiffstats
path: root/packages/main/src/index.ts
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2022-02-08 21:40:40 +0100
committerLibravatar Kristóf Marussy <kristof@marussy.com>2022-02-14 12:13:22 +0100
commitcc214bb1afd37068c2bbc93f33990ca93f9a900f (patch)
tree7707415d5c4e42b2542d3f482060d4935c892fe1 /packages/main/src/index.ts
parentfeat: Unread message badges (diff)
downloadsophie-cc214bb1afd37068c2bbc93f33990ca93f9a900f.tar.gz
sophie-cc214bb1afd37068c2bbc93f33990ca93f9a900f.tar.zst
sophie-cc214bb1afd37068c2bbc93f33990ca93f9a900f.zip
feat: Load and switch services
Signed-off-by: Kristóf Marussy <kristof@marussy.com>
Diffstat (limited to 'packages/main/src/index.ts')
-rw-r--r--packages/main/src/index.ts260
1 files changed, 8 insertions, 252 deletions
diff --git a/packages/main/src/index.ts b/packages/main/src/index.ts
index f5f6be4..40b15f0 100644
--- a/packages/main/src/index.ts
+++ b/packages/main/src/index.ts
@@ -19,34 +19,13 @@
19 * SPDX-License-Identifier: AGPL-3.0-only 19 * SPDX-License-Identifier: AGPL-3.0-only
20 */ 20 */
21 21
22import { readFileSync } from 'node:fs';
23import { readFile } from 'node:fs/promises';
24import { arch } from 'node:os'; 22import { arch } from 'node:os';
25 23
26import { 24import { app } from 'electron';
27 ServiceToMainIpcMessage,
28 UnreadCount,
29 WebSource,
30} from '@sophie/service-shared';
31import {
32 Action,
33 MainToRendererIpcMessage,
34 RendererToMainIpcMessage,
35} from '@sophie/shared';
36import { app, BrowserView, BrowserWindow, ipcMain } from 'electron';
37import { ensureDirSync } from 'fs-extra'; 25import { ensureDirSync } from 'fs-extra';
38import { autorun } from 'mobx';
39import { getSnapshot, onAction, onPatch } from 'mobx-state-tree';
40import osName from 'os-name'; 26import osName from 'os-name';
41 27
42import { 28import { enableStacktraceSourceMaps } from './infrastructure/electron/impl/devTools';
43 enableStacktraceSourceMaps,
44 installDevToolsExtensions,
45 openDevToolsWhenReady,
46} from './infrastructure/electron/impl/devTools';
47import hardenSession from './infrastructure/electron/impl/hardenSession';
48import lockWebContentsToFile from './infrastructure/electron/impl/lockWebContentsToFile';
49import getDistResources from './infrastructure/resources/impl/getDistResources';
50import initReactions from './initReactions'; 29import initReactions from './initReactions';
51import { createMainStore } from './stores/MainStore'; 30import { createMainStore } from './stores/MainStore';
52import { getLogger } from './utils/log'; 31import { getLogger } from './utils/log';
@@ -81,15 +60,6 @@ app.commandLine.appendSwitch(
81 'HardwareMediaKeyHandling,MediaSessionService', 60 'HardwareMediaKeyHandling,MediaSessionService',
82); 61);
83 62
84// Remove sophie and electron from the user-agent string to avoid detection.
85const originalUserAgent = app.userAgentFallback;
86const userAgent = originalUserAgent.replaceAll(/\s(sophie|Electron)\/\S+/g, '');
87const chromelessUserAgent = userAgent.replace(/ Chrome\/\S+/, '');
88// Removing the electron version breaks redux devtools, so we only do this in production.
89if (!isDevelopment) {
90 app.userAgentFallback = userAgent;
91}
92
93app.setAboutPanelOptions({ 63app.setAboutPanelOptions({
94 applicationVersion: [ 64 applicationVersion: [
95 `Version: ${app.getVersion()}`, 65 `Version: ${app.getVersion()}`,
@@ -107,215 +77,10 @@ app.setAboutPanelOptions({
107 version: '', 77 version: '',
108}); 78});
109 79
110const resources = getDistResources(isDevelopment);
111
112const serviceInjectPath = resources.getPath('service-inject', 'index.js');
113const serviceInject: WebSource = {
114 code: readFileSync(serviceInjectPath, 'utf8'),
115 url: resources.getFileURL('service-inject', 'index.js'),
116};
117
118let mainWindow: BrowserWindow | undefined;
119
120const store = createMainStore(); 80const store = createMainStore();
121 81
122initReactions(store)
123 // eslint-disable-next-line promise/always-return -- `then` instead of top-level await.
124 .then((disposeCompositionRoot) => {
125 app.on('will-quit', disposeCompositionRoot);
126 })
127 .catch((error) => {
128 log.log('Failed to initialize application', error);
129 });
130
131async function createWindow(): Promise<unknown> {
132 mainWindow = new BrowserWindow({
133 show: false,
134 autoHideMenuBar: true,
135 webPreferences: {
136 sandbox: true,
137 devTools: isDevelopment,
138 preload: resources.getPath('preload', 'index.cjs'),
139 },
140 });
141
142 const { webContents } = mainWindow;
143
144 webContents.userAgent = originalUserAgent;
145
146 hardenSession(resources, isDevelopment, webContents.session);
147
148 if (isDevelopment) {
149 openDevToolsWhenReady(mainWindow);
150 }
151
152 mainWindow.on('ready-to-show', () => {
153 mainWindow?.show();
154 });
155
156 const browserView = new BrowserView({
157 webPreferences: {
158 sandbox: true,
159 nodeIntegrationInSubFrames: true,
160 preload: resources.getPath('service-preload', 'index.cjs'),
161 partition: 'persist:service',
162 },
163 });
164
165 browserView.webContents.userAgent = userAgent;
166
167 autorun(() => {
168 browserView.setBounds(store.browserViewBounds);
169 });
170 mainWindow.setBrowserView(browserView);
171
172 ipcMain.handle(RendererToMainIpcMessage.GetSharedStoreSnapshot, (event) => {
173 if (event.sender.id !== webContents.id) {
174 log.warn(
175 'Unexpected',
176 RendererToMainIpcMessage.GetSharedStoreSnapshot,
177 'from webContents',
178 event.sender.id,
179 );
180 throw new Error('Invalid IPC call');
181 }
182 return getSnapshot(store.shared);
183 });
184
185 async function reloadServiceInject() {
186 try {
187 serviceInject.code = await readFile(serviceInjectPath, 'utf8');
188 } catch (error) {
189 log.error('Error while reloading', serviceInjectPath, error);
190 }
191 browserView.webContents.reload();
192 }
193
194 ipcMain.on(RendererToMainIpcMessage.DispatchAction, (event, rawAction) => {
195 if (event.sender.id !== webContents.id) {
196 log.warn(
197 'Unexpected',
198 RendererToMainIpcMessage.DispatchAction,
199 'from webContents',
200 event.sender.id,
201 );
202 return;
203 }
204 try {
205 const actionToDispatch = Action.parse(rawAction);
206 switch (actionToDispatch.action) {
207 case 'set-selected-service-id':
208 store.settings.setSelectedServiceId(actionToDispatch.serviceId);
209 break;
210 case 'set-browser-view-bounds':
211 store.setBrowserViewBounds(actionToDispatch.browserViewBounds);
212 break;
213 case 'set-theme-source':
214 store.settings.setThemeSource(actionToDispatch.themeSource);
215 break;
216 case 'reload-all-services':
217 reloadServiceInject().catch((error) => {
218 log.error('Failed to reload browserView', error);
219 });
220 break;
221 default:
222 log.error('Unexpected action from UI renderer:', actionToDispatch);
223 break;
224 }
225 } catch (error) {
226 log.error('Error while dispatching renderer action', rawAction, error);
227 }
228 });
229
230 const batchedPatches: unknown[] = [];
231 onPatch(store.shared, (patch) => {
232 batchedPatches.push(patch);
233 });
234 onAction(
235 store,
236 () => {
237 if (batchedPatches.length > 0) {
238 webContents.send(
239 MainToRendererIpcMessage.SharedStorePatch,
240 batchedPatches,
241 );
242 batchedPatches.splice(0);
243 }
244 },
245 true,
246 );
247
248 ipcMain.handle(ServiceToMainIpcMessage.ApiExposedInMainWorld, (event) => {
249 if (event.sender.id !== browserView.webContents.id) {
250 log.warn(
251 'Unexpected',
252 ServiceToMainIpcMessage.ApiExposedInMainWorld,
253 'from webContents',
254 event.sender.id,
255 );
256 throw new Error('Invalid IPC call');
257 }
258 return serviceInject;
259 });
260
261 browserView.webContents.on('ipc-message', (_event, channel, ...args) => {
262 try {
263 switch (channel) {
264 case ServiceToMainIpcMessage.ApiExposedInMainWorld:
265 // Asynchronous message with reply must be handled in `ipcMain.handle`,
266 // otherwise electron emits a no handler registered warning.
267 break;
268 case ServiceToMainIpcMessage.SetUnreadCount:
269 log.log('Unread count:', UnreadCount.parse(args[0]));
270 break;
271 default:
272 log.error('Unknown IPC message:', channel, args);
273 break;
274 }
275 } catch (error) {
276 log.error('Error while processing IPC message:', channel, args, error);
277 }
278 });
279
280 browserView.webContents.session.setPermissionRequestHandler(
281 (_webContents, _permission, callback) => {
282 callback(false);
283 },
284 );
285
286 browserView.webContents.session.webRequest.onBeforeSendHeaders(
287 ({ url, requestHeaders }, callback) => {
288 const requestUserAgent = /^[^:]+:\/\/accounts\.google\.[^./]+\//.test(url)
289 ? chromelessUserAgent
290 : userAgent;
291 callback({
292 requestHeaders: {
293 ...requestHeaders,
294 'User-Agent': requestUserAgent,
295 },
296 });
297 },
298 );
299
300 browserView.webContents
301 .loadURL('https://gitlab.com/say-hi-to-sophie/sophie')
302 .catch((error) => {
303 log.error('Failed to load browser', error);
304 });
305
306 return lockWebContentsToFile(resources, 'index.html', webContents);
307}
308
309app.on('second-instance', () => { 82app.on('second-instance', () => {
310 if (mainWindow !== undefined) { 83 store.mainWindow?.bringToForeground();
311 if (!mainWindow.isVisible()) {
312 mainWindow.show();
313 }
314 if (mainWindow.isMinimized()) {
315 mainWindow.restore();
316 }
317 mainWindow.focus();
318 }
319}); 84});
320 85
321app.on('window-all-closed', () => { 86app.on('window-all-closed', () => {
@@ -324,20 +89,11 @@ app.on('window-all-closed', () => {
324 } 89 }
325}); 90});
326 91
327app 92initReactions(store, isDevelopment)
328 .whenReady() 93 // eslint-disable-next-line promise/always-return -- `then` instead of top-level await.
329 .then(async () => { 94 .then((disposeCompositionRoot) => {
330 if (isDevelopment) { 95 app.on('will-quit', disposeCompositionRoot);
331 try {
332 await installDevToolsExtensions();
333 } catch (error) {
334 log.error('Failed to install devtools extensions', error);
335 }
336 }
337
338 return createWindow();
339 }) 96 })
340 .catch((error) => { 97 .catch((error) => {
341 log.error('Failed to create window', error); 98 log.log('Failed to initialize application', error);
342 process.exit(1);
343 }); 99 });