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/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/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/settings/EditSettingsForm.js1
-rw-r--r--src/components/ui/Button.js2
-rw-r--r--src/config.js1
-rw-r--r--src/containers/layout/AppLayoutContainer.js8
-rw-r--r--src/containers/settings/AccountScreen.js5
-rw-r--r--src/containers/settings/EditSettingsScreen.js10
-rw-r--r--src/electron/deepLinking.js5
-rw-r--r--src/helpers/async-helpers.js5
-rw-r--r--src/i18n/locales/el.json2
-rw-r--r--src/i18n/locales/en-US.json13
-rw-r--r--src/i18n/locales/es.json2
-rw-r--r--src/index.js30
-rw-r--r--src/lib/Menu.js2
-rw-r--r--src/models/Settings.js1
-rw-r--r--src/stores/AppStore.js40
-rw-r--r--src/stores/ServicesStore.js17
-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/settings.scss4
-rw-r--r--src/styles/tabs.scss20
-rw-r--r--src/styles/welcome.scss25
33 files changed, 307 insertions, 57 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/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/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..8403d9462 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) {
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/settings/EditSettingsForm.js b/src/components/settings/settings/EditSettingsForm.js
index 4ce9b7ab2..878e46d6d 100644
--- a/src/components/settings/settings/EditSettingsForm.js
+++ b/src/components/settings/settings/EditSettingsForm.js
@@ -142,6 +142,7 @@ export default class EditSettingsForm extends Component {
142 {/* Appearance */} 142 {/* Appearance */}
143 <h2 id="apperance">{intl.formatMessage(messages.headlineAppearance)}</h2> 143 <h2 id="apperance">{intl.formatMessage(messages.headlineAppearance)}</h2>
144 <Toggle field={form.$('showDisabledServices')} /> 144 <Toggle field={form.$('showDisabledServices')} />
145 <Toggle field={form.$('showMessageBadgeWhenMuted')} />
145 146
146 {/* Language */} 147 {/* Language */}
147 <h2 id="language">{intl.formatMessage(messages.headlineLanguage)}</h2> 148 <h2 id="language">{intl.formatMessage(messages.headlineLanguage)}</h2>
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..e6d8958e6 100644
--- a/src/config.js
+++ b/src/config.js
@@ -11,6 +11,7 @@ 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: 'en-US',
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/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..16e68b914
--- /dev/null
+++ b/src/electron/deepLinking.js
@@ -0,0 +1,5 @@
1export default function handleDeepLink(window, rawUrl) {
2 const url = rawUrl.replace('franz://', '');
3
4 window.webContents.send('navigateFromDeepLink', { url });
5}
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/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..48b408e59 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",
@@ -148,6 +152,7 @@
148 "settings.app.form.language": "Language", 152 "settings.app.form.language": "Language",
149 "settings.app.form.enableSpellchecking": "Enable spell checking", 153 "settings.app.form.enableSpellchecking": "Enable spell checking",
150 "settings.app.form.showDisabledServices": "Display disabled services tabs", 154 "settings.app.form.showDisabledServices": "Display disabled services tabs",
155 "settings.app.form.showMessagesBadgesWhenMuted": "Show unread message badge when notifications are disabled",
151 "settings.app.form.beta": "Include beta versions", 156 "settings.app.form.beta": "Include beta versions",
152 "settings.app.translationHelp": "Help us to translate Franz into your language.", 157 "settings.app.translationHelp": "Help us to translate Franz into your language.",
153 "settings.app.currentVersion": "Current version:", 158 "settings.app.currentVersion": "Current version:",
@@ -188,5 +193,5 @@
188 "service.crashHandler.action": "Reload {name}", 193 "service.crashHandler.action": "Reload {name}",
189 "service.crashHandler.autoReload": "Trying to automatically restore {name} in {seconds} seconds", 194 "service.crashHandler.autoReload": "Trying to automatically restore {name} in {seconds} seconds",
190 "service.disabledHandler.headline": "{name} is disabled", 195 "service.disabledHandler.headline": "{name} is disabled",
191 "service.disabledHandler.action": "Enable {name}" 196 "service.disabledHandler.action": "Enable {name}"
192} 197}
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..4253b681f 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,20 @@ 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 console.log(url.toString());
41 handleDeepLink(mainWindow, url.toString());
42 }
43 }
33 } 44 }
34}); 45});
35 46
@@ -70,7 +81,8 @@ const createWindow = () => {
70 const trayIcon = new Tray(); 81 const trayIcon = new Tray();
71 82
72 // Initialize ipcApi 83 // Initialize ipcApi
73 ipcApi({ mainWindow, settings, trayIcon }); 84 const franzIpcApi = ipcApi({ mainWindow, settings, trayIcon });
85 console.log(franzIpcApi);
74 86
75 // Manage Window State 87 // Manage Window State
76 mainWindowState.manage(mainWindow); 88 mainWindowState.manage(mainWindow);
@@ -137,6 +149,8 @@ const createWindow = () => {
137 149
138 mainWindow.on('show', () => { 150 mainWindow.on('show', () => {
139 mainWindow.setSkipTaskbar(false); 151 mainWindow.setSkipTaskbar(false);
152
153 handleDeepLink(mainWindow, 'franz://settings/services/add/msteams');
140 }); 154 });
141 155
142 app.mainWindow = mainWindow; 156 app.mainWindow = mainWindow;
@@ -176,3 +190,15 @@ app.on('activate', () => {
176 mainWindow.show(); 190 mainWindow.show();
177 } 191 }
178}); 192});
193
194app.on('will-finish-launching', () => {
195 // Protocol handler for osx
196 app.on('open-url', (event, url) => {
197 event.preventDefault();
198 console.log(`open-url event: ${url}`);
199 handleDeepLink(mainWindow, url);
200 });
201});
202
203// Register App URL
204app.setAsDefaultProtocolClient('franz');
diff --git a/src/lib/Menu.js b/src/lib/Menu.js
index 6624ab75e..d9c30466b 100644
--- a/src/lib/Menu.js
+++ b/src/lib/Menu.js
@@ -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/Settings.js b/src/models/Settings.js
index 3b352f9aa..35bfe0d05 100644
--- a/src/models/Settings.js
+++ b/src/models/Settings.js
@@ -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;
diff --git a/src/stores/AppStore.js b/src/stores/AppStore.js
index 0b7c60bce..3e6d4d288 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,
@@ -296,6 +314,14 @@ export default class AppStore extends Store {
296 } 314 }
297 } 315 }
298 316
317 _muteAppHandler() {
318 const showMessageBadgesEvenWhenMuted = this.stores.ui.showMessageBadgesEvenWhenMuted;
319
320 if (!showMessageBadgesEvenWhenMuted) {
321 this.actions.app.setBadge({ unreadDirectMessageCount: 0, unreadIndirectMessageCount: 0 });
322 }
323 }
324
299 // Helpers 325 // Helpers
300 async _appStartsCounter() { 326 async _appStartsCounter() {
301 // we need to wait until the settings request is resolved 327 // we need to wait until the settings request is resolved
@@ -326,6 +352,12 @@ export default class AppStore extends Store {
326 } 352 }
327 353
328 _systemDND() { 354 _systemDND() {
329 this.isSystemMuted = getDoNotDisturb(); 355 const dnd = getDoNotDisturb();
356 if (dnd === this.stores.settings.all.isAppMuted || !this.isSystemMuteOverriden) {
357 this.actions.app.muteApp({
358 isMuted: dnd,
359 overrideSystemMute: false,
360 });
361 }
330 } 362 }
331} 363}
diff --git a/src/stores/ServicesStore.js b/src/stores/ServicesStore.js
index 22c376c06..b04aafd78 100644
--- a/src/stores/ServicesStore.js
+++ b/src/stores/ServicesStore.js
@@ -488,19 +488,26 @@ export default class ServicesStore extends Store {
488 } 488 }
489 489
490 _getUnreadMessageCountReaction() { 490 _getUnreadMessageCountReaction() {
491 const showMessageBadgeWhenMuted = this.stores.settings.all.showMessageBadgeWhenMuted;
492 const showMessageBadgesEvenWhenMuted = this.stores.ui.showMessageBadgesEvenWhenMuted;
493
491 const unreadDirectMessageCount = this.enabled 494 const unreadDirectMessageCount = this.enabled
495 .filter(s => (showMessageBadgeWhenMuted || s.isNotificationEnabled) && showMessageBadgesEvenWhenMuted)
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.enabled
496 .filter(s => s.isIndirectMessageBadgeEnabled) 500 .filter(s => (showMessageBadgeWhenMuted || s.isIndirectMessageBadgeEnabled) && showMessageBadgesEvenWhenMuted)
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..33473f16d 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 this.allSettingsRequest.result || new SettingsModel();
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/settings.scss b/src/styles/settings.scss
index 6e93094b4..73cef0813 100644
--- a/src/styles/settings.scss
+++ b/src/styles/settings.scss
@@ -281,6 +281,10 @@
281 margin-left: auto; 281 margin-left: auto;
282 } 282 }
283 283
284 .franz-form__button {
285 white-space: nowrap;
286 }
287
284 div { 288 div {
285 height: auto; 289 height: auto;
286 } 290 }
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 }