diff options
Diffstat (limited to 'src/containers/settings/EditServiceScreen.tsx')
-rw-r--r-- | src/containers/settings/EditServiceScreen.tsx | 480 |
1 files changed, 480 insertions, 0 deletions
diff --git a/src/containers/settings/EditServiceScreen.tsx b/src/containers/settings/EditServiceScreen.tsx new file mode 100644 index 000000000..6545c3d7d --- /dev/null +++ b/src/containers/settings/EditServiceScreen.tsx | |||
@@ -0,0 +1,480 @@ | |||
1 | import { Component, ReactElement } from 'react'; | ||
2 | import { inject, observer } from 'mobx-react'; | ||
3 | import { defineMessages, injectIntl } from 'react-intl'; | ||
4 | |||
5 | import { RouterStore } from 'mobx-react-router'; | ||
6 | import { StoresProps } from 'src/@types/ferdium-components.types'; | ||
7 | import { IRecipe } from 'src/models/Recipe'; | ||
8 | import Service from 'src/models/Service'; | ||
9 | import { FormFields } from 'src/@types/mobx-form.types'; | ||
10 | import Form from '../../lib/Form'; | ||
11 | |||
12 | import ServiceError from '../../components/settings/services/ServiceError'; | ||
13 | import EditServiceForm from '../../components/settings/services/EditServiceForm'; | ||
14 | import ErrorBoundary from '../../components/util/ErrorBoundary'; | ||
15 | |||
16 | import { required, url, oneRequired } from '../../helpers/validation-helpers'; | ||
17 | import { getSelectOptions } from '../../helpers/i18n-helpers'; | ||
18 | |||
19 | import { config as proxyFeature } from '../../features/serviceProxy'; | ||
20 | |||
21 | import { SPELLCHECKER_LOCALES } from '../../i18n/languages'; | ||
22 | |||
23 | import globalMessages from '../../i18n/globalMessages'; | ||
24 | import { DEFAULT_APP_SETTINGS, DEFAULT_SERVICE_SETTINGS } from '../../config'; | ||
25 | |||
26 | const messages = defineMessages({ | ||
27 | name: { | ||
28 | id: 'settings.service.form.name', | ||
29 | defaultMessage: 'Name', | ||
30 | }, | ||
31 | enableService: { | ||
32 | id: 'settings.service.form.enableService', | ||
33 | defaultMessage: 'Enable service', | ||
34 | }, | ||
35 | enableHibernation: { | ||
36 | id: 'settings.service.form.enableHibernation', | ||
37 | defaultMessage: 'Enable hibernation', | ||
38 | }, | ||
39 | enableWakeUp: { | ||
40 | id: 'settings.service.form.enableWakeUp', | ||
41 | defaultMessage: 'Enable wake up', | ||
42 | }, | ||
43 | enableNotification: { | ||
44 | id: 'settings.service.form.enableNotification', | ||
45 | defaultMessage: 'Enable notifications', | ||
46 | }, | ||
47 | enableBadge: { | ||
48 | id: 'settings.service.form.enableBadge', | ||
49 | defaultMessage: 'Show unread message badges', | ||
50 | }, | ||
51 | enableAudio: { | ||
52 | id: 'settings.service.form.enableAudio', | ||
53 | defaultMessage: 'Enable audio', | ||
54 | }, | ||
55 | team: { | ||
56 | id: 'settings.service.form.team', | ||
57 | defaultMessage: 'Team', | ||
58 | }, | ||
59 | customUrl: { | ||
60 | id: 'settings.service.form.customUrl', | ||
61 | defaultMessage: 'Custom server', | ||
62 | }, | ||
63 | indirectMessages: { | ||
64 | id: 'settings.service.form.indirectMessages', | ||
65 | defaultMessage: 'Show message badge for all new messages', | ||
66 | }, | ||
67 | icon: { | ||
68 | id: 'settings.service.form.icon', | ||
69 | defaultMessage: 'Custom icon', | ||
70 | }, | ||
71 | enableDarkMode: { | ||
72 | id: 'settings.service.form.enableDarkMode', | ||
73 | defaultMessage: 'Enable Dark Mode', | ||
74 | }, | ||
75 | darkReaderBrightness: { | ||
76 | id: 'settings.service.form.darkReaderBrightness', | ||
77 | defaultMessage: 'Dark Reader Brightness', | ||
78 | }, | ||
79 | darkReaderContrast: { | ||
80 | id: 'settings.service.form.darkReaderContrast', | ||
81 | defaultMessage: 'Dark Reader Contrast', | ||
82 | }, | ||
83 | darkReaderSepia: { | ||
84 | id: 'settings.service.form.darkReaderSepia', | ||
85 | defaultMessage: 'Dark Reader Sepia', | ||
86 | }, | ||
87 | enableProgressbar: { | ||
88 | id: 'settings.service.form.enableProgressbar', | ||
89 | defaultMessage: 'Enable Progress bar', | ||
90 | }, | ||
91 | trapLinkClicks: { | ||
92 | id: 'settings.service.form.trapLinkClicks', | ||
93 | defaultMessage: 'Open URLs within Ferdium', | ||
94 | }, | ||
95 | onlyShowFavoritesInUnreadCount: { | ||
96 | id: 'settings.service.form.onlyShowFavoritesInUnreadCount', | ||
97 | defaultMessage: 'Only show Favorites in unread count', | ||
98 | }, | ||
99 | enableProxy: { | ||
100 | id: 'settings.service.form.proxy.isEnabled', | ||
101 | defaultMessage: 'Use Proxy', | ||
102 | }, | ||
103 | proxyHost: { | ||
104 | id: 'settings.service.form.proxy.host', | ||
105 | defaultMessage: 'Proxy Host/IP', | ||
106 | }, | ||
107 | proxyPort: { | ||
108 | id: 'settings.service.form.proxy.port', | ||
109 | defaultMessage: 'Port', | ||
110 | }, | ||
111 | proxyUser: { | ||
112 | id: 'settings.service.form.proxy.user', | ||
113 | defaultMessage: 'User (optional)', | ||
114 | }, | ||
115 | proxyPassword: { | ||
116 | id: 'settings.service.form.proxy.password', | ||
117 | defaultMessage: 'Password (optional)', | ||
118 | }, | ||
119 | }); | ||
120 | |||
121 | interface EditServicesScreenProps extends StoresProps { | ||
122 | router: RouterStore; | ||
123 | intl: any; | ||
124 | } | ||
125 | |||
126 | class EditServiceScreen extends Component<EditServicesScreenProps> { | ||
127 | onSubmit(data: any) { | ||
128 | // @ts-ignore TODO: This is actually there and we don't have a correct type right now. | ||
129 | const { action } = this.props.router.params; | ||
130 | const { recipes, services } = this.props.stores; | ||
131 | const { createService, updateService } = this.props.actions.service; | ||
132 | data.darkReaderSettings = { | ||
133 | brightness: data.darkReaderBrightness, | ||
134 | contrast: data.darkReaderContrast, | ||
135 | sepia: data.darkReaderSepia, | ||
136 | }; | ||
137 | delete data.darkReaderContrast; | ||
138 | delete data.darkReaderBrightness; | ||
139 | delete data.darkReaderSepia; | ||
140 | |||
141 | const serviceData = data; | ||
142 | serviceData.isMuted = !serviceData.isMuted; | ||
143 | |||
144 | if (action === 'edit') { | ||
145 | updateService({ serviceId: services.activeSettings?.id, serviceData }); | ||
146 | } else { | ||
147 | createService({ recipeId: recipes.active?.id, serviceData }); | ||
148 | } | ||
149 | } | ||
150 | |||
151 | prepareForm(recipe: IRecipe, service: Service | null, proxy: any): Form { | ||
152 | const { intl } = this.props; | ||
153 | |||
154 | const { stores, router } = this.props; | ||
155 | |||
156 | // @ts-ignore TODO: This is actually there and we don't have a correct type right now. | ||
157 | const { action } = router.params; | ||
158 | |||
159 | let defaultSpellcheckerLanguage = | ||
160 | SPELLCHECKER_LOCALES[stores.settings.app.spellcheckerLanguage]; | ||
161 | |||
162 | if (stores.settings.app.spellcheckerLanguage === 'automatic') { | ||
163 | defaultSpellcheckerLanguage = intl.formatMessage( | ||
164 | globalMessages.spellcheckerAutomaticDetectionShort, | ||
165 | ); | ||
166 | } | ||
167 | |||
168 | const spellcheckerLanguage = getSelectOptions({ | ||
169 | locales: SPELLCHECKER_LOCALES, | ||
170 | resetToDefaultText: intl.formatMessage( | ||
171 | globalMessages.spellcheckerSystemDefault, | ||
172 | { default: defaultSpellcheckerLanguage }, | ||
173 | ), | ||
174 | automaticDetectionText: | ||
175 | stores.settings.app.spellcheckerLanguage !== 'automatic' | ||
176 | ? intl.formatMessage(globalMessages.spellcheckerAutomaticDetection) | ||
177 | : '', | ||
178 | }); | ||
179 | |||
180 | const config: FormFields = { | ||
181 | fields: { | ||
182 | name: { | ||
183 | label: intl.formatMessage(messages.name), | ||
184 | placeholder: intl.formatMessage(messages.name), | ||
185 | value: service?.id ? service.name : recipe.name, | ||
186 | }, | ||
187 | isEnabled: { | ||
188 | label: intl.formatMessage(messages.enableService), | ||
189 | value: service?.isEnabled, | ||
190 | default: DEFAULT_SERVICE_SETTINGS.isEnabled, | ||
191 | }, | ||
192 | isHibernationEnabled: { | ||
193 | label: intl.formatMessage(messages.enableHibernation), | ||
194 | value: | ||
195 | action !== 'edit' | ||
196 | ? recipe.autoHibernate | ||
197 | : service?.isHibernationEnabled, | ||
198 | default: DEFAULT_SERVICE_SETTINGS.isHibernationEnabled, | ||
199 | }, | ||
200 | isWakeUpEnabled: { | ||
201 | label: intl.formatMessage(messages.enableWakeUp), | ||
202 | value: service?.isWakeUpEnabled, | ||
203 | default: DEFAULT_SERVICE_SETTINGS.isWakeUpEnabled, | ||
204 | }, | ||
205 | isNotificationEnabled: { | ||
206 | label: intl.formatMessage(messages.enableNotification), | ||
207 | value: service?.isNotificationEnabled, | ||
208 | default: DEFAULT_SERVICE_SETTINGS.isNotificationEnabled, | ||
209 | }, | ||
210 | isBadgeEnabled: { | ||
211 | label: intl.formatMessage(messages.enableBadge), | ||
212 | value: service?.isBadgeEnabled, | ||
213 | default: DEFAULT_SERVICE_SETTINGS.isBadgeEnabled, | ||
214 | }, | ||
215 | trapLinkClicks: { | ||
216 | label: intl.formatMessage(messages.trapLinkClicks), | ||
217 | value: service?.trapLinkClicks, | ||
218 | default: DEFAULT_SERVICE_SETTINGS.trapLinkClicks, | ||
219 | }, | ||
220 | isMuted: { | ||
221 | label: intl.formatMessage(messages.enableAudio), | ||
222 | value: !service?.isMuted, | ||
223 | default: DEFAULT_SERVICE_SETTINGS.isMuted, | ||
224 | }, | ||
225 | customIcon: { | ||
226 | label: intl.formatMessage(messages.icon), | ||
227 | value: service?.hasCustomUploadedIcon ? service?.icon : false, | ||
228 | default: null, | ||
229 | type: 'file', | ||
230 | }, | ||
231 | isDarkModeEnabled: { | ||
232 | label: intl.formatMessage(messages.enableDarkMode), | ||
233 | value: service?.isDarkModeEnabled, | ||
234 | default: stores.settings.app.darkMode, | ||
235 | }, | ||
236 | darkReaderBrightness: { | ||
237 | label: intl.formatMessage(messages.darkReaderBrightness), | ||
238 | value: service?.darkReaderSettings | ||
239 | ? service?.darkReaderSettings.brightness | ||
240 | : undefined, | ||
241 | default: 100, | ||
242 | }, | ||
243 | darkReaderContrast: { | ||
244 | label: intl.formatMessage(messages.darkReaderContrast), | ||
245 | value: service?.darkReaderSettings | ||
246 | ? service?.darkReaderSettings.contrast | ||
247 | : undefined, | ||
248 | default: 90, | ||
249 | }, | ||
250 | darkReaderSepia: { | ||
251 | label: intl.formatMessage(messages.darkReaderSepia), | ||
252 | value: service?.darkReaderSettings | ||
253 | ? service?.darkReaderSettings.sepia | ||
254 | : undefined, | ||
255 | default: 10, | ||
256 | }, | ||
257 | isProgressbarEnabled: { | ||
258 | label: intl.formatMessage(messages.enableProgressbar), | ||
259 | value: service?.isProgressbarEnabled, | ||
260 | default: DEFAULT_SERVICE_SETTINGS.isProgressbarEnabled, | ||
261 | }, | ||
262 | spellcheckerLanguage: { | ||
263 | label: intl.formatMessage(globalMessages.spellcheckerLanguage), | ||
264 | value: service?.spellcheckerLanguage, | ||
265 | options: spellcheckerLanguage, | ||
266 | disabled: !stores.settings.app.enableSpellchecking, | ||
267 | }, | ||
268 | userAgentPref: { | ||
269 | label: intl.formatMessage(globalMessages.userAgentPref), | ||
270 | placeholder: service?.defaultUserAgent, | ||
271 | value: service?.userAgentPref ? service.userAgentPref : '', | ||
272 | }, | ||
273 | }, | ||
274 | }; | ||
275 | |||
276 | if (recipe.hasTeamId) { | ||
277 | Object.assign(config.fields, { | ||
278 | team: { | ||
279 | label: intl.formatMessage(messages.team), | ||
280 | placeholder: intl.formatMessage(messages.team), | ||
281 | value: service?.team, | ||
282 | validators: [required], | ||
283 | }, | ||
284 | }); | ||
285 | } | ||
286 | |||
287 | if (recipe.hasCustomUrl) { | ||
288 | Object.assign(config.fields, { | ||
289 | customUrl: { | ||
290 | label: intl.formatMessage(messages.customUrl), | ||
291 | placeholder: "'http://' or 'https://' or 'file:///'", | ||
292 | value: service?.customUrl || recipe.serviceURL, | ||
293 | validators: [required, url], | ||
294 | }, | ||
295 | }); | ||
296 | } | ||
297 | |||
298 | // More fine grained and use case specific validation rules | ||
299 | if (recipe.hasTeamId && recipe.hasCustomUrl) { | ||
300 | config.fields.team.validators = [oneRequired(['team', 'customUrl'])]; | ||
301 | config.fields.customUrl.validators = [ | ||
302 | url, | ||
303 | oneRequired(['team', 'customUrl']), | ||
304 | ]; | ||
305 | } | ||
306 | |||
307 | // If a service can be hosted and has a teamId or customUrl | ||
308 | if (recipe.hasHostedOption && (recipe.hasTeamId || recipe.hasCustomUrl)) { | ||
309 | if (config.fields.team) { | ||
310 | config.fields.team.validators = []; | ||
311 | } | ||
312 | if (config.fields.customUrl) { | ||
313 | config.fields.customUrl.validators = [url]; | ||
314 | } | ||
315 | } | ||
316 | |||
317 | if (recipe.hasIndirectMessages) { | ||
318 | Object.assign(config.fields, { | ||
319 | isIndirectMessageBadgeEnabled: { | ||
320 | label: intl.formatMessage(messages.indirectMessages), | ||
321 | value: service?.isIndirectMessageBadgeEnabled, | ||
322 | default: DEFAULT_SERVICE_SETTINGS.hasIndirectMessages, | ||
323 | }, | ||
324 | }); | ||
325 | } | ||
326 | |||
327 | if (recipe.allowFavoritesDelineationInUnreadCount) { | ||
328 | Object.assign(config.fields, { | ||
329 | onlyShowFavoritesInUnreadCount: { | ||
330 | label: intl.formatMessage(messages.onlyShowFavoritesInUnreadCount), | ||
331 | value: service?.onlyShowFavoritesInUnreadCount, | ||
332 | default: DEFAULT_APP_SETTINGS.onlyShowFavoritesInUnreadCount, | ||
333 | }, | ||
334 | }); | ||
335 | } | ||
336 | |||
337 | if (proxy.isEnabled) { | ||
338 | let serviceProxyConfig: { | ||
339 | isEnabled?: boolean; | ||
340 | host?: string; | ||
341 | port?: number; | ||
342 | user?: string; | ||
343 | password?: string; | ||
344 | } = {}; | ||
345 | if (service) { | ||
346 | serviceProxyConfig = stores.settings.proxy[service.id] || {}; | ||
347 | } | ||
348 | |||
349 | Object.assign(config.fields, { | ||
350 | proxy: { | ||
351 | name: 'proxy', | ||
352 | label: 'proxy', | ||
353 | fields: { | ||
354 | isEnabled: { | ||
355 | label: intl.formatMessage(messages.enableProxy), | ||
356 | value: serviceProxyConfig.isEnabled, | ||
357 | default: DEFAULT_APP_SETTINGS.proxyFeatureEnabled, | ||
358 | }, | ||
359 | host: { | ||
360 | label: intl.formatMessage(messages.proxyHost), | ||
361 | value: serviceProxyConfig.host, | ||
362 | default: '', | ||
363 | }, | ||
364 | port: { | ||
365 | label: intl.formatMessage(messages.proxyPort), | ||
366 | value: serviceProxyConfig.port, | ||
367 | default: '', | ||
368 | }, | ||
369 | user: { | ||
370 | label: intl.formatMessage(messages.proxyUser), | ||
371 | value: serviceProxyConfig.user, | ||
372 | default: '', | ||
373 | }, | ||
374 | password: { | ||
375 | label: intl.formatMessage(messages.proxyPassword), | ||
376 | value: serviceProxyConfig.password, | ||
377 | default: '', | ||
378 | type: 'password', | ||
379 | }, | ||
380 | }, | ||
381 | }, | ||
382 | }); | ||
383 | } | ||
384 | |||
385 | // @ts-ignore: Remove this ignore once mobx-react-form v4 with typescript | ||
386 | // support has been released. | ||
387 | return new Form(config); | ||
388 | } | ||
389 | |||
390 | deleteService(): void { | ||
391 | const { deleteService } = this.props.actions.service; | ||
392 | // @ts-ignore TODO: This is actually there and we don't have a correct type right now. | ||
393 | const { action } = this.props.router.params; | ||
394 | |||
395 | if (action === 'edit') { | ||
396 | const { activeSettings: service } = this.props.stores.services; | ||
397 | deleteService({ | ||
398 | serviceId: service?.id, | ||
399 | redirect: '/settings/services', | ||
400 | }); | ||
401 | } | ||
402 | } | ||
403 | |||
404 | openRecipeFile(file: any): void { | ||
405 | const { openRecipeFile } = this.props.actions.service; | ||
406 | // @ts-ignore TODO: This is actually there and we don't have a correct type right now. | ||
407 | const { action } = this.props.router.params; | ||
408 | |||
409 | if (action === 'edit') { | ||
410 | const { activeSettings: service } = this.props.stores.services; | ||
411 | openRecipeFile({ | ||
412 | recipe: service?.recipe.id, | ||
413 | file, | ||
414 | }); | ||
415 | } | ||
416 | } | ||
417 | |||
418 | render(): ReactElement { | ||
419 | const { recipes, services, user } = this.props.stores; | ||
420 | |||
421 | // @ts-ignore TODO: This is actually there and we don't have a correct type right now. | ||
422 | const { action } = this.props.router.params; | ||
423 | |||
424 | let recipe: null | IRecipe = null; | ||
425 | let service: null | Service = null; | ||
426 | let isLoading = false; | ||
427 | |||
428 | if (action === 'add') { | ||
429 | recipe = recipes.active; | ||
430 | |||
431 | // TODO: render error message when recipe is `null` | ||
432 | if (!recipe) { | ||
433 | return <ServiceError />; | ||
434 | } | ||
435 | } else { | ||
436 | service = services.activeSettings; | ||
437 | isLoading = services.allServicesRequest.isExecuting; | ||
438 | |||
439 | if (!isLoading && service) { | ||
440 | recipe = service.recipe; | ||
441 | } | ||
442 | } | ||
443 | |||
444 | if (isLoading) { | ||
445 | return <div>Loading...</div>; | ||
446 | } | ||
447 | |||
448 | if (!recipe) { | ||
449 | return <div>something went wrong</div>; | ||
450 | } | ||
451 | |||
452 | const form = this.prepareForm(recipe, service, proxyFeature); | ||
453 | |||
454 | return ( | ||
455 | <ErrorBoundary> | ||
456 | <EditServiceForm | ||
457 | action={action} | ||
458 | recipe={recipe} | ||
459 | service={service} | ||
460 | user={user.data} | ||
461 | form={form} | ||
462 | status={services.actionStatus} | ||
463 | isSaving={ | ||
464 | services.updateServiceRequest.isExecuting || | ||
465 | services.createServiceRequest.isExecuting | ||
466 | } | ||
467 | isDeleting={services.deleteServiceRequest.isExecuting} | ||
468 | onSubmit={d => this.onSubmit(d)} | ||
469 | onDelete={() => this.deleteService()} | ||
470 | openRecipeFile={file => this.openRecipeFile(file)} | ||
471 | isProxyFeatureEnabled={proxyFeature.isEnabled} | ||
472 | /> | ||
473 | </ErrorBoundary> | ||
474 | ); | ||
475 | } | ||
476 | } | ||
477 | |||
478 | export default injectIntl<'intl', EditServicesScreenProps>( | ||
479 | inject('stores', 'actions')(observer(EditServiceScreen)), | ||
480 | ); | ||