aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorLibravatar Stefan Malzner <stefan@adlk.io>2019-10-03 17:15:46 +0200
committerLibravatar Stefan Malzner <stefan@adlk.io>2019-10-03 17:15:46 +0200
commit6e5bf64ef3ef858a5cc40025f61f3b1cf550d4fe (patch)
tree0429f9b7af2ff312dfbdb4b184a6cbeec28969ca /src
parentAutomatic i18n update (i18n.meetfranz.com) (diff)
parentupdate strings (diff)
downloadferdium-app-6e5bf64ef3ef858a5cc40025f61f3b1cf550d4fe.tar.gz
ferdium-app-6e5bf64ef3ef858a5cc40025f61f3b1cf550d4fe.tar.zst
ferdium-app-6e5bf64ef3ef858a5cc40025f61f3b1cf550d4fe.zip
Merge branch 'release/5.4.0' into i18n
Diffstat (limited to 'src')
-rw-r--r--src/api/server/LocalApi.js4
-rw-r--r--src/components/layout/Sidebar.js2
-rw-r--r--src/components/services/content/ServiceView.js16
-rw-r--r--src/components/services/content/ServiceWebview.js13
-rw-r--r--src/components/services/content/Services.js12
-rw-r--r--src/components/ui/Modal/styles.js2
-rw-r--r--src/features/spellchecker/index.js2
-rw-r--r--src/features/todos/components/TodosWebview.js3
-rw-r--r--src/features/todos/store.js4
-rw-r--r--src/features/webControls/components/WebControls.js239
-rw-r--r--src/features/webControls/containers/WebControlsScreen.js139
-rw-r--r--src/features/workspaces/components/WorkspaceDrawer.js4
-rw-r--r--src/features/workspaces/components/WorkspaceDrawerItem.js4
-rw-r--r--src/i18n/locales/defaultMessages.json78
-rw-r--r--src/i18n/locales/en-US.json5
-rw-r--r--src/i18n/messages/src/features/webControls/components/WebControls.json67
-rw-r--r--src/i18n/messages/src/features/workspaces/components/WorkspaceDrawerItem.json8
-rw-r--r--src/index.js17
-rw-r--r--src/lib/Menu.js16
-rw-r--r--src/lib/TouchBar.js4
-rw-r--r--src/models/Recipe.js4
-rw-r--r--src/models/Service.js44
-rw-r--r--src/stores/AppStore.js3
-rw-r--r--src/stores/ServicesStore.js3
-rw-r--r--src/stores/SettingsStore.js36
-rw-r--r--src/webview/contextMenu.js9
-rw-r--r--src/webview/recipe.js36
-rw-r--r--src/webview/spellchecker.js48
28 files changed, 711 insertions, 111 deletions
diff --git a/src/api/server/LocalApi.js b/src/api/server/LocalApi.js
index ab1604a27..383f38b16 100644
--- a/src/api/server/LocalApi.js
+++ b/src/api/server/LocalApi.js
@@ -45,13 +45,13 @@ export default class LocalApi {
45 const s = session.fromPartition(`persist:service-${serviceId}`); 45 const s = session.fromPartition(`persist:service-${serviceId}`);
46 46
47 debug('LocalApi::clearCache resolves', serviceId); 47 debug('LocalApi::clearCache resolves', serviceId);
48 return new Promise(resolve => s.clearCache(resolve)); 48 return s.clearCache();
49 } 49 }
50 50
51 async clearAppCache() { 51 async clearAppCache() {
52 const s = session.defaultSession; 52 const s = session.defaultSession;
53 53
54 debug('LocalApi::clearCache clearAppCache'); 54 debug('LocalApi::clearCache clearAppCache');
55 return new Promise(resolve => s.clearCache(resolve)); 55 return s.clearCache();
56 } 56 }
57} 57}
diff --git a/src/components/layout/Sidebar.js b/src/components/layout/Sidebar.js
index bac57d4dc..918298011 100644
--- a/src/components/layout/Sidebar.js
+++ b/src/components/layout/Sidebar.js
@@ -112,7 +112,7 @@ export default @observer class Sidebar extends Component {
112 this.updateToolTip(); 112 this.updateToolTip();
113 gaEvent(GA_CATEGORY_TODOS, 'toggleDrawer', 'sidebar'); 113 gaEvent(GA_CATEGORY_TODOS, 'toggleDrawer', 'sidebar');
114 }} 114 }}
115 className="sidebar__button sidebar__button--workspaces" 115 className={`sidebar__button sidebar__button--todos ${todosStore.isTodosPanelVisible ? 'is-active' : ''}`}
116 data-tip={`${intl.formatMessage(todosToggleMessage)} (${ctrlKey}+T)`} 116 data-tip={`${intl.formatMessage(todosToggleMessage)} (${ctrlKey}+T)`}
117 > 117 >
118 <i className="mdi mdi-check-all" /> 118 <i className="mdi mdi-check-all" />
diff --git a/src/components/services/content/ServiceView.js b/src/components/services/content/ServiceView.js
index f65f51346..e8df58a1e 100644
--- a/src/components/services/content/ServiceView.js
+++ b/src/components/services/content/ServiceView.js
@@ -12,6 +12,7 @@ import WebviewErrorHandler from './ErrorHandlers/WebviewErrorHandler';
12import ServiceDisabled from './ServiceDisabled'; 12import ServiceDisabled from './ServiceDisabled';
13import ServiceRestricted from './ServiceRestricted'; 13import ServiceRestricted from './ServiceRestricted';
14import ServiceWebview from './ServiceWebview'; 14import ServiceWebview from './ServiceWebview';
15import WebControlsScreen from '../../../features/webControls/containers/WebControlsScreen';
15 16
16export default @observer class ServiceView extends Component { 17export default @observer class ServiceView extends Component {
17 static propTypes = { 18 static propTypes = {
@@ -137,11 +138,16 @@ export default @observer class ServiceView extends Component {
137 type={service.restrictionType} 138 type={service.restrictionType}
138 /> 139 />
139 ) : ( 140 ) : (
140 <ServiceWebview 141 <>
141 service={service} 142 {service.recipe.id === 'franz-custom-website' && (
142 setWebviewReference={setWebviewReference} 143 <WebControlsScreen service={service} />
143 detachService={detachService} 144 )}
144 /> 145 <ServiceWebview
146 service={service}
147 setWebviewReference={setWebviewReference}
148 detachService={detachService}
149 />
150 </>
145 )} 151 )}
146 </> 152 </>
147 )} 153 )}
diff --git a/src/components/services/content/ServiceWebview.js b/src/components/services/content/ServiceWebview.js
index 7252c695f..b3198d36a 100644
--- a/src/components/services/content/ServiceWebview.js
+++ b/src/components/services/content/ServiceWebview.js
@@ -20,6 +20,13 @@ class ServiceWebview extends Component {
20 detachService({ service }); 20 detachService({ service });
21 } 21 }
22 22
23 refocusWebview = () => {
24 const { webview } = this;
25 if (!webview) return;
26 webview.view.blur();
27 webview.view.focus();
28 };
29
23 render() { 30 render() {
24 const { 31 const {
25 service, 32 service,
@@ -28,7 +35,10 @@ class ServiceWebview extends Component {
28 35
29 return ( 36 return (
30 <ElectronWebView 37 <ElectronWebView
31 ref={(webview) => { this.webview = webview; }} 38 ref={(webview) => {
39 this.webview = webview;
40 webview.view.addEventListener('did-stop-loading', this.refocusWebview);
41 }}
32 autosize 42 autosize
33 src={service.url} 43 src={service.url}
34 preload="./webview/recipe.js" 44 preload="./webview/recipe.js"
@@ -41,6 +51,7 @@ class ServiceWebview extends Component {
41 }} 51 }}
42 onUpdateTargetUrl={this.updateTargetUrl} 52 onUpdateTargetUrl={this.updateTargetUrl}
43 useragent={service.userAgent} 53 useragent={service.userAgent}
54 disablewebsecurity={service.recipe.disablewebsecurity}
44 allowpopups 55 allowpopups
45 /> 56 />
46 ); 57 );
diff --git a/src/components/services/content/Services.js b/src/components/services/content/Services.js
index 73c27bfb6..b6291666b 100644
--- a/src/components/services/content/Services.js
+++ b/src/components/services/content/Services.js
@@ -56,16 +56,24 @@ export default @observer @injectSheet(styles) class Services extends Component {
56 56
57 state = { 57 state = {
58 showConfetti: true, 58 showConfetti: true,
59 } 59 };
60
61 _confettiTimeout = null;
60 62
61 componentDidMount() { 63 componentDidMount() {
62 window.setTimeout(() => { 64 this._confettiTimeout = window.setTimeout(() => {
63 this.setState({ 65 this.setState({
64 showConfetti: false, 66 showConfetti: false,
65 }); 67 });
66 }, ms('8s')); 68 }, ms('8s'));
67 } 69 }
68 70
71 componentWillUnmount() {
72 if (this._confettiTimeout) {
73 clearTimeout(this._confettiTimeout);
74 }
75 }
76
69 render() { 77 render() {
70 const { 78 const {
71 services, 79 services,
diff --git a/src/components/ui/Modal/styles.js b/src/components/ui/Modal/styles.js
index 49b970c97..c2bebf9bb 100644
--- a/src/components/ui/Modal/styles.js
+++ b/src/components/ui/Modal/styles.js
@@ -13,7 +13,7 @@ export default theme => ({
13 display: 'flex', 13 display: 'flex',
14 }, 14 },
15 modal: { 15 modal: {
16 background: '#FFF', 16 background: theme.colorModalBackground,
17 maxWidth: '90%', 17 maxWidth: '90%',
18 height: 'auto', 18 height: 'auto',
19 margin: 'auto auto', 19 margin: 'auto auto',
diff --git a/src/features/spellchecker/index.js b/src/features/spellchecker/index.js
index a07f9f63a..fd8bc738a 100644
--- a/src/features/spellchecker/index.js
+++ b/src/features/spellchecker/index.js
@@ -16,7 +16,7 @@ export default function init(stores) {
16 16
17 config.isIncludedInCurrentPlan = isSpellcheckerIncludedInCurrentPlan !== undefined ? isSpellcheckerIncludedInCurrentPlan : DEFAULT_FEATURES_CONFIG.isSpellcheckerIncludedInCurrentPlan; 17 config.isIncludedInCurrentPlan = isSpellcheckerIncludedInCurrentPlan !== undefined ? isSpellcheckerIncludedInCurrentPlan : DEFAULT_FEATURES_CONFIG.isSpellcheckerIncludedInCurrentPlan;
18 18
19 if (!stores.user.data.isPremium && config.isIncludedInCurrentPlan && stores.settings.app.enableSpellchecking) { 19 if (!stores.user.data.isPremium && !config.isIncludedInCurrentPlan && stores.settings.app.enableSpellchecking) {
20 debug('Override settings.spellcheckerEnabled flag to false'); 20 debug('Override settings.spellcheckerEnabled flag to false');
21 21
22 Object.assign(stores.settings.app, { 22 Object.assign(stores.settings.app, {
diff --git a/src/features/todos/components/TodosWebview.js b/src/features/todos/components/TodosWebview.js
index d052da6f2..f24c0b044 100644
--- a/src/features/todos/components/TodosWebview.js
+++ b/src/features/todos/components/TodosWebview.js
@@ -35,9 +35,6 @@ const styles = theme => ({
35 35
36 transform: ({ isVisible, width }) => `translateX(${isVisible ? 0 : width}px)`, 36 transform: ({ isVisible, width }) => `translateX(${isVisible ? 0 : width}px)`,
37 37
38 '&:hover $closeTodosButton': {
39 opacity: 1,
40 },
41 '& webview': { 38 '& webview': {
42 height: '100%', 39 height: '100%',
43 }, 40 },
diff --git a/src/features/todos/store.js b/src/features/todos/store.js
index abf176604..4480b2545 100644
--- a/src/features/todos/store.js
+++ b/src/features/todos/store.js
@@ -162,6 +162,10 @@ export default class TodoStore extends FeatureStore {
162 theme: isDarkThemeActive ? ThemeType.dark : ThemeType.default, 162 theme: isDarkThemeActive ? ThemeType.dark : ThemeType.default,
163 }, 163 },
164 }); 164 });
165
166 this.webview.addEventListener('new-window', ({ url }) => {
167 this.actions.app.openExternalUrl({ url });
168 });
165 }; 169 };
166 170
167 _goToService = ({ url, serviceId }) => { 171 _goToService = ({ url, serviceId }) => {
diff --git a/src/features/webControls/components/WebControls.js b/src/features/webControls/components/WebControls.js
new file mode 100644
index 000000000..a39fcfe0e
--- /dev/null
+++ b/src/features/webControls/components/WebControls.js
@@ -0,0 +1,239 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react';
4import injectSheet from 'react-jss';
5import { Icon } from '@meetfranz/ui';
6import { defineMessages, intlShape } from 'react-intl';
7
8import {
9 mdiReload, mdiArrowRight, mdiArrowLeft, mdiHomeOutline, mdiEarth,
10} from '@mdi/js';
11
12const messages = defineMessages({
13 goHome: {
14 id: 'webControls.goHome',
15 defaultMessage: '!!!Home',
16 },
17 openInBrowser: {
18 id: 'webControls.openInBrowser',
19 defaultMessage: '!!!Open in Browser',
20 },
21 back: {
22 id: 'webControls.back',
23 defaultMessage: '!!!Back',
24 },
25 forward: {
26 id: 'webControls.forward',
27 defaultMessage: '!!!Forward',
28 },
29 reload: {
30 id: 'webControls.reload',
31 defaultMessage: '!!!Reload',
32 },
33});
34
35const styles = theme => ({
36 root: {
37 background: theme.colorBackground,
38 position: 'relative',
39 borderLeft: [1, 'solid', theme.todos.todosLayer.borderLeftColor],
40 zIndex: 300,
41 height: 50,
42 display: 'flex',
43 flexDirection: 'row',
44 alignItems: 'center',
45 padding: [0, 10],
46
47 '& + div': {
48 height: 'calc(100% - 50px)',
49 },
50 },
51 button: {
52 width: 30,
53 height: 50,
54 transition: 'opacity 0.25s',
55
56 '&:hover': {
57 opacity: 0.8,
58 },
59
60 '&:disabled': {
61 opacity: 0.5,
62 },
63 },
64 icon: {
65 width: '20px !important',
66 height: 20,
67 marginTop: 5,
68 },
69 input: {
70 marginBottom: 0,
71 height: 'auto',
72 margin: [0, 10],
73 flex: 1,
74 border: 0,
75 padding: [4, 10],
76 borderRadius: theme.borderRadius,
77 background: theme.inputBackground,
78 color: theme.inputColor,
79 },
80 inputButton: {
81 color: theme.colorText,
82 },
83});
84
85@injectSheet(styles) @observer
86class WebControls extends Component {
87 static propTypes = {
88 classes: PropTypes.object.isRequired,
89 goHome: PropTypes.func.isRequired,
90 canGoBack: PropTypes.bool.isRequired,
91 goBack: PropTypes.func.isRequired,
92 canGoForward: PropTypes.bool.isRequired,
93 goForward: PropTypes.func.isRequired,
94 reload: PropTypes.func.isRequired,
95 openInBrowser: PropTypes.func.isRequired,
96 url: PropTypes.string.isRequired,
97 navigate: PropTypes.func.isRequired,
98 }
99
100 static contextTypes = {
101 intl: intlShape,
102 };
103
104 static getDerivedStateFromProps(props, state) {
105 const { url } = props;
106 const { editUrl } = state;
107
108 if (!editUrl) {
109 return {
110 inputUrl: url,
111 editUrl: state.editUrl,
112 };
113 }
114 }
115
116 inputRef = React.createRef();
117
118 state = {
119 inputUrl: '',
120 editUrl: false,
121 }
122
123 render() {
124 const {
125 classes,
126 goHome,
127 canGoBack,
128 goBack,
129 canGoForward,
130 goForward,
131 reload,
132 openInBrowser,
133 url,
134 navigate,
135 } = this.props;
136
137 const {
138 inputUrl,
139 editUrl,
140 } = this.state;
141
142 const { intl } = this.context;
143
144 return (
145 <div className={classes.root}>
146 <button
147 onClick={goHome}
148 type="button"
149 className={classes.button}
150 data-tip={intl.formatMessage(messages.goHome)}
151 >
152 <Icon
153 icon={mdiHomeOutline}
154 className={classes.icon}
155 />
156 </button>
157 <button
158 onClick={goBack}
159 type="button"
160 className={classes.button}
161 disabled={!canGoBack}
162 data-tip={intl.formatMessage(messages.back)}
163 >
164 <Icon
165 icon={mdiArrowLeft}
166 className={classes.icon}
167 />
168 </button>
169 <button
170 onClick={goForward}
171 type="button"
172 className={classes.button}
173 disabled={!canGoForward}
174 data-tip={intl.formatMessage(messages.forward)}
175 >
176 <Icon
177 icon={mdiArrowRight}
178 className={classes.icon}
179 />
180 </button>
181 <button
182 onClick={reload}
183 type="button"
184 className={classes.button}
185 data-tip={intl.formatMessage(messages.reload)}
186 >
187 <Icon
188 icon={mdiReload}
189 className={classes.icon}
190 />
191 </button>
192 <input
193 value={editUrl ? inputUrl : url}
194 className={classes.input}
195 onChange={event => this.setState({
196 inputUrl: event.target.value,
197 })}
198 onFocus={(event) => {
199 event.target.select();
200 this.setState({
201 editUrl: true,
202 });
203 }}
204 onKeyDown={(event) => {
205 if (event.key === 'Enter') {
206 this.setState({
207 editUrl: false,
208 });
209 navigate(inputUrl);
210 this.inputRef.current.blur();
211 } else if (event.key === 'Escape') {
212 this.setState({
213 editUrl: false,
214 inputUrl: url,
215 });
216 event.target.blur();
217 }
218 }}
219 ref={this.inputRef}
220 />
221 <button
222 onClick={openInBrowser}
223 type="button"
224 className={classes.button}
225 data-tip={intl.formatMessage(messages.openInBrowser)}
226 data-place="left"
227 >
228 <Icon
229 icon={mdiEarth}
230 className={classes.icon}
231 />
232 </button>
233 {/* <ReactTooltip place="bottom" type="dark" effect="solid" /> */}
234 </div>
235 );
236 }
237}
238
239export default WebControls;
diff --git a/src/features/webControls/containers/WebControlsScreen.js b/src/features/webControls/containers/WebControlsScreen.js
new file mode 100644
index 000000000..cada01a6f
--- /dev/null
+++ b/src/features/webControls/containers/WebControlsScreen.js
@@ -0,0 +1,139 @@
1import React, { Component } from 'react';
2import { observer, inject } from 'mobx-react';
3import PropTypes from 'prop-types';
4
5import { autorun, observable } from 'mobx';
6import WebControls from '../components/WebControls';
7import ServicesStore from '../../../stores/ServicesStore';
8import Service from '../../../models/Service';
9
10const URL_EVENTS = [
11 'load-commit',
12 'will-navigate',
13 'did-navigate',
14 'did-navigate-in-page',
15];
16
17@inject('stores', 'actions') @observer
18class WebControlsScreen extends Component {
19 @observable url = '';
20
21 @observable canGoBack = false;
22
23 @observable canGoForward = false;
24
25 webview = null;
26
27 autorunDisposer = null;
28
29 componentDidMount() {
30 const { service } = this.props;
31
32 this.autorunDisposer = autorun(() => {
33 if (service.isAttached) {
34 this.webview = service.webview;
35
36 URL_EVENTS.forEach((event) => {
37 this.webview.addEventListener(event, (e) => {
38 if (!e.isMainFrame) return;
39
40 this.url = e.url;
41 this.canGoBack = this.webview.canGoBack();
42 this.canGoForward = this.webview.canGoForward();
43 });
44 });
45 }
46 });
47 }
48
49 componentWillUnmount() {
50 this.autorunDisposer();
51 }
52
53 goHome() {
54 const { reloadActive } = this.props.actions.service;
55
56 if (!this.webview) return;
57
58 reloadActive();
59 }
60
61 reload() {
62 if (!this.webview) return;
63
64 this.webview.reload();
65 }
66
67 goBack() {
68 if (!this.webview) return;
69
70 this.webview.goBack();
71 }
72
73 goForward() {
74 if (!this.webview) return;
75
76 this.webview.goForward();
77 }
78
79 navigate(newUrl) {
80 if (!this.webview) return;
81
82 let url = newUrl;
83
84 try {
85 url = new URL(url).toString();
86 } catch (err) {
87 // eslint-disable-next-line no-useless-escape
88 if (url.match(/^((?!-))(xn--)?[a-z0-9][a-z0-9-_]{0,61}[a-z0-9]{0,1}\.(xn--)?([a-z0-9\-]{1,61}|[a-z0-9-]{1,30}\.[a-z]{2,})$/)) {
89 url = `http://${url}`;
90 } else {
91 url = `https://www.google.com/search?query=${url}`;
92 }
93 }
94
95 this.webview.loadURL(url);
96 this.url = url;
97 }
98
99 openInBrowser() {
100 const { openExternalUrl } = this.props.actions.app;
101
102 if (!this.webview) return;
103
104 openExternalUrl({ url: this.url });
105 }
106
107 render() {
108 return (
109 <WebControls
110 goHome={() => this.goHome()}
111 reload={() => this.reload()}
112 openInBrowser={() => this.openInBrowser()}
113 canGoBack={this.canGoBack}
114 goBack={() => this.goBack()}
115 canGoForward={this.canGoForward}
116 goForward={() => this.goForward()}
117 navigate={url => this.navigate(url)}
118 url={this.url}
119 />
120 );
121 }
122}
123
124export default WebControlsScreen;
125
126WebControlsScreen.wrappedComponent.propTypes = {
127 service: PropTypes.instanceOf(Service).isRequired,
128 stores: PropTypes.shape({
129 services: PropTypes.instanceOf(ServicesStore).isRequired,
130 }).isRequired,
131 actions: PropTypes.shape({
132 app: PropTypes.shape({
133 openExternalUrl: PropTypes.func.isRequired,
134 }).isRequired,
135 service: PropTypes.shape({
136 reloadActive: PropTypes.func.isRequired,
137 }).isRequired,
138 }).isRequired,
139};
diff --git a/src/features/workspaces/components/WorkspaceDrawer.js b/src/features/workspaces/components/WorkspaceDrawer.js
index e7bc0b157..ee6f8416c 100644
--- a/src/features/workspaces/components/WorkspaceDrawer.js
+++ b/src/features/workspaces/components/WorkspaceDrawer.js
@@ -204,8 +204,9 @@ class WorkspaceDrawer extends Component {
204 }} 204 }}
205 services={getServicesForWorkspace(null)} 205 services={getServicesForWorkspace(null)}
206 isActive={actualWorkspace == null} 206 isActive={actualWorkspace == null}
207 shortcutIndex={0}
207 /> 208 />
208 {workspaces.map(workspace => ( 209 {workspaces.map((workspace, index) => (
209 <WorkspaceDrawerItem 210 <WorkspaceDrawerItem
210 key={workspace.id} 211 key={workspace.id}
211 name={workspace.name} 212 name={workspace.name}
@@ -218,6 +219,7 @@ class WorkspaceDrawer extends Component {
218 }} 219 }}
219 onContextMenuEditClick={() => workspaceActions.edit({ workspace })} 220 onContextMenuEditClick={() => workspaceActions.edit({ workspace })}
220 services={getServicesForWorkspace(workspace)} 221 services={getServicesForWorkspace(workspace)}
222 shortcutIndex={index + 1}
221 /> 223 />
222 ))} 224 ))}
223 <div 225 <div
diff --git a/src/features/workspaces/components/WorkspaceDrawerItem.js b/src/features/workspaces/components/WorkspaceDrawerItem.js
index 59a2144d3..18f424d8a 100644
--- a/src/features/workspaces/components/WorkspaceDrawerItem.js
+++ b/src/features/workspaces/components/WorkspaceDrawerItem.js
@@ -5,6 +5,7 @@ import { observer } from 'mobx-react';
5import injectSheet from 'react-jss'; 5import injectSheet from 'react-jss';
6import classnames from 'classnames'; 6import classnames from 'classnames';
7import { defineMessages, intlShape } from 'react-intl'; 7import { defineMessages, intlShape } from 'react-intl';
8import { ctrlKey } from '../../../environment';
8 9
9const { Menu } = remote; 10const { Menu } = remote;
10 11
@@ -69,6 +70,7 @@ class WorkspaceDrawerItem extends Component {
69 onClick: PropTypes.func.isRequired, 70 onClick: PropTypes.func.isRequired,
70 services: PropTypes.arrayOf(PropTypes.string).isRequired, 71 services: PropTypes.arrayOf(PropTypes.string).isRequired,
71 onContextMenuEditClick: PropTypes.func, 72 onContextMenuEditClick: PropTypes.func,
73 shortcutIndex: PropTypes.number.isRequired,
72 }; 74 };
73 75
74 static defaultProps = { 76 static defaultProps = {
@@ -87,6 +89,7 @@ class WorkspaceDrawerItem extends Component {
87 onClick, 89 onClick,
88 onContextMenuEditClick, 90 onContextMenuEditClick,
89 services, 91 services,
92 shortcutIndex,
90 } = this.props; 93 } = this.props;
91 const { intl } = this.context; 94 const { intl } = this.context;
92 95
@@ -112,6 +115,7 @@ class WorkspaceDrawerItem extends Component {
112 onContextMenu={() => ( 115 onContextMenu={() => (
113 onContextMenuEditClick && contextMenu.popup(remote.getCurrentWindow()) 116 onContextMenuEditClick && contextMenu.popup(remote.getCurrentWindow())
114 )} 117 )}
118 data-tip={`${shortcutIndex <= 9 ? `(${ctrlKey}+Alt+${shortcutIndex})` : ''}`}
115 > 119 >
116 <span 120 <span
117 className={classnames([ 121 className={classnames([
diff --git a/src/i18n/locales/defaultMessages.json b/src/i18n/locales/defaultMessages.json
index 0dfe2055a..e12b6b49b 100644
--- a/src/i18n/locales/defaultMessages.json
+++ b/src/i18n/locales/defaultMessages.json
@@ -4023,6 +4023,76 @@
4023 { 4023 {
4024 "descriptors": [ 4024 "descriptors": [
4025 { 4025 {
4026 "defaultMessage": "!!!Home",
4027 "end": {
4028 "column": 3,
4029 "line": 16
4030 },
4031 "file": "src/features/webControls/components/WebControls.js",
4032 "id": "webControls.goHome",
4033 "start": {
4034 "column": 10,
4035 "line": 13
4036 }
4037 },
4038 {
4039 "defaultMessage": "!!!Open in Browser",
4040 "end": {
4041 "column": 3,
4042 "line": 20
4043 },
4044 "file": "src/features/webControls/components/WebControls.js",
4045 "id": "webControls.openInBrowser",
4046 "start": {
4047 "column": 17,
4048 "line": 17
4049 }
4050 },
4051 {
4052 "defaultMessage": "!!!Back",
4053 "end": {
4054 "column": 3,
4055 "line": 24
4056 },
4057 "file": "src/features/webControls/components/WebControls.js",
4058 "id": "webControls.back",
4059 "start": {
4060 "column": 8,
4061 "line": 21
4062 }
4063 },
4064 {
4065 "defaultMessage": "!!!Forward",
4066 "end": {
4067 "column": 3,
4068 "line": 28
4069 },
4070 "file": "src/features/webControls/components/WebControls.js",
4071 "id": "webControls.forward",
4072 "start": {
4073 "column": 11,
4074 "line": 25
4075 }
4076 },
4077 {
4078 "defaultMessage": "!!!Reload",
4079 "end": {
4080 "column": 3,
4081 "line": 32
4082 },
4083 "file": "src/features/webControls/components/WebControls.js",
4084 "id": "webControls.reload",
4085 "start": {
4086 "column": 10,
4087 "line": 29
4088 }
4089 }
4090 ],
4091 "path": "src/features/webControls/components/WebControls.json"
4092 },
4093 {
4094 "descriptors": [
4095 {
4026 "defaultMessage": "!!!Create workspace", 4096 "defaultMessage": "!!!Create workspace",
4027 "end": { 4097 "end": {
4028 "column": 3, 4098 "column": 3,
@@ -4262,26 +4332,26 @@
4262 "defaultMessage": "!!!No services added yet", 4332 "defaultMessage": "!!!No services added yet",
4263 "end": { 4333 "end": {
4264 "column": 3, 4334 "column": 3,
4265 "line": 15 4335 "line": 16
4266 }, 4336 },
4267 "file": "src/features/workspaces/components/WorkspaceDrawerItem.js", 4337 "file": "src/features/workspaces/components/WorkspaceDrawerItem.js",
4268 "id": "workspaceDrawer.item.noServicesAddedYet", 4338 "id": "workspaceDrawer.item.noServicesAddedYet",
4269 "start": { 4339 "start": {
4270 "column": 22, 4340 "column": 22,
4271 "line": 12 4341 "line": 13
4272 } 4342 }
4273 }, 4343 },
4274 { 4344 {
4275 "defaultMessage": "!!!edit", 4345 "defaultMessage": "!!!edit",
4276 "end": { 4346 "end": {
4277 "column": 3, 4347 "column": 3,
4278 "line": 19 4348 "line": 20
4279 }, 4349 },
4280 "file": "src/features/workspaces/components/WorkspaceDrawerItem.js", 4350 "file": "src/features/workspaces/components/WorkspaceDrawerItem.js",
4281 "id": "workspaceDrawer.item.contextMenuEdit", 4351 "id": "workspaceDrawer.item.contextMenuEdit",
4282 "start": { 4352 "start": {
4283 "column": 19, 4353 "column": 19,
4284 "line": 16 4354 "line": 17
4285 } 4355 }
4286 } 4356 }
4287 ], 4357 ],
diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json
index dcb9d92a2..c2c6a9863 100644
--- a/src/i18n/locales/en-US.json
+++ b/src/i18n/locales/en-US.json
@@ -370,6 +370,11 @@
370 "validation.oneRequired": "At least one is required", 370 "validation.oneRequired": "At least one is required",
371 "validation.required": "{field} is required", 371 "validation.required": "{field} is required",
372 "validation.url": "{field} is not a valid URL", 372 "validation.url": "{field} is not a valid URL",
373 "webControls.back": "Back",
374 "webControls.forward": "Forward",
375 "webControls.goHome": "Home",
376 "webControls.openInBrowser": "Open in Browser",
377 "webControls.reload": "Reload",
373 "welcome.loginButton": "Login to your account", 378 "welcome.loginButton": "Login to your account",
374 "welcome.signupButton": "Create a free account", 379 "welcome.signupButton": "Create a free account",
375 "workspaceDrawer.addNewWorkspaceLabel": "Add new workspace", 380 "workspaceDrawer.addNewWorkspaceLabel": "Add new workspace",
diff --git a/src/i18n/messages/src/features/webControls/components/WebControls.json b/src/i18n/messages/src/features/webControls/components/WebControls.json
new file mode 100644
index 000000000..969437e98
--- /dev/null
+++ b/src/i18n/messages/src/features/webControls/components/WebControls.json
@@ -0,0 +1,67 @@
1[
2 {
3 "id": "webControls.goHome",
4 "defaultMessage": "!!!Home",
5 "file": "src/features/webControls/components/WebControls.js",
6 "start": {
7 "line": 13,
8 "column": 10
9 },
10 "end": {
11 "line": 16,
12 "column": 3
13 }
14 },
15 {
16 "id": "webControls.openInBrowser",
17 "defaultMessage": "!!!Open in Browser",
18 "file": "src/features/webControls/components/WebControls.js",
19 "start": {
20 "line": 17,
21 "column": 17
22 },
23 "end": {
24 "line": 20,
25 "column": 3
26 }
27 },
28 {
29 "id": "webControls.back",
30 "defaultMessage": "!!!Back",
31 "file": "src/features/webControls/components/WebControls.js",
32 "start": {
33 "line": 21,
34 "column": 8
35 },
36 "end": {
37 "line": 24,
38 "column": 3
39 }
40 },
41 {
42 "id": "webControls.forward",
43 "defaultMessage": "!!!Forward",
44 "file": "src/features/webControls/components/WebControls.js",
45 "start": {
46 "line": 25,
47 "column": 11
48 },
49 "end": {
50 "line": 28,
51 "column": 3
52 }
53 },
54 {
55 "id": "webControls.reload",
56 "defaultMessage": "!!!Reload",
57 "file": "src/features/webControls/components/WebControls.js",
58 "start": {
59 "line": 29,
60 "column": 10
61 },
62 "end": {
63 "line": 32,
64 "column": 3
65 }
66 }
67] \ No newline at end of file
diff --git a/src/i18n/messages/src/features/workspaces/components/WorkspaceDrawerItem.json b/src/i18n/messages/src/features/workspaces/components/WorkspaceDrawerItem.json
index 4ff190606..1b6664787 100644
--- a/src/i18n/messages/src/features/workspaces/components/WorkspaceDrawerItem.json
+++ b/src/i18n/messages/src/features/workspaces/components/WorkspaceDrawerItem.json
@@ -4,11 +4,11 @@
4 "defaultMessage": "!!!No services added yet", 4 "defaultMessage": "!!!No services added yet",
5 "file": "src/features/workspaces/components/WorkspaceDrawerItem.js", 5 "file": "src/features/workspaces/components/WorkspaceDrawerItem.js",
6 "start": { 6 "start": {
7 "line": 12, 7 "line": 13,
8 "column": 22 8 "column": 22
9 }, 9 },
10 "end": { 10 "end": {
11 "line": 15, 11 "line": 16,
12 "column": 3 12 "column": 3
13 } 13 }
14 }, 14 },
@@ -17,11 +17,11 @@
17 "defaultMessage": "!!!edit", 17 "defaultMessage": "!!!edit",
18 "file": "src/features/workspaces/components/WorkspaceDrawerItem.js", 18 "file": "src/features/workspaces/components/WorkspaceDrawerItem.js",
19 "start": { 19 "start": {
20 "line": 16, 20 "line": 17,
21 "column": 19 21 "column": 19
22 }, 22 },
23 "end": { 23 "end": {
24 "line": 19, 24 "line": 20,
25 "column": 3 25 "column": 3
26 } 26 }
27 } 27 }
diff --git a/src/index.js b/src/index.js
index d9d51fd5b..7de7a5e1c 100644
--- a/src/index.js
+++ b/src/index.js
@@ -331,22 +331,7 @@ app.on('login', (event, webContents, request, authInfo, callback) => {
331 debug('browser login event', authInfo); 331 debug('browser login event', authInfo);
332 event.preventDefault(); 332 event.preventDefault();
333 333
334 if (authInfo.isProxy && authInfo.scheme === 'basic') { 334 if (!authInfo.isProxy && authInfo.scheme === 'basic') {
335 debug('Sending service echo ping');
336 webContents.send('get-service-id');
337
338 ipcMain.once('service-id', (e, id) => {
339 debug('Received service id', id);
340
341 const ps = proxySettings.get(id);
342 if (ps) {
343 debug('Sending proxy auth callback for service', id);
344 callback(ps.user, ps.password);
345 } else {
346 debug('No proxy auth config found for', id);
347 }
348 });
349 } else if (authInfo.scheme === 'basic') {
350 debug('basic auth handler', authInfo); 335 debug('basic auth handler', authInfo);
351 basicAuthHandler(mainWindow, authInfo); 336 basicAuthHandler(mainWindow, authInfo);
352 } 337 }
diff --git a/src/lib/Menu.js b/src/lib/Menu.js
index 4aa2edaba..32bd1644b 100644
--- a/src/lib/Menu.js
+++ b/src/lib/Menu.js
@@ -302,6 +302,9 @@ const _templateFactory = intl => [
302 label: intl.formatMessage(menuItems.pasteAndMatchStyle), 302 label: intl.formatMessage(menuItems.pasteAndMatchStyle),
303 accelerator: 'Cmd+Shift+V', 303 accelerator: 'Cmd+Shift+V',
304 selector: 'pasteAndMatchStyle:', 304 selector: 'pasteAndMatchStyle:',
305 click() {
306 getActiveWebview().pasteAndMatchStyle();
307 },
305 }, 308 },
306 { 309 {
307 label: intl.formatMessage(menuItems.delete), 310 label: intl.formatMessage(menuItems.delete),
@@ -548,6 +551,11 @@ const _titleBarTemplateFactory = intl => [
548 visible: workspaceStore.isFeatureEnabled, 551 visible: workspaceStore.isFeatureEnabled,
549 }, 552 },
550 { 553 {
554 label: intl.formatMessage(menuItems.todos),
555 submenu: [],
556 visible: todosStore.isFeatureEnabled,
557 },
558 {
551 label: intl.formatMessage(menuItems.window), 559 label: intl.formatMessage(menuItems.window),
552 submenu: [ 560 submenu: [
553 { 561 {
@@ -862,6 +870,10 @@ export default class FranzMenu {
862 checked: service.isActive, 870 checked: service.isActive,
863 click: () => { 871 click: () => {
864 this.actions.service.setActive({ serviceId: service.id }); 872 this.actions.service.setActive({ serviceId: service.id });
873
874 if (isMac && i === 0) {
875 app.mainWindow.restore();
876 }
865 }, 877 },
866 }))); 878 })));
867 879
@@ -943,12 +955,12 @@ export default class FranzMenu {
943 gaEvent(GA_CATEGORY_TODOS, 'toggleDrawer', 'menu'); 955 gaEvent(GA_CATEGORY_TODOS, 'toggleDrawer', 'menu');
944 }, 956 },
945 enabled: this.stores.user.isLoggedIn && isFeatureEnabledByUser, 957 enabled: this.stores.user.isLoggedIn && isFeatureEnabledByUser,
946 }, {
947 type: 'separator',
948 }); 958 });
949 959
950 if (!isFeatureEnabledByUser) { 960 if (!isFeatureEnabledByUser) {
951 menu.push({ 961 menu.push({
962 type: 'separator',
963 }, {
952 label: intl.formatMessage(menuItems.enableTodos), 964 label: intl.formatMessage(menuItems.enableTodos),
953 click: () => { 965 click: () => {
954 todoActions.toggleTodosFeatureVisibility(); 966 todoActions.toggleTodosFeatureVisibility();
diff --git a/src/lib/TouchBar.js b/src/lib/TouchBar.js
index 97c02d194..1de46d2a3 100644
--- a/src/lib/TouchBar.js
+++ b/src/lib/TouchBar.js
@@ -29,7 +29,7 @@ export default class FranzTouchBar {
29 const { TouchBarButton, TouchBarSpacer } = TouchBar; 29 const { TouchBarButton, TouchBarSpacer } = TouchBar;
30 30
31 const buttons = []; 31 const buttons = [];
32 this.stores.services.enabled.forEach(((service) => { 32 this.stores.services.allDisplayed.forEach(((service) => {
33 buttons.push(new TouchBarButton({ 33 buttons.push(new TouchBarButton({
34 label: `${service.name}${service.unreadDirectMessageCount > 0 34 label: `${service.name}${service.unreadDirectMessageCount > 0
35 ? ' 🔴' : ''} ${service.unreadDirectMessageCount === 0 35 ? ' 🔴' : ''} ${service.unreadDirectMessageCount === 0
@@ -42,7 +42,7 @@ export default class FranzTouchBar {
42 }), new TouchBarSpacer({ size: 'small' })); 42 }), new TouchBarSpacer({ size: 'small' }));
43 })); 43 }));
44 44
45 const touchBar = new TouchBar(buttons); 45 const touchBar = new TouchBar({ items: buttons });
46 currentWindow.setTouchBar(touchBar); 46 currentWindow.setTouchBar(touchBar);
47 } else { 47 } else {
48 currentWindow.setTouchBar(null); 48 currentWindow.setTouchBar(null);
diff --git a/src/models/Recipe.js b/src/models/Recipe.js
index b0d60e75e..00c0f699f 100644
--- a/src/models/Recipe.js
+++ b/src/models/Recipe.js
@@ -36,6 +36,8 @@ export default class Recipe {
36 36
37 message = ''; 37 message = '';
38 38
39 disablewebsecurity = false;
40
39 constructor(data) { 41 constructor(data) {
40 if (!data) { 42 if (!data) {
41 throw Error('Recipe config not valid'); 43 throw Error('Recipe config not valid');
@@ -74,6 +76,8 @@ export default class Recipe {
74 this.urlInputPrefix = data.config.urlInputPrefix || this.urlInputPrefix; 76 this.urlInputPrefix = data.config.urlInputPrefix || this.urlInputPrefix;
75 this.urlInputSuffix = data.config.urlInputSuffix || this.urlInputSuffix; 77 this.urlInputSuffix = data.config.urlInputSuffix || this.urlInputSuffix;
76 78
79 this.disablewebsecurity = data.config.disablewebsecurity || this.disablewebsecurity;
80
77 this.message = data.config.message || this.message; 81 this.message = data.config.message || this.message;
78 } 82 }
79 83
diff --git a/src/models/Service.js b/src/models/Service.js
index 848a84aa2..e45c39564 100644
--- a/src/models/Service.js
+++ b/src/models/Service.js
@@ -134,6 +134,9 @@ export default class Service {
134 id: this.id, 134 id: this.id,
135 spellcheckerLanguage: this.spellcheckerLanguage, 135 spellcheckerLanguage: this.spellcheckerLanguage,
136 isDarkModeEnabled: this.isDarkModeEnabled, 136 isDarkModeEnabled: this.isDarkModeEnabled,
137 team: this.team,
138 url: this.url,
139 hasCustomIcon: this.hasCustomIcon,
137 }; 140 };
138 } 141 }
139 142
@@ -185,19 +188,24 @@ export default class Service {
185 return userAgent; 188 return userAgent;
186 } 189 }
187 190
188 initializeWebViewEvents({ handleIPCMessage, openWindow }) { 191 initializeWebViewEvents({ handleIPCMessage, openWindow, stores }) {
192 const webContents = this.webview.getWebContents();
193
189 this.webview.addEventListener('ipc-message', e => handleIPCMessage({ 194 this.webview.addEventListener('ipc-message', e => handleIPCMessage({
190 serviceId: this.id, 195 serviceId: this.id,
191 channel: e.channel, 196 channel: e.channel,
192 args: e.args, 197 args: e.args,
193 })); 198 }));
194 199
195 this.webview.addEventListener('new-window', (event, url, frameName, options) => openWindow({ 200 this.webview.addEventListener('new-window', (event, url, frameName, options) => {
196 event, 201 console.log('open window', event, url, frameName, options);
197 url, 202 openWindow({
198 frameName, 203 event,
199 options, 204 url,
200 })); 205 frameName,
206 options,
207 });
208 });
201 209
202 this.webview.addEventListener('did-start-loading', (event) => { 210 this.webview.addEventListener('did-start-loading', (event) => {
203 debug('Did start load', this.name, event); 211 debug('Did start load', this.name, event);
@@ -231,6 +239,28 @@ export default class Service {
231 debug('Service crashed', this.name); 239 debug('Service crashed', this.name);
232 this.hasCrashed = true; 240 this.hasCrashed = true;
233 }); 241 });
242
243 webContents.on('login', (event, request, authInfo, callback) => {
244 // const authCallback = callback;
245 debug('browser login event', authInfo);
246 event.preventDefault();
247
248 if (authInfo.isProxy && authInfo.scheme === 'basic') {
249 debug('Sending service echo ping');
250 webContents.send('get-service-id');
251
252 debug('Received service id', this.id);
253
254 const ps = stores.settings.proxy[this.id];
255
256 if (ps) {
257 debug('Sending proxy auth callback for service', this.id);
258 callback(ps.user, ps.password);
259 } else {
260 debug('No proxy auth config found for', this.id);
261 }
262 }
263 });
234 } 264 }
235 265
236 initializeWebViewListener() { 266 initializeWebViewListener() {
diff --git a/src/stores/AppStore.js b/src/stores/AppStore.js
index 315235ba4..0398b7533 100644
--- a/src/stores/AppStore.js
+++ b/src/stores/AppStore.js
@@ -23,6 +23,7 @@ import { getLocale } from '../helpers/i18n-helpers';
23 23
24import { getServiceIdsFromPartitions, removeServicePartitionDirectory } from '../helpers/service-helpers.js'; 24import { getServiceIdsFromPartitions, removeServicePartitionDirectory } from '../helpers/service-helpers.js';
25import { isValidExternalURL } from '../helpers/url-helpers'; 25import { isValidExternalURL } from '../helpers/url-helpers';
26import { sleep } from '../helpers/async-helpers';
26 27
27const debug = require('debug')('Franz:AppStore'); 28const debug = require('debug')('Franz:AppStore');
28 29
@@ -327,6 +328,8 @@ export default class AppStore extends Store {
327 328
328 await clearAppCache._promise; 329 await clearAppCache._promise;
329 330
331 await sleep(ms('1s'));
332
330 this.getAppCacheSizeRequest.execute(); 333 this.getAppCacheSizeRequest.execute();
331 334
332 this.isClearingAllCache = false; 335 this.isClearingAllCache = false;
diff --git a/src/stores/ServicesStore.js b/src/stores/ServicesStore.js
index d1fd2be3d..70b775503 100644
--- a/src/stores/ServicesStore.js
+++ b/src/stores/ServicesStore.js
@@ -345,6 +345,7 @@ export default class ServicesStore extends Store {
345 service.initializeWebViewEvents({ 345 service.initializeWebViewEvents({
346 handleIPCMessage: this.actions.service.handleIPCMessage, 346 handleIPCMessage: this.actions.service.handleIPCMessage,
347 openWindow: this.actions.service.openWindow, 347 openWindow: this.actions.service.openWindow,
348 stores: this.stores,
348 }); 349 });
349 service.initializeWebViewListener(); 350 service.initializeWebViewListener();
350 } 351 }
@@ -683,6 +684,8 @@ export default class ServicesStore extends Store {
683 const serviceData = data; 684 const serviceData = data;
684 const recipe = this.stores.recipes.one(recipeId); 685 const recipe = this.stores.recipes.one(recipeId);
685 686
687 if (!recipe) return;
688
686 if (recipe.hasTeamId && recipe.hasCustomUrl && data.team && data.customUrl) { 689 if (recipe.hasTeamId && recipe.hasCustomUrl && data.team && data.customUrl) {
687 delete serviceData.team; 690 delete serviceData.team;
688 } 691 }
diff --git a/src/stores/SettingsStore.js b/src/stores/SettingsStore.js
index a456195bf..75bb38fe0 100644
--- a/src/stores/SettingsStore.js
+++ b/src/stores/SettingsStore.js
@@ -1,12 +1,11 @@
1import { ipcRenderer } from 'electron'; 1import { ipcRenderer } from 'electron';
2import { 2import {
3 action, computed, observable, set, 3 action, computed, observable,
4} from 'mobx'; 4} from 'mobx';
5import localStorage from 'mobx-localstorage'; 5import localStorage from 'mobx-localstorage';
6 6
7import Store from './lib/Store'; 7import Store from './lib/Store';
8import Request from './lib/Request'; 8import Request from './lib/Request';
9import CachedRequest from './lib/CachedRequest';
10import { getLocale } from '../helpers/i18n-helpers'; 9import { getLocale } from '../helpers/i18n-helpers';
11 10
12import { DEFAULT_APP_SETTINGS, FILE_SYSTEM_SETTINGS_TYPES } from '../config'; 11import { DEFAULT_APP_SETTINGS, FILE_SYSTEM_SETTINGS_TYPES } from '../config';
@@ -15,12 +14,8 @@ import { SPELLCHECKER_LOCALES } from '../i18n/languages';
15const debug = require('debug')('Franz:SettingsStore'); 14const debug = require('debug')('Franz:SettingsStore');
16 15
17export default class SettingsStore extends Store { 16export default class SettingsStore extends Store {
18 @observable appSettingsRequest = new CachedRequest(this.api.local, 'getAppSettings');
19
20 @observable updateAppSettingsRequest = new Request(this.api.local, 'updateAppSettings'); 17 @observable updateAppSettingsRequest = new Request(this.api.local, 'updateAppSettings');
21 18
22 fileSystemSettingsRequests = [];
23
24 fileSystemSettingsTypes = FILE_SYSTEM_SETTINGS_TYPES; 19 fileSystemSettingsTypes = FILE_SYSTEM_SETTINGS_TYPES;
25 20
26 @observable _fileSystemSettingsCache = { 21 @observable _fileSystemSettingsCache = {
@@ -35,14 +30,10 @@ export default class SettingsStore extends Store {
35 this.actions.settings.update.listen(this._update.bind(this)); 30 this.actions.settings.update.listen(this._update.bind(this));
36 this.actions.settings.remove.listen(this._remove.bind(this)); 31 this.actions.settings.remove.listen(this._remove.bind(this));
37 32
38 this.fileSystemSettingsTypes.forEach((type) => {
39 this.fileSystemSettingsRequests[type] = new CachedRequest(this.api.local, 'getAppSettings');
40 });
41
42 ipcRenderer.on('appSettings', (event, resp) => { 33 ipcRenderer.on('appSettings', (event, resp) => {
43 debug('Get appSettings resolves', resp.type, resp.data); 34 debug('Get appSettings resolves', resp.type, resp.data);
44 35
45 this._fileSystemSettingsCache[resp.type] = resp.data; 36 Object.assign(this._fileSystemSettingsCache[resp.type], resp.data);
46 }); 37 });
47 38
48 this.fileSystemSettingsTypes.forEach((type) => { 39 this.fileSystemSettingsTypes.forEach((type) => {
@@ -51,8 +42,6 @@ export default class SettingsStore extends Store {
51 } 42 }
52 43
53 async setup() { 44 async setup() {
54 // We need to wait until `appSettingsRequest` has been executed once, otherwise we can't patch the result. If we don't wait we'd run into an issue with mobx not reacting to changes of previously not existing keys
55 await this.appSettingsRequest._promise;
56 await this._migrate(); 45 await this._migrate();
57 } 46 }
58 47
@@ -61,21 +50,6 @@ export default class SettingsStore extends Store {
61 } 50 }
62 51
63 @computed get proxy() { 52 @computed get proxy() {
64 // // We need to provide the final data structure as mobx autoruns won't work
65 // const proxySettings = observable({});
66 // this.stores.services.all.forEach((service) => {
67 // proxySettings[service.id] = {
68 // isEnabled: false,
69 // host: null,
70 // user: null,
71 // password: null,
72 // };
73 // });
74
75 // debug('this._fileSystemSettingsCache.proxy', this._fileSystemSettingsCache.proxy, proxySettings);
76
77 // return Object.assign(proxySettings, this._fileSystemSettingsCache.proxy);
78
79 return this._fileSystemSettingsCache.proxy || {}; 53 return this._fileSystemSettingsCache.proxy || {};
80 } 54 }
81 55
@@ -117,7 +91,7 @@ export default class SettingsStore extends Store {
117 data, 91 data,
118 }); 92 });
119 93
120 set(this._fileSystemSettingsCache[type], data); 94 Object.assign(this._fileSystemSettingsCache[type], data);
121 } 95 }
122 } 96 }
123 97
@@ -197,8 +171,4 @@ export default class SettingsStore extends Store {
197 }); 171 });
198 } 172 }
199 } 173 }
200
201 _getFileBasedSettings(type) {
202 ipcRenderer.send('getAppSettings', type);
203 }
204} 174}
diff --git a/src/webview/contextMenu.js b/src/webview/contextMenu.js
index 83914f581..d3b976554 100644
--- a/src/webview/contextMenu.js
+++ b/src/webview/contextMenu.js
@@ -255,9 +255,9 @@ const buildMenuTpl = (props, suggestions, isSpellcheckEnabled, defaultSpellcheck
255 }, 255 },
256 { 256 {
257 id: 'resetToDefault', 257 id: 'resetToDefault',
258 label: `Reset to system default (${SPELLCHECKER_LOCALES[defaultSpellcheckerLanguage]})`, 258 label: `Reset to system default (${defaultSpellcheckerLanguage === 'automatic' ? 'Automatic' : SPELLCHECKER_LOCALES[defaultSpellcheckerLanguage]})`,
259 type: 'radio', 259 type: 'radio',
260 visible: defaultSpellcheckerLanguage !== spellcheckerLanguage, 260 visible: defaultSpellcheckerLanguage !== spellcheckerLanguage || (defaultSpellcheckerLanguage !== 'automatic' && spellcheckerLanguage === 'automatic'),
261 click() { 261 click() {
262 debug('Resetting service spellchecker to system default'); 262 debug('Resetting service spellchecker to system default');
263 ipcRenderer.sendToHost('set-service-spellchecker-language', 'reset'); 263 ipcRenderer.sendToHost('set-service-spellchecker-language', 'reset');
@@ -297,12 +297,13 @@ const buildMenuTpl = (props, suggestions, isSpellcheckEnabled, defaultSpellcheck
297}; 297};
298 298
299export default function contextMenu(spellcheckProvider, isSpellcheckEnabled, getDefaultSpellcheckerLanguage, getSpellcheckerLanguage) { 299export default function contextMenu(spellcheckProvider, isSpellcheckEnabled, getDefaultSpellcheckerLanguage, getSpellcheckerLanguage) {
300 webContents.on('context-menu', (e, props) => { 300 webContents.on('context-menu', async (e, props) => {
301 e.preventDefault(); 301 e.preventDefault();
302 302
303 let suggestions = []; 303 let suggestions = [];
304 if (spellcheckProvider && props.misspelledWord) { 304 if (spellcheckProvider && props.misspelledWord) {
305 suggestions = spellcheckProvider.getSuggestion(props.misspelledWord); 305 debug('Mispelled word', props.misspelledWord);
306 suggestions = await spellcheckProvider.getSuggestion(props.misspelledWord);
306 307
307 debug('Suggestions', suggestions); 308 debug('Suggestions', suggestions);
308 } 309 }
diff --git a/src/webview/recipe.js b/src/webview/recipe.js
index c223b73de..e3e13b726 100644
--- a/src/webview/recipe.js
+++ b/src/webview/recipe.js
@@ -12,6 +12,7 @@ import contextMenu from './contextMenu';
12import './notifications'; 12import './notifications';
13 13
14import { DEFAULT_APP_SETTINGS } from '../config'; 14import { DEFAULT_APP_SETTINGS } from '../config';
15import { isDevMode } from '../environment';
15 16
16const debug = require('debug')('Franz:Plugin'); 17const debug = require('debug')('Franz:Plugin');
17 18
@@ -32,7 +33,7 @@ class RecipeController {
32 'settings-update': 'updateAppSettings', 33 'settings-update': 'updateAppSettings',
33 'service-settings-update': 'updateServiceSettings', 34 'service-settings-update': 'updateServiceSettings',
34 'get-service-id': 'serviceIdEcho', 35 'get-service-id': 'serviceIdEcho',
35 } 36 };
36 37
37 constructor() { 38 constructor() {
38 this.initialize(); 39 this.initialize();
@@ -173,11 +174,42 @@ new RecipeController();
173// Patching window.open 174// Patching window.open
174const originalWindowOpen = window.open; 175const originalWindowOpen = window.open;
175 176
177
176window.open = (url, frameName, features) => { 178window.open = (url, frameName, features) => {
179 if (!url && !frameName && !features) {
180 // The service hasn't yet supplied a URL (as used in Skype).
181 // Return a new dummy window object and wait for the service to change the properties
182 const newWindow = {
183 location: {
184 href: '',
185 },
186 };
187
188 const checkInterval = setInterval(() => {
189 // Has the service changed the URL yet?
190 if (newWindow.location.href !== '') {
191 // Open the new URL
192 ipcRenderer.sendToHost('new-window', newWindow.location.href);
193 clearInterval(checkInterval);
194 }
195 }, 0);
196
197 setTimeout(() => {
198 // Stop checking for location changes after 1 second
199 clearInterval(checkInterval);
200 }, 1000);
201
202 return newWindow;
203 }
204
177 // We need to differentiate if the link should be opened in a popup or in the systems default browser 205 // We need to differentiate if the link should be opened in a popup or in the systems default browser
178 if (!frameName && !features) { 206 if (!frameName && !features && typeof features !== 'string') {
179 return ipcRenderer.sendToHost('new-window', url); 207 return ipcRenderer.sendToHost('new-window', url);
180 } 208 }
181 209
182 return originalWindowOpen(url, frameName, features); 210 return originalWindowOpen(url, frameName, features);
183}; 211};
212
213if (isDevMode) {
214 window.log = console.log;
215}
diff --git a/src/webview/spellchecker.js b/src/webview/spellchecker.js
index 9158b3b94..06cbd283a 100644
--- a/src/webview/spellchecker.js
+++ b/src/webview/spellchecker.js
@@ -1,6 +1,7 @@
1import { webFrame } from 'electron'; 1import { webFrame } from 'electron';
2import { SpellCheckerProvider } from 'electron-hunspell'; 2import { attachSpellCheckProvider, SpellCheckerProvider } from 'electron-hunspell';
3import path from 'path'; 3import path from 'path';
4import { readFileSync } from 'fs';
4 5
5import { DICTIONARY_PATH } from '../config'; 6import { DICTIONARY_PATH } from '../config';
6import { SPELLCHECKER_LOCALES } from '../i18n/languages'; 7import { SPELLCHECKER_LOCALES } from '../i18n/languages';
@@ -10,18 +11,21 @@ const debug = require('debug')('Franz:spellchecker');
10let provider; 11let provider;
11let currentDict; 12let currentDict;
12let _isEnabled = false; 13let _isEnabled = false;
14let attached;
15
16const DEFAULT_LOCALE = 'en-us';
13 17
14async function loadDictionary(locale) { 18async function loadDictionary(locale) {
15 try { 19 try {
16 const fileLocation = path.join(DICTIONARY_PATH, `hunspell-dict-${locale}/${locale}`); 20 const fileLocation = path.join(DICTIONARY_PATH, `hunspell-dict-${locale}/${locale}`);
17 await provider.loadDictionary(locale, `${fileLocation}.dic`, `${fileLocation}.aff`);
18 debug('Loaded dictionary', locale, 'from', fileLocation); 21 debug('Loaded dictionary', locale, 'from', fileLocation);
22 return provider.loadDictionary(locale, readFileSync(`${fileLocation}.dic`), readFileSync(`${fileLocation}.aff`));
19 } catch (err) { 23 } catch (err) {
20 console.error('Could not load dictionary', err); 24 console.error('Could not load dictionary', err);
21 } 25 }
22} 26}
23 27
24export async function switchDict(locale) { 28export async function switchDict(locale = DEFAULT_LOCALE) {
25 try { 29 try {
26 debug('Trying to load dictionary', locale); 30 debug('Trying to load dictionary', locale);
27 31
@@ -40,8 +44,8 @@ export async function switchDict(locale) {
40 if (currentDict) { 44 if (currentDict) {
41 provider.unloadDictionary(locale); 45 provider.unloadDictionary(locale);
42 } 46 }
43 loadDictionary(locale); 47 await loadDictionary(locale);
44 provider.switchDictionary(locale); 48 await attached.switchLanguage(locale);
45 49
46 debug('Switched dictionary to', locale); 50 debug('Switched dictionary to', locale);
47 51
@@ -52,18 +56,32 @@ export async function switchDict(locale) {
52 } 56 }
53} 57}
54 58
55export default async function initialize(languageCode = 'en-us') { 59export function getSpellcheckerLocaleByFuzzyIdentifier(identifier) {
60 const locales = Object.keys(SPELLCHECKER_LOCALES).filter(key => key === identifier.toLowerCase() || key.split('-')[0] === identifier.toLowerCase());
61
62 if (locales.length >= 1) {
63 return locales[0];
64 }
65
66 return null;
67}
68
69export default async function initialize(languageCode = DEFAULT_LOCALE) {
56 try { 70 try {
57 provider = new SpellCheckerProvider(); 71 provider = new SpellCheckerProvider();
58 const locale = languageCode.toLowerCase(); 72 const locale = getSpellcheckerLocaleByFuzzyIdentifier(languageCode);
59 73
60 debug('Init spellchecker'); 74 debug('Init spellchecker');
61 await provider.initialize(); 75 await provider.initialize();
62 // await loadDictionaries();
63 76
64 debug('Available spellchecker dictionaries', provider.availableDictionaries); 77 debug('Attaching spellcheck provider');
78 attached = await attachSpellCheckProvider(provider);
79
80 const availableDictionaries = await provider.getAvailableDictionaries();
65 81
66 switchDict(locale); 82 debug('Available spellchecker dictionaries', availableDictionaries);
83
84 await switchDict(locale);
67 85
68 return provider; 86 return provider;
69 } catch (err) { 87 } catch (err) {
@@ -83,13 +101,3 @@ export function disable() {
83 currentDict = null; 101 currentDict = null;
84 } 102 }
85} 103}
86
87export function getSpellcheckerLocaleByFuzzyIdentifier(identifier) {
88 const locales = Object.keys(SPELLCHECKER_LOCALES).filter(key => key === identifier.toLowerCase() || key.split('-')[0] === identifier.toLowerCase());
89
90 if (locales.length >= 1) {
91 return locales[0];
92 }
93
94 return null;
95}