aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Stefan Malzner <stefan@adlk.io>2017-12-15 14:44:46 +0100
committerLibravatar GitHub <noreply@github.com>2017-12-15 14:44:46 +0100
commitdc1dd2e857114fac2462f18ea774ddacb287fa81 (patch)
tree32b701de50c505abc95ceddc7c429df85c81f041
parentRemove IME handlers (diff)
parentMerge pull request #475 from meetfranz/feature/service-improvements (diff)
downloadferdium-app-dc1dd2e857114fac2462f18ea774ddacb287fa81.tar.gz
ferdium-app-dc1dd2e857114fac2462f18ea774ddacb287fa81.tar.zst
ferdium-app-dc1dd2e857114fac2462f18ea774ddacb287fa81.zip
Merge branch 'develop' into feature/macOS-copy-paste
-rw-r--r--gulpfile.babel.js6
-rw-r--r--src/api/server/LocalApi.js4
-rw-r--r--src/components/services/tabs/TabItem.js2
-rw-r--r--src/components/settings/services/EditServiceForm.js77
-rw-r--r--src/components/settings/settings/EditSettingsForm.js5
-rw-r--r--src/containers/settings/EditServiceScreen.js20
-rw-r--r--src/i18n/locales/en-US.json5
-rw-r--r--src/lib/Menu.js2
-rw-r--r--src/models/Recipe.js9
-rw-r--r--src/models/Service.js4
-rw-r--r--src/models/Settings.js6
-rw-r--r--src/stores/ServicesStore.js10
-rw-r--r--src/stores/SettingsStore.js2
-rw-r--r--src/styles/content-tabs.scss12
-rw-r--r--src/styles/input.scss4
-rw-r--r--src/styles/settings.scss23
-rw-r--r--src/webview/plugin.js28
-rw-r--r--src/webview/spellchecker.js65
18 files changed, 208 insertions, 76 deletions
diff --git a/gulpfile.babel.js b/gulpfile.babel.js
index d947974b3..b50001b2d 100644
--- a/gulpfile.babel.js
+++ b/gulpfile.babel.js
@@ -110,7 +110,11 @@ export function watch() {
110} 110}
111 111
112export function webserver() { 112export function webserver() {
113 gulp.src(paths.dest) 113 gulp.src([
114 paths.dest,
115 `!${paths.dest}/electron/**`,
116 `!${paths.dest}/webview/**`,
117 ])
114 .pipe(server({ 118 .pipe(server({
115 livereload: true, 119 livereload: true,
116 })); 120 }));
diff --git a/src/api/server/LocalApi.js b/src/api/server/LocalApi.js
index eba236f16..79ac6e12f 100644
--- a/src/api/server/LocalApi.js
+++ b/src/api/server/LocalApi.js
@@ -1,5 +1,3 @@
1import SettingsModel from '../../models/Settings';
2
3export default class LocalApi { 1export default class LocalApi {
4 // App 2 // App
5 async updateAppSettings(data) { 3 async updateAppSettings(data) {
@@ -15,7 +13,7 @@ export default class LocalApi {
15 async getAppSettings() { 13 async getAppSettings() {
16 const settingsString = localStorage.getItem('app'); 14 const settingsString = localStorage.getItem('app');
17 try { 15 try {
18 const settings = new SettingsModel(JSON.parse(settingsString) || {}); 16 const settings = JSON.parse(settingsString) || {};
19 console.debug('LocalApi::getAppSettings resolves', settings); 17 console.debug('LocalApi::getAppSettings resolves', settings);
20 18
21 return settings; 19 return settings;
diff --git a/src/components/services/tabs/TabItem.js b/src/components/services/tabs/TabItem.js
index 8403d9462..7aed8fda7 100644
--- a/src/components/services/tabs/TabItem.js
+++ b/src/components/services/tabs/TabItem.js
@@ -126,7 +126,7 @@ class TabItem extends Component {
126 const menu = Menu.buildFromTemplate(menuTemplate); 126 const menu = Menu.buildFromTemplate(menuTemplate);
127 127
128 let notificationBadge = null; 128 let notificationBadge = null;
129 if ((showMessageBadgeWhenMutedSetting || service.isNotificationEnabled) && showMessageBadgesEvenWhenMuted) { 129 if ((showMessageBadgeWhenMutedSetting || service.isNotificationEnabled) && showMessageBadgesEvenWhenMuted && service.isBadgeEnabled) {
130 notificationBadge = ( 130 notificationBadge = (
131 <span> 131 <span>
132 {service.unreadDirectMessageCount > 0 && ( 132 {service.unreadDirectMessageCount > 0 && (
diff --git a/src/components/settings/services/EditServiceForm.js b/src/components/settings/services/EditServiceForm.js
index 36cefe87c..4458c4c5a 100644
--- a/src/components/settings/services/EditServiceForm.js
+++ b/src/components/settings/services/EditServiceForm.js
@@ -47,6 +47,10 @@ const messages = defineMessages({
47 id: 'settings.service.form.tabOnPremise', 47 id: 'settings.service.form.tabOnPremise',
48 defaultMessage: '!!!Self hosted ⭐️', 48 defaultMessage: '!!!Self hosted ⭐️',
49 }, 49 },
50 useHostedService: {
51 id: 'settings.service.form.useHostedService',
52 defaultMessage: '!!!Use the hosted {name} service.',
53 },
50 customUrlValidationError: { 54 customUrlValidationError: {
51 id: 'settings.service.form.customUrlValidationError', 55 id: 'settings.service.form.customUrlValidationError',
52 defaultMessage: '!!!Could not validate custom {name} server.', 56 defaultMessage: '!!!Could not validate custom {name} server.',
@@ -67,6 +71,18 @@ const messages = defineMessages({
67 id: 'settings.service.form.isMutedInfo', 71 id: 'settings.service.form.isMutedInfo',
68 defaultMessage: '!!!When disabled, all notification sounds and audio playback are muted', 72 defaultMessage: '!!!When disabled, all notification sounds and audio playback are muted',
69 }, 73 },
74 headlineNotifications: {
75 id: 'settings.service.form.headlineNotifications',
76 defaultMessage: '!!!Notifications',
77 },
78 headlineBadges: {
79 id: 'settings.service.form.headlineBadges',
80 defaultMessage: '!!!Unread message dadges',
81 },
82 headlineGeneral: {
83 id: 'settings.service.form.headlineGeneral',
84 defaultMessage: '!!!General',
85 },
70}); 86});
71 87
72@observer 88@observer
@@ -108,7 +124,6 @@ export default class EditServiceForm extends Component {
108 this.props.form.submit({ 124 this.props.form.submit({
109 onSuccess: async (form) => { 125 onSuccess: async (form) => {
110 const values = form.values(); 126 const values = form.values();
111
112 let isValid = true; 127 let isValid = true;
113 128
114 if (recipe.validateUrl && values.customUrl) { 129 if (recipe.validateUrl && values.customUrl) {
@@ -166,6 +181,13 @@ export default class EditServiceForm extends Component {
166 /> 181 />
167 ); 182 );
168 183
184 let activeTabIndex = 0;
185 if (recipe.hasHostedOption && service.team) {
186 activeTabIndex = 1;
187 } else if (recipe.hasHostedOption && service.customUrl) {
188 activeTabIndex = 2;
189 }
190
169 return ( 191 return (
170 <div className="settings__main"> 192 <div className="settings__main">
171 <div className="settings__header"> 193 <div className="settings__header">
@@ -198,11 +220,20 @@ export default class EditServiceForm extends Component {
198 <Input field={form.$('name')} focus /> 220 <Input field={form.$('name')} focus />
199 {(recipe.hasTeamId || recipe.hasCustomUrl) && ( 221 {(recipe.hasTeamId || recipe.hasCustomUrl) && (
200 <Tabs 222 <Tabs
201 active={service.customUrl ? 1 : 0} 223 active={activeTabIndex}
202 > 224 >
225 {recipe.hasHostedOption && (
226 <TabItem title={recipe.name}>
227 {intl.formatMessage(messages.useHostedService, { name: recipe.name })}
228 </TabItem>
229 )}
203 {recipe.hasTeamId && ( 230 {recipe.hasTeamId && (
204 <TabItem title={intl.formatMessage(messages.tabHosted)}> 231 <TabItem title={intl.formatMessage(messages.tabHosted)}>
205 <Input field={form.$('team')} suffix={recipe.urlInputSuffix} /> 232 <Input
233 field={form.$('team')}
234 prefix={recipe.urlInputPrefix}
235 suffix={recipe.urlInputSuffix}
236 />
206 </TabItem> 237 </TabItem>
207 )} 238 )}
208 {recipe.hasCustomUrl && ( 239 {recipe.hasCustomUrl && (
@@ -231,20 +262,32 @@ export default class EditServiceForm extends Component {
231 </Tabs> 262 </Tabs>
232 )} 263 )}
233 <div className="settings__options"> 264 <div className="settings__options">
234 <Toggle field={form.$('isNotificationEnabled')} /> 265 <div className="settings__settings-group">
235 {recipe.hasIndirectMessages && ( 266 <h3>{intl.formatMessage(messages.headlineNotifications)}</h3>
236 <div> 267 <Toggle field={form.$('isNotificationEnabled')} />
237 <Toggle field={form.$('isIndirectMessageBadgeEnabled')} /> 268 <Toggle field={form.$('isMuted')} />
238 <p className="settings__help"> 269 <p className="settings__help">
239 {intl.formatMessage(messages.indirectMessageInfo)} 270 {intl.formatMessage(messages.isMutedInfo)}
240 </p> 271 </p>
241 </div> 272 </div>
242 )} 273
243 <Toggle field={form.$('isMuted')} /> 274 <div className="settings__settings-group">
244 <p className="settings__help"> 275 <h3>{intl.formatMessage(messages.headlineBadges)}</h3>
245 {intl.formatMessage(messages.isMutedInfo)} 276 <Toggle field={form.$('isBadgeEnabled')} />
246 </p> 277 {recipe.hasIndirectMessages && form.$('isBadgeEnabled').value && (
247 <Toggle field={form.$('isEnabled')} /> 278 <div>
279 <Toggle field={form.$('isIndirectMessageBadgeEnabled')} />
280 <p className="settings__help">
281 {intl.formatMessage(messages.indirectMessageInfo)}
282 </p>
283 </div>
284 )}
285 </div>
286
287 <div className="settings__settings-group">
288 <h3>{intl.formatMessage(messages.headlineGeneral)}</h3>
289 <Toggle field={form.$('isEnabled')} />
290 </div>
248 </div> 291 </div>
249 {recipe.message && ( 292 {recipe.message && (
250 <p className="settings__message"> 293 <p className="settings__message">
diff --git a/src/components/settings/settings/EditSettingsForm.js b/src/components/settings/settings/EditSettingsForm.js
index 878e46d6d..ff398aa33 100644
--- a/src/components/settings/settings/EditSettingsForm.js
+++ b/src/components/settings/settings/EditSettingsForm.js
@@ -64,10 +64,6 @@ const messages = defineMessages({
64 id: 'settings.app.currentVersion', 64 id: 'settings.app.currentVersion',
65 defaultMessage: '!!!Current version:', 65 defaultMessage: '!!!Current version:',
66 }, 66 },
67 restartRequired: {
68 id: 'settings.app.restartRequired',
69 defaultMessage: '!!!Changes require restart',
70 },
71}); 67});
72 68
73@observer 69@observer
@@ -158,7 +154,6 @@ export default class EditSettingsForm extends Component {
158 {/* Advanced */} 154 {/* Advanced */}
159 <h2 id="advanced">{intl.formatMessage(messages.headlineAdvanced)}</h2> 155 <h2 id="advanced">{intl.formatMessage(messages.headlineAdvanced)}</h2>
160 <Toggle field={form.$('enableSpellchecking')} /> 156 <Toggle field={form.$('enableSpellchecking')} />
161 <p className="settings__help">{intl.formatMessage(messages.restartRequired)}</p>
162 {/* <Select field={form.$('spellcheckingLanguage')} /> */} 157 {/* <Select field={form.$('spellcheckingLanguage')} /> */}
163 158
164 {/* Updates */} 159 {/* Updates */}
diff --git a/src/containers/settings/EditServiceScreen.js b/src/containers/settings/EditServiceScreen.js
index 191ef447b..3c52152b1 100644
--- a/src/containers/settings/EditServiceScreen.js
+++ b/src/containers/settings/EditServiceScreen.js
@@ -26,6 +26,10 @@ const messages = defineMessages({
26 id: 'settings.service.form.enableNotification', 26 id: 'settings.service.form.enableNotification',
27 defaultMessage: '!!!Enable Notifications', 27 defaultMessage: '!!!Enable Notifications',
28 }, 28 },
29 enableBadge: {
30 id: 'settings.service.form.enableBadge',
31 defaultMessage: '!!!Show unread message badges',
32 },
29 enableAudio: { 33 enableAudio: {
30 id: 'settings.service.form.enableAudio', 34 id: 'settings.service.form.enableAudio',
31 defaultMessage: '!!!Enable audio', 35 defaultMessage: '!!!Enable audio',
@@ -88,6 +92,11 @@ export default class EditServiceScreen extends Component {
88 value: service.isNotificationEnabled, 92 value: service.isNotificationEnabled,
89 default: true, 93 default: true,
90 }, 94 },
95 isBadgeEnabled: {
96 label: intl.formatMessage(messages.enableBadge),
97 value: service.isBadgeEnabled,
98 default: true,
99 },
91 isMuted: { 100 isMuted: {
92 label: intl.formatMessage(messages.enableAudio), 101 label: intl.formatMessage(messages.enableAudio),
93 value: !service.isMuted, 102 value: !service.isMuted,
@@ -118,11 +127,22 @@ export default class EditServiceScreen extends Component {
118 }); 127 });
119 } 128 }
120 129
130 // More fine grained and use case specific validation rules
121 if (recipe.hasTeamId && recipe.hasCustomUrl) { 131 if (recipe.hasTeamId && recipe.hasCustomUrl) {
122 config.fields.team.validate = [oneRequired(['team', 'customUrl'])]; 132 config.fields.team.validate = [oneRequired(['team', 'customUrl'])];
123 config.fields.customUrl.validate = [url, oneRequired(['team', 'customUrl'])]; 133 config.fields.customUrl.validate = [url, oneRequired(['team', 'customUrl'])];
124 } 134 }
125 135
136 // If a service can be hosted and has a teamId or customUrl
137 if (recipe.hasHostedOption && (recipe.hasTeamId || recipe.hasCustomUrl)) {
138 if (config.fields.team) {
139 config.fields.team.validate = [];
140 }
141 if (config.fields.customUrl) {
142 config.fields.customUrl.validate = [url];
143 }
144 }
145
126 if (recipe.hasIndirectMessages) { 146 if (recipe.hasIndirectMessages) {
127 Object.assign(config.fields, { 147 Object.assign(config.fields, {
128 isIndirectMessageBadgeEnabled: { 148 isIndirectMessageBadgeEnabled: {
diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json
index 48b408e59..567537d75 100644
--- a/src/i18n/locales/en-US.json
+++ b/src/i18n/locales/en-US.json
@@ -110,6 +110,7 @@
110 "settings.service.form.editServiceHeadline": "Edit {name}", 110 "settings.service.form.editServiceHeadline": "Edit {name}",
111 "settings.service.form.tabHosted": "Hosted", 111 "settings.service.form.tabHosted": "Hosted",
112 "settings.service.form.tabOnPremise": "Self hosted ⭐️", 112 "settings.service.form.tabOnPremise": "Self hosted ⭐️",
113 "settings.service.form.useHostedService": "Use the hosted {name} service.",
113 "settings.service.form.customUrlValidationError": "Could not validate custom {name} server.", 114 "settings.service.form.customUrlValidationError": "Could not validate custom {name} server.",
114 "settings.service.form.customUrlPremiumInfo": "To add self hosted services, you need a Franz Premium Supporter Account.", 115 "settings.service.form.customUrlPremiumInfo": "To add self hosted services, you need a Franz Premium Supporter Account.",
115 "settings.service.form.customUrlUpgradeAccount": "Upgrade your account", 116 "settings.service.form.customUrlUpgradeAccount": "Upgrade your account",
@@ -117,11 +118,15 @@
117 "settings.service.form.name": "Name", 118 "settings.service.form.name": "Name",
118 "settings.service.form.enableService": "Enable service", 119 "settings.service.form.enableService": "Enable service",
119 "settings.service.form.enableNotification": "Enable notifications", 120 "settings.service.form.enableNotification": "Enable notifications",
121 "settings.service.form.enableBadge": "Show unread message badges",
120 "settings.service.form.team": "Team", 122 "settings.service.form.team": "Team",
121 "settings.service.form.customUrl": "Custom server", 123 "settings.service.form.customUrl": "Custom server",
122 "settings.service.form.indirectMessages": "Show message badge for all new messages", 124 "settings.service.form.indirectMessages": "Show message badge for all new messages",
123 "settings.service.form.enableAudio": "Enable audio", 125 "settings.service.form.enableAudio": "Enable audio",
124 "settings.service.form.isMutedInfo": "When disabled, all notification sounds and audio playback are muted", 126 "settings.service.form.isMutedInfo": "When disabled, all notification sounds and audio playback are muted",
127 "settings.service.form.headlineNotifications": "Notifications",
128 "settings.service.form.headlineBadges": "Unread message badges",
129 "settings.service.form.headlineGeneral": "General",
125 "settings.service.error.headline": "Error", 130 "settings.service.error.headline": "Error",
126 "settings.service.error.goBack": "Back to services", 131 "settings.service.error.goBack": "Back to services",
127 "settings.service.error.message": "Could not load service recipe.", 132 "settings.service.error.message": "Could not load service recipe.",
diff --git a/src/lib/Menu.js b/src/lib/Menu.js
index d9c30466b..d01666d49 100644
--- a/src/lib/Menu.js
+++ b/src/lib/Menu.js
@@ -167,7 +167,7 @@ export default class FranzMenu {
167 label: 'Settings', 167 label: 'Settings',
168 accelerator: 'CmdOrCtrl+,', 168 accelerator: 'CmdOrCtrl+,',
169 click: () => { 169 click: () => {
170 this.actions.ui.openSettings({ path: '' }); 170 this.actions.ui.openSettings({ path: 'app' });
171 }, 171 },
172 }, 172 },
173 { 173 {
diff --git a/src/models/Recipe.js b/src/models/Recipe.js
index 9971df77c..1fc23ac89 100644
--- a/src/models/Recipe.js
+++ b/src/models/Recipe.js
@@ -1,10 +1,11 @@
1import emailParser from 'address-rfc2822'; 1import emailParser from 'address-rfc2822';
2import semver from 'semver';
2 3
3export default class Recipe { 4export default class Recipe {
4 id = ''; 5 id = '';
5 name = ''; 6 name = '';
6 description = ''; 7 description = '';
7 version = '1.0'; 8 version = '';
8 path = ''; 9 path = '';
9 10
10 serviceURL = ''; 11 serviceURL = '';
@@ -15,6 +16,7 @@ export default class Recipe {
15 hasTeamId = false; 16 hasTeamId = false;
16 hasPredefinedUrl = false; 17 hasPredefinedUrl = false;
17 hasCustomUrl = false; 18 hasCustomUrl = false;
19 hasHostedOption = false;
18 urlInputPrefix = ''; 20 urlInputPrefix = '';
19 urlInputSuffix = ''; 21 urlInputSuffix = '';
20 22
@@ -30,6 +32,10 @@ export default class Recipe {
30 throw Error(`Recipe '${data.name}' requires Id`); 32 throw Error(`Recipe '${data.name}' requires Id`);
31 } 33 }
32 34
35 if (!semver.valid(data.version)) {
36 throw Error(`Version ${data.version} of recipe '${data.name}' is not a valid semver version`);
37 }
38
33 this.id = data.id || this.id; 39 this.id = data.id || this.id;
34 this.name = data.name || this.name; 40 this.name = data.name || this.name;
35 this.rawAuthor = data.author || this.author; 41 this.rawAuthor = data.author || this.author;
@@ -45,6 +51,7 @@ export default class Recipe {
45 this.hasTeamId = data.config.hasTeamId || this.hasTeamId; 51 this.hasTeamId = data.config.hasTeamId || this.hasTeamId;
46 this.hasPredefinedUrl = data.config.hasPredefinedUrl || this.hasPredefinedUrl; 52 this.hasPredefinedUrl = data.config.hasPredefinedUrl || this.hasPredefinedUrl;
47 this.hasCustomUrl = data.config.hasCustomUrl || this.hasCustomUrl; 53 this.hasCustomUrl = data.config.hasCustomUrl || this.hasCustomUrl;
54 this.hasHostedOption = data.config.hasHostedOption || this.hasHostedOption;
48 55
49 this.urlInputPrefix = data.config.urlInputPrefix || this.urlInputPrefix; 56 this.urlInputPrefix = data.config.urlInputPrefix || this.urlInputPrefix;
50 this.urlInputSuffix = data.config.urlInputSuffix || this.urlInputSuffix; 57 this.urlInputSuffix = data.config.urlInputSuffix || this.urlInputSuffix;
diff --git a/src/models/Service.js b/src/models/Service.js
index 958e4b11e..0b19440e7 100644
--- a/src/models/Service.js
+++ b/src/models/Service.js
@@ -22,6 +22,7 @@ export default class Service {
22 @observable team = ''; 22 @observable team = '';
23 @observable customUrl = ''; 23 @observable customUrl = '';
24 @observable isNotificationEnabled = true; 24 @observable isNotificationEnabled = true;
25 @observable isBadgeEnabled = true;
25 @observable isIndirectMessageBadgeEnabled = true; 26 @observable isIndirectMessageBadgeEnabled = true;
26 @observable customIconUrl = ''; 27 @observable customIconUrl = '';
27 @observable hasCrashed = false; 28 @observable hasCrashed = false;
@@ -52,6 +53,9 @@ export default class Service {
52 this.isNotificationEnabled = data.isNotificationEnabled !== undefined 53 this.isNotificationEnabled = data.isNotificationEnabled !== undefined
53 ? data.isNotificationEnabled : this.isNotificationEnabled; 54 ? data.isNotificationEnabled : this.isNotificationEnabled;
54 55
56 this.isBadgeEnabled = data.isBadgeEnabled !== undefined
57 ? data.isBadgeEnabled : this.isBadgeEnabled;
58
55 this.isIndirectMessageBadgeEnabled = data.isIndirectMessageBadgeEnabled !== undefined 59 this.isIndirectMessageBadgeEnabled = data.isIndirectMessageBadgeEnabled !== undefined
56 ? data.isIndirectMessageBadgeEnabled : this.isIndirectMessageBadgeEnabled; 60 ? data.isIndirectMessageBadgeEnabled : this.isIndirectMessageBadgeEnabled;
57 61
diff --git a/src/models/Settings.js b/src/models/Settings.js
index 35bfe0d05..ca44da258 100644
--- a/src/models/Settings.js
+++ b/src/models/Settings.js
@@ -1,4 +1,4 @@
1import { observable } from 'mobx'; 1import { observable, extendObservable } from 'mobx';
2import { DEFAULT_APP_SETTINGS } from '../config'; 2import { DEFAULT_APP_SETTINGS } from '../config';
3 3
4export default class Settings { 4export default class Settings {
@@ -17,4 +17,8 @@ export default class Settings {
17 constructor(data) { 17 constructor(data) {
18 Object.assign(this, data); 18 Object.assign(this, data);
19 } 19 }
20
21 update(data) {
22 extendObservable(this, data);
23 }
20} 24}
diff --git a/src/stores/ServicesStore.js b/src/stores/ServicesStore.js
index b04aafd78..66f37af26 100644
--- a/src/stores/ServicesStore.js
+++ b/src/stores/ServicesStore.js
@@ -297,7 +297,7 @@ export default class ServicesStore extends Store {
297 }); 297 });
298 } else if (channel === 'notification') { 298 } else if (channel === 'notification') {
299 const options = args[0].options; 299 const options = args[0].options;
300 if (service.recipe.hasNotificationSound || service.isMuted) { 300 if (service.recipe.hasNotificationSound || service.isMuted || this.stores.settings.all.isAppMuted) {
301 Object.assign(options, { 301 Object.assign(options, {
302 silent: true, 302 silent: true,
303 }); 303 });
@@ -491,13 +491,13 @@ export default class ServicesStore extends Store {
491 const showMessageBadgeWhenMuted = this.stores.settings.all.showMessageBadgeWhenMuted; 491 const showMessageBadgeWhenMuted = this.stores.settings.all.showMessageBadgeWhenMuted;
492 const showMessageBadgesEvenWhenMuted = this.stores.ui.showMessageBadgesEvenWhenMuted; 492 const showMessageBadgesEvenWhenMuted = this.stores.ui.showMessageBadgesEvenWhenMuted;
493 493
494 const unreadDirectMessageCount = this.enabled 494 const unreadDirectMessageCount = this.allDisplayed
495 .filter(s => (showMessageBadgeWhenMuted || s.isNotificationEnabled) && showMessageBadgesEvenWhenMuted) 495 .filter(s => (showMessageBadgeWhenMuted || s.isNotificationEnabled) && showMessageBadgesEvenWhenMuted && s.isBadgeEnabled)
496 .map(s => s.unreadDirectMessageCount) 496 .map(s => s.unreadDirectMessageCount)
497 .reduce((a, b) => a + b, 0); 497 .reduce((a, b) => a + b, 0);
498 498
499 const unreadIndirectMessageCount = this.enabled 499 const unreadIndirectMessageCount = this.allDisplayed
500 .filter(s => (showMessageBadgeWhenMuted || s.isIndirectMessageBadgeEnabled) && showMessageBadgesEvenWhenMuted) 500 .filter(s => (showMessageBadgeWhenMuted || s.isIndirectMessageBadgeEnabled) && showMessageBadgesEvenWhenMuted && s.isBadgeEnabled)
501 .map(s => s.unreadIndirectMessageCount) 501 .map(s => s.unreadIndirectMessageCount)
502 .reduce((a, b) => a + b, 0); 502 .reduce((a, b) => a + b, 0);
503 503
diff --git a/src/stores/SettingsStore.js b/src/stores/SettingsStore.js
index 33473f16d..da99a720f 100644
--- a/src/stores/SettingsStore.js
+++ b/src/stores/SettingsStore.js
@@ -26,7 +26,7 @@ export default class SettingsStore extends Store {
26 } 26 }
27 27
28 @computed get all() { 28 @computed get all() {
29 return this.allSettingsRequest.result || new SettingsModel(); 29 return new SettingsModel(this.allSettingsRequest.result);
30 } 30 }
31 31
32 @action async _update({ settings }) { 32 @action async _update({ settings }) {
diff --git a/src/styles/content-tabs.scss b/src/styles/content-tabs.scss
index aa3c8594b..47dfea2c4 100644
--- a/src/styles/content-tabs.scss
+++ b/src/styles/content-tabs.scss
@@ -12,15 +12,17 @@
12 flex: 1; 12 flex: 1;
13 // border: 1px solid $theme-gray-lightest; 13 // border: 1px solid $theme-gray-lightest;
14 color: $theme-gray-dark; 14 color: $theme-gray-dark;
15 background: $theme-gray-lightest; 15 background: linear-gradient($theme-gray-lightest 80%, darken($theme-gray-lightest, 3%));
16 border-bottom: 1px solid $theme-gray-lighter; 16 border-right: 1px solid $theme-gray-lighter;
17 box-shadow: inset 0px -3px 10px rgba(black, 0.05); 17 transition: background $theme-transition-time;
18 transition: all $theme-transition-time; 18
19 &:last-of-type {
20 border-right: 0;
21 }
19 22
20 &.is-active { 23 &.is-active {
21 background: $theme-brand-primary; 24 background: $theme-brand-primary;
22 color: #FFF; 25 color: #FFF;
23 border-bottom: 1px solid $theme-brand-primary;
24 box-shadow: none; 26 box-shadow: none;
25 } 27 }
26 } 28 }
diff --git a/src/styles/input.scss b/src/styles/input.scss
index 814dce5f8..7042f56e8 100644
--- a/src/styles/input.scss
+++ b/src/styles/input.scss
@@ -47,6 +47,10 @@
47 padding: 8px; 47 padding: 8px;
48 // font-size: 18px; 48 // font-size: 18px;
49 color: $theme-gray; 49 color: $theme-gray;
50
51 &::placeholder {
52 color: lighten($theme-gray-light, 10%);
53 }
50 } 54 }
51 55
52 .franz-form__input-prefix, 56 .franz-form__input-prefix,
diff --git a/src/styles/settings.scss b/src/styles/settings.scss
index 73cef0813..b29ed5468 100644
--- a/src/styles/settings.scss
+++ b/src/styles/settings.scss
@@ -151,8 +151,23 @@
151 } 151 }
152 } 152 }
153 153
154 .settings__options { 154 &__options {
155 margin-top: 30px; 155 margin-top: 20px;
156 }
157
158 &__settings-group {
159 margin-top: 10px;
160
161 h3 {
162 font-weight: bold;
163 margin: 25px 0 15px;
164 color: $theme-gray-light;
165 letter-spacing: -0.1px;
166
167 &:first-of-type {
168 margin-top: 0;
169 }
170 }
156 } 171 }
157 172
158 .settings__message { 173 .settings__message {
@@ -173,10 +188,6 @@
173 margin: -10px 0 20px 55px;; 188 margin: -10px 0 20px 55px;;
174 font-size: 12px; 189 font-size: 12px;
175 color: $theme-gray-light; 190 color: $theme-gray-light;
176
177 &:last-of-type {
178 margin-bottom: 30px;
179 }
180 } 191 }
181 192
182 .settings__controls { 193 .settings__controls {
diff --git a/src/webview/plugin.js b/src/webview/plugin.js
index e2daf09dd..9903ee07a 100644
--- a/src/webview/plugin.js
+++ b/src/webview/plugin.js
@@ -1,13 +1,13 @@
1import { ipcRenderer } from 'electron'; 1import { ipcRenderer } from 'electron';
2import { ContextMenuListener, ContextMenuBuilder } from 'electron-spellchecker';
2import path from 'path'; 3import path from 'path';
3 4
5import { isDevMode } from '../environment';
4import RecipeWebview from './lib/RecipeWebview'; 6import RecipeWebview from './lib/RecipeWebview';
5 7
6import Spellchecker from './spellchecker.js'; 8import Spellchecker from './spellchecker.js';
7import './notifications.js'; 9import './notifications.js';
8 10
9const spellchecker = new Spellchecker();
10
11ipcRenderer.on('initializeRecipe', (e, data) => { 11ipcRenderer.on('initializeRecipe', (e, data) => {
12 const modulePath = path.join(data.recipe.path, 'webview.js'); 12 const modulePath = path.join(data.recipe.path, 'webview.js');
13 // Delete module from cache 13 // Delete module from cache
@@ -20,20 +20,22 @@ ipcRenderer.on('initializeRecipe', (e, data) => {
20 } 20 }
21}); 21});
22 22
23const spellchecker = new Spellchecker();
24spellchecker.initialize();
25
26const contextMenuBuilder = new ContextMenuBuilder(spellchecker.handler, null, isDevMode);
27
28new ContextMenuListener((info) => { // eslint-disable-line
29 contextMenuBuilder.showPopupMenu(info);
30});
31
23ipcRenderer.on('settings-update', (e, data) => { 32ipcRenderer.on('settings-update', (e, data) => {
24 if (data.enableSpellchecking) { 33 console.log('settings-update', data);
25 if (!spellchecker.isEnabled) { 34 spellchecker.toggleSpellchecker(data.enableSpellchecking);
26 spellchecker.enable();
27
28 // TODO: this does not work yet, needs more testing
29 // if (data.spellcheckingLanguage !== 'auto') {
30 // console.log('set spellchecking language to', data.spellcheckingLanguage);
31 // spellchecker.switchLanguage(data.spellcheckingLanguage);
32 // }
33 }
34 }
35}); 35});
36 36
37// initSpellche
38
37document.addEventListener('DOMContentLoaded', () => { 39document.addEventListener('DOMContentLoaded', () => {
38 ipcRenderer.sendToHost('hello'); 40 ipcRenderer.sendToHost('hello');
39}, false); 41}, false);
diff --git a/src/webview/spellchecker.js b/src/webview/spellchecker.js
index 5beb77e03..a504a4039 100644
--- a/src/webview/spellchecker.js
+++ b/src/webview/spellchecker.js
@@ -1,30 +1,63 @@
1import { SpellCheckHandler, ContextMenuListener, ContextMenuBuilder } from 'electron-spellchecker'; 1import { SpellCheckHandler } from 'electron-spellchecker';
2 2
3import { isMac } from '../environment'; 3import { isMac } from '../environment';
4 4
5export default class Spellchecker { 5export default class Spellchecker {
6 isEnabled = false; 6 isInitialized = false;
7 spellchecker = null; 7 handler = null;
8 initRetries = 0;
9 DOMCheckInterval = null;
10
11 get inputs() {
12 return document.querySelectorAll('input[type="text"], [contenteditable="true"], textarea');
13 }
14
15 initialize() {
16 this.handler = new SpellCheckHandler();
8 17
9 enable() {
10 this.spellchecker = new SpellCheckHandler();
11 if (!isMac) { 18 if (!isMac) {
12 this.spellchecker.attachToInput(); 19 this.attach();
13 this.spellchecker.switchLanguage(navigator.language); 20 } else {
21 this.isInitialized = true;
22 }
23 }
24
25 attach() {
26 let initFailed = false;
27
28 if (this.initRetries > 3) {
29 console.error('Could not initialize spellchecker');
30 return;
14 } 31 }
15 32
16 const contextMenuBuilder = new ContextMenuBuilder(this.spellchecker); 33 try {
34 this.handler.attachToInput();
35 this.handler.switchLanguage(navigator.language);
36 } catch (err) {
37 initFailed = true;
38 this.initRetries = +1;
39 setTimeout(() => { this.attach(); console.warn('Spellchecker init failed, trying again in 5s'); }, 5000);
40 }
17 41
18 new ContextMenuListener((info) => { // eslint-disable-line 42 if (!initFailed) {
19 contextMenuBuilder.showPopupMenu(info); 43 this.isInitialized = true;
44 }
45 }
46
47 toggleSpellchecker(enable = false) {
48 this.inputs.forEach((input) => {
49 input.setAttribute('spellcheck', enable);
20 }); 50 });
51
52 this.intervalHandler(enable);
21 } 53 }
22 54
23 // TODO: this does not work yet, needs more testing 55 intervalHandler(enable) {
24 // switchLanguage(language) { 56 clearInterval(this.DOMCheckInterval);
25 // if (language !== 'auto') { 57
26 // this.spellchecker.switchLanguage(language); 58 if (enable) {
27 // } 59 this.DOMCheckInterval = setInterval(() => this.toggleSpellchecker(enable), 30000);
28 // } 60 }
61 }
29} 62}
30 63