aboutsummaryrefslogtreecommitdiffstats
path: root/packages/main/src/stores/Service.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/main/src/stores/Service.ts')
-rw-r--r--packages/main/src/stores/Service.ts313
1 files changed, 282 insertions, 31 deletions
diff --git a/packages/main/src/stores/Service.ts b/packages/main/src/stores/Service.ts
index 5302dd4..d062fe1 100644
--- a/packages/main/src/stores/Service.ts
+++ b/packages/main/src/stores/Service.ts
@@ -19,18 +19,43 @@
19 */ 19 */
20 20
21import type { UnreadCount } from '@sophie/service-shared'; 21import type { UnreadCount } from '@sophie/service-shared';
22import { Service as ServiceBase } from '@sophie/shared'; 22import {
23import { Instance, getSnapshot } from 'mobx-state-tree'; 23 type Certificate,
24 type CertificateSnapshotIn,
25 defineServiceModel,
26 ServiceAction,
27 type ServiceStateSnapshotIn,
28 type BrowserViewBounds,
29} from '@sophie/shared';
30import { type Instance, getSnapshot, cast, flow } from 'mobx-state-tree';
24 31
25import type { ServiceView } from '../infrastructure/electron/types'; 32import type { ServiceView } from '../infrastructure/electron/types.js';
26import overrideProps from '../utils/overrideProps'; 33import getLogger from '../utils/getLogger.js';
27 34
28import ServiceSettings from './ServiceSettings'; 35import { getEnv } from './MainEnv.js';
29import type ServiceConfig from './config/ServiceConfig'; 36import ServiceSettings from './ServiceSettings.js';
37import type ServiceConfig from './config/ServiceConfig.js';
30 38
31const Service = overrideProps(ServiceBase, { 39const log = getLogger('Service');
32 settings: ServiceSettings, 40
33}) 41const Service = defineServiceModel(ServiceSettings)
42 .volatile(
43 (): {
44 serviceView: ServiceView | undefined;
45 x: number;
46 y: number;
47 width: number;
48 height: number;
49 hasBounds: boolean;
50 } => ({
51 serviceView: undefined,
52 x: 0,
53 y: 0,
54 width: 0,
55 height: 0,
56 hasBounds: false,
57 }),
58 )
34 .views((self) => ({ 59 .views((self) => ({
35 get config(): ServiceConfig { 60 get config(): ServiceConfig {
36 const { id, settings } = self; 61 const { id, settings } = self;
@@ -40,17 +65,33 @@ const Service = overrideProps(ServiceBase, {
40 return self.currentUrl ?? self.settings.url; 65 return self.currentUrl ?? self.settings.url;
41 }, 66 },
42 get shouldBeLoaded(): boolean { 67 get shouldBeLoaded(): boolean {
43 return self.state !== 'crashed'; 68 return !self.crashed;
69 },
70 isCertificateTemporarilyTrusted(
71 certificate: CertificateSnapshotIn,
72 ): boolean {
73 return self.settings.profile.isCertificateTemporarilyTrusted(certificate);
74 },
75 }))
76 .views((self) => ({
77 get shouldBeVisible(): boolean {
78 // Do not attach service views for which we don't know the appropriate frame size,
79 // because they will just appear in a random location until the frame size is determined.
80 return self.shouldBeLoaded && !self.hasError && self.hasBounds;
44 }, 81 },
45 })) 82 }))
46 .volatile(
47 (): {
48 serviceView: ServiceView | undefined;
49 } => ({
50 serviceView: undefined,
51 }),
52 )
53 .actions((self) => ({ 83 .actions((self) => ({
84 setBrowserViewBounds(bounds: BrowserViewBounds): void {
85 self.x = bounds.x;
86 self.y = bounds.y;
87 self.width = bounds.width;
88 self.height = bounds.height;
89 self.hasBounds = true;
90 self.serviceView?.updateBounds();
91 },
92 setServiceView(serviceView: ServiceView | undefined): void {
93 self.serviceView = serviceView;
94 },
54 setLocation({ 95 setLocation({
55 url, 96 url,
56 canGoBack, 97 canGoBack,
@@ -67,18 +108,6 @@ const Service = overrideProps(ServiceBase, {
67 setTitle(title: string): void { 108 setTitle(title: string): void {
68 self.title = title; 109 self.title = title;
69 }, 110 },
70 startedLoading(): void {
71 self.state = 'loading';
72 },
73 finishedLoading(): void {
74 if (self.state === 'loading') {
75 // Do not overwrite crashed state if the service haven't been reloaded yet.
76 self.state = 'loaded';
77 }
78 },
79 crashed(): void {
80 self.state = 'crashed';
81 },
82 setUnreadCount({ direct, indirect }: UnreadCount): void { 111 setUnreadCount({ direct, indirect }: UnreadCount): void {
83 if (direct !== undefined) { 112 if (direct !== undefined) {
84 self.directMessageCount = direct; 113 self.directMessageCount = direct;
@@ -87,8 +116,230 @@ const Service = overrideProps(ServiceBase, {
87 self.indirectMessageCount = indirect; 116 self.indirectMessageCount = indirect;
88 } 117 }
89 }, 118 },
90 setServiceView(serviceView: ServiceView | undefined): void { 119 goBack(): void {
91 self.serviceView = serviceView; 120 self.serviceView?.goBack();
121 },
122 goForward(): void {
123 self.serviceView?.goForward();
124 },
125 stop(): void {
126 self.serviceView?.stop();
127 },
128 openCurrentURLInExternalBrowser(): void {
129 if (self.currentUrl === undefined) {
130 log.error('Cannot open empty URL in external browser');
131 return;
132 }
133 getEnv(self).openURLInExternalBrowser(self.currentUrl);
134 },
135 addBlockedPopup(url: string): void {
136 const index = self.popups.indexOf(url);
137 if (index >= 0) {
138 // Move existing popup to the end of the array,
139 // because later popups have precedence over earlier ones.
140 self.popups.splice(index, 1);
141 }
142 self.popups.push(url);
143 },
144 dismissPopup(url: string): boolean {
145 const index = self.popups.indexOf(url);
146 if (index < 0) {
147 log.warn('Service', self.id, 'has no pending popup', url);
148 return false;
149 }
150 self.popups.splice(index, 1);
151 return true;
152 },
153 dismissAllPopups(): void {
154 self.popups.splice(0);
155 },
156 toggleDeveloperTools(): void {
157 self.serviceView?.toggleDeveloperTools();
158 },
159 downloadCertificate: flow(function* downloadCertificate(
160 fingerprint: string,
161 ) {
162 const { state } = self;
163 if (state.type !== 'certificateError') {
164 log.warn(
165 'Tried to save certificate',
166 fingerprint,
167 'when there is no certificate error',
168 );
169 return;
170 }
171 let { certificate } = state;
172 while (
173 certificate !== undefined &&
174 certificate.fingerprint !== fingerprint
175 ) {
176 certificate = certificate.issuerCert as Certificate;
177 }
178 if (certificate === undefined) {
179 log.warn(
180 'Tried to save certificate',
181 fingerprint,
182 'which is not part of the current certificate chain',
183 );
184 return;
185 }
186 yield getEnv(self).saveTextFile('certificate.pem', certificate.data);
187 }),
188 }))
189 .actions((self) => {
190 function setState(state: ServiceStateSnapshotIn): void {
191 self.state = cast(state);
192 }
193
194 return {
195 startLoading(): void {
196 setState({ type: 'loading' });
197 },
198 finishLoading(): void {
199 if (self.loading) {
200 // Do not overwrite any error state state if the service haven't been reloaded yet.
201 setState({ type: 'loaded' });
202 }
203 },
204 setFailed(errorCode: number, errorDesc: string): void {
205 if (!self.hasError) {
206 setState({
207 type: 'failed',
208 errorCode,
209 errorDesc,
210 });
211 }
212 },
213 setCertificateError(
214 errorCode: string,
215 certificate: CertificateSnapshotIn,
216 ): void {
217 if (!self.crashed && self.state.type !== 'certificateError') {
218 setState({
219 type: 'certificateError',
220 errorCode,
221 certificate,
222 });
223 }
224 },
225 setCrashed(reason: string, exitCode: number): void {
226 if (!self.crashed) {
227 setState({
228 type: 'crashed',
229 reason,
230 exitCode,
231 });
232 }
233 },
234 reload(ignoreCache = false): void {
235 if (self.serviceView === undefined) {
236 setState({ type: 'initializing' });
237 } else {
238 self.serviceView?.reload(ignoreCache);
239 }
240 },
241 go(url: string): void {
242 if (self.serviceView === undefined) {
243 self.currentUrl = url;
244 setState({ type: 'initializing' });
245 } else {
246 self.serviceView?.loadURL(url);
247 }
248 },
249 };
250 })
251 .actions((self) => ({
252 goHome(): void {
253 self.go(self.settings.url);
254 },
255 temporarilyTrustCurrentCertificate(fingerprint: string): void {
256 if (self.state.type !== 'certificateError') {
257 log.error('Tried to trust certificate without any certificate error');
258 return;
259 }
260 if (self.state.certificate.fingerprint !== fingerprint) {
261 log.error(
262 'Tried to trust certificate',
263 fingerprint,
264 'but the currently pending fingerprint is',
265 self.state.certificate.fingerprint,
266 );
267 return;
268 }
269 self.settings.profile.temporarilyTrustCertificate(self.state.certificate);
270 self.state.trust = 'accepted';
271 self.reload();
272 },
273 followPopup(url: string): void {
274 if (self.dismissPopup(url)) {
275 self.go(url);
276 }
277 },
278 openPopupInExternalBrowser(url: string): void {
279 if (self.dismissPopup(url)) {
280 getEnv(self).openURLInExternalBrowser(url);
281 }
282 },
283 openAllPopupsInExternalBrowser(): void {
284 const env = getEnv(self);
285 self.popups.forEach((popup) => env.openURLInExternalBrowser(popup));
286 self.dismissAllPopups();
287 },
288 }))
289 .actions((self) => ({
290 dispatch(action: ServiceAction): void {
291 switch (action.action) {
292 case 'set-browser-view-bounds':
293 self.setBrowserViewBounds(action.browserViewBounds);
294 break;
295 case 'back':
296 self.goBack();
297 break;
298 case 'forward':
299 self.goForward();
300 break;
301 case 'reload':
302 self.reload(action.ignoreCache);
303 break;
304 case 'stop':
305 self.stop();
306 break;
307 case 'go-home':
308 self.goHome();
309 break;
310 case 'go':
311 self.go(action.url);
312 break;
313 case 'temporarily-trust-current-certificate':
314 self.temporarilyTrustCurrentCertificate(action.fingerprint);
315 break;
316 case 'open-current-url-in-external-browser':
317 self.openCurrentURLInExternalBrowser();
318 break;
319 case 'follow-popup':
320 self.followPopup(action.url);
321 break;
322 case 'open-popup-in-external-browser':
323 self.openPopupInExternalBrowser(action.url);
324 break;
325 case 'open-all-popups-in-external-browser':
326 self.openAllPopupsInExternalBrowser();
327 break;
328 case 'dismiss-popup':
329 self.dismissPopup(action.url);
330 break;
331 case 'dismiss-all-popups':
332 self.dismissAllPopups();
333 break;
334 case 'download-certificate':
335 self.downloadCertificate(action.fingerprint).catch((error) => {
336 log.error('Error while saving certificate', error);
337 });
338 break;
339 default:
340 log.error('Unknown action to dispatch', action);
341 break;
342 }
92 }, 343 },
93 })); 344 }));
94 345