aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar kytwb <412895+kytwb@users.noreply.github.com>2021-06-12 19:51:28 +0200
committerLibravatar GitHub <noreply@github.com>2021-06-12 19:51:28 +0200
commitb0ecce5eab2a6d0eed3ade7c43e252ca9bac7edb (patch)
treeaed0253b61ca035f3388a7d6a369ffe75f195a58
parentBypassed code signing since that is also incorrect in GH settings. (diff)
downloadferdi-b0ecce5eab2a6d0eed3ade7c43e252ca9bac7edb.tar.gz
ferdi-b0ecce5eab2a6d0eed3ade7c43e252ca9bac7edb.tar.zst
ferdi-b0ecce5eab2a6d0eed3ade7c43e252ca9bac7edb.zip
Fix active Todos service behaviour (#1481)
* Return false instead of null in isTodosServiceAdded * Resolve from TODOS_RECIPES_ID instead of hardcoded TODOS_RECIPE_ID * Fix TodosWebview width toggling when isTodosServiceActive * Add more todo service recipe IDs * Refactor todos state management * Moved todos service URL and recipe ID computation logic to todos/store * Simplified TodosWebview by delegating to the store for the URL and removing the (unused) payment logic * Made the todos service computation logic in the Service model depend on the logic in todos/store * Made ServicesStore depend on the todos service logic from the Service model * Todos appearance fixes * Hide double horizontal rules if todo settings are hidden due to an added todo service * Hide todos panel border when the panel is hidden or expanded * Make expanded todos panel obey sidebar width and vertical style settings * Make todos/store use isValidExternalURL * Harden isValidExternalURL against malformed URLs * Reduce todo URL string duplication in config.js Co-authored-by: Kristóf Marussy <kristof@marussy.com>
-rw-r--r--src/components/services/content/Services.js3
-rw-r--r--src/components/settings/settings/EditSettingsForm.js4
-rw-r--r--src/config.js47
-rw-r--r--src/containers/auth/SetupAssistantScreen.js8
-rw-r--r--src/features/appearance/index.js11
-rw-r--r--src/features/todos/components/TodosWebview.js96
-rw-r--r--src/features/todos/containers/TodosScreen.js3
-rw-r--r--src/features/todos/index.js1
-rw-r--r--src/features/todos/store.js29
-rw-r--r--src/helpers/url-helpers.js7
-rw-r--r--src/i18n/locales/defaultMessages.json20
-rw-r--r--src/i18n/messages/src/components/services/content/Services.json20
-rw-r--r--src/models/Service.js9
-rw-r--r--src/stores/ServicesStore.js11
14 files changed, 136 insertions, 133 deletions
diff --git a/src/components/services/content/Services.js b/src/components/services/content/Services.js
index f679eeed..7cf02c23 100644
--- a/src/components/services/content/Services.js
+++ b/src/components/services/content/Services.js
@@ -10,7 +10,6 @@ import injectSheet from 'react-jss';
10import ServiceView from './ServiceView'; 10import ServiceView from './ServiceView';
11import Appear from '../../ui/effects/Appear'; 11import Appear from '../../ui/effects/Appear';
12import serverlessLogin from '../../../helpers/serverless-helpers'; 12import serverlessLogin from '../../../helpers/serverless-helpers';
13import { TODOS_RECIPE_ID } from '../../../features/todos';
14 13
15const messages = defineMessages({ 14const messages = defineMessages({
16 welcome: { 15 welcome: {
@@ -171,7 +170,7 @@ export default @injectSheet(styles) @inject('actions') @observer class Services
171 </div> 170 </div>
172 </Appear> 171 </Appear>
173 )} 172 )}
174 {services.filter(service => service.recipe.id !== TODOS_RECIPE_ID).map(service => ( 173 {services.filter(service => !service.isTodosService).map(service => (
175 <ServiceView 174 <ServiceView
176 key={service.id} 175 key={service.id}
177 service={service} 176 service={service}
diff --git a/src/components/settings/settings/EditSettingsForm.js b/src/components/settings/settings/EditSettingsForm.js
index 8a362d3c..b7e00718 100644
--- a/src/components/settings/settings/EditSettingsForm.js
+++ b/src/components/settings/settings/EditSettingsForm.js
@@ -390,12 +390,10 @@ export default @observer class EditSettingsForm extends Component {
390 )} 390 )}
391 </div> 391 </div>
392 )} 392 )}
393 <Hr />
393 </> 394 </>
394 )} 395 )}
395 396
396
397 <Hr />
398
399 <Toggle field={form.$('scheduledDNDEnabled')} /> 397 <Toggle field={form.$('scheduledDNDEnabled')} />
400 {scheduledDNDEnabled && ( 398 {scheduledDNDEnabled && (
401 <> 399 <>
diff --git a/src/config.js b/src/config.js
index 8930c755..3a728b55 100644
--- a/src/config.js
+++ b/src/config.js
@@ -62,19 +62,44 @@ export const SEARCH_ENGINE_URLS = {
62 [SEARCH_ENGINE_DDG]: ({ searchTerm }) => `https://duckduckgo.com/?q=${searchTerm}`, 62 [SEARCH_ENGINE_DDG]: ({ searchTerm }) => `https://duckduckgo.com/?q=${searchTerm}`,
63}; 63};
64 64
65export const CUSTOM_TODO_SERVICE = 'isUsingCustomTodoService';
66
67const TODO_TODOIST_URL = 'https://todoist.com/app';
68const TODO_FRANZ_TODOS_URL = 'https://app.franztodos.com';
69const TODO_TICKTICK_URL = 'https://ticktick.com/signin';
70const TODO_MSTODO_URL = 'https://todo.microsoft.com/?app#';
71const TODO_HABITICA_URL = 'https://habitica.com/login';
72const TODO_NOZBE_URL = 'https://app.nozbe.com/#login';
73const TODO_RTM_URL = 'https://www.rememberthemilk.com/login/';
74const TODO_ANYDO_URL = 'https://desktop.any.do/';
75const TODO_GOOGLETASKS_URL = 'https://tasks.google.com/embed/?origin=https%3A%2F%2Fcalendar.google.com&fullWidth=1';
76
77export const TODO_SERVICE_RECIPE_IDS = {
78 [TODO_TODOIST_URL]: 'todoist',
79 [TODO_FRANZ_TODOS_URL]: 'franz-todos',
80 [TODO_TICKTICK_URL]: 'TickTick',
81 [TODO_MSTODO_URL]: 'mstodo',
82 [TODO_HABITICA_URL]: 'habitica',
83 [TODO_ANYDO_URL]: 'anydo',
84};
85
65export const TODO_APPS = { 86export const TODO_APPS = {
66 'https://todoist.com/app': 'Todoist', 87 [TODO_TODOIST_URL]: 'Todoist',
67 'https://app.franztodos.com': 'Franz Todo', 88 [TODO_FRANZ_TODOS_URL]: 'Franz Todo',
68 'https://ticktick.com/signin': 'TickTick', 89 [TODO_TICKTICK_URL]: 'TickTick',
69 'https://todo.microsoft.com/?app#': 'Microsoft To Do', 90 [TODO_MSTODO_URL]: 'Microsoft To Do',
70 'https://habitica.com/login': 'Habitica', 91 [TODO_HABITICA_URL]: 'Habitica',
71 'https://app.nozbe.com/#login': 'Nozbe', 92 [TODO_NOZBE_URL]: 'Nozbe',
72 'https://www.rememberthemilk.com/login/': 'Remember The Milk', 93 [TODO_RTM_URL]: 'Remember The Milk',
73 'https://desktop.any.do/': 'Any.do', 94 [TODO_ANYDO_URL]: 'Any.do',
74 'https://tasks.google.com/embed/?origin=https%3A%2F%2Fcalendar.google.com&fullWidth=1': 'Google Tasks', 95 [TODO_GOOGLETASKS_URL]: 'Google Tasks',
75 isUsingCustomTodoService: 'Other service', 96 [CUSTOM_TODO_SERVICE]: 'Other service',
76}; 97};
77 98
99export const DEFAULT_TODO_SERVICE = TODO_FRANZ_TODOS_URL;
100export const DEFAULT_TODO_RECIPE_ID = TODO_SERVICE_RECIPE_IDS[DEFAULT_TODO_SERVICE];
101export const DEFAULT_TODO_SERVICE_NAME = TODO_APPS[DEFAULT_TODO_SERVICE];
102
78export const SIDEBAR_WIDTH = { 103export const SIDEBAR_WIDTH = {
79 35: 'Extremely slim sidebar', 104 35: 'Extremely slim sidebar',
80 45: 'Very slim sidebar', 105 45: 'Very slim sidebar',
@@ -123,7 +148,7 @@ export const DEFAULT_APP_SETTINGS = {
123 148
124 // Ferdi specific options 149 // Ferdi specific options
125 server: LIVE_FERDI_API, 150 server: LIVE_FERDI_API,
126 predefinedTodoServer: 'https://app.franztodos.com', 151 predefinedTodoServer: DEFAULT_TODO_SERVICE,
127 autohideMenuBar: false, 152 autohideMenuBar: false,
128 lockingFeatureEnabled: false, 153 lockingFeatureEnabled: false,
129 locked: false, 154 locked: false,
diff --git a/src/containers/auth/SetupAssistantScreen.js b/src/containers/auth/SetupAssistantScreen.js
index 960ed7e3..1c4fa6c2 100644
--- a/src/containers/auth/SetupAssistantScreen.js
+++ b/src/containers/auth/SetupAssistantScreen.js
@@ -3,11 +3,11 @@ import React, { Component } from 'react';
3import PropTypes from 'prop-types'; 3import PropTypes from 'prop-types';
4import { inject, observer } from 'mobx-react'; 4import { inject, observer } from 'mobx-react';
5 5
6import { DEFAULT_TODO_RECIPE_ID, DEFAULT_TODO_SERVICE_NAME } from '../../config';
6import { sleep } from '../../helpers/async-helpers'; 7import { sleep } from '../../helpers/async-helpers';
7import SetupAssistant from '../../components/auth/SetupAssistant'; 8import SetupAssistant from '../../components/auth/SetupAssistant';
8import ServicesStore from '../../stores/ServicesStore'; 9import ServicesStore from '../../stores/ServicesStore';
9import RecipesStore from '../../stores/RecipesStore'; 10import RecipesStore from '../../stores/RecipesStore';
10import { TODOS_RECIPE_ID } from '../../features/todos';
11import UserStore from '../../stores/UserStore'; 11import UserStore from '../../stores/UserStore';
12 12
13export default @inject('stores', 'actions') @observer class SetupAssistantScreen extends Component { 13export default @inject('stores', 'actions') @observer class SetupAssistantScreen extends Component {
@@ -82,11 +82,11 @@ export default @inject('stores', 'actions') @observer class SetupAssistantScreen
82 await sleep(100); 82 await sleep(100);
83 } 83 }
84 84
85 // Add Franz ToDos 85 // Add todo service
86 await services._createService({ 86 await services._createService({
87 recipeId: TODOS_RECIPE_ID, 87 recipeId: DEFAULT_TODO_RECIPE_ID,
88 serviceData: { 88 serviceData: {
89 name: 'Franz ToDos', 89 name: DEFAULT_TODO_SERVICE_NAME,
90 }, 90 },
91 redirect: false, 91 redirect: false,
92 skipCleanup: true, 92 skipCleanup: true,
diff --git a/src/features/appearance/index.js b/src/features/appearance/index.js
index 82a0971e..b5648250 100644
--- a/src/features/appearance/index.js
+++ b/src/features/appearance/index.js
@@ -93,10 +93,10 @@ function generateServiceRibbonWidthStyle(widthStr, iconSizeStr, vertical) {
93 } 93 }
94 ` : ` 94 ` : `
95 .sidebar { 95 .sidebar {
96 width: ${width - 2}px !important; 96 width: ${width}px !important;
97 } 97 }
98 .tab-item { 98 .tab-item {
99 width: ${width - 2}px !important; 99 width: ${width}px !important;
100 height: ${width - 5 + iconSize}px !important; 100 height: ${width - 5 + iconSize}px !important;
101 } 101 }
102 .tab-item .tab-item__icon { 102 .tab-item .tab-item__icon {
@@ -105,6 +105,9 @@ function generateServiceRibbonWidthStyle(widthStr, iconSizeStr, vertical) {
105 .sidebar__button { 105 .sidebar__button {
106 font-size: ${width / 3}px !important; 106 font-size: ${width / 3}px !important;
107 } 107 }
108 .todos__todos-panel--expanded {
109 width: calc(100% - ${300 + width}px) !important;
110 }
108 `; 111 `;
109} 112}
110 113
@@ -156,6 +159,10 @@ function generateVerticalStyle(widthStr, alwaysShowWorkspaces) {
156 .workspaces-drawer { 159 .workspaces-drawer {
157 maring-top: -${sidebarWidthStr} !important; 160 maring-top: -${sidebarWidthStr} !important;
158 } 161 }
162
163 .todos__todos-panel--expanded {
164 width: calc(100% - 300px) !important;
165 }
159 `; 166 `;
160} 167}
161 168
diff --git a/src/features/todos/components/TodosWebview.js b/src/features/todos/components/TodosWebview.js
index 634ec4ca..03bb5efe 100644
--- a/src/features/todos/components/TodosWebview.js
+++ b/src/features/todos/components/TodosWebview.js
@@ -1,47 +1,12 @@
1import React, { Component } from 'react'; 1import React, { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer, inject } from 'mobx-react'; 3import { observer } from 'mobx-react';
4import injectSheet from 'react-jss'; 4import injectSheet from 'react-jss';
5import Webview from 'react-electron-web-view'; 5import Webview from 'react-electron-web-view';
6import { Icon } from '@meetfranz/ui';
7import { defineMessages, intlShape } from 'react-intl';
8import classnames from 'classnames'; 6import classnames from 'classnames';
9 7
10import { mdiCheckAll } from '@mdi/js';
11import SettingsStore from '../../../stores/SettingsStore';
12
13import Appear from '../../../components/ui/effects/Appear';
14import UpgradeButton from '../../../components/ui/UpgradeButton';
15import { TODOS_PARTITION_ID } from '..'; 8import { TODOS_PARTITION_ID } from '..';
16 9
17// NOTE: https://stackoverflow.com/questions/5717093/check-if-a-javascript-string-is-a-url
18function validURL(str) {
19 let url;
20
21 try {
22 url = new URL(str);
23 } catch (_) {
24 return false;
25 }
26
27 return url.protocol === 'http:' || url.protocol === 'https:';
28}
29
30const messages = defineMessages({
31 premiumInfo: {
32 id: 'feature.todos.premium.info',
33 defaultMessage: '!!!Franz Todos are available to premium users now!',
34 },
35 upgradeCTA: {
36 id: 'feature.todos.premium.upgrade',
37 defaultMessage: '!!!Upgrade Account',
38 },
39 rolloutInfo: {
40 id: 'feature.todos.premium.rollout',
41 defaultMessage: '!!!Everyone else will have to wait a little longer.',
42 },
43});
44
45const styles = theme => ({ 10const styles = theme => ({
46 root: { 11 root: {
47 background: theme.colorBackground, 12 background: theme.colorBackground,
@@ -96,10 +61,14 @@ const styles = theme => ({
96 position: 'absolute', 61 position: 'absolute',
97 right: 0, 62 right: 0,
98 zIndex: 0, 63 zIndex: 0,
64 borderLeftWidth: 0,
65 },
66 hidden: {
67 borderLeftWidth: 0,
99 }, 68 },
100}); 69});
101 70
102@injectSheet(styles) @inject('stores') @observer 71@injectSheet(styles) @observer
103class TodosWebview extends Component { 72class TodosWebview extends Component {
104 static propTypes = { 73 static propTypes = {
105 classes: PropTypes.object.isRequired, 74 classes: PropTypes.object.isRequired,
@@ -111,10 +80,8 @@ class TodosWebview extends Component {
111 width: PropTypes.number.isRequired, 80 width: PropTypes.number.isRequired,
112 minWidth: PropTypes.number.isRequired, 81 minWidth: PropTypes.number.isRequired,
113 userAgent: PropTypes.string.isRequired, 82 userAgent: PropTypes.string.isRequired,
114 isTodosIncludedInCurrentPlan: PropTypes.bool.isRequired, 83 todoUrl: PropTypes.string.isRequired,
115 stores: PropTypes.shape({ 84 isTodoUrlValid: PropTypes.bool.isRequired,
116 settings: PropTypes.instanceOf(SettingsStore).isRequired,
117 }).isRequired,
118 }; 85 };
119 86
120 state = { 87 state = {
@@ -122,10 +89,6 @@ class TodosWebview extends Component {
122 width: 300, 89 width: 300,
123 }; 90 };
124 91
125 static contextTypes = {
126 intl: intlShape,
127 };
128
129 componentWillMount() { 92 componentWillMount() {
130 const { width } = this.props; 93 const { width } = this.props;
131 94
@@ -209,8 +172,8 @@ class TodosWebview extends Component {
209 isTodosServiceActive, 172 isTodosServiceActive,
210 isVisible, 173 isVisible,
211 userAgent, 174 userAgent,
212 isTodosIncludedInCurrentPlan, 175 todoUrl,
213 stores, 176 isTodoUrlValid,
214 } = this.props; 177 } = this.props;
215 178
216 const { 179 const {
@@ -219,29 +182,20 @@ class TodosWebview extends Component {
219 isDragging, 182 isDragging,
220 } = this.state; 183 } = this.state;
221 184
222 const { intl } = this.context; 185 let displayedWidth = isVisible ? width : 0;
223 186 if (isTodosServiceActive) {
224 const isUsingPredefinedTodoServer = stores.settings.all.app.predefinedTodoServer !== 'isUsingCustomTodoService'; 187 displayedWidth = null;
225 const todoUrl = isUsingPredefinedTodoServer
226 ? stores.settings.all.app.predefinedTodoServer
227 : stores.settings.all.app.customTodoServer;
228 let isTodoUrlValid = true;
229 if (isUsingPredefinedTodoServer === false) {
230 isTodoUrlValid = validURL(todoUrl);
231 } 188 }
232 189
233 const todosPanelStyle = {
234 width: isVisible ? width : 0,
235 borderLeftWidth: isVisible ? '1px' : 0,
236 };
237
238 return ( 190 return (
239 <div 191 <div
240 className={classnames({ 192 className={classnames({
241 [classes.root]: true, 193 [classes.root]: true,
242 [classes.isTodosServiceActive]: isTodosServiceActive, 194 [classes.isTodosServiceActive]: isTodosServiceActive,
195 'todos__todos-panel--expanded': isTodosServiceActive,
196 [classes.hidden]: !isVisible,
243 })} 197 })}
244 style={todosPanelStyle} 198 style={{ width: displayedWidth }}
245 onMouseUp={() => this.stopResize()} 199 onMouseUp={() => this.stopResize()}
246 ref={(node) => { this.node = node; }} 200 ref={(node) => { this.node = node; }}
247 id="todos-panel" 201 id="todos-panel"
@@ -257,9 +211,7 @@ class TodosWebview extends Component {
257 style={{ left: delta }} // This hack is required as resizing with webviews beneath behaves quite bad 211 style={{ left: delta }} // This hack is required as resizing with webviews beneath behaves quite bad
258 /> 212 />
259 )} 213 )}
260 {isTodosIncludedInCurrentPlan ? ( 214 {isTodoUrlValid && (
261 isTodoUrlValid
262 && (
263 <Webview 215 <Webview
264 className={classes.webview} 216 className={classes.webview}
265 onDidAttach={() => { 217 onDidAttach={() => {
@@ -273,20 +225,6 @@ class TodosWebview extends Component {
273 useragent={userAgent} 225 useragent={userAgent}
274 src={todoUrl} 226 src={todoUrl}
275 /> 227 />
276 )
277 ) : (
278 <Appear>
279 <div className={classes.premiumContainer}>
280 <Icon icon={mdiCheckAll} className={classes.premiumIcon} size={4} />
281 <p>{intl.formatMessage(messages.premiumInfo)}</p>
282 <p>{intl.formatMessage(messages.rolloutInfo)}</p>
283 <UpgradeButton
284 className={classes.premiumCTA}
285 gaEventInfo={{ category: 'Todos', event: 'upgrade' }}
286 short
287 />
288 </div>
289 </Appear>
290 )} 228 )}
291 </div> 229 </div>
292 ); 230 );
diff --git a/src/features/todos/containers/TodosScreen.js b/src/features/todos/containers/TodosScreen.js
index 631893f9..96147d5a 100644
--- a/src/features/todos/containers/TodosScreen.js
+++ b/src/features/todos/containers/TodosScreen.js
@@ -28,7 +28,8 @@ class TodosScreen extends Component {
28 minWidth={TODOS_MIN_WIDTH} 28 minWidth={TODOS_MIN_WIDTH}
29 resize={width => todoActions.resize({ width })} 29 resize={width => todoActions.resize({ width })}
30 userAgent={todosStore.userAgent} 30 userAgent={todosStore.userAgent}
31 isTodosIncludedInCurrentPlan 31 todoUrl={todosStore.todoUrl}
32 isTodoUrlValid={todosStore.isTodoUrlValid}
32 /> 33 />
33 </ErrorBoundary> 34 </ErrorBoundary>
34 ); 35 );
diff --git a/src/features/todos/index.js b/src/features/todos/index.js
index ebd2c60e..b6d13e5e 100644
--- a/src/features/todos/index.js
+++ b/src/features/todos/index.js
@@ -9,7 +9,6 @@ export const DEFAULT_TODOS_WIDTH = 300;
9export const TODOS_MIN_WIDTH = 200; 9export const TODOS_MIN_WIDTH = 200;
10export const DEFAULT_TODOS_VISIBLE = true; 10export const DEFAULT_TODOS_VISIBLE = true;
11export const DEFAULT_IS_FEATURE_ENABLED_BY_USER = true; 11export const DEFAULT_IS_FEATURE_ENABLED_BY_USER = true;
12export const TODOS_RECIPE_ID = 'franz-todos';
13export const TODOS_PARTITION_ID = 'persist:todos'; 12export const TODOS_PARTITION_ID = 'persist:todos';
14 13
15export const TODOS_ROUTES = { 14export const TODOS_ROUTES = {
diff --git a/src/features/todos/store.js b/src/features/todos/store.js
index af8519d9..4febd7bb 100644
--- a/src/features/todos/store.js
+++ b/src/features/todos/store.js
@@ -7,6 +7,8 @@ import {
7import localStorage from 'mobx-localstorage'; 7import localStorage from 'mobx-localstorage';
8 8
9import { todoActions } from './actions'; 9import { todoActions } from './actions';
10import { CUSTOM_TODO_SERVICE, TODO_SERVICE_RECIPE_IDS } from '../../config';
11import { isValidExternalURL } from '../../helpers/url-helpers';
10import { FeatureStore } from '../utils/FeatureStore'; 12import { FeatureStore } from '../utils/FeatureStore';
11import { createReactions } from '../../stores/lib/Reaction'; 13import { createReactions } from '../../stores/lib/Reaction';
12import { createActionBindings } from '../utils/ActionBinding'; 14import { createActionBindings } from '../utils/ActionBinding';
@@ -21,6 +23,8 @@ import UserAgent from '../../models/UserAgent';
21const debug = require('debug')('Ferdi:feature:todos:store'); 23const debug = require('debug')('Ferdi:feature:todos:store');
22 24
23export default class TodoStore extends FeatureStore { 25export default class TodoStore extends FeatureStore {
26 @observable stores = null;
27
24 @observable isFeatureEnabled = false; 28 @observable isFeatureEnabled = false;
25 29
26 @observable isFeatureActive = false; 30 @observable isFeatureActive = false;
@@ -59,6 +63,31 @@ export default class TodoStore extends FeatureStore {
59 return this.userAgentModel.userAgent; 63 return this.userAgentModel.userAgent;
60 } 64 }
61 65
66 @computed get isUsingPredefinedTodoServer() {
67 return this.stores && this.stores.settings.app.predefinedTodoServer !== CUSTOM_TODO_SERVICE;
68 }
69
70 @computed get todoUrl() {
71 if (!this.stores) {
72 return null;
73 }
74 return this.isUsingPredefinedTodoServer
75 ? this.stores.settings.app.predefinedTodoServer
76 : this.stores.settings.app.customTodoServer;
77 }
78
79 @computed get isTodoUrlValid() {
80 return !this.isUsingPredefinedTodoServer || isValidExternalURL(this.todoUrl);
81 }
82
83 @computed get todoRecipeId() {
84 if (this.isFeatureEnabledByUser && this.isUsingPredefinedTodoServer
85 && this.todoUrl in TODO_SERVICE_RECIPE_IDS) {
86 return TODO_SERVICE_RECIPE_IDS[this.todoUrl];
87 }
88 return null;
89 }
90
62 // ========== PUBLIC API ========= // 91 // ========== PUBLIC API ========= //
63 92
64 @action start(stores, actions) { 93 @action start(stores, actions) {
diff --git a/src/helpers/url-helpers.js b/src/helpers/url-helpers.js
index 2f429a25..972f9b79 100644
--- a/src/helpers/url-helpers.js
+++ b/src/helpers/url-helpers.js
@@ -5,7 +5,12 @@ import { ALLOWED_PROTOCOLS } from '../config';
5const debug = require('debug')('Ferdi:Helpers:url'); 5const debug = require('debug')('Ferdi:Helpers:url');
6 6
7export function isValidExternalURL(url) { 7export function isValidExternalURL(url) {
8 const parsedUrl = new URL(url); 8 let parsedUrl;
9 try {
10 parsedUrl = new URL(url);
11 } catch (_) {
12 return false;
13 }
9 14
10 const isAllowed = ALLOWED_PROTOCOLS.includes(parsedUrl.protocol); 15 const isAllowed = ALLOWED_PROTOCOLS.includes(parsedUrl.protocol);
11 16
diff --git a/src/i18n/locales/defaultMessages.json b/src/i18n/locales/defaultMessages.json
index 2f998a2c..b8ea986c 100644
--- a/src/i18n/locales/defaultMessages.json
+++ b/src/i18n/locales/defaultMessages.json
@@ -1743,65 +1743,65 @@
1743 "defaultMessage": "!!!Welcome to Ferdi", 1743 "defaultMessage": "!!!Welcome to Ferdi",
1744 "end": { 1744 "end": {
1745 "column": 3, 1745 "column": 3,
1746 "line": 19 1746 "line": 18
1747 }, 1747 },
1748 "file": "src/components/services/content/Services.js", 1748 "file": "src/components/services/content/Services.js",
1749 "id": "services.welcome", 1749 "id": "services.welcome",
1750 "start": { 1750 "start": {
1751 "column": 11, 1751 "column": 11,
1752 "line": 16 1752 "line": 15
1753 } 1753 }
1754 }, 1754 },
1755 { 1755 {
1756 "defaultMessage": "!!!Get started", 1756 "defaultMessage": "!!!Get started",
1757 "end": { 1757 "end": {
1758 "column": 3, 1758 "column": 3,
1759 "line": 23 1759 "line": 22
1760 }, 1760 },
1761 "file": "src/components/services/content/Services.js", 1761 "file": "src/components/services/content/Services.js",
1762 "id": "services.getStarted", 1762 "id": "services.getStarted",
1763 "start": { 1763 "start": {
1764 "column": 14, 1764 "column": 14,
1765 "line": 20 1765 "line": 19
1766 } 1766 }
1767 }, 1767 },
1768 { 1768 {
1769 "defaultMessage": "!!!Please login to use Ferdi.", 1769 "defaultMessage": "!!!Please login to use Ferdi.",
1770 "end": { 1770 "end": {
1771 "column": 3, 1771 "column": 3,
1772 "line": 27 1772 "line": 26
1773 }, 1773 },
1774 "file": "src/components/services/content/Services.js", 1774 "file": "src/components/services/content/Services.js",
1775 "id": "services.login", 1775 "id": "services.login",
1776 "start": { 1776 "start": {
1777 "column": 9, 1777 "column": 9,
1778 "line": 24 1778 "line": 23
1779 } 1779 }
1780 }, 1780 },
1781 { 1781 {
1782 "defaultMessage": "!!!Use Ferdi without an Account", 1782 "defaultMessage": "!!!Use Ferdi without an Account",
1783 "end": { 1783 "end": {
1784 "column": 3, 1784 "column": 3,
1785 "line": 31 1785 "line": 30
1786 }, 1786 },
1787 "file": "src/components/services/content/Services.js", 1787 "file": "src/components/services/content/Services.js",
1788 "id": "services.serverless", 1788 "id": "services.serverless",
1789 "start": { 1789 "start": {
1790 "column": 14, 1790 "column": 14,
1791 "line": 28 1791 "line": 27
1792 } 1792 }
1793 }, 1793 },
1794 { 1794 {
1795 "defaultMessage": "!!!Optionally, you can change your Ferdi server by clicking the cog in the bottom left corner.", 1795 "defaultMessage": "!!!Optionally, you can change your Ferdi server by clicking the cog in the bottom left corner.",
1796 "end": { 1796 "end": {
1797 "column": 3, 1797 "column": 3,
1798 "line": 35 1798 "line": 34
1799 }, 1799 },
1800 "file": "src/components/services/content/Services.js", 1800 "file": "src/components/services/content/Services.js",
1801 "id": "services.serverInfo", 1801 "id": "services.serverInfo",
1802 "start": { 1802 "start": {
1803 "column": 14, 1803 "column": 14,
1804 "line": 32 1804 "line": 31
1805 } 1805 }
1806 } 1806 }
1807 ], 1807 ],
diff --git a/src/i18n/messages/src/components/services/content/Services.json b/src/i18n/messages/src/components/services/content/Services.json
index 3c40b11a..6a5eb052 100644
--- a/src/i18n/messages/src/components/services/content/Services.json
+++ b/src/i18n/messages/src/components/services/content/Services.json
@@ -4,11 +4,11 @@
4 "defaultMessage": "!!!Welcome to Ferdi", 4 "defaultMessage": "!!!Welcome to Ferdi",
5 "file": "src/components/services/content/Services.js", 5 "file": "src/components/services/content/Services.js",
6 "start": { 6 "start": {
7 "line": 16, 7 "line": 15,
8 "column": 11 8 "column": 11
9 }, 9 },
10 "end": { 10 "end": {
11 "line": 19, 11 "line": 18,
12 "column": 3 12 "column": 3
13 } 13 }
14 }, 14 },
@@ -17,11 +17,11 @@
17 "defaultMessage": "!!!Get started", 17 "defaultMessage": "!!!Get started",
18 "file": "src/components/services/content/Services.js", 18 "file": "src/components/services/content/Services.js",
19 "start": { 19 "start": {
20 "line": 20, 20 "line": 19,
21 "column": 14 21 "column": 14
22 }, 22 },
23 "end": { 23 "end": {
24 "line": 23, 24 "line": 22,
25 "column": 3 25 "column": 3
26 } 26 }
27 }, 27 },
@@ -30,11 +30,11 @@
30 "defaultMessage": "!!!Please login to use Ferdi.", 30 "defaultMessage": "!!!Please login to use Ferdi.",
31 "file": "src/components/services/content/Services.js", 31 "file": "src/components/services/content/Services.js",
32 "start": { 32 "start": {
33 "line": 24, 33 "line": 23,
34 "column": 9 34 "column": 9
35 }, 35 },
36 "end": { 36 "end": {
37 "line": 27, 37 "line": 26,
38 "column": 3 38 "column": 3
39 } 39 }
40 }, 40 },
@@ -43,11 +43,11 @@
43 "defaultMessage": "!!!Use Ferdi without an Account", 43 "defaultMessage": "!!!Use Ferdi without an Account",
44 "file": "src/components/services/content/Services.js", 44 "file": "src/components/services/content/Services.js",
45 "start": { 45 "start": {
46 "line": 28, 46 "line": 27,
47 "column": 14 47 "column": 14
48 }, 48 },
49 "end": { 49 "end": {
50 "line": 31, 50 "line": 30,
51 "column": 3 51 "column": 3
52 } 52 }
53 }, 53 },
@@ -56,11 +56,11 @@
56 "defaultMessage": "!!!Optionally, you can change your Ferdi server by clicking the cog in the bottom left corner.", 56 "defaultMessage": "!!!Optionally, you can change your Ferdi server by clicking the cog in the bottom left corner.",
57 "file": "src/components/services/content/Services.js", 57 "file": "src/components/services/content/Services.js",
58 "start": { 58 "start": {
59 "line": 32, 59 "line": 31,
60 "column": 14 60 "column": 14
61 }, 61 },
62 "end": { 62 "end": {
63 "line": 35, 63 "line": 34,
64 "column": 3 64 "column": 3
65 } 65 }
66 } 66 }
diff --git a/src/models/Service.js b/src/models/Service.js
index 0d1dff43..d0c6a710 100644
--- a/src/models/Service.js
+++ b/src/models/Service.js
@@ -4,7 +4,7 @@ import { webContents } from '@electron/remote';
4import normalizeUrl from 'normalize-url'; 4import normalizeUrl from 'normalize-url';
5import path from 'path'; 5import path from 'path';
6 6
7import { TODOS_RECIPE_ID, todosStore } from '../features/todos'; 7import { todosStore } from '../features/todos';
8import { isValidExternalURL } from '../helpers/url-helpers'; 8import { isValidExternalURL } from '../helpers/url-helpers';
9import UserAgent from './UserAgent'; 9import UserAgent from './UserAgent';
10 10
@@ -184,8 +184,12 @@ export default class Service {
184 }; 184 };
185 } 185 }
186 186
187 @computed get isTodosService() {
188 return this.recipe.id === todosStore.todoRecipeId;
189 }
190
187 get webview() { 191 get webview() {
188 if (this.recipe.id === TODOS_RECIPE_ID) { 192 if (this.isTodosService) {
189 return todosStore.webview; 193 return todosStore.webview;
190 } 194 }
191 195
@@ -243,7 +247,6 @@ export default class Service {
243 return this.recipe.partition || `persist:service-${this.id}`; 247 return this.recipe.partition || `persist:service-${this.id}`;
244 } 248 }
245 249
246
247 initializeWebViewEvents({ handleIPCMessage, openWindow, stores }) { 250 initializeWebViewEvents({ handleIPCMessage, openWindow, stores }) {
248 const webviewWebContents = webContents.fromId(this.webview.getWebContentsId()); 251 const webviewWebContents = webContents.fromId(this.webview.getWebContentsId());
249 252
diff --git a/src/stores/ServicesStore.js b/src/stores/ServicesStore.js
index 48b12896..752f287e 100644
--- a/src/stores/ServicesStore.js
+++ b/src/stores/ServicesStore.js
@@ -21,7 +21,6 @@ import { workspaceStore } from '../features/workspaces';
21import { serviceLimitStore } from '../features/serviceLimit'; 21import { serviceLimitStore } from '../features/serviceLimit';
22import { RESTRICTION_TYPES } from '../models/Service'; 22import { RESTRICTION_TYPES } from '../models/Service';
23import { KEEP_WS_LOADED_USID } from '../config'; 23import { KEEP_WS_LOADED_USID } from '../config';
24import { TODOS_RECIPE_ID } from '../features/todos';
25import { SPELLCHECKER_LOCALES } from '../i18n/languages'; 24import { SPELLCHECKER_LOCALES } from '../i18n/languages';
26 25
27const debug = require('debug')('Ferdi:ServiceStore'); 26const debug = require('debug')('Ferdi:ServiceStore');
@@ -279,11 +278,11 @@ export default class ServicesStore extends Store {
279 } 278 }
280 279
281 @computed get isTodosServiceAdded() { 280 @computed get isTodosServiceAdded() {
282 return this.allDisplayed.find(service => service.recipe.id === TODOS_RECIPE_ID && service.isEnabled) || null; 281 return this.allDisplayed.find(service => service.isTodosService && service.isEnabled) || false;
283 } 282 }
284 283
285 @computed get isTodosServiceActive() { 284 @computed get isTodosServiceActive() {
286 return this.active && this.active.recipe.id === TODOS_RECIPE_ID; 285 return this.active && this.active.isTodosService;
287 } 286 }
288 287
289 one(id) { 288 one(id) {
@@ -484,7 +483,7 @@ export default class ServicesStore extends Store {
484 this._awake({ serviceId: service.id }); 483 this._awake({ serviceId: service.id });
485 service.lastUsed = Date.now(); 484 service.lastUsed = Date.now();
486 485
487 if (this.active.recipe.id === TODOS_RECIPE_ID && !this.stores.todos.settings.isFeatureEnabledByUser) { 486 if (this.isTodosServiceActive && !this.stores.todos.settings.isFeatureEnabledByUser) {
488 this.actions.todos.toggleTodosFeatureVisibility(); 487 this.actions.todos.toggleTodosFeatureVisibility();
489 } 488 }
490 489
@@ -718,7 +717,7 @@ export default class ServicesStore extends Store {
718 service.resetMessageCount(); 717 service.resetMessageCount();
719 service.lostRecipeConnection = false; 718 service.lostRecipeConnection = false;
720 719
721 if (service.recipe.id === TODOS_RECIPE_ID) { 720 if (service.isTodosService) {
722 return this.actions.todos.reload(); 721 return this.actions.todos.reload();
723 } 722 }
724 723
@@ -805,7 +804,7 @@ export default class ServicesStore extends Store {
805 804
806 @action _openDevTools({ serviceId }) { 805 @action _openDevTools({ serviceId }) {
807 const service = this.one(serviceId); 806 const service = this.one(serviceId);
808 if (service.recipe.id === TODOS_RECIPE_ID) { 807 if (service.isTodosService) {
809 this.actions.todos.openDevTools(); 808 this.actions.todos.openDevTools();
810 } else { 809 } else {
811 service.webview.openDevTools(); 810 service.webview.openDevTools();