aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/actions/app.js1
-rw-r--r--src/actions/user.js1
-rw-r--r--src/api/UserApi.js4
-rw-r--r--src/api/server/LocalApi.js4
-rw-r--r--src/api/server/ServerApi.js28
-rw-r--r--src/components/auth/Import.js2
-rw-r--r--src/components/auth/Welcome.js12
-rw-r--r--src/components/layout/Sidebar.js8
-rw-r--r--src/components/services/content/ServiceWebview.js1
-rw-r--r--src/components/services/tabs/TabBarSortableList.js6
-rw-r--r--src/components/services/tabs/TabItem.js37
-rw-r--r--src/components/services/tabs/Tabbar.js6
-rw-r--r--src/components/settings/account/AccountDashboard.js49
-rw-r--r--src/components/settings/services/EditServiceForm.js77
-rw-r--r--src/components/settings/settings/EditSettingsForm.js6
-rw-r--r--src/components/ui/Button.js2
-rw-r--r--src/config.js4
-rw-r--r--src/containers/layout/AppLayoutContainer.js8
-rw-r--r--src/containers/settings/AccountScreen.js5
-rw-r--r--src/containers/settings/EditServiceScreen.js20
-rw-r--r--src/containers/settings/EditSettingsScreen.js10
-rw-r--r--src/electron/deepLinking.js7
-rw-r--r--src/helpers/async-helpers.js5
-rw-r--r--src/helpers/webview-ime-focus-helpers.js38
-rw-r--r--src/i18n/languages.js1
-rw-r--r--src/i18n/locales/el.json2
-rw-r--r--src/i18n/locales/en-US.json18
-rw-r--r--src/i18n/locales/es.json2
-rw-r--r--src/index.js24
-rw-r--r--src/lib/Menu.js4
-rw-r--r--src/models/Recipe.js9
-rw-r--r--src/models/Service.js4
-rw-r--r--src/models/Settings.js7
-rw-r--r--src/stores/AppStore.js48
-rw-r--r--src/stores/ServicesStore.js23
-rw-r--r--src/stores/SettingsStore.js3
-rw-r--r--src/stores/UIStore.js8
-rw-r--r--src/stores/UserStore.js6
-rw-r--r--src/styles/content-tabs.scss12
-rw-r--r--src/styles/input.scss4
-rw-r--r--src/styles/services.scss1
-rw-r--r--src/styles/settings.scss27
-rw-r--r--src/styles/tabs.scss20
-rw-r--r--src/styles/welcome.scss25
-rw-r--r--src/webview/ime.js10
-rw-r--r--src/webview/lib/RecipeWebview.js4
-rw-r--r--src/webview/plugin.js29
-rw-r--r--src/webview/spellchecker.js65
48 files changed, 517 insertions, 180 deletions
diff --git a/src/actions/app.js b/src/actions/app.js
index 25ff9344d..e4f648fc9 100644
--- a/src/actions/app.js
+++ b/src/actions/app.js
@@ -22,6 +22,7 @@ export default {
22 healthCheck: {}, 22 healthCheck: {},
23 muteApp: { 23 muteApp: {
24 isMuted: PropTypes.bool.isRequired, 24 isMuted: PropTypes.bool.isRequired,
25 overrideSystemMute: PropTypes.bool,
25 }, 26 },
26 toggleMuteApp: {}, 27 toggleMuteApp: {},
27}; 28};
diff --git a/src/actions/user.js b/src/actions/user.js
index fe32b8a05..ccf1fa56a 100644
--- a/src/actions/user.js
+++ b/src/actions/user.js
@@ -27,4 +27,5 @@ export default {
27 importLegacyServices: PropTypes.arrayOf(PropTypes.shape({ 27 importLegacyServices: PropTypes.arrayOf(PropTypes.shape({
28 recipe: PropTypes.string.isRequired, 28 recipe: PropTypes.string.isRequired,
29 })).isRequired, 29 })).isRequired,
30 delete: {},
30}; 31};
diff --git a/src/api/UserApi.js b/src/api/UserApi.js
index e8fd75bed..edfb88988 100644
--- a/src/api/UserApi.js
+++ b/src/api/UserApi.js
@@ -46,4 +46,8 @@ export default class UserApi {
46 getLegacyServices() { 46 getLegacyServices() {
47 return this.server.getLegacyServices(); 47 return this.server.getLegacyServices();
48 } 48 }
49
50 delete() {
51 return this.server.deleteAccount();
52 }
49} 53}
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/api/server/ServerApi.js b/src/api/server/ServerApi.js
index f25f02eaa..8b3136d27 100644
--- a/src/api/server/ServerApi.js
+++ b/src/api/server/ServerApi.js
@@ -12,6 +12,8 @@ import NewsModel from '../../models/News';
12import UserModel from '../../models/User'; 12import UserModel from '../../models/User';
13import OrderModel from '../../models/Order'; 13import OrderModel from '../../models/Order';
14 14
15import { sleep } from '../../helpers/async-helpers';
16
15import { API } from '../../environment'; 17import { API } from '../../environment';
16 18
17import { 19import {
@@ -125,6 +127,19 @@ export default class ServerApi {
125 return user; 127 return user;
126 } 128 }
127 129
130 async deleteAccount() {
131 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/me`, this._prepareAuthRequest({
132 method: 'DELETE',
133 }));
134 if (!request.ok) {
135 throw request;
136 }
137 const data = await request.json();
138
139 console.debug('ServerApi::deleteAccount resolves', data);
140 return data;
141 }
142
128 // Services 143 // Services
129 async getServices() { 144 async getServices() {
130 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/me/services`, this._prepareAuthRequest({ 145 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/me/services`, this._prepareAuthRequest({
@@ -290,18 +305,25 @@ export default class ServerApi {
290 305
291 fs.ensureDirSync(recipeTempDirectory); 306 fs.ensureDirSync(recipeTempDirectory);
292 const res = await fetch(packageUrl); 307 const res = await fetch(packageUrl);
308 console.debug('Recipe downloaded', recipeId);
293 const buffer = await res.buffer(); 309 const buffer = await res.buffer();
294 fs.writeFileSync(archivePath, buffer); 310 fs.writeFileSync(archivePath, buffer);
295 311
296 tar.x({ 312 await sleep(10);
313
314 await tar.x({
297 file: archivePath, 315 file: archivePath,
298 cwd: recipeTempDirectory, 316 cwd: recipeTempDirectory,
299 sync: true, 317 preservePaths: true,
318 unlink: true,
319 preserveOwner: false,
320 onwarn: x => console.log('warn', recipeId, x),
300 }); 321 });
301 322
323 await sleep(10);
324
302 const { id } = fs.readJsonSync(path.join(recipeTempDirectory, 'package.json')); 325 const { id } = fs.readJsonSync(path.join(recipeTempDirectory, 'package.json'));
303 const recipeDirectory = path.join(recipesDirectory, id); 326 const recipeDirectory = path.join(recipesDirectory, id);
304
305 fs.copySync(recipeTempDirectory, recipeDirectory); 327 fs.copySync(recipeTempDirectory, recipeDirectory);
306 fs.remove(recipeTempDirectory); 328 fs.remove(recipeTempDirectory);
307 fs.remove(path.join(recipesDirectory, recipeId, 'recipe.tar.gz')); 329 fs.remove(path.join(recipesDirectory, recipeId, 'recipe.tar.gz'));
diff --git a/src/components/auth/Import.js b/src/components/auth/Import.js
index 06493a0fd..078244434 100644
--- a/src/components/auth/Import.js
+++ b/src/components/auth/Import.js
@@ -24,7 +24,7 @@ const messages = defineMessages({
24 }, 24 },
25 skipButtonLabel: { 25 skipButtonLabel: {
26 id: 'import.skip.label', 26 id: 'import.skip.label',
27 defaultMessage: '!!!I want add services manually', 27 defaultMessage: '!!!I want to add services manually',
28 }, 28 },
29}); 29});
30 30
diff --git a/src/components/auth/Welcome.js b/src/components/auth/Welcome.js
index 06b10ecfe..eb9fbb847 100644
--- a/src/components/auth/Welcome.js
+++ b/src/components/auth/Welcome.js
@@ -55,12 +55,16 @@ export default class Login extends Component {
55 </div> 55 </div>
56 <div className="welcome__featured-services"> 56 <div className="welcome__featured-services">
57 {recipes.map(recipe => ( 57 {recipes.map(recipe => (
58 <img 58 <div
59 key={recipe.id} 59 key={recipe.id}
60 src={recipe.icons.svg}
61 className="welcome__featured-service" 60 className="welcome__featured-service"
62 alt="" 61 >
63 /> 62 <img
63 key={recipe.id}
64 src={recipe.icons.svg}
65 alt=""
66 />
67 </div>
64 ))} 68 ))}
65 </div> 69 </div>
66 </div> 70 </div>
diff --git a/src/components/layout/Sidebar.js b/src/components/layout/Sidebar.js
index cb2ecc8ce..915ebeace 100644
--- a/src/components/layout/Sidebar.js
+++ b/src/components/layout/Sidebar.js
@@ -17,12 +17,12 @@ const messages = defineMessages({
17 defaultMessage: '!!!Add new service', 17 defaultMessage: '!!!Add new service',
18 }, 18 },
19 mute: { 19 mute: {
20 id: 'sidebar.mute', 20 id: 'sidebar.muteApp',
21 defaultMessage: '!!!Disable audio', 21 defaultMessage: '!!!Disable notifications & audio',
22 }, 22 },
23 unmute: { 23 unmute: {
24 id: 'sidebar.unmute', 24 id: 'sidebar.unmuteApp',
25 defaultMessage: '!!!Enable audio', 25 defaultMessage: '!!!Enable notifications & audio',
26 }, 26 },
27}); 27});
28 28
diff --git a/src/components/services/content/ServiceWebview.js b/src/components/services/content/ServiceWebview.js
index 11911e834..c146abf4e 100644
--- a/src/components/services/content/ServiceWebview.js
+++ b/src/components/services/content/ServiceWebview.js
@@ -65,6 +65,7 @@ export default class ServiceWebview extends Component {
65 65
66 const webviewClasses = classnames({ 66 const webviewClasses = classnames({
67 services__webview: true, 67 services__webview: true,
68 'services__webview-wrapper': true,
68 'is-active': service.isActive, 69 'is-active': service.isActive,
69 'services__webview--force-repaint': this.state.forceRepaint, 70 'services__webview--force-repaint': this.state.forceRepaint,
70 }); 71 });
diff --git a/src/components/services/tabs/TabBarSortableList.js b/src/components/services/tabs/TabBarSortableList.js
index 2daf55676..489027d57 100644
--- a/src/components/services/tabs/TabBarSortableList.js
+++ b/src/components/services/tabs/TabBarSortableList.js
@@ -17,6 +17,8 @@ class TabBarSortableList extends Component {
17 deleteService: PropTypes.func.isRequired, 17 deleteService: PropTypes.func.isRequired,
18 disableService: PropTypes.func.isRequired, 18 disableService: PropTypes.func.isRequired,
19 enableService: PropTypes.func.isRequired, 19 enableService: PropTypes.func.isRequired,
20 showMessageBadgeWhenMutedSetting: PropTypes.bool.isRequired,
21 showMessageBadgesEvenWhenMuted: PropTypes.bool.isRequired,
20 } 22 }
21 23
22 render() { 24 render() {
@@ -30,6 +32,8 @@ class TabBarSortableList extends Component {
30 disableService, 32 disableService,
31 enableService, 33 enableService,
32 openSettings, 34 openSettings,
35 showMessageBadgeWhenMutedSetting,
36 showMessageBadgesEvenWhenMuted,
33 } = this.props; 37 } = this.props;
34 38
35 return ( 39 return (
@@ -50,6 +54,8 @@ class TabBarSortableList extends Component {
50 disableService={() => disableService({ serviceId: service.id })} 54 disableService={() => disableService({ serviceId: service.id })}
51 enableService={() => enableService({ serviceId: service.id })} 55 enableService={() => enableService({ serviceId: service.id })}
52 openSettings={openSettings} 56 openSettings={openSettings}
57 showMessageBadgeWhenMutedSetting={showMessageBadgeWhenMutedSetting}
58 showMessageBadgesEvenWhenMuted={showMessageBadgesEvenWhenMuted}
53 /> 59 />
54 ))} 60 ))}
55 {/* <li> 61 {/* <li>
diff --git a/src/components/services/tabs/TabItem.js b/src/components/services/tabs/TabItem.js
index a7136c43f..7aed8fda7 100644
--- a/src/components/services/tabs/TabItem.js
+++ b/src/components/services/tabs/TabItem.js
@@ -63,6 +63,8 @@ class TabItem extends Component {
63 deleteService: PropTypes.func.isRequired, 63 deleteService: PropTypes.func.isRequired,
64 disableService: PropTypes.func.isRequired, 64 disableService: PropTypes.func.isRequired,
65 enableService: PropTypes.func.isRequired, 65 enableService: PropTypes.func.isRequired,
66 showMessageBadgeWhenMutedSetting: PropTypes.bool.isRequired,
67 showMessageBadgesEvenWhenMuted: PropTypes.bool.isRequired,
66 }; 68 };
67 69
68 static contextTypes = { 70 static contextTypes = {
@@ -81,6 +83,8 @@ class TabItem extends Component {
81 disableService, 83 disableService,
82 enableService, 84 enableService,
83 openSettings, 85 openSettings,
86 showMessageBadgeWhenMutedSetting,
87 showMessageBadgesEvenWhenMuted,
84 } = this.props; 88 } = this.props;
85 const { intl } = this.context; 89 const { intl } = this.context;
86 90
@@ -121,6 +125,26 @@ class TabItem extends Component {
121 }]; 125 }];
122 const menu = Menu.buildFromTemplate(menuTemplate); 126 const menu = Menu.buildFromTemplate(menuTemplate);
123 127
128 let notificationBadge = null;
129 if ((showMessageBadgeWhenMutedSetting || service.isNotificationEnabled) && showMessageBadgesEvenWhenMuted && service.isBadgeEnabled) {
130 notificationBadge = (
131 <span>
132 {service.unreadDirectMessageCount > 0 && (
133 <span className="tab-item__message-count">
134 {service.unreadDirectMessageCount}
135 </span>
136 )}
137 {service.unreadIndirectMessageCount > 0
138 && service.unreadDirectMessageCount === 0
139 && service.isIndirectMessageBadgeEnabled && (
140 <span className="tab-item__message-count is-indirect">
141
142 </span>
143 )}
144 </span>
145 );
146 }
147
124 return ( 148 return (
125 <li 149 <li
126 className={classnames({ 150 className={classnames({
@@ -138,18 +162,7 @@ class TabItem extends Component {
138 className="tab-item__icon" 162 className="tab-item__icon"
139 alt="" 163 alt=""
140 /> 164 />
141 {service.unreadDirectMessageCount > 0 && ( 165 {notificationBadge}
142 <span className="tab-item__message-count">
143 {service.unreadDirectMessageCount}
144 </span>
145 )}
146 {service.unreadIndirectMessageCount > 0
147 && service.unreadDirectMessageCount === 0
148 && service.isIndirectMessageBadgeEnabled && (
149 <span className="tab-item__message-count is-indirect">
150
151 </span>
152 )}
153 </li> 166 </li>
154 ); 167 );
155 } 168 }
diff --git a/src/components/services/tabs/Tabbar.js b/src/components/services/tabs/Tabbar.js
index 9da1090b7..ceb88c51c 100644
--- a/src/components/services/tabs/Tabbar.js
+++ b/src/components/services/tabs/Tabbar.js
@@ -18,6 +18,8 @@ export default class TabBar extends Component {
18 toggleAudio: PropTypes.func.isRequired, 18 toggleAudio: PropTypes.func.isRequired,
19 deleteService: PropTypes.func.isRequired, 19 deleteService: PropTypes.func.isRequired,
20 updateService: PropTypes.func.isRequired, 20 updateService: PropTypes.func.isRequired,
21 showMessageBadgeWhenMutedSetting: PropTypes.bool.isRequired,
22 showMessageBadgesEvenWhenMuted: PropTypes.bool.isRequired,
21 } 23 }
22 24
23 onSortEnd = ({ oldIndex, newIndex }) => { 25 onSortEnd = ({ oldIndex, newIndex }) => {
@@ -64,6 +66,8 @@ export default class TabBar extends Component {
64 toggleNotifications, 66 toggleNotifications,
65 toggleAudio, 67 toggleAudio,
66 deleteService, 68 deleteService,
69 showMessageBadgeWhenMutedSetting,
70 showMessageBadgesEvenWhenMuted,
67 } = this.props; 71 } = this.props;
68 72
69 return ( 73 return (
@@ -85,6 +89,8 @@ export default class TabBar extends Component {
85 axis="y" 89 axis="y"
86 lockAxis="y" 90 lockAxis="y"
87 helperClass="is-reordering" 91 helperClass="is-reordering"
92 showMessageBadgeWhenMutedSetting={showMessageBadgeWhenMutedSetting}
93 showMessageBadgesEvenWhenMuted={showMessageBadgesEvenWhenMuted}
88 /> 94 />
89 </div> 95 </div>
90 ); 96 );
diff --git a/src/components/settings/account/AccountDashboard.js b/src/components/settings/account/AccountDashboard.js
index 75dbdef49..89fa07800 100644
--- a/src/components/settings/account/AccountDashboard.js
+++ b/src/components/settings/account/AccountDashboard.js
@@ -28,6 +28,10 @@ const messages = defineMessages({
28 id: 'settings.account.headlineInvoices', 28 id: 'settings.account.headlineInvoices',
29 defaultMessage: '!!Invoices', 29 defaultMessage: '!!Invoices',
30 }, 30 },
31 headlineDangerZone: {
32 id: 'settings.account.headlineDangerZone',
33 defaultMessage: '!!Danger Zone',
34 },
31 manageSubscriptionButtonLabel: { 35 manageSubscriptionButtonLabel: {
32 id: 'settings.account.manageSubscription.label', 36 id: 'settings.account.manageSubscription.label',
33 defaultMessage: '!!!Manage your subscription', 37 defaultMessage: '!!!Manage your subscription',
@@ -72,6 +76,18 @@ const messages = defineMessages({
72 id: 'settings.account.mining.cancel', 76 id: 'settings.account.mining.cancel',
73 defaultMessage: '!!!Cancel mining', 77 defaultMessage: '!!!Cancel mining',
74 }, 78 },
79 deleteAccount: {
80 id: 'settings.account.deleteAccount',
81 defaultMessage: '!!!Delete account',
82 },
83 deleteInfo: {
84 id: 'settings.account.deleteInfo',
85 defaultMessage: '!!!If you don\'t need your Franz account any longer, you can delete your account and all related data here.',
86 },
87 deleteEmailSent: {
88 id: 'settings.account.deleteEmailSent',
89 defaultMessage: '!!!You have received an email with a link to confirm your account deletion. Your account and data cannot be restored!',
90 },
75}); 91});
76 92
77@observer 93@observer
@@ -90,6 +106,9 @@ export default class AccountDashboard extends Component {
90 openExternalUrl: PropTypes.func.isRequired, 106 openExternalUrl: PropTypes.func.isRequired,
91 onCloseSubscriptionWindow: PropTypes.func.isRequired, 107 onCloseSubscriptionWindow: PropTypes.func.isRequired,
92 stopMiner: PropTypes.func.isRequired, 108 stopMiner: PropTypes.func.isRequired,
109 deleteAccount: PropTypes.func.isRequired,
110 isLoadingDeleteAccount: PropTypes.bool.isRequired,
111 isDeleteAccountSuccessful: PropTypes.bool.isRequired,
93 }; 112 };
94 113
95 static contextTypes = { 114 static contextTypes = {
@@ -111,6 +130,9 @@ export default class AccountDashboard extends Component {
111 retryUserInfoRequest, 130 retryUserInfoRequest,
112 onCloseSubscriptionWindow, 131 onCloseSubscriptionWindow,
113 stopMiner, 132 stopMiner,
133 deleteAccount,
134 isLoadingDeleteAccount,
135 isDeleteAccountSuccessful,
114 } = this.props; 136 } = this.props;
115 const { intl } = this.context; 137 const { intl } = this.context;
116 138
@@ -201,7 +223,7 @@ export default class AccountDashboard extends Component {
201 /> 223 />
202 </div> 224 </div>
203 </div> 225 </div>
204 <div className="account__box account__box--last"> 226 <div className="account__box">
205 <h2>{intl.formatMessage(messages.headlineInvoices)}</h2> 227 <h2>{intl.formatMessage(messages.headlineInvoices)}</h2>
206 <table className="invoices"> 228 <table className="invoices">
207 <tbody> 229 <tbody>
@@ -230,7 +252,7 @@ export default class AccountDashboard extends Component {
230 252
231 {user.isMiner && ( 253 {user.isMiner && (
232 <div className="account franz-form"> 254 <div className="account franz-form">
233 <div className="account__box"> 255 <div className="account__box account__box--last">
234 <h2>{intl.formatMessage(messages.headlineSubscription)}</h2> 256 <h2>{intl.formatMessage(messages.headlineSubscription)}</h2>
235 <div className="account__subscription"> 257 <div className="account__subscription">
236 <div> 258 <div>
@@ -267,7 +289,7 @@ export default class AccountDashboard extends Component {
267 <Loader /> 289 <Loader />
268 ) : ( 290 ) : (
269 <div className="account franz-form"> 291 <div className="account franz-form">
270 <div className="account__box account__box--last"> 292 <div className="account__box">
271 <h2>{intl.formatMessage(messages.headlineUpgrade)}</h2> 293 <h2>{intl.formatMessage(messages.headlineUpgrade)}</h2>
272 <SubscriptionForm 294 <SubscriptionForm
273 onCloseWindow={onCloseSubscriptionWindow} 295 onCloseWindow={onCloseSubscriptionWindow}
@@ -276,8 +298,29 @@ export default class AccountDashboard extends Component {
276 </div> 298 </div>
277 ) 299 )
278 )} 300 )}
301
302 <div className="account franz-form">
303 <div className="account__box">
304 <h2>{intl.formatMessage(messages.headlineDangerZone)}</h2>
305 {!isDeleteAccountSuccessful && (
306 <div className="account__subscription">
307 <p>{intl.formatMessage(messages.deleteInfo)}</p>
308 <Button
309 label={intl.formatMessage(messages.deleteAccount)}
310 buttonType="danger"
311 onClick={() => deleteAccount()}
312 loaded={!isLoadingDeleteAccount}
313 />
314 </div>
315 )}
316 {isDeleteAccountSuccessful && (
317 <p>{intl.formatMessage(messages.deleteEmailSent)}</p>
318 )}
319 </div>
320 </div>
279 </div> 321 </div>
280 )} 322 )}
323
281 </div> 324 </div>
282 <ReactTooltip place="right" type="dark" effect="solid" /> 325 <ReactTooltip place="right" type="dark" effect="solid" />
283 </div> 326 </div>
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 4ce9b7ab2..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
@@ -142,6 +138,7 @@ export default class EditSettingsForm extends Component {
142 {/* Appearance */} 138 {/* Appearance */}
143 <h2 id="apperance">{intl.formatMessage(messages.headlineAppearance)}</h2> 139 <h2 id="apperance">{intl.formatMessage(messages.headlineAppearance)}</h2>
144 <Toggle field={form.$('showDisabledServices')} /> 140 <Toggle field={form.$('showDisabledServices')} />
141 <Toggle field={form.$('showMessageBadgeWhenMuted')} />
145 142
146 {/* Language */} 143 {/* Language */}
147 <h2 id="language">{intl.formatMessage(messages.headlineLanguage)}</h2> 144 <h2 id="language">{intl.formatMessage(messages.headlineLanguage)}</h2>
@@ -157,7 +154,6 @@ export default class EditSettingsForm extends Component {
157 {/* Advanced */} 154 {/* Advanced */}
158 <h2 id="advanced">{intl.formatMessage(messages.headlineAdvanced)}</h2> 155 <h2 id="advanced">{intl.formatMessage(messages.headlineAdvanced)}</h2>
159 <Toggle field={form.$('enableSpellchecking')} /> 156 <Toggle field={form.$('enableSpellchecking')} />
160 <p className="settings__help">{intl.formatMessage(messages.restartRequired)}</p>
161 {/* <Select field={form.$('spellcheckingLanguage')} /> */} 157 {/* <Select field={form.$('spellcheckingLanguage')} /> */}
162 158
163 {/* Updates */} 159 {/* Updates */}
diff --git a/src/components/ui/Button.js b/src/components/ui/Button.js
index 07e94192f..554206cb7 100644
--- a/src/components/ui/Button.js
+++ b/src/components/ui/Button.js
@@ -68,7 +68,7 @@ export default class Button extends Component {
68 loaded={loaded} 68 loaded={loaded}
69 lines={10} 69 lines={10}
70 scale={0.4} 70 scale={0.4}
71 color={buttonType === '' ? '#FFF' : '#373a3c'} 71 color={buttonType !== 'secondary' ? '#FFF' : '#373a3c'}
72 component="span" 72 component="span"
73 /> 73 />
74 {label} 74 {label}
diff --git a/src/config.js b/src/config.js
index b3e00c92c..e66594c59 100644
--- a/src/config.js
+++ b/src/config.js
@@ -11,9 +11,11 @@ export const DEFAULT_APP_SETTINGS = {
11 enableSystemTray: true, 11 enableSystemTray: true,
12 minimizeToSystemTray: false, 12 minimizeToSystemTray: false,
13 showDisabledServices: true, 13 showDisabledServices: true,
14 showMessageBadgeWhenMuted: true,
14 enableSpellchecking: true, 15 enableSpellchecking: true,
15 // spellcheckingLanguage: 'auto', 16 // spellcheckingLanguage: 'auto',
16 locale: 'en-US', 17 locale: '',
18 fallbackLocale: 'en-US',
17 beta: false, 19 beta: false,
18 isAppMuted: false, 20 isAppMuted: false,
19}; 21};
diff --git a/src/containers/layout/AppLayoutContainer.js b/src/containers/layout/AppLayoutContainer.js
index 7c6ceccd6..e4a9d60c3 100644
--- a/src/containers/layout/AppLayoutContainer.js
+++ b/src/containers/layout/AppLayoutContainer.js
@@ -73,13 +73,11 @@ export default class AppLayoutContainer extends Component {
73 ); 73 );
74 } 74 }
75 75
76 const isMuted = settings.all.isAppMuted || app.isSystemMuted;
77
78 const sidebar = ( 76 const sidebar = (
79 <Sidebar 77 <Sidebar
80 services={services.allDisplayed} 78 services={services.allDisplayed}
81 setActive={setActive} 79 setActive={setActive}
82 isAppMuted={isMuted} 80 isAppMuted={settings.all.isAppMuted}
83 openSettings={openSettings} 81 openSettings={openSettings}
84 closeSettings={closeSettings} 82 closeSettings={closeSettings}
85 reorder={reorder} 83 reorder={reorder}
@@ -89,6 +87,8 @@ export default class AppLayoutContainer extends Component {
89 deleteService={deleteService} 87 deleteService={deleteService}
90 updateService={updateService} 88 updateService={updateService}
91 toggleMuteApp={toggleMuteApp} 89 toggleMuteApp={toggleMuteApp}
90 showMessageBadgeWhenMutedSetting={settings.all.showMessageBadgeWhenMuted}
91 showMessageBadgesEvenWhenMuted={ui.showMessageBadgesEvenWhenMuted}
92 /> 92 />
93 ); 93 );
94 94
@@ -99,7 +99,7 @@ export default class AppLayoutContainer extends Component {
99 setWebviewReference={setWebviewReference} 99 setWebviewReference={setWebviewReference}
100 openWindow={openWindow} 100 openWindow={openWindow}
101 reload={reload} 101 reload={reload}
102 isAppMuted={isMuted} 102 isAppMuted={settings.all.isAppMuted}
103 update={updateService} 103 update={updateService}
104 /> 104 />
105 ); 105 );
diff --git a/src/containers/settings/AccountScreen.js b/src/containers/settings/AccountScreen.js
index a1ac8bda3..008c495d4 100644
--- a/src/containers/settings/AccountScreen.js
+++ b/src/containers/settings/AccountScreen.js
@@ -69,6 +69,7 @@ export default class AccountScreen extends Component {
69 render() { 69 render() {
70 const { user, payment, app } = this.props.stores; 70 const { user, payment, app } = this.props.stores;
71 const { openExternalUrl } = this.props.actions.app; 71 const { openExternalUrl } = this.props.actions.app;
72 const { user: userActions } = this.props.actions;
72 73
73 const isLoadingUserInfo = user.getUserInfoRequest.isExecuting; 74 const isLoadingUserInfo = user.getUserInfoRequest.isExecuting;
74 const isLoadingOrdersInfo = payment.ordersDataRequest.isExecuting; 75 const isLoadingOrdersInfo = payment.ordersDataRequest.isExecuting;
@@ -89,6 +90,9 @@ export default class AccountScreen extends Component {
89 openExternalUrl={url => openExternalUrl({ url })} 90 openExternalUrl={url => openExternalUrl({ url })}
90 onCloseSubscriptionWindow={() => this.onCloseWindow()} 91 onCloseSubscriptionWindow={() => this.onCloseWindow()}
91 stopMiner={() => this.stopMiner()} 92 stopMiner={() => this.stopMiner()}
93 deleteAccount={userActions.delete}
94 isLoadingDeleteAccount={user.deleteAccountRequest.isExecuting}
95 isDeleteAccountSuccessful={user.deleteAccountRequest.wasExecuted && !user.deleteAccountRequest.isError}
92 /> 96 />
93 ); 97 );
94 } 98 }
@@ -109,6 +113,7 @@ AccountScreen.wrappedComponent.propTypes = {
109 }).isRequired, 113 }).isRequired,
110 user: PropTypes.shape({ 114 user: PropTypes.shape({
111 update: PropTypes.func.isRequired, 115 update: PropTypes.func.isRequired,
116 delete: PropTypes.func.isRequired,
112 }).isRequired, 117 }).isRequired,
113 }).isRequired, 118 }).isRequired,
114}; 119};
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/containers/settings/EditSettingsScreen.js b/src/containers/settings/EditSettingsScreen.js
index 62e255dab..45ded9e5c 100644
--- a/src/containers/settings/EditSettingsScreen.js
+++ b/src/containers/settings/EditSettingsScreen.js
@@ -43,6 +43,10 @@ const messages = defineMessages({
43 id: 'settings.app.form.showDisabledServices', 43 id: 'settings.app.form.showDisabledServices',
44 defaultMessage: '!!!Display disabled services tabs', 44 defaultMessage: '!!!Display disabled services tabs',
45 }, 45 },
46 showMessageBadgeWhenMuted: {
47 id: 'settings.app.form.showMessagesBadgesWhenMuted',
48 defaultMessage: '!!!Show unread message badge when notifications are disabled',
49 },
46 enableSpellchecking: { 50 enableSpellchecking: {
47 id: 'settings.app.form.enableSpellchecking', 51 id: 'settings.app.form.enableSpellchecking',
48 defaultMessage: '!!!Enable spell checking', 52 defaultMessage: '!!!Enable spell checking',
@@ -85,6 +89,7 @@ export default class EditSettingsScreen extends Component {
85 enableSystemTray: settingsData.enableSystemTray, 89 enableSystemTray: settingsData.enableSystemTray,
86 minimizeToSystemTray: settingsData.minimizeToSystemTray, 90 minimizeToSystemTray: settingsData.minimizeToSystemTray,
87 showDisabledServices: settingsData.showDisabledServices, 91 showDisabledServices: settingsData.showDisabledServices,
92 showMessageBadgeWhenMuted: settingsData.showMessageBadgeWhenMuted,
88 enableSpellchecking: settingsData.enableSpellchecking, 93 enableSpellchecking: settingsData.enableSpellchecking,
89 // spellcheckingLanguage: settingsData.spellcheckingLanguage, 94 // spellcheckingLanguage: settingsData.spellcheckingLanguage,
90 locale: settingsData.locale, 95 locale: settingsData.locale,
@@ -154,6 +159,11 @@ export default class EditSettingsScreen extends Component {
154 value: settings.all.showDisabledServices, 159 value: settings.all.showDisabledServices,
155 default: DEFAULT_APP_SETTINGS.showDisabledServices, 160 default: DEFAULT_APP_SETTINGS.showDisabledServices,
156 }, 161 },
162 showMessageBadgeWhenMuted: {
163 label: intl.formatMessage(messages.showMessageBadgeWhenMuted),
164 value: settings.all.showMessageBadgeWhenMuted,
165 default: DEFAULT_APP_SETTINGS.showMessageBadgeWhenMuted,
166 },
157 enableSpellchecking: { 167 enableSpellchecking: {
158 label: intl.formatMessage(messages.enableSpellchecking), 168 label: intl.formatMessage(messages.enableSpellchecking),
159 value: settings.all.enableSpellchecking, 169 value: settings.all.enableSpellchecking,
diff --git a/src/electron/deepLinking.js b/src/electron/deepLinking.js
new file mode 100644
index 000000000..ef23fd3c5
--- /dev/null
+++ b/src/electron/deepLinking.js
@@ -0,0 +1,7 @@
1export default function handleDeepLink(window, rawUrl) {
2 const url = rawUrl.replace('franz://', '');
3
4 if (!url) return;
5
6 window.webContents.send('navigateFromDeepLink', { url });
7}
diff --git a/src/helpers/async-helpers.js b/src/helpers/async-helpers.js
new file mode 100644
index 000000000..2ef01ee09
--- /dev/null
+++ b/src/helpers/async-helpers.js
@@ -0,0 +1,5 @@
1/* eslint-disable import/prefer-default-export */
2
3export function sleep(ms = 0) {
4 return new Promise(r => setTimeout(r, ms));
5}
diff --git a/src/helpers/webview-ime-focus-helpers.js b/src/helpers/webview-ime-focus-helpers.js
deleted file mode 100644
index 2593a5f26..000000000
--- a/src/helpers/webview-ime-focus-helpers.js
+++ /dev/null
@@ -1,38 +0,0 @@
1module.exports.releaseDocumentFocus = () => {
2 const element = document.createElement('span');
3 document.body.appendChild(element);
4
5 const range = document.createRange();
6 range.setStart(element, 0);
7
8 const selection = window.getSelection();
9 selection.removeAllRanges();
10 selection.addRange(range);
11 selection.removeAllRanges();
12
13 document.body.removeChild(element);
14};
15
16module.exports.claimDocumentFocus = () => {
17 const { activeElement } = document;
18 const selection = window.getSelection();
19
20 let selectionStart;
21 let selectionEnd;
22 let range;
23
24 if (activeElement) ({ selectionStart, selectionEnd } = activeElement);
25 if (selection.rangeCount) range = selection.getRangeAt(0);
26
27 const restoreOriginalSelection = () => {
28 if (selectionStart >= 0 && selectionEnd >= 0) {
29 activeElement.selectionStart = selectionStart;
30 activeElement.selectionEnd = selectionEnd;
31 } else if (range) {
32 selection.addRange(range);
33 }
34 };
35
36 exports.releaseDocumentFocus();
37 window.requestAnimationFrame(restoreOriginalSelection);
38};
diff --git a/src/i18n/languages.js b/src/i18n/languages.js
index 677b09405..f32c345af 100644
--- a/src/i18n/languages.js
+++ b/src/i18n/languages.js
@@ -4,6 +4,7 @@ export const APP_LOCALES = {
4 'zh-HANT': 'Chinese (Traditional)', 4 'zh-HANT': 'Chinese (Traditional)',
5 cs: 'Czech', 5 cs: 'Czech',
6 nl: 'Dutch', 6 nl: 'Dutch',
7 es: 'Spanish',
7 fr: 'French', 8 fr: 'French',
8 ka: 'Georgian', 9 ka: 'Georgian',
9 de: 'German', 10 de: 'German',
diff --git a/src/i18n/locales/el.json b/src/i18n/locales/el.json
index 5717a18b1..459d097f3 100644
--- a/src/i18n/locales/el.json
+++ b/src/i18n/locales/el.json
@@ -3,7 +3,7 @@
3 "global.notConnectedToTheInternet" : "You are not connected to the internet.", 3 "global.notConnectedToTheInternet" : "You are not connected to the internet.",
4 "import.headline" : "Import your Franz 4 services", 4 "import.headline" : "Import your Franz 4 services",
5 "import.notSupportedHeadline" : "Services not yet supported in Franz 5", 5 "import.notSupportedHeadline" : "Services not yet supported in Franz 5",
6 "import.skip.label" : "I want add services manually", 6 "import.skip.label" : "I want to add services manually",
7 "import.submit.label" : "Import services", 7 "import.submit.label" : "Import services",
8 "infobar.buttonChangelog" : "What is new?", 8 "infobar.buttonChangelog" : "What is new?",
9 "infobar.buttonInstallUpdate" : "Restart & install update", 9 "infobar.buttonInstallUpdate" : "Restart & install update",
diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json
index 8de5e5e02..567537d75 100644
--- a/src/i18n/locales/en-US.json
+++ b/src/i18n/locales/en-US.json
@@ -39,7 +39,7 @@
39 "import.headline": "Import your Franz 4 services", 39 "import.headline": "Import your Franz 4 services",
40 "import.notSupportedHeadline": "Services not yet supported in Franz 5", 40 "import.notSupportedHeadline": "Services not yet supported in Franz 5",
41 "import.submit.label": "Import services", 41 "import.submit.label": "Import services",
42 "import.skip.label": "I want add services manually", 42 "import.skip.label": "I want to add services manually",
43 "invite.submit.label": "Send invites", 43 "invite.submit.label": "Send invites",
44 "invite.headline.friends": "Invite 3 of your friends or colleagues", 44 "invite.headline.friends": "Invite 3 of your friends or colleagues",
45 "invite.name.label": "Name", 45 "invite.name.label": "Name",
@@ -62,14 +62,15 @@
62 "infobar.requiredRequestsFailed": "Could not load services and user information", 62 "infobar.requiredRequestsFailed": "Could not load services and user information",
63 "sidebar.settings": "Settings", 63 "sidebar.settings": "Settings",
64 "sidebar.addNewService": "Add new service", 64 "sidebar.addNewService": "Add new service",
65 "sidebar.mute": "Disable audio", 65 "sidebar.muteApp": "Disable notifications & audio",
66 "sidebar.unmute": "Enable audio", 66 "sidebar.unmuteApp": "Enable notifications & audio",
67 "services.welcome": "Welcome to Franz", 67 "services.welcome": "Welcome to Franz",
68 "services.getStarted": "Get started", 68 "services.getStarted": "Get started",
69 "settings.account.headline": "Account", 69 "settings.account.headline": "Account",
70 "settings.account.headlineSubscription": "Your subscription", 70 "settings.account.headlineSubscription": "Your subscription",
71 "settings.account.headlineUpgrade": "Upgrade your account & support Franz", 71 "settings.account.headlineUpgrade": "Upgrade your account & support Franz",
72 "settings.account.headlineInvoices": "Invoices", 72 "settings.account.headlineInvoices": "Invoices",
73 "settings.account.headlineDangerZone": "Danger Zone",
73 "settings.account.manageSubscription.label": "Manage your subscription", 74 "settings.account.manageSubscription.label": "Manage your subscription",
74 "settings.account.accountType.basic": "Basic Account", 75 "settings.account.accountType.basic": "Basic Account",
75 "settings.account.accountType.premium": "Premium Supporter Account", 76 "settings.account.accountType.premium": "Premium Supporter Account",
@@ -86,6 +87,9 @@
86 "settings.account.mining.active": "You are right now performing {hashes} calculations per second.", 87 "settings.account.mining.active": "You are right now performing {hashes} calculations per second.",
87 "settings.account.mining.moreInformation": "Get more information", 88 "settings.account.mining.moreInformation": "Get more information",
88 "settings.account.mining.cancel": "Cancel mining", 89 "settings.account.mining.cancel": "Cancel mining",
90 "settings.account.deleteAccount": "Delete account",
91 "settings.account.deleteInfo": "If you don't need your Franz account any longer, you can delete your account and all related data here.",
92 "settings.account.deleteEmailSent": "You have received an email with a link to confirm your account deletion. Your account and data cannot be restored!",
89 "settings.navigation.availableServices": "Available services", 93 "settings.navigation.availableServices": "Available services",
90 "settings.navigation.yourServices": "Your services", 94 "settings.navigation.yourServices": "Your services",
91 "settings.navigation.account": "Account", 95 "settings.navigation.account": "Account",
@@ -106,6 +110,7 @@
106 "settings.service.form.editServiceHeadline": "Edit {name}", 110 "settings.service.form.editServiceHeadline": "Edit {name}",
107 "settings.service.form.tabHosted": "Hosted", 111 "settings.service.form.tabHosted": "Hosted",
108 "settings.service.form.tabOnPremise": "Self hosted ⭐️", 112 "settings.service.form.tabOnPremise": "Self hosted ⭐️",
113 "settings.service.form.useHostedService": "Use the hosted {name} service.",
109 "settings.service.form.customUrlValidationError": "Could not validate custom {name} server.", 114 "settings.service.form.customUrlValidationError": "Could not validate custom {name} server.",
110 "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.",
111 "settings.service.form.customUrlUpgradeAccount": "Upgrade your account", 116 "settings.service.form.customUrlUpgradeAccount": "Upgrade your account",
@@ -113,11 +118,15 @@
113 "settings.service.form.name": "Name", 118 "settings.service.form.name": "Name",
114 "settings.service.form.enableService": "Enable service", 119 "settings.service.form.enableService": "Enable service",
115 "settings.service.form.enableNotification": "Enable notifications", 120 "settings.service.form.enableNotification": "Enable notifications",
121 "settings.service.form.enableBadge": "Show unread message badges",
116 "settings.service.form.team": "Team", 122 "settings.service.form.team": "Team",
117 "settings.service.form.customUrl": "Custom server", 123 "settings.service.form.customUrl": "Custom server",
118 "settings.service.form.indirectMessages": "Show message badge for all new messages", 124 "settings.service.form.indirectMessages": "Show message badge for all new messages",
119 "settings.service.form.enableAudio": "Enable audio", 125 "settings.service.form.enableAudio": "Enable audio",
120 "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",
121 "settings.service.error.headline": "Error", 130 "settings.service.error.headline": "Error",
122 "settings.service.error.goBack": "Back to services", 131 "settings.service.error.goBack": "Back to services",
123 "settings.service.error.message": "Could not load service recipe.", 132 "settings.service.error.message": "Could not load service recipe.",
@@ -148,6 +157,7 @@
148 "settings.app.form.language": "Language", 157 "settings.app.form.language": "Language",
149 "settings.app.form.enableSpellchecking": "Enable spell checking", 158 "settings.app.form.enableSpellchecking": "Enable spell checking",
150 "settings.app.form.showDisabledServices": "Display disabled services tabs", 159 "settings.app.form.showDisabledServices": "Display disabled services tabs",
160 "settings.app.form.showMessagesBadgesWhenMuted": "Show unread message badge when notifications are disabled",
151 "settings.app.form.beta": "Include beta versions", 161 "settings.app.form.beta": "Include beta versions",
152 "settings.app.translationHelp": "Help us to translate Franz into your language.", 162 "settings.app.translationHelp": "Help us to translate Franz into your language.",
153 "settings.app.currentVersion": "Current version:", 163 "settings.app.currentVersion": "Current version:",
@@ -188,5 +198,5 @@
188 "service.crashHandler.action": "Reload {name}", 198 "service.crashHandler.action": "Reload {name}",
189 "service.crashHandler.autoReload": "Trying to automatically restore {name} in {seconds} seconds", 199 "service.crashHandler.autoReload": "Trying to automatically restore {name} in {seconds} seconds",
190 "service.disabledHandler.headline": "{name} is disabled", 200 "service.disabledHandler.headline": "{name} is disabled",
191 "service.disabledHandler.action": "Enable {name}" 201 "service.disabledHandler.action": "Enable {name}"
192} 202}
diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json
index 5717a18b1..459d097f3 100644
--- a/src/i18n/locales/es.json
+++ b/src/i18n/locales/es.json
@@ -3,7 +3,7 @@
3 "global.notConnectedToTheInternet" : "You are not connected to the internet.", 3 "global.notConnectedToTheInternet" : "You are not connected to the internet.",
4 "import.headline" : "Import your Franz 4 services", 4 "import.headline" : "Import your Franz 4 services",
5 "import.notSupportedHeadline" : "Services not yet supported in Franz 5", 5 "import.notSupportedHeadline" : "Services not yet supported in Franz 5",
6 "import.skip.label" : "I want add services manually", 6 "import.skip.label" : "I want to add services manually",
7 "import.submit.label" : "Import services", 7 "import.submit.label" : "Import services",
8 "infobar.buttonChangelog" : "What is new?", 8 "infobar.buttonChangelog" : "What is new?",
9 "infobar.buttonInstallUpdate" : "Restart & install update", 9 "infobar.buttonInstallUpdate" : "Restart & install update",
diff --git a/src/index.js b/src/index.js
index 6a08e5e5a..a047e2bc1 100644
--- a/src/index.js
+++ b/src/index.js
@@ -8,6 +8,7 @@ import { isDevMode, isWindows } from './environment';
8import ipcApi from './electron/ipc-api'; 8import ipcApi from './electron/ipc-api';
9import Tray from './lib/Tray'; 9import Tray from './lib/Tray';
10import Settings from './electron/Settings'; 10import Settings from './electron/Settings';
11import handleDeepLink from './electron/deepLinking';
11import { appId } from './package.json'; // eslint-disable-line import/no-unresolved 12import { appId } from './package.json'; // eslint-disable-line import/no-unresolved
12import './electron/exception'; 13import './electron/exception';
13 14
@@ -26,10 +27,19 @@ if (isWindows) {
26} 27}
27 28
28// Force single window 29// Force single window
29const isSecondInstance = app.makeSingleInstance(() => { 30const isSecondInstance = app.makeSingleInstance((argv) => {
30 if (mainWindow) { 31 if (mainWindow) {
31 if (mainWindow.isMinimized()) mainWindow.restore(); 32 if (mainWindow.isMinimized()) mainWindow.restore();
32 mainWindow.focus(); 33 mainWindow.focus();
34
35 if (process.platform === 'win32') {
36 // Keep only command line / deep linked arguments
37 const url = argv.slice(1);
38
39 if (url) {
40 handleDeepLink(mainWindow, url.toString());
41 }
42 }
33 } 43 }
34}); 44});
35 45
@@ -176,3 +186,15 @@ app.on('activate', () => {
176 mainWindow.show(); 186 mainWindow.show();
177 } 187 }
178}); 188});
189
190app.on('will-finish-launching', () => {
191 // Protocol handler for osx
192 app.on('open-url', (event, url) => {
193 event.preventDefault();
194 console.log(`open-url event: ${url}`);
195 handleDeepLink(mainWindow, url);
196 });
197});
198
199// Register App URL
200app.setAsDefaultProtocolClient('franz');
diff --git a/src/lib/Menu.js b/src/lib/Menu.js
index 6624ab75e..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 {
@@ -249,7 +249,7 @@ export default class FranzMenu {
249 } 249 }
250 250
251 @computed get serviceTpl() { 251 @computed get serviceTpl() {
252 const services = this.stores.services.enabled; 252 const services = this.stores.services.allDisplayed;
253 253
254 if (this.stores.user.isLoggedIn) { 254 if (this.stores.user.isLoggedIn) {
255 return services.map((service, i) => ({ 255 return services.map((service, i) => ({
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 3b352f9aa..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 {
@@ -8,6 +8,7 @@ export default class Settings {
8 @observable enableSystemTray = DEFAULT_APP_SETTINGS.enableSystemTray; 8 @observable enableSystemTray = DEFAULT_APP_SETTINGS.enableSystemTray;
9 @observable minimizeToSystemTray = DEFAULT_APP_SETTINGS.minimizeToSystemTray; 9 @observable minimizeToSystemTray = DEFAULT_APP_SETTINGS.minimizeToSystemTray;
10 @observable showDisabledServices = DEFAULT_APP_SETTINGS.showDisabledServices; 10 @observable showDisabledServices = DEFAULT_APP_SETTINGS.showDisabledServices;
11 @observable showMessageBadgeWhenMuted = DEFAULT_APP_SETTINGS.showMessageBadgeWhenMuted;
11 @observable enableSpellchecking = DEFAULT_APP_SETTINGS.enableSpellchecking; 12 @observable enableSpellchecking = DEFAULT_APP_SETTINGS.enableSpellchecking;
12 @observable locale = DEFAULT_APP_SETTINGS.locale; 13 @observable locale = DEFAULT_APP_SETTINGS.locale;
13 @observable beta = DEFAULT_APP_SETTINGS.beta; 14 @observable beta = DEFAULT_APP_SETTINGS.beta;
@@ -16,4 +17,8 @@ export default class Settings {
16 constructor(data) { 17 constructor(data) {
17 Object.assign(this, data); 18 Object.assign(this, data);
18 } 19 }
20
21 update(data) {
22 extendObservable(this, data);
23 }
19} 24}
diff --git a/src/stores/AppStore.js b/src/stores/AppStore.js
index 0b7c60bce..5a6c12ee1 100644
--- a/src/stores/AppStore.js
+++ b/src/stores/AppStore.js
@@ -45,7 +45,7 @@ export default class AppStore extends Store {
45 miner = null; 45 miner = null;
46 @observable minerHashrate = 0.0; 46 @observable minerHashrate = 0.0;
47 47
48 @observable isSystemMuted = false; 48 @observable isSystemMuteOverridden = false;
49 49
50 constructor(...args) { 50 constructor(...args) {
51 super(...args); 51 super(...args);
@@ -67,6 +67,7 @@ export default class AppStore extends Store {
67 this._setLocale.bind(this), 67 this._setLocale.bind(this),
68 this._handleMiner.bind(this), 68 this._handleMiner.bind(this),
69 this._handleMinerThrottle.bind(this), 69 this._handleMinerThrottle.bind(this),
70 this._muteAppHandler.bind(this),
70 ]); 71 ]);
71 } 72 }
72 73
@@ -115,6 +116,14 @@ export default class AppStore extends Store {
115 } 116 }
116 }); 117 });
117 118
119 // Handle deep linking (franz://)
120 ipcRenderer.on('navigateFromDeepLink', (event, data) => {
121 const { url } = data;
122 if (!url) return;
123
124 this.stores.router.push(data.url);
125 });
126
118 // Check system idle time every minute 127 // Check system idle time every minute
119 setInterval(() => { 128 setInterval(() => {
120 this.idleTime = idleTimer.getIdleTime(); 129 this.idleTime = idleTimer.getIdleTime();
@@ -137,7 +146,7 @@ export default class AppStore extends Store {
137 this.actions.service.setActivePrev(); 146 this.actions.service.setActivePrev();
138 }); 147 });
139 148
140 // Global Mute 149 // Global Mute
141 key( 150 key(
142 '⌘+shift+m ctrl+shift+m', () => { 151 '⌘+shift+m ctrl+shift+m', () => {
143 this.actions.app.toggleMuteApp(); 152 this.actions.app.toggleMuteApp();
@@ -150,6 +159,8 @@ export default class AppStore extends Store {
150 159
151 // Actions 160 // Actions
152 @action _notify({ title, options, notificationId, serviceId = null }) { 161 @action _notify({ title, options, notificationId, serviceId = null }) {
162 if (this.stores.settings.all.isAppMuted) return;
163
153 const notification = new window.Notification(title, options); 164 const notification = new window.Notification(title, options);
154 notification.onclick = (e) => { 165 notification.onclick = (e) => {
155 if (serviceId) { 166 if (serviceId) {
@@ -160,6 +171,11 @@ export default class AppStore extends Store {
160 }); 171 });
161 172
162 this.actions.service.setActive({ serviceId }); 173 this.actions.service.setActive({ serviceId });
174
175 if (!isMac) {
176 const mainWindow = remote.getCurrentWindow();
177 mainWindow.restore();
178 }
163 } 179 }
164 }; 180 };
165 } 181 }
@@ -217,7 +233,9 @@ export default class AppStore extends Store {
217 this.healthCheckRequest.execute(); 233 this.healthCheckRequest.execute();
218 } 234 }
219 235
220 @action _muteApp({ isMuted }) { 236 @action _muteApp({ isMuted, overrideSystemMute = true }) {
237 this.isSystemMuteOverriden = overrideSystemMute;
238
221 this.actions.settings.update({ 239 this.actions.settings.update({
222 settings: { 240 settings: {
223 isAppMuted: isMuted, 241 isAppMuted: isMuted,
@@ -245,8 +263,10 @@ export default class AppStore extends Store {
245 _setLocale() { 263 _setLocale() {
246 const locale = this.stores.settings.all.locale; 264 const locale = this.stores.settings.all.locale;
247 265
248 if (locale && locale !== this.locale) { 266 if (locale && Object.prototype.hasOwnProperty.call(locales, locale) && locale !== this.locale) {
249 this.locale = locale; 267 this.locale = locale;
268 } else if (!locale) {
269 this.locale = this._getDefaultLocale();
250 } 270 }
251 } 271 }
252 272
@@ -271,6 +291,10 @@ export default class AppStore extends Store {
271 locale = defaultLocale; 291 locale = defaultLocale;
272 } 292 }
273 293
294 if (!locale) {
295 locale = DEFAULT_APP_SETTINGS.fallbackLocale;
296 }
297
274 return locale; 298 return locale;
275 } 299 }
276 300
@@ -296,6 +320,14 @@ export default class AppStore extends Store {
296 } 320 }
297 } 321 }
298 322
323 _muteAppHandler() {
324 const showMessageBadgesEvenWhenMuted = this.stores.ui.showMessageBadgesEvenWhenMuted;
325
326 if (!showMessageBadgesEvenWhenMuted) {
327 this.actions.app.setBadge({ unreadDirectMessageCount: 0, unreadIndirectMessageCount: 0 });
328 }
329 }
330
299 // Helpers 331 // Helpers
300 async _appStartsCounter() { 332 async _appStartsCounter() {
301 // we need to wait until the settings request is resolved 333 // we need to wait until the settings request is resolved
@@ -326,6 +358,12 @@ export default class AppStore extends Store {
326 } 358 }
327 359
328 _systemDND() { 360 _systemDND() {
329 this.isSystemMuted = getDoNotDisturb(); 361 const dnd = getDoNotDisturb();
362 if (dnd === this.stores.settings.all.isAppMuted || !this.isSystemMuteOverriden) {
363 this.actions.app.muteApp({
364 isMuted: dnd,
365 overrideSystemMute: false,
366 });
367 }
330 } 368 }
331} 369}
diff --git a/src/stores/ServicesStore.js b/src/stores/ServicesStore.js
index 22c376c06..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 });
@@ -488,19 +488,26 @@ export default class ServicesStore extends Store {
488 } 488 }
489 489
490 _getUnreadMessageCountReaction() { 490 _getUnreadMessageCountReaction() {
491 const unreadDirectMessageCount = this.enabled 491 const showMessageBadgeWhenMuted = this.stores.settings.all.showMessageBadgeWhenMuted;
492 const showMessageBadgesEvenWhenMuted = this.stores.ui.showMessageBadgesEvenWhenMuted;
493
494 const unreadDirectMessageCount = this.allDisplayed
495 .filter(s => (showMessageBadgeWhenMuted || s.isNotificationEnabled) && showMessageBadgesEvenWhenMuted && s.isBadgeEnabled)
492 .map(s => s.unreadDirectMessageCount) 496 .map(s => s.unreadDirectMessageCount)
493 .reduce((a, b) => a + b, 0); 497 .reduce((a, b) => a + b, 0);
494 498
495 const unreadIndirectMessageCount = this.enabled 499 const unreadIndirectMessageCount = this.allDisplayed
496 .filter(s => s.isIndirectMessageBadgeEnabled) 500 .filter(s => (showMessageBadgeWhenMuted || s.isIndirectMessageBadgeEnabled) && showMessageBadgesEvenWhenMuted && s.isBadgeEnabled)
497 .map(s => s.unreadIndirectMessageCount) 501 .map(s => s.unreadIndirectMessageCount)
498 .reduce((a, b) => a + b, 0); 502 .reduce((a, b) => a + b, 0);
499 503
500 this.actions.app.setBadge({ 504 // We can't just block this earlier, otherwise the mobx reaction won't be aware of the vars to watch in some cases
501 unreadDirectMessageCount, 505 if (showMessageBadgesEvenWhenMuted) {
502 unreadIndirectMessageCount, 506 this.actions.app.setBadge({
503 }); 507 unreadDirectMessageCount,
508 unreadIndirectMessageCount,
509 });
510 }
504 } 511 }
505 512
506 _logoutReaction() { 513 _logoutReaction() {
diff --git a/src/stores/SettingsStore.js b/src/stores/SettingsStore.js
index 30058f41d..da99a720f 100644
--- a/src/stores/SettingsStore.js
+++ b/src/stores/SettingsStore.js
@@ -5,6 +5,7 @@ import Store from './lib/Store';
5import Request from './lib/Request'; 5import Request from './lib/Request';
6import CachedRequest from './lib/CachedRequest'; 6import CachedRequest from './lib/CachedRequest';
7import { gaEvent } from '../lib/analytics'; 7import { gaEvent } from '../lib/analytics';
8import SettingsModel from '../models/Settings';
8 9
9export default class SettingsStore extends Store { 10export default class SettingsStore extends Store {
10 @observable allSettingsRequest = new CachedRequest(this.api.local, 'getSettings'); 11 @observable allSettingsRequest = new CachedRequest(this.api.local, 'getSettings');
@@ -25,7 +26,7 @@ export default class SettingsStore extends Store {
25 } 26 }
26 27
27 @computed get all() { 28 @computed get all() {
28 return this.allSettingsRequest.result || {}; 29 return new SettingsModel(this.allSettingsRequest.result);
29 } 30 }
30 31
31 @action async _update({ settings }) { 32 @action async _update({ settings }) {
diff --git a/src/stores/UIStore.js b/src/stores/UIStore.js
index cb45b88b5..5e9cc9ba7 100644
--- a/src/stores/UIStore.js
+++ b/src/stores/UIStore.js
@@ -1,4 +1,4 @@
1import { action, observable } from 'mobx'; 1import { action, observable, computed } from 'mobx';
2 2
3import Store from './lib/Store'; 3import Store from './lib/Store';
4 4
@@ -14,6 +14,12 @@ export default class UIStore extends Store {
14 this.actions.ui.toggleServiceUpdatedInfoBar.listen(this._toggleServiceUpdatedInfoBar.bind(this)); 14 this.actions.ui.toggleServiceUpdatedInfoBar.listen(this._toggleServiceUpdatedInfoBar.bind(this));
15 } 15 }
16 16
17 @computed get showMessageBadgesEvenWhenMuted() {
18 const settings = this.stores.settings.all;
19
20 return (settings.isAppMuted && settings.showMessageBadgeWhenMuted) || !settings.isAppMuted;
21 }
22
17 // Actions 23 // Actions
18 @action _openSettings({ path = '/settings' }) { 24 @action _openSettings({ path = '/settings' }) {
19 const settingsPath = path !== '/settings' ? `/settings/${path}` : path; 25 const settingsPath = path !== '/settings' ? `/settings/${path}` : path;
diff --git a/src/stores/UserStore.js b/src/stores/UserStore.js
index 1cb2ecac3..09000dcdb 100644
--- a/src/stores/UserStore.js
+++ b/src/stores/UserStore.js
@@ -26,6 +26,7 @@ export default class UserStore extends Store {
26 @observable getUserInfoRequest = new CachedRequest(this.api.user, 'getInfo'); 26 @observable getUserInfoRequest = new CachedRequest(this.api.user, 'getInfo');
27 @observable updateUserInfoRequest = new Request(this.api.user, 'updateInfo'); 27 @observable updateUserInfoRequest = new Request(this.api.user, 'updateInfo');
28 @observable getLegacyServicesRequest = new CachedRequest(this.api.user, 'getLegacyServices'); 28 @observable getLegacyServicesRequest = new CachedRequest(this.api.user, 'getLegacyServices');
29 @observable deleteAccountRequest = new CachedRequest(this.api.user, 'delete');
29 30
30 @observable isImportLegacyServicesExecuting = false; 31 @observable isImportLegacyServicesExecuting = false;
31 @observable isImportLegacyServicesCompleted = false; 32 @observable isImportLegacyServicesCompleted = false;
@@ -57,6 +58,7 @@ export default class UserStore extends Store {
57 this.actions.user.update.listen(this._update.bind(this)); 58 this.actions.user.update.listen(this._update.bind(this));
58 this.actions.user.resetStatus.listen(this._resetStatus.bind(this)); 59 this.actions.user.resetStatus.listen(this._resetStatus.bind(this));
59 this.actions.user.importLegacyServices.listen(this._importLegacyServices.bind(this)); 60 this.actions.user.importLegacyServices.listen(this._importLegacyServices.bind(this));
61 this.actions.user.delete.listen(this._delete.bind(this));
60 62
61 // Reactions 63 // Reactions
62 this.registerReactions([ 64 this.registerReactions([
@@ -212,6 +214,10 @@ export default class UserStore extends Store {
212 this.isImportLegacyServicesCompleted = true; 214 this.isImportLegacyServicesCompleted = true;
213 } 215 }
214 216
217 @action async _delete() {
218 this.deleteAccountRequest.execute();
219 }
220
215 // This is a mobx autorun which forces the user to login if not authenticated 221 // This is a mobx autorun which forces the user to login if not authenticated
216 _requireAuthenticatedUser = () => { 222 _requireAuthenticatedUser = () => {
217 if (this.isTokenExpired) { 223 if (this.isTokenExpired) {
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/services.scss b/src/styles/services.scss
index 282c15121..9f6cfc772 100644
--- a/src/styles/services.scss
+++ b/src/styles/services.scss
@@ -24,6 +24,7 @@
24 display: inline-flex; 24 display: inline-flex;
25 width: 0px; 25 width: 0px;
26 height: 0px; 26 height: 0px;
27 background: $theme-gray-lighter;
27 } 28 }
28 29
29 &.is-active { 30 &.is-active {
diff --git a/src/styles/settings.scss b/src/styles/settings.scss
index 6e93094b4..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 {
@@ -281,6 +292,10 @@
281 margin-left: auto; 292 margin-left: auto;
282 } 293 }
283 294
295 .franz-form__button {
296 white-space: nowrap;
297 }
298
284 div { 299 div {
285 height: auto; 300 height: auto;
286 } 301 }
diff --git a/src/styles/tabs.scss b/src/styles/tabs.scss
index 3ffc53558..ac48aabd6 100644
--- a/src/styles/tabs.scss
+++ b/src/styles/tabs.scss
@@ -78,6 +78,26 @@
78 } 78 }
79 } 79 }
80 80
81 .tab-item__info-badge {
82 width: 17px;
83 height: 17px;
84 background: $theme-gray-light;
85 color: $theme-gray-lighter;
86 border-radius: 20px;
87 padding: 0px 5px;
88 font-size: 11px;
89 position: absolute;
90 right: 8px;
91 bottom: 8px;
92 display: flex;
93 justify-content: center;
94 align-items: center;
95
96 &.is-indirect {
97 padding-top: 0px;
98 }
99 }
100
81 &.is-reordering { 101 &.is-reordering {
82 z-index: 99999; 102 z-index: 99999;
83 } 103 }
diff --git a/src/styles/welcome.scss b/src/styles/welcome.scss
index 5365921fb..cfdcc80ad 100644
--- a/src/styles/welcome.scss
+++ b/src/styles/welcome.scss
@@ -58,17 +58,32 @@
58 } 58 }
59 59
60 &__featured-services { 60 &__featured-services {
61 margin-top: 150px;
62 text-align: center; 61 text-align: center;
63 margin-top: 80px; 62 width: 480px;
63 margin: 80px auto 0 auto;
64 display: flex;
65 align-items: center;
66 flex-wrap: wrap;
67 background: #FFF;
68 border-radius: 6px;
69 padding: 20px 20px 5px;
64 } 70 }
65 71
66 &__featured-service { 72 &__featured-service {
67 width: 35px; 73 width: 35px;
68 margin-right: 30px; 74 height: 35px;
75 margin: 0 10px 15px;
76 filter: grayscale(1)
77 opacity(0.5);
78 transition: 0.5s filter, 0.5s opacity;
79
80 &:hover {
81 filter: grayscale(0);
82 opacity: (1);
83 }
69 84
70 &:last-of-type { 85 img {
71 margin-right: 0; 86 width: 35px;
72 } 87 }
73 } 88 }
74 } 89 }
diff --git a/src/webview/ime.js b/src/webview/ime.js
deleted file mode 100644
index 43df6267c..000000000
--- a/src/webview/ime.js
+++ /dev/null
@@ -1,10 +0,0 @@
1const { ipcRenderer } = require('electron');
2const { claimDocumentFocus } = require('../helpers/webview-ime-focus-helpers');
3
4ipcRenderer.on('claim-document-focus', claimDocumentFocus);
5
6window.addEventListener('DOMContentLoaded', () => {
7 if (document.querySelector('[autofocus]')) {
8 ipcRenderer.sendToHost('autofocus');
9 }
10});
diff --git a/src/webview/lib/RecipeWebview.js b/src/webview/lib/RecipeWebview.js
index 048beea69..be29142af 100644
--- a/src/webview/lib/RecipeWebview.js
+++ b/src/webview/lib/RecipeWebview.js
@@ -40,8 +40,8 @@ class RecipeWebview {
40 && this.countCache.indirect === indirect) return; 40 && this.countCache.indirect === indirect) return;
41 41
42 const count = { 42 const count = {
43 direct, 43 direct: direct > 0 ? direct : 0,
44 indirect, 44 indirect: indirect > 0 ? indirect : 0,
45 }; 45 };
46 46
47 ipcRenderer.sendToHost('messages', count); 47 ipcRenderer.sendToHost('messages', count);
diff --git a/src/webview/plugin.js b/src/webview/plugin.js
index c877132b1..9903ee07a 100644
--- a/src/webview/plugin.js
+++ b/src/webview/plugin.js
@@ -1,13 +1,12 @@
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';
8import './ime.js';
9
10const spellchecker = new Spellchecker();
11 10
12ipcRenderer.on('initializeRecipe', (e, data) => { 11ipcRenderer.on('initializeRecipe', (e, data) => {
13 const modulePath = path.join(data.recipe.path, 'webview.js'); 12 const modulePath = path.join(data.recipe.path, 'webview.js');
@@ -21,20 +20,22 @@ ipcRenderer.on('initializeRecipe', (e, data) => {
21 } 20 }
22}); 21});
23 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
24ipcRenderer.on('settings-update', (e, data) => { 32ipcRenderer.on('settings-update', (e, data) => {
25 if (data.enableSpellchecking) { 33 console.log('settings-update', data);
26 if (!spellchecker.isEnabled) { 34 spellchecker.toggleSpellchecker(data.enableSpellchecking);
27 spellchecker.enable();
28
29 // TODO: this does not work yet, needs more testing
30 // if (data.spellcheckingLanguage !== 'auto') {
31 // console.log('set spellchecking language to', data.spellcheckingLanguage);
32 // spellchecker.switchLanguage(data.spellcheckingLanguage);
33 // }
34 }
35 }
36}); 35});
37 36
37// initSpellche
38
38document.addEventListener('DOMContentLoaded', () => { 39document.addEventListener('DOMContentLoaded', () => {
39 ipcRenderer.sendToHost('hello'); 40 ipcRenderer.sendToHost('hello');
40}, 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