diff options
Diffstat (limited to 'packages/main/src/stores/Service.ts')
-rw-r--r-- | packages/main/src/stores/Service.ts | 313 |
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 | ||
21 | import type { UnreadCount } from '@sophie/service-shared'; | 21 | import type { UnreadCount } from '@sophie/service-shared'; |
22 | import { Service as ServiceBase } from '@sophie/shared'; | 22 | import { |
23 | import { 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'; | ||
30 | import { type Instance, getSnapshot, cast, flow } from 'mobx-state-tree'; | ||
24 | 31 | ||
25 | import type { ServiceView } from '../infrastructure/electron/types'; | 32 | import type { ServiceView } from '../infrastructure/electron/types.js'; |
26 | import overrideProps from '../utils/overrideProps'; | 33 | import getLogger from '../utils/getLogger.js'; |
27 | 34 | ||
28 | import ServiceSettings from './ServiceSettings'; | 35 | import { getEnv } from './MainEnv.js'; |
29 | import type ServiceConfig from './config/ServiceConfig'; | 36 | import ServiceSettings from './ServiceSettings.js'; |
37 | import type ServiceConfig from './config/ServiceConfig.js'; | ||
30 | 38 | ||
31 | const Service = overrideProps(ServiceBase, { | 39 | const log = getLogger('Service'); |
32 | settings: ServiceSettings, | 40 | |
33 | }) | 41 | const 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 | ||