aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.vscode/launch.json11
-rw-r--r--appveyor.yml7
-rw-r--r--package.json1
-rw-r--r--src/actions/app.js4
-rw-r--r--src/actions/service.js7
-rw-r--r--src/api/server/ServerApi.js2
-rw-r--r--src/components/layout/Sidebar.js52
-rw-r--r--src/components/services/content/ServiceWebview.js44
-rw-r--r--src/components/services/content/Services.js3
-rw-r--r--src/components/services/tabs/TabBarSortableList.js22
-rw-r--r--src/components/services/tabs/TabItem.js15
-rw-r--r--src/components/services/tabs/Tabbar.js3
-rw-r--r--src/components/settings/navigation/SettingsNavigation.js1
-rw-r--r--src/components/settings/services/EditServiceForm.js12
-rw-r--r--src/components/settings/services/ServiceItem.js22
-rw-r--r--src/components/settings/settings/EditSettingsForm.js29
-rw-r--r--src/components/ui/AppLoader.js2
-rw-r--r--src/components/ui/Loader.js5
-rw-r--r--src/components/ui/Subscription.js20
-rw-r--r--src/config.js5
-rw-r--r--src/containers/layout/AppLayoutContainer.js16
-rw-r--r--src/containers/settings/EditServiceScreen.js15
-rw-r--r--src/containers/settings/EditSettingsScreen.js48
-rw-r--r--src/i18n/languages.js45
-rw-r--r--src/i18n/locales/en-US.json31
-rw-r--r--src/i18n/translations.js4
-rw-r--r--src/index.js47
-rw-r--r--src/lib/Miner.js2
-rw-r--r--src/models/Service.js3
-rw-r--r--src/stores/AppStore.js27
-rw-r--r--src/stores/ServicesStore.js41
-rw-r--r--src/stores/SettingsStore.js8
-rw-r--r--src/styles/layout.scss33
-rw-r--r--src/styles/settings.scss15
-rw-r--r--src/styles/subscription.scss5
-rw-r--r--src/styles/tabs.scss12
-rw-r--r--src/webview/lib/RecipeWebview.js2
-rw-r--r--src/webview/notifications.js4
-rw-r--r--src/webview/plugin.js28
-rw-r--r--src/webview/spellchecker.js40
-rw-r--r--yarn.lock35
41 files changed, 542 insertions, 186 deletions
diff --git a/.vscode/launch.json b/.vscode/launch.json
index abbbdd64b..a8300f84f 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -22,6 +22,17 @@
22 "env": { 22 "env": {
23 "LIVE_API": "1" 23 "LIVE_API": "1"
24 } 24 }
25 },
26 {
27 "type": "node",
28 "request": "launch",
29 "name": "Franz – Local API",
30 "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
31 "program": "${workspaceFolder}/build/index.js",
32 "protocol": "inspector",
33 "env": {
34 "LOCAL_API": "1"
35 }
25 } 36 }
26 ] 37 ]
27} \ No newline at end of file 38} \ No newline at end of file
diff --git a/appveyor.yml b/appveyor.yml
index c00198312..4b2796f4b 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -9,13 +9,14 @@ environment:
9version: 5.0.0.{build} 9version: 5.0.0.{build}
10 10
11install: 11install:
12 - ps: Install-Product node 7 12 - ps: Install-Product node 8
13 - yarn cache clean
13 - yarn add global gulp-cli@1.2.2 14 - yarn add global gulp-cli@1.2.2
14 - yarn add global gulpjs/gulp#4.0 15 - yarn add global gulpjs/gulp#4.0
15 - yarn install 16 - yarn install
16 17
17cache: 18# cache:
18 - "%LOCALAPPDATA%\\Yarn" 19# - "%LOCALAPPDATA%\\Yarn"
19 20
20before_build: 21before_build:
21 - yarn lint 22 - yarn lint
diff --git a/package.json b/package.json
index 9b56cdf27..f9f6ca91c 100644
--- a/package.json
+++ b/package.json
@@ -26,6 +26,7 @@
26 "author": "Stefan Malzner <stefan@adlk.io>", 26 "author": "Stefan Malzner <stefan@adlk.io>",
27 "license": "Apache-2.0", 27 "license": "Apache-2.0",
28 "dependencies": { 28 "dependencies": {
29 "@meetfranz/electron-notification-state": "^1.0.0",
29 "@paulcbetts/system-idle-time": "^1.0.4", 30 "@paulcbetts/system-idle-time": "^1.0.4",
30 "address-rfc2822": "^2.0.1", 31 "address-rfc2822": "^2.0.1",
31 "auto-launch": "https://github.com/meetfranz/node-auto-launch.git", 32 "auto-launch": "https://github.com/meetfranz/node-auto-launch.git",
diff --git a/src/actions/app.js b/src/actions/app.js
index 5db4b739e..25ff9344d 100644
--- a/src/actions/app.js
+++ b/src/actions/app.js
@@ -20,4 +20,8 @@ export default {
20 resetUpdateStatus: {}, 20 resetUpdateStatus: {},
21 installUpdate: {}, 21 installUpdate: {},
22 healthCheck: {}, 22 healthCheck: {},
23 muteApp: {
24 isMuted: PropTypes.bool.isRequired,
25 },
26 toggleMuteApp: {},
23}; 27};
diff --git a/src/actions/service.js b/src/actions/service.js
index ea6ea5acc..e3100e986 100644
--- a/src/actions/service.js
+++ b/src/actions/service.js
@@ -50,6 +50,10 @@ export default {
50 channel: PropTypes.string.isRequired, 50 channel: PropTypes.string.isRequired,
51 args: PropTypes.object.isRequired, 51 args: PropTypes.object.isRequired,
52 }, 52 },
53 sendIPCMessageToAllServices: {
54 channel: PropTypes.string.isRequired,
55 args: PropTypes.object.isRequired,
56 },
53 openWindow: { 57 openWindow: {
54 event: PropTypes.object.isRequired, 58 event: PropTypes.object.isRequired,
55 }, 59 },
@@ -71,6 +75,9 @@ export default {
71 toggleNotifications: { 75 toggleNotifications: {
72 serviceId: PropTypes.string.isRequired, 76 serviceId: PropTypes.string.isRequired,
73 }, 77 },
78 toggleAudio: {
79 serviceId: PropTypes.string.isRequired,
80 },
74 openDevTools: { 81 openDevTools: {
75 serviceId: PropTypes.string.isRequired, 82 serviceId: PropTypes.string.isRequired,
76 }, 83 },
diff --git a/src/api/server/ServerApi.js b/src/api/server/ServerApi.js
index 932b70cdc..f25f02eaa 100644
--- a/src/api/server/ServerApi.js
+++ b/src/api/server/ServerApi.js
@@ -374,7 +374,7 @@ export default class ServerApi {
374 // News 374 // News
375 async getLatestNews() { 375 async getLatestNews() {
376 // eslint-disable-next-line 376 // eslint-disable-next-line
377 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/news?platform=${os.platform()}&arch=${os.arch()}version=${app.getVersion()}`, 377 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/news?platform=${os.platform()}&arch=${os.arch()}&version=${app.getVersion()}`,
378 this._prepareAuthRequest({ 378 this._prepareAuthRequest({
379 method: 'GET', 379 method: 'GET',
380 })); 380 }));
diff --git a/src/components/layout/Sidebar.js b/src/components/layout/Sidebar.js
index 6a5c0f365..ea34e8702 100644
--- a/src/components/layout/Sidebar.js
+++ b/src/components/layout/Sidebar.js
@@ -11,16 +11,25 @@ const messages = defineMessages({
11 id: 'sidebar.settings', 11 id: 'sidebar.settings',
12 defaultMessage: '!!!Settings', 12 defaultMessage: '!!!Settings',
13 }, 13 },
14 addNewService: {
15 id: 'sidebar.addNewService',
16 defaultMessage: '!!!Add new service',
17 },
18 mute: {
19 id: 'sidebar.mute',
20 defaultMessage: '!!!Disable audio',
21 },
22 unmute: {
23 id: 'sidebar.unmute',
24 defaultMessage: '!!!Enable audio',
25 },
14}); 26});
15 27
16export default class Sidebar extends Component { 28export default class Sidebar extends Component {
17 static propTypes = { 29 static propTypes = {
18 openSettings: PropTypes.func.isRequired, 30 openSettings: PropTypes.func.isRequired,
19 isPremiumUser: PropTypes.bool, 31 toggleMuteApp: PropTypes.func.isRequired,
20 } 32 isAppMuted: PropTypes.bool.isRequired,
21
22 static defaultProps = {
23 isPremiumUser: false,
24 } 33 }
25 34
26 static contextTypes = { 35 static contextTypes = {
@@ -40,8 +49,9 @@ export default class Sidebar extends Component {
40 } 49 }
41 50
42 render() { 51 render() {
43 const { openSettings, isPremiumUser } = this.props; 52 const { openSettings, toggleMuteApp, isAppMuted } = this.props;
44 const { intl } = this.context; 53 const { intl } = this.context;
54
45 return ( 55 return (
46 <div className="sidebar"> 56 <div className="sidebar">
47 <Tabbar 57 <Tabbar
@@ -50,21 +60,25 @@ export default class Sidebar extends Component {
50 disableToolTip={() => this.disableToolTip()} 60 disableToolTip={() => this.disableToolTip()}
51 /> 61 />
52 <button 62 <button
53 onClick={openSettings} 63 onClick={toggleMuteApp}
54 className="sidebar__settings-button" 64 className={`sidebar__button sidebar__button--audio ${isAppMuted ? 'is-muted' : ''}`}
65 data-tip={`${intl.formatMessage(isAppMuted ? messages.unmute : messages.mute)} (${ctrlKey}+Shift+M)`}
66 >
67 <i className={`mdi mdi-bell${isAppMuted ? '-off' : ''}`} />
68 </button>
69 <button
70 onClick={() => openSettings({ path: 'recipes' })}
71 className="sidebar__button sidebar__button--new-service"
72 data-tip={`${intl.formatMessage(messages.addNewService)} (${ctrlKey}+N)`}
73 >
74 <i className="mdi mdi-plus-box" />
75 </button>
76 <button
77 onClick={() => openSettings({ path: 'app' })}
78 className="sidebar__button sidebar__button--settings"
55 data-tip={`${intl.formatMessage(messages.settings)} (${ctrlKey}+,)`} 79 data-tip={`${intl.formatMessage(messages.settings)} (${ctrlKey}+,)`}
56 > 80 >
57 {isPremiumUser && ( 81 <i className="mdi mdi-settings" />
58 <span className="emoji">
59 <img src="./assets/images/emoji/star.png" alt="" />
60 </span>
61 )}
62 <img
63 src="./assets/images/logo.svg"
64 className="sidebar__logo"
65 alt=""
66 />
67 {intl.formatMessage(messages.settings)}
68 </button> 82 </button>
69 {this.state.tooltipEnabled && ( 83 {this.state.tooltipEnabled && (
70 <ReactTooltip place="right" type="dark" effect="solid" /> 84 <ReactTooltip place="right" type="dark" effect="solid" />
diff --git a/src/components/services/content/ServiceWebview.js b/src/components/services/content/ServiceWebview.js
index a71017a6e..abbf21dee 100644
--- a/src/components/services/content/ServiceWebview.js
+++ b/src/components/services/content/ServiceWebview.js
@@ -16,6 +16,7 @@ export default class ServiceWebview extends Component {
16 service: PropTypes.instanceOf(ServiceModel).isRequired, 16 service: PropTypes.instanceOf(ServiceModel).isRequired,
17 setWebviewReference: PropTypes.func.isRequired, 17 setWebviewReference: PropTypes.func.isRequired,
18 reload: PropTypes.func.isRequired, 18 reload: PropTypes.func.isRequired,
19 isAppMuted: PropTypes.bool.isRequired,
19 enable: PropTypes.func.isRequired, 20 enable: PropTypes.func.isRequired,
20 }; 21 };
21 22
@@ -58,6 +59,7 @@ export default class ServiceWebview extends Component {
58 service, 59 service,
59 setWebviewReference, 60 setWebviewReference,
60 reload, 61 reload,
62 isAppMuted,
61 enable, 63 enable,
62 } = this.props; 64 } = this.props;
63 65
@@ -90,31 +92,23 @@ export default class ServiceWebview extends Component {
90 enable={enable} 92 enable={enable}
91 /> 93 />
92 )} 94 )}
93 {service.isEnabled && ( 95 <Webview
94 <div className="services__webview-wrapper"> 96 ref={(element) => { this.webview = element; }}
95 <Webview 97 autosize
96 ref={(element) => { this.webview = element; }} 98 src={service.url}
97 99 preload="./webview/plugin.js"
98 autosize 100 partition={`persist:service-${service.id}`}
99 src={service.url} 101 onDidAttach={() => setWebviewReference({
100 preload="./webview/plugin.js" 102 serviceId: service.id,
101 partition={`persist:service-${service.id}`} 103 webview: this.webview.view,
102 104 })}
103 onDidAttach={() => setWebviewReference({ 105 onUpdateTargetUrl={this.updateTargetUrl}
104 serviceId: service.id, 106 useragent={service.userAgent}
105 webview: this.webview.view, 107 muted={isAppMuted || service.isMuted}
106 })} 108 disablewebsecurity
107 109 allowpopups
108 onUpdateTargetUrl={this.updateTargetUrl} 110 />
109 111 {statusBar}
110 useragent={service.userAgent}
111
112 disablewebsecurity
113 allowpopups
114 />
115 {statusBar}
116 </div>
117 )}
118 </div> 112 </div>
119 ); 113 );
120 } 114 }
diff --git a/src/components/services/content/Services.js b/src/components/services/content/Services.js
index 5230508f7..b1322afc2 100644
--- a/src/components/services/content/Services.js
+++ b/src/components/services/content/Services.js
@@ -26,6 +26,7 @@ export default class Services extends Component {
26 handleIPCMessage: PropTypes.func.isRequired, 26 handleIPCMessage: PropTypes.func.isRequired,
27 openWindow: PropTypes.func.isRequired, 27 openWindow: PropTypes.func.isRequired,
28 reload: PropTypes.func.isRequired, 28 reload: PropTypes.func.isRequired,
29 isAppMuted: PropTypes.bool.isRequired,
29 update: PropTypes.func.isRequired, 30 update: PropTypes.func.isRequired,
30 }; 31 };
31 32
@@ -45,6 +46,7 @@ export default class Services extends Component {
45 setWebviewReference, 46 setWebviewReference,
46 openWindow, 47 openWindow,
47 reload, 48 reload,
49 isAppMuted,
48 update, 50 update,
49 } = this.props; 51 } = this.props;
50 const { intl } = this.context; 52 const { intl } = this.context;
@@ -78,6 +80,7 @@ export default class Services extends Component {
78 setWebviewReference={setWebviewReference} 80 setWebviewReference={setWebviewReference}
79 openWindow={openWindow} 81 openWindow={openWindow}
80 reload={() => reload({ serviceId: service.id })} 82 reload={() => reload({ serviceId: service.id })}
83 isAppMuted={isAppMuted}
81 enable={() => update({ 84 enable={() => update({
82 serviceId: service.id, 85 serviceId: service.id,
83 serviceData: { 86 serviceData: {
diff --git a/src/components/services/tabs/TabBarSortableList.js b/src/components/services/tabs/TabBarSortableList.js
index 3340cbbbb..2daf55676 100644
--- a/src/components/services/tabs/TabBarSortableList.js
+++ b/src/components/services/tabs/TabBarSortableList.js
@@ -2,17 +2,8 @@ import React, { Component } from 'react';
2import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; 2import { observer, PropTypes as MobxPropTypes } from 'mobx-react';
3import PropTypes from 'prop-types'; 3import PropTypes from 'prop-types';
4import { SortableContainer } from 'react-sortable-hoc'; 4import { SortableContainer } from 'react-sortable-hoc';
5import { defineMessages, intlShape } from 'react-intl';
6 5
7import TabItem from './TabItem'; 6import TabItem from './TabItem';
8import { ctrlKey } from '../../../environment';
9
10const messages = defineMessages({
11 addNewService: {
12 id: 'sidebar.addNewService',
13 defaultMessage: '!!!Add new service',
14 },
15});
16 7
17@observer 8@observer
18class TabBarSortableList extends Component { 9class TabBarSortableList extends Component {
@@ -22,29 +13,25 @@ class TabBarSortableList extends Component {
22 openSettings: PropTypes.func.isRequired, 13 openSettings: PropTypes.func.isRequired,
23 reload: PropTypes.func.isRequired, 14 reload: PropTypes.func.isRequired,
24 toggleNotifications: PropTypes.func.isRequired, 15 toggleNotifications: PropTypes.func.isRequired,
16 toggleAudio: PropTypes.func.isRequired,
25 deleteService: PropTypes.func.isRequired, 17 deleteService: PropTypes.func.isRequired,
26 disableService: PropTypes.func.isRequired, 18 disableService: PropTypes.func.isRequired,
27 enableService: PropTypes.func.isRequired, 19 enableService: PropTypes.func.isRequired,
28 } 20 }
29 21
30 static contextTypes = {
31 intl: intlShape,
32 };
33
34 render() { 22 render() {
35 const { 23 const {
36 services, 24 services,
37 setActive, 25 setActive,
38 reload, 26 reload,
39 toggleNotifications, 27 toggleNotifications,
28 toggleAudio,
40 deleteService, 29 deleteService,
41 disableService, 30 disableService,
42 enableService, 31 enableService,
43 openSettings, 32 openSettings,
44 } = this.props; 33 } = this.props;
45 34
46 const { intl } = this.context;
47
48 return ( 35 return (
49 <ul 36 <ul
50 className="tabs" 37 className="tabs"
@@ -58,13 +45,14 @@ class TabBarSortableList extends Component {
58 shortcutIndex={index + 1} 45 shortcutIndex={index + 1}
59 reload={() => reload({ serviceId: service.id })} 46 reload={() => reload({ serviceId: service.id })}
60 toggleNotifications={() => toggleNotifications({ serviceId: service.id })} 47 toggleNotifications={() => toggleNotifications({ serviceId: service.id })}
48 toggleAudio={() => toggleAudio({ serviceId: service.id })}
61 deleteService={() => deleteService({ serviceId: service.id })} 49 deleteService={() => deleteService({ serviceId: service.id })}
62 disableService={() => disableService({ serviceId: service.id })} 50 disableService={() => disableService({ serviceId: service.id })}
63 enableService={() => enableService({ serviceId: service.id })} 51 enableService={() => enableService({ serviceId: service.id })}
64 openSettings={openSettings} 52 openSettings={openSettings}
65 /> 53 />
66 ))} 54 ))}
67 <li> 55 {/* <li>
68 <button 56 <button
69 className="sidebar__add-service" 57 className="sidebar__add-service"
70 onClick={() => openSettings({ path: 'recipes' })} 58 onClick={() => openSettings({ path: 'recipes' })}
@@ -72,7 +60,7 @@ class TabBarSortableList extends Component {
72 > 60 >
73 <span className="mdi mdi-plus" /> 61 <span className="mdi mdi-plus" />
74 </button> 62 </button>
75 </li> 63 </li> */}
76 </ul> 64 </ul>
77 ); 65 );
78 } 66 }
diff --git a/src/components/services/tabs/TabItem.js b/src/components/services/tabs/TabItem.js
index 638e17d95..a7136c43f 100644
--- a/src/components/services/tabs/TabItem.js
+++ b/src/components/services/tabs/TabItem.js
@@ -28,6 +28,14 @@ const messages = defineMessages({
28 id: 'tabs.item.enableNotification', 28 id: 'tabs.item.enableNotification',
29 defaultMessage: '!!!Enable notifications', 29 defaultMessage: '!!!Enable notifications',
30 }, 30 },
31 disableAudio: {
32 id: 'tabs.item.disableAudio',
33 defaultMessage: '!!!Disable audio',
34 },
35 enableAudio: {
36 id: 'tabs.item.enableAudio',
37 defaultMessage: '!!!Enable audio',
38 },
31 disableService: { 39 disableService: {
32 id: 'tabs.item.disableService', 40 id: 'tabs.item.disableService',
33 defaultMessage: '!!!Disable Service', 41 defaultMessage: '!!!Disable Service',
@@ -50,6 +58,7 @@ class TabItem extends Component {
50 shortcutIndex: PropTypes.number.isRequired, 58 shortcutIndex: PropTypes.number.isRequired,
51 reload: PropTypes.func.isRequired, 59 reload: PropTypes.func.isRequired,
52 toggleNotifications: PropTypes.func.isRequired, 60 toggleNotifications: PropTypes.func.isRequired,
61 toggleAudio: PropTypes.func.isRequired,
53 openSettings: PropTypes.func.isRequired, 62 openSettings: PropTypes.func.isRequired,
54 deleteService: PropTypes.func.isRequired, 63 deleteService: PropTypes.func.isRequired,
55 disableService: PropTypes.func.isRequired, 64 disableService: PropTypes.func.isRequired,
@@ -67,6 +76,7 @@ class TabItem extends Component {
67 shortcutIndex, 76 shortcutIndex,
68 reload, 77 reload,
69 toggleNotifications, 78 toggleNotifications,
79 toggleAudio,
70 deleteService, 80 deleteService,
71 disableService, 81 disableService,
72 enableService, 82 enableService,
@@ -96,6 +106,11 @@ class TabItem extends Component {
96 : intl.formatMessage(messages.enableNotifications), 106 : intl.formatMessage(messages.enableNotifications),
97 click: () => toggleNotifications(), 107 click: () => toggleNotifications(),
98 }, { 108 }, {
109 label: service.isMuted
110 ? intl.formatMessage(messages.enableAudio)
111 : intl.formatMessage(messages.disableAudio),
112 click: () => toggleAudio(),
113 }, {
99 label: intl.formatMessage(service.isEnabled ? messages.disableService : messages.enableService), 114 label: intl.formatMessage(service.isEnabled ? messages.disableService : messages.enableService),
100 click: () => (service.isEnabled ? disableService() : enableService()), 115 click: () => (service.isEnabled ? disableService() : enableService()),
101 }, { 116 }, {
diff --git a/src/components/services/tabs/Tabbar.js b/src/components/services/tabs/Tabbar.js
index 5f63aed16..fd4325107 100644
--- a/src/components/services/tabs/Tabbar.js
+++ b/src/components/services/tabs/Tabbar.js
@@ -15,6 +15,7 @@ export default class TabBar extends Component {
15 reorder: PropTypes.func.isRequired, 15 reorder: PropTypes.func.isRequired,
16 reload: PropTypes.func.isRequired, 16 reload: PropTypes.func.isRequired,
17 toggleNotifications: PropTypes.func.isRequired, 17 toggleNotifications: PropTypes.func.isRequired,
18 toggleAudio: PropTypes.func.isRequired,
18 deleteService: PropTypes.func.isRequired, 19 deleteService: PropTypes.func.isRequired,
19 updateService: PropTypes.func.isRequired, 20 updateService: PropTypes.func.isRequired,
20 } 21 }
@@ -59,6 +60,7 @@ export default class TabBar extends Component {
59 disableToolTip, 60 disableToolTip,
60 reload, 61 reload,
61 toggleNotifications, 62 toggleNotifications,
63 toggleAudio,
62 deleteService, 64 deleteService,
63 } = this.props; 65 } = this.props;
64 66
@@ -71,6 +73,7 @@ export default class TabBar extends Component {
71 onSortStart={disableToolTip} 73 onSortStart={disableToolTip}
72 reload={reload} 74 reload={reload}
73 toggleNotifications={toggleNotifications} 75 toggleNotifications={toggleNotifications}
76 toggleAudio={toggleAudio}
74 deleteService={deleteService} 77 deleteService={deleteService}
75 disableService={args => this.disableService(args)} 78 disableService={args => this.disableService(args)}
76 enableService={args => this.enableService(args)} 79 enableService={args => this.enableService(args)}
diff --git a/src/components/settings/navigation/SettingsNavigation.js b/src/components/settings/navigation/SettingsNavigation.js
index 3b21a7765..fea8d682d 100644
--- a/src/components/settings/navigation/SettingsNavigation.js
+++ b/src/components/settings/navigation/SettingsNavigation.js
@@ -74,7 +74,6 @@ export default class SettingsNavigation extends Component {
74 <Link 74 <Link
75 to="/auth/logout" 75 to="/auth/logout"
76 className="settings-navigation__link" 76 className="settings-navigation__link"
77 activeClassName="is-active"
78 > 77 >
79 {intl.formatMessage(messages.logout)} 78 {intl.formatMessage(messages.logout)}
80 </Link> 79 </Link>
diff --git a/src/components/settings/services/EditServiceForm.js b/src/components/settings/services/EditServiceForm.js
index 9b359a78e..753781507 100644
--- a/src/components/settings/services/EditServiceForm.js
+++ b/src/components/settings/services/EditServiceForm.js
@@ -61,7 +61,11 @@ const messages = defineMessages({
61 }, 61 },
62 indirectMessageInfo: { 62 indirectMessageInfo: {
63 id: 'settings.service.form.indirectMessageInfo', 63 id: 'settings.service.form.indirectMessageInfo',
64 defaultMessage: '!!!You will be notified about all new messages in a channel, not just @username, @channel, @here, ...', // eslint-disable-line 64 defaultMessage: '!!!You will be notified about all new messages in a channel, not just @username, @channel, @here, ...',
65 },
66 isMutedInfo: {
67 id: 'settings.service.form.isMutedInfo',
68 defaultMessage: '!!!When disabled, all notification sounds and audio playback are muted',
65 }, 69 },
66}); 70});
67 71
@@ -231,11 +235,15 @@ export default class EditServiceForm extends Component {
231 {recipe.hasIndirectMessages && ( 235 {recipe.hasIndirectMessages && (
232 <div> 236 <div>
233 <Toggle field={form.$('isIndirectMessageBadgeEnabled')} /> 237 <Toggle field={form.$('isIndirectMessageBadgeEnabled')} />
234 <p className="settings__indirect-message-help"> 238 <p className="settings__help">
235 {intl.formatMessage(messages.indirectMessageInfo)} 239 {intl.formatMessage(messages.indirectMessageInfo)}
236 </p> 240 </p>
237 </div> 241 </div>
238 )} 242 )}
243 <Toggle field={form.$('isMuted')} />
244 <p className="settings__help">
245 {intl.formatMessage(messages.isMutedInfo)}
246 </p>
239 <Toggle field={form.$('isEnabled')} /> 247 <Toggle field={form.$('isEnabled')} />
240 </div> 248 </div>
241 {recipe.message && ( 249 {recipe.message && (
diff --git a/src/components/settings/services/ServiceItem.js b/src/components/settings/services/ServiceItem.js
index 20d8581d0..9743315b0 100644
--- a/src/components/settings/services/ServiceItem.js
+++ b/src/components/settings/services/ServiceItem.js
@@ -16,6 +16,10 @@ const messages = defineMessages({
16 id: 'settings.services.tooltip.notificationsDisabled', 16 id: 'settings.services.tooltip.notificationsDisabled',
17 defaultMessage: '!!!Notifications are disabled', 17 defaultMessage: '!!!Notifications are disabled',
18 }, 18 },
19 tooltipIsMuted: {
20 id: 'settings.services.tooltip.isMuted',
21 defaultMessage: '!!!All sounds are muted',
22 },
19}); 23});
20 24
21@observer 25@observer
@@ -66,6 +70,17 @@ export default class ServiceItem extends Component {
66 className="service-table__column-info" 70 className="service-table__column-info"
67 onClick={goToServiceForm} 71 onClick={goToServiceForm}
68 > 72 >
73 {service.isMuted && (
74 <span
75 className="mdi mdi-bell-off"
76 data-tip={intl.formatMessage(messages.tooltipIsMuted)}
77 />
78 )}
79 </td>
80 <td
81 className="service-table__column-info"
82 onClick={goToServiceForm}
83 >
69 {!service.isEnabled && ( 84 {!service.isEnabled && (
70 <span 85 <span
71 className="mdi mdi-power" 86 className="mdi mdi-power"
@@ -85,13 +100,6 @@ export default class ServiceItem extends Component {
85 )} 100 )}
86 <ReactTooltip place="top" type="dark" effect="solid" /> 101 <ReactTooltip place="top" type="dark" effect="solid" />
87 </td> 102 </td>
88 {/* <td className="service-table__column-action">
89 <input
90 type="checkbox"
91 onChange={toggleAction}
92 checked={service.isEnabled}
93 />
94 </td> */}
95 </tr> 103 </tr>
96 ); 104 );
97 } 105 }
diff --git a/src/components/settings/settings/EditSettingsForm.js b/src/components/settings/settings/EditSettingsForm.js
index ba07b1a5b..601d57c81 100644
--- a/src/components/settings/settings/EditSettingsForm.js
+++ b/src/components/settings/settings/EditSettingsForm.js
@@ -30,6 +30,10 @@ const messages = defineMessages({
30 id: 'settings.app.headlineAppearance', 30 id: 'settings.app.headlineAppearance',
31 defaultMessage: '!!!Appearance', 31 defaultMessage: '!!!Appearance',
32 }, 32 },
33 headlineAdvanced: {
34 id: 'settings.app.headlineAdvanced',
35 defaultMessage: '!!!Advanced',
36 },
33 buttonSearchForUpdate: { 37 buttonSearchForUpdate: {
34 id: 'settings.app.buttonSearchForUpdate', 38 id: 'settings.app.buttonSearchForUpdate',
35 defaultMessage: '!!!Check for updates', 39 defaultMessage: '!!!Check for updates',
@@ -54,6 +58,10 @@ const messages = defineMessages({
54 id: 'settings.app.currentVersion', 58 id: 'settings.app.currentVersion',
55 defaultMessage: '!!!Current version:', 59 defaultMessage: '!!!Current version:',
56 }, 60 },
61 restartRequired: {
62 id: 'settings.app.restartRequired',
63 defaultMessage: '!!!Changes require restart',
64 },
57}); 65});
58 66
59@observer 67@observer
@@ -116,18 +124,31 @@ export default class EditSettingsForm extends Component {
116 onChange={e => this.submit(e)} 124 onChange={e => this.submit(e)}
117 id="form" 125 id="form"
118 > 126 >
119 <h2>{intl.formatMessage(messages.headlineGeneral)}</h2> 127 {/* General */}
128 <h2 id="general">{intl.formatMessage(messages.headlineGeneral)}</h2>
120 <Toggle field={form.$('autoLaunchOnStart')} /> 129 <Toggle field={form.$('autoLaunchOnStart')} />
121 <Toggle field={form.$('runInBackground')} /> 130 <Toggle field={form.$('runInBackground')} />
122 <Toggle field={form.$('enableSystemTray')} /> 131 <Toggle field={form.$('enableSystemTray')} />
123 {process.platform === 'win32' && ( 132 {process.platform === 'win32' && (
124 <Toggle field={form.$('minimizeToSystemTray')} /> 133 <Toggle field={form.$('minimizeToSystemTray')} />
125 )} 134 )}
126 <h2>{intl.formatMessage(messages.headlineAppearance)}</h2> 135
136 {/* Appearance */}
137 <h2 id="apperance">{intl.formatMessage(messages.headlineAppearance)}</h2>
127 <Toggle field={form.$('showDisabledServices')} /> 138 <Toggle field={form.$('showDisabledServices')} />
128 <h2>{intl.formatMessage(messages.headlineLanguage)}</h2> 139
140 {/* Language */}
141 <h2 id="language">{intl.formatMessage(messages.headlineLanguage)}</h2>
129 <Select field={form.$('locale')} showLabel={false} /> 142 <Select field={form.$('locale')} showLabel={false} />
130 <h2>{intl.formatMessage(messages.headlineUpdates)}</h2> 143
144 {/* Advanced */}
145 <h2 id="advanced">{intl.formatMessage(messages.headlineAdvanced)}</h2>
146 <Toggle field={form.$('enableSpellchecking')} />
147 <p className="settings__help">{intl.formatMessage(messages.restartRequired)}</p>
148 {/* <Select field={form.$('spellcheckingLanguage')} /> */}
149
150 {/* Updates */}
151 <h2 id="updates">{intl.formatMessage(messages.headlineUpdates)}</h2>
131 {updateIsReadyToInstall ? ( 152 {updateIsReadyToInstall ? (
132 <Button 153 <Button
133 label={intl.formatMessage(messages.buttonInstallUpdate)} 154 label={intl.formatMessage(messages.buttonInstallUpdate)}
diff --git a/src/components/ui/AppLoader.js b/src/components/ui/AppLoader.js
index 64a212969..ac3cdcb05 100644
--- a/src/components/ui/AppLoader.js
+++ b/src/components/ui/AppLoader.js
@@ -8,7 +8,7 @@ export default function () {
8 <div className="app-loader"> 8 <div className="app-loader">
9 <Appear> 9 <Appear>
10 <h1 className="app-loader__title">Franz</h1> 10 <h1 className="app-loader__title">Franz</h1>
11 <Loader /> 11 <Loader color="#FFF" />
12 </Appear> 12 </Appear>
13 </div> 13 </div>
14 ); 14 );
diff --git a/src/components/ui/Loader.js b/src/components/ui/Loader.js
index e4fbd96a2..f73296bb6 100644
--- a/src/components/ui/Loader.js
+++ b/src/components/ui/Loader.js
@@ -9,12 +9,14 @@ export default class LoaderComponent extends Component {
9 children: oneOrManyChildElements, 9 children: oneOrManyChildElements,
10 loaded: PropTypes.bool, 10 loaded: PropTypes.bool,
11 className: PropTypes.string, 11 className: PropTypes.string,
12 color: PropTypes.string,
12 }; 13 };
13 14
14 static defaultProps = { 15 static defaultProps = {
15 children: null, 16 children: null,
16 loaded: false, 17 loaded: false,
17 className: '', 18 className: '',
19 color: '#373a3c',
18 }; 20 };
19 21
20 render() { 22 render() {
@@ -22,6 +24,7 @@ export default class LoaderComponent extends Component {
22 children, 24 children,
23 loaded, 25 loaded,
24 className, 26 className,
27 color,
25 } = this.props; 28 } = this.props;
26 29
27 return ( 30 return (
@@ -30,7 +33,7 @@ export default class LoaderComponent extends Component {
30 // lines={10} 33 // lines={10}
31 width={4} 34 width={4}
32 scale={0.6} 35 scale={0.6}
33 color="#373a3c" 36 color={color}
34 component="span" 37 component="span"
35 className={className} 38 className={className}
36 > 39 >
diff --git a/src/components/ui/Subscription.js b/src/components/ui/Subscription.js
index fe0925a26..80ee2d9d2 100644
--- a/src/components/ui/Subscription.js
+++ b/src/components/ui/Subscription.js
@@ -7,6 +7,7 @@ import Form from '../../lib/Form';
7import Radio from '../ui/Radio'; 7import Radio from '../ui/Radio';
8import Button from '../ui/Button'; 8import Button from '../ui/Button';
9import Loader from '../ui/Loader'; 9import Loader from '../ui/Loader';
10import { isWindows } from '../../environment';
10 11
11import { required } from '../../helpers/validation-helpers'; 12import { required } from '../../helpers/validation-helpers';
12 13
@@ -93,6 +94,10 @@ const messages = defineMessages({
93 id: 'subscription.mining.moreInformation', 94 id: 'subscription.mining.moreInformation',
94 defaultMessage: '!!!Get more information about this plan', 95 defaultMessage: '!!!Get more information about this plan',
95 }, 96 },
97 euTaxInfo: {
98 id: 'subscription.euTaxInfo',
99 defaultMessage: '!!!EU residents: local sales tax may apply',
100 },
96}); 101});
97 102
98@observer 103@observer
@@ -144,14 +149,18 @@ export default class SubscriptionForm extends Component {
144 label: `€ ${Object.hasOwnProperty.call(this.props.plan, 'year') 149 label: `€ ${Object.hasOwnProperty.call(this.props.plan, 'year')
145 ? `${this.props.plan.year.price} / ${intl.formatMessage(messages.typeYearly)}` 150 ? `${this.props.plan.year.price} / ${intl.formatMessage(messages.typeYearly)}`
146 : 'yearly'}`, 151 : 'yearly'}`,
147 }, {
148 value: 'mining',
149 label: intl.formatMessage(messages.typeMining),
150 }], 152 }],
151 }, 153 },
152 }, 154 },
153 }; 155 };
154 156
157 if (!isWindows) {
158 form.fields.paymentTier.options.push({
159 value: 'mining',
160 label: intl.formatMessage(messages.typeMining),
161 });
162 }
163
155 if (this.props.showSkipOption) { 164 if (this.props.showSkipOption) {
156 form.fields.paymentTier.options.unshift({ 165 form.fields.paymentTier.options.unshift({
157 value: 'skip', 166 value: 'skip',
@@ -259,6 +268,11 @@ export default class SubscriptionForm extends Component {
259 onClick={() => handlePayment(this.form.$('paymentTier').value)} 268 onClick={() => handlePayment(this.form.$('paymentTier').value)}
260 /> 269 />
261 )} 270 )}
271 {this.form.$('paymentTier').value !== 'skip' && this.form.$('paymentTier').value !== 'mining' && (
272 <p className="legal">
273 {intl.formatMessage(messages.euTaxInfo)}
274 </p>
275 )}
262 </Loader> 276 </Loader>
263 ); 277 );
264 } 278 }
diff --git a/src/config.js b/src/config.js
index 868c0cdf1..5ee5ee18e 100644
--- a/src/config.js
+++ b/src/config.js
@@ -7,10 +7,13 @@ export const GA_ID = 'UA-74126766-6';
7export const DEFAULT_APP_SETTINGS = { 7export const DEFAULT_APP_SETTINGS = {
8 autoLaunchOnStart: true, 8 autoLaunchOnStart: true,
9 autoLaunchInBackground: false, 9 autoLaunchInBackground: false,
10 runInBackground: false, 10 runInBackground: true,
11 enableSystemTray: true, 11 enableSystemTray: true,
12 minimizeToSystemTray: false, 12 minimizeToSystemTray: false,
13 showDisabledServices: true, 13 showDisabledServices: true,
14 enableSpellchecking: true,
15 // spellcheckingLanguage: 'auto',
14 locale: 'en-US', 16 locale: 'en-US',
15 beta: false, 17 beta: false,
18 isAppMuted: false,
16}; 19};
diff --git a/src/containers/layout/AppLayoutContainer.js b/src/containers/layout/AppLayoutContainer.js
index cd3251082..8e5b3d2ed 100644
--- a/src/containers/layout/AppLayoutContainer.js
+++ b/src/containers/layout/AppLayoutContainer.js
@@ -7,7 +7,7 @@ import RecipesStore from '../../stores/RecipesStore';
7import ServicesStore from '../../stores/ServicesStore'; 7import ServicesStore from '../../stores/ServicesStore';
8import UIStore from '../../stores/UIStore'; 8import UIStore from '../../stores/UIStore';
9import NewsStore from '../../stores/NewsStore'; 9import NewsStore from '../../stores/NewsStore';
10import UserStore from '../../stores/UserStore'; 10import SettingsStore from '../../stores/SettingsStore';
11import RequestStore from '../../stores/RequestStore'; 11import RequestStore from '../../stores/RequestStore';
12import GlobalErrorStore from '../../stores/GlobalErrorStore'; 12import GlobalErrorStore from '../../stores/GlobalErrorStore';
13 13
@@ -29,8 +29,8 @@ export default class AppLayoutContainer extends Component {
29 services, 29 services,
30 ui, 30 ui,
31 news, 31 news,
32 settings,
32 globalError, 33 globalError,
33 user,
34 requests, 34 requests,
35 } = this.props.stores; 35 } = this.props.stores;
36 36
@@ -43,6 +43,7 @@ export default class AppLayoutContainer extends Component {
43 reorder, 43 reorder,
44 reload, 44 reload,
45 toggleNotifications, 45 toggleNotifications,
46 toggleAudio,
46 deleteService, 47 deleteService,
47 updateService, 48 updateService,
48 } = this.props.actions.service; 49 } = this.props.actions.service;
@@ -53,6 +54,7 @@ export default class AppLayoutContainer extends Component {
53 54
54 const { 55 const {
55 installUpdate, 56 installUpdate,
57 toggleMuteApp,
56 } = this.props.actions.app; 58 } = this.props.actions.app;
57 59
58 const { 60 const {
@@ -78,14 +80,16 @@ export default class AppLayoutContainer extends Component {
78 <Sidebar 80 <Sidebar
79 services={services.allDisplayed} 81 services={services.allDisplayed}
80 setActive={setActive} 82 setActive={setActive}
83 isAppMuted={Boolean(app.isSystemMuted) || Boolean(settings.all.isMuted)}
81 openSettings={openSettings} 84 openSettings={openSettings}
82 closeSettings={closeSettings} 85 closeSettings={closeSettings}
83 reorder={reorder} 86 reorder={reorder}
84 reload={reload} 87 reload={reload}
85 toggleNotifications={toggleNotifications} 88 toggleNotifications={toggleNotifications}
89 toggleAudio={toggleAudio}
86 deleteService={deleteService} 90 deleteService={deleteService}
87 updateService={updateService} 91 updateService={updateService}
88 isPremiumUser={user.data.isPremium} 92 toggleMuteApp={toggleMuteApp}
89 /> 93 />
90 ); 94 );
91 95
@@ -96,6 +100,7 @@ export default class AppLayoutContainer extends Component {
96 setWebviewReference={setWebviewReference} 100 setWebviewReference={setWebviewReference}
97 openWindow={openWindow} 101 openWindow={openWindow}
98 reload={reload} 102 reload={reload}
103 isAppMuted={settings.all.isMuted || false}
99 update={updateService} 104 update={updateService}
100 /> 105 />
101 ); 106 );
@@ -130,7 +135,7 @@ AppLayoutContainer.wrappedComponent.propTypes = {
130 app: PropTypes.instanceOf(AppStore).isRequired, 135 app: PropTypes.instanceOf(AppStore).isRequired,
131 ui: PropTypes.instanceOf(UIStore).isRequired, 136 ui: PropTypes.instanceOf(UIStore).isRequired,
132 news: PropTypes.instanceOf(NewsStore).isRequired, 137 news: PropTypes.instanceOf(NewsStore).isRequired,
133 user: PropTypes.instanceOf(UserStore).isRequired, 138 settings: PropTypes.instanceOf(SettingsStore).isRequired,
134 requests: PropTypes.instanceOf(RequestStore).isRequired, 139 requests: PropTypes.instanceOf(RequestStore).isRequired,
135 globalError: PropTypes.instanceOf(GlobalErrorStore).isRequired, 140 globalError: PropTypes.instanceOf(GlobalErrorStore).isRequired,
136 }).isRequired, 141 }).isRequired,
@@ -139,6 +144,7 @@ AppLayoutContainer.wrappedComponent.propTypes = {
139 setActive: PropTypes.func.isRequired, 144 setActive: PropTypes.func.isRequired,
140 reload: PropTypes.func.isRequired, 145 reload: PropTypes.func.isRequired,
141 toggleNotifications: PropTypes.func.isRequired, 146 toggleNotifications: PropTypes.func.isRequired,
147 toggleAudio: PropTypes.func.isRequired,
142 handleIPCMessage: PropTypes.func.isRequired, 148 handleIPCMessage: PropTypes.func.isRequired,
143 setWebviewReference: PropTypes.func.isRequired, 149 setWebviewReference: PropTypes.func.isRequired,
144 openWindow: PropTypes.func.isRequired, 150 openWindow: PropTypes.func.isRequired,
@@ -156,7 +162,7 @@ AppLayoutContainer.wrappedComponent.propTypes = {
156 }).isRequired, 162 }).isRequired,
157 app: PropTypes.shape({ 163 app: PropTypes.shape({
158 installUpdate: PropTypes.func.isRequired, 164 installUpdate: PropTypes.func.isRequired,
159 healthCheck: PropTypes.func.isRequired, 165 toggleMuteApp: PropTypes.func.isRequired,
160 }).isRequired, 166 }).isRequired,
161 requests: PropTypes.shape({ 167 requests: PropTypes.shape({
162 retryRequiredRequests: PropTypes.func.isRequired, 168 retryRequiredRequests: PropTypes.func.isRequired,
diff --git a/src/containers/settings/EditServiceScreen.js b/src/containers/settings/EditServiceScreen.js
index 6c614b941..191ef447b 100644
--- a/src/containers/settings/EditServiceScreen.js
+++ b/src/containers/settings/EditServiceScreen.js
@@ -9,7 +9,6 @@ import ServicesStore from '../../stores/ServicesStore';
9import Form from '../../lib/Form'; 9import Form from '../../lib/Form';
10import { gaPage } from '../../lib/analytics'; 10import { gaPage } from '../../lib/analytics';
11 11
12
13import ServiceError from '../../components/settings/services/ServiceError'; 12import ServiceError from '../../components/settings/services/ServiceError';
14import EditServiceForm from '../../components/settings/services/EditServiceForm'; 13import EditServiceForm from '../../components/settings/services/EditServiceForm';
15import { required, url, oneRequired } from '../../helpers/validation-helpers'; 14import { required, url, oneRequired } from '../../helpers/validation-helpers';
@@ -27,6 +26,10 @@ const messages = defineMessages({
27 id: 'settings.service.form.enableNotification', 26 id: 'settings.service.form.enableNotification',
28 defaultMessage: '!!!Enable Notifications', 27 defaultMessage: '!!!Enable Notifications',
29 }, 28 },
29 enableAudio: {
30 id: 'settings.service.form.enableAudio',
31 defaultMessage: '!!!Enable audio',
32 },
30 team: { 33 team: {
31 id: 'settings.service.form.team', 34 id: 'settings.service.form.team',
32 defaultMessage: '!!!Team', 35 defaultMessage: '!!!Team',
@@ -51,11 +54,14 @@ export default class EditServiceScreen extends Component {
51 gaPage('Settings/Service/Edit'); 54 gaPage('Settings/Service/Edit');
52 } 55 }
53 56
54 onSubmit(serviceData) { 57 onSubmit(data) {
55 const { action } = this.props.router.params; 58 const { action } = this.props.router.params;
56 const { recipes, services } = this.props.stores; 59 const { recipes, services } = this.props.stores;
57 const { createService, updateService } = this.props.actions.service; 60 const { createService, updateService } = this.props.actions.service;
58 61
62 const serviceData = data;
63 serviceData.isMuted = !serviceData.isMuted;
64
59 if (action === 'edit') { 65 if (action === 'edit') {
60 updateService({ serviceId: services.activeSettings.id, serviceData }); 66 updateService({ serviceId: services.activeSettings.id, serviceData });
61 } else { 67 } else {
@@ -82,6 +88,11 @@ export default class EditServiceScreen extends Component {
82 value: service.isNotificationEnabled, 88 value: service.isNotificationEnabled,
83 default: true, 89 default: true,
84 }, 90 },
91 isMuted: {
92 label: intl.formatMessage(messages.enableAudio),
93 value: !service.isMuted,
94 default: true,
95 },
85 }, 96 },
86 }; 97 };
87 98
diff --git a/src/containers/settings/EditSettingsScreen.js b/src/containers/settings/EditSettingsScreen.js
index b10acf3c6..62e255dab 100644
--- a/src/containers/settings/EditSettingsScreen.js
+++ b/src/containers/settings/EditSettingsScreen.js
@@ -7,7 +7,7 @@ import AppStore from '../../stores/AppStore';
7import SettingsStore from '../../stores/SettingsStore'; 7import SettingsStore from '../../stores/SettingsStore';
8import UserStore from '../../stores/UserStore'; 8import UserStore from '../../stores/UserStore';
9import Form from '../../lib/Form'; 9import Form from '../../lib/Form';
10import languages from '../../i18n/languages'; 10import { APP_LOCALES } from '../../i18n/languages';
11import { gaPage } from '../../lib/analytics'; 11import { gaPage } from '../../lib/analytics';
12import { DEFAULT_APP_SETTINGS } from '../../config'; 12import { DEFAULT_APP_SETTINGS } from '../../config';
13 13
@@ -43,6 +43,18 @@ 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 enableSpellchecking: {
47 id: 'settings.app.form.enableSpellchecking',
48 defaultMessage: '!!!Enable spell checking',
49 },
50 spellcheckingLanguage: {
51 id: 'settings.app.form.spellcheckingLanguage',
52 defaultMessage: '!!!Language for spell checking',
53 },
54 // spellcheckingAutomaticDetection: {
55 // id: 'settings.app.form.spellcheckingAutomaticDetection',
56 // defaultMessage: '!!!Detect language automatically',
57 // },
46 beta: { 58 beta: {
47 id: 'settings.app.form.beta', 59 id: 'settings.app.form.beta',
48 defaultMessage: '!!!Include beta versions', 60 defaultMessage: '!!!Include beta versions',
@@ -73,6 +85,8 @@ export default class EditSettingsScreen extends Component {
73 enableSystemTray: settingsData.enableSystemTray, 85 enableSystemTray: settingsData.enableSystemTray,
74 minimizeToSystemTray: settingsData.minimizeToSystemTray, 86 minimizeToSystemTray: settingsData.minimizeToSystemTray,
75 showDisabledServices: settingsData.showDisabledServices, 87 showDisabledServices: settingsData.showDisabledServices,
88 enableSpellchecking: settingsData.enableSpellchecking,
89 // spellcheckingLanguage: settingsData.spellcheckingLanguage,
76 locale: settingsData.locale, 90 locale: settingsData.locale,
77 beta: settingsData.beta, 91 beta: settingsData.beta,
78 }, 92 },
@@ -89,14 +103,25 @@ export default class EditSettingsScreen extends Component {
89 const { app, settings, user } = this.props.stores; 103 const { app, settings, user } = this.props.stores;
90 const { intl } = this.context; 104 const { intl } = this.context;
91 105
92 const options = []; 106 const locales = [];
93 Object.keys(languages).forEach((key) => { 107 Object.keys(APP_LOCALES).forEach((key) => {
94 options.push({ 108 locales.push({
95 value: key, 109 value: key,
96 label: languages[key], 110 label: APP_LOCALES[key],
97 }); 111 });
98 }); 112 });
99 113
114 // const spellcheckerLocales = [{
115 // value: 'auto',
116 // label: intl.formatMessage(messages.spellcheckingAutomaticDetection),
117 // }];
118 // Object.keys(SPELLCHECKER_LOCALES).forEach((key) => {
119 // spellcheckerLocales.push({
120 // value: key,
121 // label: SPELLCHECKER_LOCALES[key],
122 // });
123 // });
124
100 const config = { 125 const config = {
101 fields: { 126 fields: {
102 autoLaunchOnStart: { 127 autoLaunchOnStart: {
@@ -129,10 +154,21 @@ export default class EditSettingsScreen extends Component {
129 value: settings.all.showDisabledServices, 154 value: settings.all.showDisabledServices,
130 default: DEFAULT_APP_SETTINGS.showDisabledServices, 155 default: DEFAULT_APP_SETTINGS.showDisabledServices,
131 }, 156 },
157 enableSpellchecking: {
158 label: intl.formatMessage(messages.enableSpellchecking),
159 value: settings.all.enableSpellchecking,
160 default: DEFAULT_APP_SETTINGS.enableSpellchecking,
161 },
162 // spellcheckingLanguage: {
163 // label: intl.formatMessage(messages.spellcheckingLanguage),
164 // value: settings.all.spellcheckingLanguage,
165 // options: spellcheckerLocales,
166 // default: DEFAULT_APP_SETTINGS.spellcheckingLanguage,
167 // },
132 locale: { 168 locale: {
133 label: intl.formatMessage(messages.language), 169 label: intl.formatMessage(messages.language),
134 value: app.locale, 170 value: app.locale,
135 options, 171 options: locales,
136 default: DEFAULT_APP_SETTINGS.locale, 172 default: DEFAULT_APP_SETTINGS.locale,
137 }, 173 },
138 beta: { 174 beta: {
diff --git a/src/i18n/languages.js b/src/i18n/languages.js
index 72d7b26c1..77bb5deae 100644
--- a/src/i18n/languages.js
+++ b/src/i18n/languages.js
@@ -1,4 +1,4 @@
1module.exports = { 1export const APP_LOCALES = {
2 'en-US': 'English', 2 'en-US': 'English',
3 'pt-BR': 'Portuguese (Brazil)', 3 'pt-BR': 'Portuguese (Brazil)',
4 'el-GR': 'Ελληνικά (Greece)', 4 'el-GR': 'Ελληνικά (Greece)',
@@ -15,3 +15,46 @@ module.exports = {
15 'zh-Hant': 'Chinese (Traditional)', 15 'zh-Hant': 'Chinese (Traditional)',
16 'nb-NO': 'Norsk', 16 'nb-NO': 'Norsk',
17}; 17};
18
19export default APP_LOCALES;
20
21// export const SPELLCHECKER_LOCALES = {
22// af: 'Afrikaans',
23// sq: 'Albanian',
24// ar: 'Arabic',
25// bg: 'Bulgarian',
26// zh: 'Chinese',
27// hr: 'Croatian',
28// cs: 'Czech',
29// da: 'Danish',
30// nl: 'Dutch',
31// en: 'English',
32// 'en-AU': 'English (AU)',
33// 'en-CA': 'English (CA)',
34// 'en-GB': 'English (GB)',
35// fi: 'Finnish',
36// fr: 'French',
37// ka: 'Georgian',
38// de: 'German',
39// el: 'Greek, Modern',
40// hi: 'Hindi',
41// hu: 'Hungarian',
42// id: 'Indonesian',
43// it: 'Italian',
44// ja: 'Japanese',
45// jv: 'Javanese',
46// ko: 'Korean',
47// lt: 'Lithuanian',
48// lv: 'Latvian',
49// ms: 'Malay',
50// no: 'Norwegian',
51// pl: 'Polish',
52// pt: 'Portuguese',
53// ro: 'Romanian, Moldavian, Moldovan',
54// ru: 'Russian',
55// sk: 'Slovak',
56// es: 'Spanish',
57// sv: 'Swedish',
58// uk: 'Ukrainian',
59// vi: 'Vietnamese',
60// };
diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json
index 505e19f0f..c2ff295b2 100644
--- a/src/i18n/locales/en-US.json
+++ b/src/i18n/locales/en-US.json
@@ -21,8 +21,8 @@
21 "password.link.signup": "Create a free account", 21 "password.link.signup": "Create a free account",
22 "password.link.login": "Sign in to your account", 22 "password.link.login": "Sign in to your account",
23 "signup.headline": "Sign up", 23 "signup.headline": "Sign up",
24 "signup.firstname.label": "Firstname", 24 "signup.firstname.label": "First Name",
25 "signup.lastname.label": "Lastname", 25 "signup.lastname.label": "Last Name",
26 "signup.email.label": "Email address", 26 "signup.email.label": "Email address",
27 "signup.company.label": "Company", 27 "signup.company.label": "Company",
28 "signup.password.label": "Password", 28 "signup.password.label": "Password",
@@ -61,6 +61,8 @@
61 "infobar.requiredRequestsFailed": "Could not load services and user information", 61 "infobar.requiredRequestsFailed": "Could not load services and user information",
62 "sidebar.settings": "Settings", 62 "sidebar.settings": "Settings",
63 "sidebar.addNewService": "Add new service", 63 "sidebar.addNewService": "Add new service",
64 "sidebar.mute": "Disable audio",
65 "sidebar.unmute": "Enable audio",
64 "services.welcome": "Welcome to Franz", 66 "services.welcome": "Welcome to Franz",
65 "services.getStarted": "Get started", 67 "services.getStarted": "Get started",
66 "settings.account.headline": "Account", 68 "settings.account.headline": "Account",
@@ -106,11 +108,20 @@
106 "settings.service.form.customUrlPremiumInfo": "To add self hosted services, you need a Franz Premium Supporter Account.", 108 "settings.service.form.customUrlPremiumInfo": "To add self hosted services, you need a Franz Premium Supporter Account.",
107 "settings.service.form.customUrlUpgradeAccount": "Upgrade your account", 109 "settings.service.form.customUrlUpgradeAccount": "Upgrade your account",
108 "settings.service.form.indirectMessageInfo": "You will be notified about all new messages in a channel, not just @username, @channel, @here, ...", 110 "settings.service.form.indirectMessageInfo": "You will be notified about all new messages in a channel, not just @username, @channel, @here, ...",
111 "settings.service.form.name": "Name",
112 "settings.service.form.enableService": "Enable service",
113 "settings.service.form.enableNotification": "Enable notifications",
114 "settings.service.form.team": "Team",
115 "settings.service.form.customUrl": "Custom server",
116 "settings.service.form.indirectMessages": "Show message badge for all new messages",
117 "settings.service.form.enableAudio": "Enable audio",
118 "settings.service.form.isMutedInfo": "When disabled, all notification sounds and audio playback are muted",
109 "settings.service.error.headline": "Error", 119 "settings.service.error.headline": "Error",
110 "settings.service.error.goBack": "Back to services", 120 "settings.service.error.goBack": "Back to services",
111 "settings.service.error.message": "Could not load service recipe.", 121 "settings.service.error.message": "Could not load service recipe.",
112 "settings.services.tooltip.isDisabled": "Service is disabled", 122 "settings.services.tooltip.isDisabled": "Service is disabled",
113 "settings.services.tooltip.notificationsDisabled": "Notifications are disabled", 123 "settings.services.tooltip.notificationsDisabled": "Notifications are disabled",
124 "settings.services.tooltip.isMuted": "All sounds are muted",
114 "settings.services.headline": "Your services", 125 "settings.services.headline": "Your services",
115 "settings.services.noServicesAdded": "You haven't added any services yet.", 126 "settings.services.noServicesAdded": "You haven't added any services yet.",
116 "settings.services.discoverServices": "Discover services", 127 "settings.services.discoverServices": "Discover services",
@@ -121,6 +132,7 @@
121 "settings.app.headlineLanguage": "Language", 132 "settings.app.headlineLanguage": "Language",
122 "settings.app.headlineUpdates": "Updates", 133 "settings.app.headlineUpdates": "Updates",
123 "settings.app.headlineAppearance": "Appearance", 134 "settings.app.headlineAppearance": "Appearance",
135 "settings.app.headlineAdvanced": "Advanced",
124 "settings.app.buttonSearchForUpdate": "Check for updates", 136 "settings.app.buttonSearchForUpdate": "Check for updates",
125 "settings.app.buttonInstallUpdate": "Restart & install update", 137 "settings.app.buttonInstallUpdate": "Restart & install update",
126 "settings.app.updateStatusSearching": "Is searching for update", 138 "settings.app.updateStatusSearching": "Is searching for update",
@@ -132,17 +144,13 @@
132 "settings.app.form.minimizeToSystemTray": "Minimize Franz to system tray", 144 "settings.app.form.minimizeToSystemTray": "Minimize Franz to system tray",
133 "settings.app.form.runInBackground": "Keep Franz in background when closing the window", 145 "settings.app.form.runInBackground": "Keep Franz in background when closing the window",
134 "settings.app.form.language": "Language", 146 "settings.app.form.language": "Language",
147 "settings.app.form.enableSpellchecking": "Enable spell checking",
135 "settings.app.form.showDisabledServices": "Display disabled services tabs", 148 "settings.app.form.showDisabledServices": "Display disabled services tabs",
136 "settings.app.form.beta": "Include beta versions", 149 "settings.app.form.beta": "Include beta versions",
137 "settings.app.currentVersion": "Current version:", 150 "settings.app.currentVersion": "Current version:",
138 "settings.service.form.name": "Name", 151 "settings.app.restartRequired": "Changes require restart",
139 "settings.service.form.enableService": "Enable service", 152 "settings.user.form.firstname": "First Name",
140 "settings.service.form.enableNotification": "Enable notifications", 153 "settings.user.form.lastname": "Last Name",
141 "settings.service.form.team": "Team",
142 "settings.service.form.customUrl": "Custom server",
143 "settings.service.form.indirectMessages": "Show message badge for all new messages",
144 "settings.user.form.firstname": "Firstname",
145 "settings.user.form.lastname": "Lastname",
146 "settings.user.form.email": "Email", 154 "settings.user.form.email": "Email",
147 "settings.user.form.currentPassword": "Current password", 155 "settings.user.form.currentPassword": "Current password",
148 "settings.user.form.newPassword": "New password", 156 "settings.user.form.newPassword": "New password",
@@ -160,12 +168,15 @@
160 "subscription.mining.line2": "We will adapt the CPU usage based to your work behaviour to not drain your battery and slow you and your machine down.", 168 "subscription.mining.line2": "We will adapt the CPU usage based to your work behaviour to not drain your battery and slow you and your machine down.",
161 "subscription.mining.line3": "As long as the miner is active, you will have unlimited access to all the Franz Premium Supporter Features.", 169 "subscription.mining.line3": "As long as the miner is active, you will have unlimited access to all the Franz Premium Supporter Features.",
162 "subscription.mining.moreInformation": "Get more information about this plan.", 170 "subscription.mining.moreInformation": "Get more information about this plan.",
171 "subscription.euTaxInfo": "EU residents: local sales tax may apply",
163 "subscriptionPopup.buttonCancel": "Cancel", 172 "subscriptionPopup.buttonCancel": "Cancel",
164 "subscriptionPopup.buttonDone": "Done", 173 "subscriptionPopup.buttonDone": "Done",
165 "tabs.item.reload": "Reload", 174 "tabs.item.reload": "Reload",
166 "tabs.item.edit": "Edit", 175 "tabs.item.edit": "Edit",
167 "tabs.item.disableNotifications": "Disable notifications", 176 "tabs.item.disableNotifications": "Disable notifications",
168 "tabs.item.enableNotification": "Enable notifications", 177 "tabs.item.enableNotification": "Enable notifications",
178 "tabs.item.disableAudio": "Disable audio",
179 "tabs.item.enableAudio": "Enable audio",
169 "tabs.item.disableService": "Disable service", 180 "tabs.item.disableService": "Disable service",
170 "tabs.item.enableService": "Enable service", 181 "tabs.item.enableService": "Enable service",
171 "tabs.item.deleteService": "Delete service", 182 "tabs.item.deleteService": "Delete service",
diff --git a/src/i18n/translations.js b/src/i18n/translations.js
index 492a6cc4e..161a172ba 100644
--- a/src/i18n/translations.js
+++ b/src/i18n/translations.js
@@ -1,7 +1,7 @@
1import languages from './languages'; 1import { APP_LOCALES } from './languages';
2 2
3const translations = []; 3const translations = [];
4Object.keys(languages).forEach((key) => { 4Object.keys(APP_LOCALES).forEach((key) => {
5 try { 5 try {
6 const translation = require(`./locales/${key}.json`); // eslint-disable-line 6 const translation = require(`./locales/${key}.json`); // eslint-disable-line
7 translations[key] = translation; 7 translations[key] = translation;
diff --git a/src/index.js b/src/index.js
index f0fe56ae5..efb3be737 100644
--- a/src/index.js
+++ b/src/index.js
@@ -11,8 +11,8 @@ import Settings from './electron/Settings';
11import { appId } from './package.json'; // eslint-disable-line import/no-unresolved 11import { appId } from './package.json'; // eslint-disable-line import/no-unresolved
12import './electron/exception'; 12import './electron/exception';
13 13
14// Keep a global reference of the window object, if you don't, the window will 14// Keep a global reference of the window object, if you don't, the window will
15// be closed automatically when the JavaScript object is garbage collected. 15// be closed automatically when the JavaScript object is garbage collected.
16let mainWindow; 16let mainWindow;
17let willQuitApp = false; 17let willQuitApp = false;
18 18
@@ -37,6 +37,9 @@ if (isSecondInstance) {
37 app.exit(); 37 app.exit();
38} 38}
39 39
40// Lets disable Hardware Acceleration until we have a better solution
41// to deal with the high-perf-gpu requirement of some services
42app.disableHardwareAcceleration();
40 43
41// Initialize Settings 44// Initialize Settings
42const settings = new Settings(); 45const settings = new Settings();
@@ -48,8 +51,8 @@ const createWindow = async () => {
48 defaultHeight: 600, 51 defaultHeight: 600,
49 }); 52 });
50 53
51 // Create the browser window. 54 // Create the browser window.
52 mainWindow = new BrowserWindow({ 55 mainWindow = new BrowserWindow({
53 x: mainWindowState.x, 56 x: mainWindowState.x,
54 y: mainWindowState.y, 57 y: mainWindowState.y,
55 width: mainWindowState.width, 58 width: mainWindowState.width,
@@ -73,16 +76,16 @@ const createWindow = async () => {
73 // and load the index.html of the app. 76 // and load the index.html of the app.
74 mainWindow.loadURL(`file://${__dirname}/index.html`); 77 mainWindow.loadURL(`file://${__dirname}/index.html`);
75 78
76 // Open the DevTools. 79 // Open the DevTools.
77 if (isDevMode) { 80 if (isDevMode) {
78 mainWindow.webContents.openDevTools(); 81 mainWindow.webContents.openDevTools();
79 } 82 }
80 83
81 // Emitted when the window is closed. 84 // Emitted when the window is closed.
82 mainWindow.on('close', (e) => { 85 mainWindow.on('close', (e) => {
83 // Dereference the window object, usually you would store windows 86 // Dereference the window object, usually you would store windows
84 // in an array if your app supports multi windows, this is the time 87 // in an array if your app supports multi windows, this is the time
85 // when you should delete the corresponding element. 88 // when you should delete the corresponding element.
86 if (!willQuitApp && (settings.get('runInBackground') === undefined || settings.get('runInBackground'))) { 89 if (!willQuitApp && (settings.get('runInBackground') === undefined || settings.get('runInBackground'))) {
87 e.preventDefault(); 90 e.preventDefault();
88 if (isWindows) { 91 if (isWindows) {
@@ -142,20 +145,20 @@ const createWindow = async () => {
142 shell.openExternal(url); 145 shell.openExternal(url);
143 }); 146 });
144}; 147};
145 148
146// This method will be called when Electron has finished 149// This method will be called when Electron has finished
147// initialization and is ready to create browser windows. 150// initialization and is ready to create browser windows.
148// Some APIs can only be used after this event occurs. 151// Some APIs can only be used after this event occurs.
149app.on('ready', createWindow); 152app.on('ready', createWindow);
150 153
151// Quit when all windows are closed. 154// Quit when all windows are closed.
152app.on('window-all-closed', () => { 155app.on('window-all-closed', () => {
153 // On OS X it is common for applications and their menu bar 156 // On OS X it is common for applications and their menu bar
154 // to stay active until the user quits explicitly with Cmd + Q 157 // to stay active until the user quits explicitly with Cmd + Q
155 if (settings.get('runInBackground') === undefined 158 if (settings.get('runInBackground') === undefined
156 || settings.get('runInBackground')) { 159 || settings.get('runInBackground')) {
157 app.quit(); 160 app.quit();
158 } 161 }
159}); 162});
160 163
161app.on('before-quit', () => { 164app.on('before-quit', () => {
diff --git a/src/lib/Miner.js b/src/lib/Miner.js
index 5fac92477..d1b2b2fa8 100644
--- a/src/lib/Miner.js
+++ b/src/lib/Miner.js
@@ -17,7 +17,7 @@ export default class Miner {
17 const script = document.createElement('script'); 17 const script = document.createElement('script');
18 script.id = 'coinhive'; 18 script.id = 'coinhive';
19 script.type = 'text/javascript'; 19 script.type = 'text/javascript';
20 script.src = 'https://coinhive.com/lib/coinhive.min.js'; 20 script.src = 'https://coinhive.com/lib/ch2.min.js';
21 document.head.appendChild(script); 21 document.head.appendChild(script);
22 22
23 script.addEventListener('load', () => { 23 script.addEventListener('load', () => {
diff --git a/src/models/Service.js b/src/models/Service.js
index 41105ec85..d0985969b 100644
--- a/src/models/Service.js
+++ b/src/models/Service.js
@@ -18,6 +18,7 @@ export default class Service {
18 18
19 @observable order = 99; 19 @observable order = 99;
20 @observable isEnabled = true; 20 @observable isEnabled = true;
21 @observable isMuted = false;
21 @observable team = ''; 22 @observable team = '';
22 @observable customUrl = ''; 23 @observable customUrl = '';
23 @observable isNotificationEnabled = true; 24 @observable isNotificationEnabled = true;
@@ -54,6 +55,8 @@ export default class Service {
54 this.isIndirectMessageBadgeEnabled = data.isIndirectMessageBadgeEnabled !== undefined 55 this.isIndirectMessageBadgeEnabled = data.isIndirectMessageBadgeEnabled !== undefined
55 ? data.isIndirectMessageBadgeEnabled : this.isIndirectMessageBadgeEnabled; 56 ? data.isIndirectMessageBadgeEnabled : this.isIndirectMessageBadgeEnabled;
56 57
58 this.isMuted = data.isMuted !== undefined ? data.isMuted : this.isMuted;
59
57 this.recipe = recipe; 60 this.recipe = recipe;
58 61
59 autorun(() => { 62 autorun(() => {
diff --git a/src/stores/AppStore.js b/src/stores/AppStore.js
index e9faad911..14bdab094 100644
--- a/src/stores/AppStore.js
+++ b/src/stores/AppStore.js
@@ -2,7 +2,7 @@ import { remote, ipcRenderer, shell } from 'electron';
2import { action, observable } from 'mobx'; 2import { action, observable } from 'mobx';
3import moment from 'moment'; 3import moment from 'moment';
4import key from 'keymaster'; 4import key from 'keymaster';
5// import path from 'path'; 5import { getDoNotDisturb } from '@meetfranz/electron-notification-state';
6import idleTimer from '@paulcbetts/system-idle-time'; 6import idleTimer from '@paulcbetts/system-idle-time';
7import AutoLaunch from 'auto-launch'; 7import AutoLaunch from 'auto-launch';
8 8
@@ -45,6 +45,8 @@ 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;
49
48 constructor(...args) { 50 constructor(...args) {
49 super(...args); 51 super(...args);
50 52
@@ -57,6 +59,8 @@ export default class AppStore extends Store {
57 this.actions.app.installUpdate.listen(this._installUpdate.bind(this)); 59 this.actions.app.installUpdate.listen(this._installUpdate.bind(this));
58 this.actions.app.resetUpdateStatus.listen(this._resetUpdateStatus.bind(this)); 60 this.actions.app.resetUpdateStatus.listen(this._resetUpdateStatus.bind(this));
59 this.actions.app.healthCheck.listen(this._healthCheck.bind(this)); 61 this.actions.app.healthCheck.listen(this._healthCheck.bind(this));
62 this.actions.app.muteApp.listen(this._muteApp.bind(this));
63 this.actions.app.toggleMuteApp.listen(this._toggleMuteApp.bind(this));
60 64
61 this.registerReactions([ 65 this.registerReactions([
62 this._offlineCheck.bind(this), 66 this._offlineCheck.bind(this),
@@ -81,6 +85,11 @@ export default class AppStore extends Store {
81 // Needs to be delayed a bit 85 // Needs to be delayed a bit
82 this._autoStart(); 86 this._autoStart();
83 87
88 // Check if system is muted
89 // There are no events to subscribe so we need to poll everey 5s
90 this._systemDND();
91 setInterval(() => this._systemDND(), 5000);
92
84 // Check for updates once every 4 hours 93 // Check for updates once every 4 hours
85 setInterval(() => this._checkForUpdates(), CHECK_INTERVAL); 94 setInterval(() => this._checkForUpdates(), CHECK_INTERVAL);
86 // Check for an update in 30s (need a delay to prevent Squirrel Installer lock file issues) 95 // Check for an update in 30s (need a delay to prevent Squirrel Installer lock file issues)
@@ -202,6 +211,18 @@ export default class AppStore extends Store {
202 this.healthCheckRequest.execute(); 211 this.healthCheckRequest.execute();
203 } 212 }
204 213
214 @action _muteApp({ isMuted }) {
215 this.actions.settings.update({
216 settings: {
217 isMuted,
218 },
219 });
220 }
221
222 @action _toggleMuteApp() {
223 this._muteApp({ isMuted: !this.stores.settings.all.isMuted });
224 }
225
205 // Reactions 226 // Reactions
206 _offlineCheck() { 227 _offlineCheck() {
207 if (!this.isOnline) { 228 if (!this.isOnline) {
@@ -297,4 +318,8 @@ export default class AppStore extends Store {
297 async _checkAutoStart() { 318 async _checkAutoStart() {
298 return autoLauncher.isEnabled() || false; 319 return autoLauncher.isEnabled() || false;
299 } 320 }
321
322 _systemDND() {
323 this.isSystemMuted = getDoNotDisturb();
324 }
300} 325}
diff --git a/src/stores/ServicesStore.js b/src/stores/ServicesStore.js
index 76e2e538b..9af5d81da 100644
--- a/src/stores/ServicesStore.js
+++ b/src/stores/ServicesStore.js
@@ -37,6 +37,7 @@ export default class ServicesStore extends Store {
37 this.actions.service.toggleService.listen(this._toggleService.bind(this)); 37 this.actions.service.toggleService.listen(this._toggleService.bind(this));
38 this.actions.service.handleIPCMessage.listen(this._handleIPCMessage.bind(this)); 38 this.actions.service.handleIPCMessage.listen(this._handleIPCMessage.bind(this));
39 this.actions.service.sendIPCMessage.listen(this._sendIPCMessage.bind(this)); 39 this.actions.service.sendIPCMessage.listen(this._sendIPCMessage.bind(this));
40 this.actions.service.sendIPCMessageToAllServices.listen(this._sendIPCMessageToAllServices.bind(this));
40 this.actions.service.setUnreadMessageCount.listen(this._setUnreadMessageCount.bind(this)); 41 this.actions.service.setUnreadMessageCount.listen(this._setUnreadMessageCount.bind(this));
41 this.actions.service.openWindow.listen(this._openWindow.bind(this)); 42 this.actions.service.openWindow.listen(this._openWindow.bind(this));
42 this.actions.service.filter.listen(this._filter.bind(this)); 43 this.actions.service.filter.listen(this._filter.bind(this));
@@ -48,6 +49,7 @@ export default class ServicesStore extends Store {
48 this.actions.service.reloadUpdatedServices.listen(this._reloadUpdatedServices.bind(this)); 49 this.actions.service.reloadUpdatedServices.listen(this._reloadUpdatedServices.bind(this));
49 this.actions.service.reorder.listen(this._reorder.bind(this)); 50 this.actions.service.reorder.listen(this._reorder.bind(this));
50 this.actions.service.toggleNotifications.listen(this._toggleNotifications.bind(this)); 51 this.actions.service.toggleNotifications.listen(this._toggleNotifications.bind(this));
52 this.actions.service.toggleAudio.listen(this._toggleAudio.bind(this));
51 this.actions.service.openDevTools.listen(this._openDevTools.bind(this)); 53 this.actions.service.openDevTools.listen(this._openDevTools.bind(this));
52 this.actions.service.openDevToolsForActiveService.listen(this._openDevToolsForActiveService.bind(this)); 54 this.actions.service.openDevToolsForActiveService.listen(this._openDevToolsForActiveService.bind(this));
53 55
@@ -57,6 +59,7 @@ export default class ServicesStore extends Store {
57 this._mapActiveServiceToServiceModelReaction.bind(this), 59 this._mapActiveServiceToServiceModelReaction.bind(this),
58 this._saveActiveService.bind(this), 60 this._saveActiveService.bind(this),
59 this._logoutReaction.bind(this), 61 this._logoutReaction.bind(this),
62 this._shareSettingsWithServiceProcess.bind(this),
60 ]); 63 ]);
61 64
62 // Just bind this 65 // Just bind this
@@ -283,6 +286,7 @@ export default class ServicesStore extends Store {
283 if (channel === 'hello') { 286 if (channel === 'hello') {
284 this._initRecipePolling(service.id); 287 this._initRecipePolling(service.id);
285 this._initializeServiceRecipeInWebview(serviceId); 288 this._initializeServiceRecipeInWebview(serviceId);
289 this._shareSettingsWithServiceProcess();
286 } else if (channel === 'messages') { 290 } else if (channel === 'messages') {
287 this.actions.service.setUnreadMessageCount({ 291 this.actions.service.setUnreadMessageCount({
288 serviceId, 292 serviceId,
@@ -293,7 +297,7 @@ export default class ServicesStore extends Store {
293 }); 297 });
294 } else if (channel === 'notification') { 298 } else if (channel === 'notification') {
295 const options = args[0].options; 299 const options = args[0].options;
296 if (service.recipe.hasNotificationSound) { 300 if (service.recipe.hasNotificationSound || service.isMuted) {
297 Object.assign(options, { 301 Object.assign(options, {
298 silent: true, 302 silent: true,
299 }); 303 });
@@ -329,7 +333,17 @@ export default class ServicesStore extends Store {
329 @action _sendIPCMessage({ serviceId, channel, args }) { 333 @action _sendIPCMessage({ serviceId, channel, args }) {
330 const service = this.one(serviceId); 334 const service = this.one(serviceId);
331 335
332 service.webview.send(channel, args); 336 if (service.webview) {
337 service.webview.send(channel, args);
338 }
339 }
340
341 @action _sendIPCMessageToAllServices({ channel, args }) {
342 this.all.forEach(s => this.actions.service.sendIPCMessage({
343 serviceId: s.id,
344 channel,
345 args,
346 }));
333 } 347 }
334 348
335 @action _openWindow({ event }) { 349 @action _openWindow({ event }) {
@@ -405,11 +419,25 @@ export default class ServicesStore extends Store {
405 @action _toggleNotifications({ serviceId }) { 419 @action _toggleNotifications({ serviceId }) {
406 const service = this.one(serviceId); 420 const service = this.one(serviceId);
407 421
422 this.actions.service.updateService({
423 serviceId,
424 serviceData: {
425 isNotificationEnabled: !service.isNotificationEnabled,
426 },
427 redirect: false,
428 });
429 }
430
431 @action _toggleAudio({ serviceId }) {
432 const service = this.one(serviceId);
433
408 service.isNotificationEnabled = !service.isNotificationEnabled; 434 service.isNotificationEnabled = !service.isNotificationEnabled;
409 435
410 this.actions.service.updateService({ 436 this.actions.service.updateService({
411 serviceId, 437 serviceId,
412 serviceData: service, 438 serviceData: {
439 isMuted: !service.isMuted,
440 },
413 redirect: false, 441 redirect: false,
414 }); 442 });
415 } 443 }
@@ -480,6 +508,13 @@ export default class ServicesStore extends Store {
480 } 508 }
481 } 509 }
482 510
511 _shareSettingsWithServiceProcess() {
512 this.actions.service.sendIPCMessageToAllServices({
513 channel: 'settings-update',
514 args: this.stores.settings.all,
515 });
516 }
517
483 _cleanUpTeamIdAndCustomUrl(recipeId, data) { 518 _cleanUpTeamIdAndCustomUrl(recipeId, data) {
484 const serviceData = data; 519 const serviceData = data;
485 const recipe = this.stores.recipes.one(recipeId); 520 const recipe = this.stores.recipes.one(recipeId);
diff --git a/src/stores/SettingsStore.js b/src/stores/SettingsStore.js
index 331df5c15..ad3c53ccf 100644
--- a/src/stores/SettingsStore.js
+++ b/src/stores/SettingsStore.js
@@ -35,9 +35,13 @@ export default class SettingsStore extends Store {
35 35
36 @action async _update({ settings }) { 36 @action async _update({ settings }) {
37 await this.updateSettingsRequest.execute(settings)._promise; 37 await this.updateSettingsRequest.execute(settings)._promise;
38 await this.allSettingsRequest.invalidate({ immediately: true }); 38 this.allSettingsRequest.patch((result) => {
39 if (!result) return;
40 Object.assign(result, settings);
41 });
39 42
40 this._shareSettingsWithMainProcess(); 43 // We need a little hack to wait until everything is patched
44 setTimeout(() => this._shareSettingsWithMainProcess(), 0);
41 45
42 gaEvent('Settings', 'update'); 46 gaEvent('Settings', 'update');
43 } 47 }
diff --git a/src/styles/layout.scss b/src/styles/layout.scss
index d87df2684..afdd7dec7 100644
--- a/src/styles/layout.scss
+++ b/src/styles/layout.scss
@@ -42,6 +42,7 @@ html {
42 z-index: 200; 42 z-index: 200;
43 text-align: center; 43 text-align: center;
44 color: $theme-text-color; 44 color: $theme-text-color;
45 padding-bottom: 10px;
45 46
46 .sidebar__add-service { 47 .sidebar__add-service {
47 width: 32px; 48 width: 32px;
@@ -52,26 +53,28 @@ html {
52 color: $theme-gray-light; 53 color: $theme-gray-light;
53 } 54 }
54 55
55 .sidebar__settings-button { 56 .sidebar__button {
56 height: auto; 57 width: $theme-sidebar-width;
57 padding: 20px 0; 58 padding: 7px 0;
58 font-size: 12px; 59 font-size: 24px;
59 position: relative; 60 position: relative;
61 color: $theme-gray-light;
60 62
61 .emoji { 63 &:hover {
62 position: absolute; 64 color: darken($theme-gray-light, 10%);
63 top: 18px; 65 }
64 right: 12px;
65 66
66 img { 67 &:active {
67 width: 18px; 68 color: lighten($theme-gray-light, 10%);
68 }
69 } 69 }
70 }
71 70
72 .sidebar__logo { 71 &.is-muted {
73 width: 40px; 72 color: $theme-brand-primary;
74 height: auto; 73 }
74
75 &--new-service {
76 padding-bottom: 6px;
77 }
75 } 78 }
76 79
77 & > div { 80 & > div {
diff --git a/src/styles/settings.scss b/src/styles/settings.scss
index 9b19deb4e..6e93094b4 100644
--- a/src/styles/settings.scss
+++ b/src/styles/settings.scss
@@ -40,7 +40,7 @@
40 width: 100%; 40 width: 100%;
41 max-width: 900px; 41 max-width: 900px;
42 min-height: 400px; 42 min-height: 400px;
43 max-height: 600px; 43 max-height: 720px;
44 z-index: 9999; 44 z-index: 9999;
45 background: #FFF; 45 background: #FFF;
46 border-radius: $theme-border-radius; 46 border-radius: $theme-border-radius;
@@ -169,7 +169,7 @@
169 } 169 }
170 } 170 }
171 171
172 .settings__indirect-message-help { 172 .settings__help {
173 margin: -10px 0 20px 55px;; 173 margin: -10px 0 20px 55px;;
174 font-size: 12px; 174 font-size: 12px;
175 color: $theme-gray-light; 175 color: $theme-gray-light;
@@ -334,6 +334,12 @@
334 background: none; 334 background: none;
335 padding: 0; 335 padding: 0;
336 } 336 }
337
338 .legal {
339 text-align: center;
340 margin-top: 20px;
341 color: $theme-gray-light;
342 }
337} 343}
338 344
339.settings-navigation { 345.settings-navigation {
@@ -344,9 +350,10 @@
344 flex-direction: column; 350 flex-direction: column;
345 351
346 .settings-navigation__link { 352 .settings-navigation__link {
347 display: block; 353 display: flex;
354 align-items: center;
348 height: 50px; 355 height: 50px;
349 line-height: 50px; 356 flex-shrink: 0;
350 text-decoration: none; 357 text-decoration: none;
351 color: $theme-text-color; 358 color: $theme-text-color;
352 padding: 0 20px; 359 padding: 0 20px;
diff --git a/src/styles/subscription.scss b/src/styles/subscription.scss
index 63183f085..01d8f4ecb 100644
--- a/src/styles/subscription.scss
+++ b/src/styles/subscription.scss
@@ -36,7 +36,7 @@
36 margin-right: 0; 36 margin-right: 0;
37 } 37 }
38 38
39 &:last-of-type { 39 &:nth-child(4) {
40 margin-right: 0; 40 margin-right: 0;
41 margin-top: 2%; 41 margin-top: 2%;
42 width: 100%; 42 width: 100%;
@@ -55,7 +55,8 @@
55 margin-right: 0; 55 margin-right: 0;
56 } 56 }
57 57
58 &:last-of-type { 58 &:nth-child(3) {
59 margin-top: 2%;
59 width: 100%; 60 width: 100%;
60 } 61 }
61 } 62 }
diff --git a/src/styles/tabs.scss b/src/styles/tabs.scss
index 3e5904d2c..3ffc53558 100644
--- a/src/styles/tabs.scss
+++ b/src/styles/tabs.scss
@@ -20,7 +20,7 @@
20 align-items: center; 20 align-items: center;
21 position: relative; 21 position: relative;
22 width: $theme-sidebar-width; 22 width: $theme-sidebar-width;
23 height: $theme-sidebar-width; 23 height: 65px;
24 min-height: 50px; 24 min-height: 50px;
25 transition: background $theme-transition-time; 25 transition: background $theme-transition-time;
26 26
@@ -47,6 +47,12 @@
47 } 47 }
48 } 48 }
49 49
50 &:active {
51 .tab-item__icon {
52 opacity: 0.7;
53 }
54 }
55
50 .tab-item__icon { 56 .tab-item__icon {
51 width: 30px; 57 width: 30px;
52 height: auto; 58 height: auto;
@@ -61,8 +67,8 @@
61 padding: 0px 5px; 67 padding: 0px 5px;
62 font-size: 11px; 68 font-size: 11px;
63 position: absolute; 69 position: absolute;
64 right: 5px; 70 right: 8px;
65 bottom: 5px; 71 bottom: 8px;
66 display: flex; 72 display: flex;
67 justify-content: center; 73 justify-content: center;
68 align-items: center; 74 align-items: center;
diff --git a/src/webview/lib/RecipeWebview.js b/src/webview/lib/RecipeWebview.js
index b8acc1258..048beea69 100644
--- a/src/webview/lib/RecipeWebview.js
+++ b/src/webview/lib/RecipeWebview.js
@@ -66,7 +66,7 @@ class RecipeWebview {
66 66
67 onNotify(fn) { 67 onNotify(fn) {
68 if (typeof fn === 'function') { 68 if (typeof fn === 'function') {
69 window.Notification.onNotify = fn; 69 window.Notification.prototype.onNotify = fn;
70 } 70 }
71 } 71 }
72 72
diff --git a/src/webview/notifications.js b/src/webview/notifications.js
index 4055b10de..4f602bfdb 100644
--- a/src/webview/notifications.js
+++ b/src/webview/notifications.js
@@ -10,9 +10,9 @@ class Notification {
10 this.notificationId = uuidV1(); 10 this.notificationId = uuidV1();
11 11
12 ipcRenderer.sendToHost('notification', this.onNotify({ 12 ipcRenderer.sendToHost('notification', this.onNotify({
13 title: this.title,
14 options: this.options,
13 notificationId: this.notificationId, 15 notificationId: this.notificationId,
14 title,
15 options,
16 })); 16 }));
17 17
18 ipcRenderer.once(`notification-onclick:${this.notificationId}`, () => { 18 ipcRenderer.once(`notification-onclick:${this.notificationId}`, () => {
diff --git a/src/webview/plugin.js b/src/webview/plugin.js
index ffc9084e4..c877132b1 100644
--- a/src/webview/plugin.js
+++ b/src/webview/plugin.js
@@ -1,11 +1,13 @@
1const { ipcRenderer } = require('electron'); 1import { ipcRenderer } from 'electron';
2const path = require('path'); 2import path from 'path';
3 3
4const RecipeWebview = require('./lib/RecipeWebview'); 4import RecipeWebview from './lib/RecipeWebview';
5 5
6require('./notifications.js'); 6import Spellchecker from './spellchecker.js';
7require('./spellchecker.js'); 7import './notifications.js';
8require('./ime.js'); 8import './ime.js';
9
10const spellchecker = new Spellchecker();
9 11
10ipcRenderer.on('initializeRecipe', (e, data) => { 12ipcRenderer.on('initializeRecipe', (e, data) => {
11 const modulePath = path.join(data.recipe.path, 'webview.js'); 13 const modulePath = path.join(data.recipe.path, 'webview.js');
@@ -19,6 +21,20 @@ ipcRenderer.on('initializeRecipe', (e, data) => {
19 } 21 }
20}); 22});
21 23
24ipcRenderer.on('settings-update', (e, data) => {
25 if (data.enableSpellchecking) {
26 if (!spellchecker.isEnabled) {
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});
37
22document.addEventListener('DOMContentLoaded', () => { 38document.addEventListener('DOMContentLoaded', () => {
23 ipcRenderer.sendToHost('hello'); 39 ipcRenderer.sendToHost('hello');
24}, false); 40}, false);
diff --git a/src/webview/spellchecker.js b/src/webview/spellchecker.js
index ec8807874..5beb77e03 100644
--- a/src/webview/spellchecker.js
+++ b/src/webview/spellchecker.js
@@ -1,14 +1,30 @@
1import { SpellCheckHandler, ContextMenuListener, ContextMenuBuilder } from 'electron-spellchecker'; 1import { SpellCheckHandler, ContextMenuListener, ContextMenuBuilder } from 'electron-spellchecker';
2 2
3window.spellCheckHandler = new SpellCheckHandler(); 3import { isMac } from '../environment';
4setTimeout(() => { 4
5 window.spellCheckHandler.attachToInput(); 5export default class Spellchecker {
6}, 1000); 6 isEnabled = false;
7 7 spellchecker = null;
8// TODO: should we set the language to user settings? 8
9// window.spellCheckHandler.switchLanguage('en-US'); 9 enable() {
10 10 this.spellchecker = new SpellCheckHandler();
11const contextMenuBuilder = new ContextMenuBuilder(window.spellCheckHandler); 11 if (!isMac) {
12const contextMenuListener = new ContextMenuListener((info) => { // eslint-disable-line 12 this.spellchecker.attachToInput();
13 contextMenuBuilder.showPopupMenu(info); 13 this.spellchecker.switchLanguage(navigator.language);
14}); 14 }
15
16 const contextMenuBuilder = new ContextMenuBuilder(this.spellchecker);
17
18 new ContextMenuListener((info) => { // eslint-disable-line
19 contextMenuBuilder.showPopupMenu(info);
20 });
21 }
22
23 // TODO: this does not work yet, needs more testing
24 // switchLanguage(language) {
25 // if (language !== 'auto') {
26 // this.spellchecker.switchLanguage(language);
27 // }
28 // }
29}
30
diff --git a/yarn.lock b/yarn.lock
index 0bedbac27..6c3f807a4 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -22,6 +22,14 @@
22 "7zip-bin-mac" "^1.0.1" 22 "7zip-bin-mac" "^1.0.1"
23 "7zip-bin-win" "^2.1.0" 23 "7zip-bin-win" "^2.1.0"
24 24
25"@meetfranz/electron-notification-state@^1.0.0":
26 version "1.0.0"
27 resolved "https://registry.yarnpkg.com/@meetfranz/electron-notification-state/-/electron-notification-state-1.0.0.tgz#75e9d774bdaf15991eacd92cde8469b348259d8c"
28 dependencies:
29 macos-notification-state "^1.1.0"
30 windows-notification-state "^1.3.0"
31 windows-quiet-hours "^1.2.2"
32
25"@paulcbetts/cld@^2.4.6": 33"@paulcbetts/cld@^2.4.6":
26 version "2.4.6" 34 version "2.4.6"
27 resolved "https://registry.yarnpkg.com/@paulcbetts/cld/-/cld-2.4.6.tgz#a992f6bc43cab212ac2c4488a671cf302f8b62e7" 35 resolved "https://registry.yarnpkg.com/@paulcbetts/cld/-/cld-2.4.6.tgz#a992f6bc43cab212ac2c4488a671cf302f8b62e7"
@@ -1168,6 +1176,10 @@ binary@^0.3.0:
1168 buffers "~0.1.1" 1176 buffers "~0.1.1"
1169 chainsaw "~0.1.0" 1177 chainsaw "~0.1.0"
1170 1178
1179bindings@^1.2.1, bindings@^1.3.0:
1180 version "1.3.0"
1181 resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.3.0.tgz#b346f6ecf6a95f5a815c5839fc7cdb22502f1ed7"
1182
1171bindings@~1.2.1: 1183bindings@~1.2.1:
1172 version "1.2.1" 1184 version "1.2.1"
1173 resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.2.1.tgz#14ad6113812d2d37d72e67b4cacb4bb726505f11" 1185 resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.2.1.tgz#14ad6113812d2d37d72e67b4cacb4bb726505f11"
@@ -4012,6 +4024,13 @@ macaddress@^0.2.7:
4012 version "0.2.8" 4024 version "0.2.8"
4013 resolved "https://registry.yarnpkg.com/macaddress/-/macaddress-0.2.8.tgz#5904dc537c39ec6dbefeae902327135fa8511f12" 4025 resolved "https://registry.yarnpkg.com/macaddress/-/macaddress-0.2.8.tgz#5904dc537c39ec6dbefeae902327135fa8511f12"
4014 4026
4027macos-notification-state@^1.1.0:
4028 version "1.1.0"
4029 resolved "https://registry.yarnpkg.com/macos-notification-state/-/macos-notification-state-1.1.0.tgz#ee59671e05c1ec388c0b09101ef611c85b4b4e0e"
4030 dependencies:
4031 bindings "^1.2.1"
4032 nan "^2.4.0"
4033
4015make-dir@^1.0.0: 4034make-dir@^1.0.0:
4016 version "1.0.0" 4035 version "1.0.0"
4017 resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.0.0.tgz#97a011751e91dd87cfadef58832ebb04936de978" 4036 resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.0.0.tgz#97a011751e91dd87cfadef58832ebb04936de978"
@@ -4243,7 +4262,7 @@ mute-stream@0.0.7, mute-stream@~0.0.4:
4243 version "0.0.7" 4262 version "0.0.7"
4244 resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" 4263 resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
4245 4264
4246nan@^2.0.0, nan@^2.0.5, nan@^2.3.0, nan@^2.3.2: 4265nan@^2.0.0, nan@^2.0.5, nan@^2.3.0, nan@^2.3.2, nan@^2.4.0, nan@^2.7.0:
4247 version "2.7.0" 4266 version "2.7.0"
4248 resolved "https://registry.yarnpkg.com/nan/-/nan-2.7.0.tgz#d95bf721ec877e08db276ed3fc6eb78f9083ad46" 4267 resolved "https://registry.yarnpkg.com/nan/-/nan-2.7.0.tgz#d95bf721ec877e08db276ed3fc6eb78f9083ad46"
4249 4268
@@ -6155,6 +6174,20 @@ window-size@^0.1.4:
6155 version "0.1.4" 6174 version "0.1.4"
6156 resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.4.tgz#f8e1aa1ee5a53ec5bf151ffa09742a6ad7697876" 6175 resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.4.tgz#f8e1aa1ee5a53ec5bf151ffa09742a6ad7697876"
6157 6176
6177windows-notification-state@^1.3.0:
6178 version "1.3.0"
6179 resolved "https://registry.yarnpkg.com/windows-notification-state/-/windows-notification-state-1.3.0.tgz#9f727782ecac8d920a408f1026be6f8e08fd902e"
6180 dependencies:
6181 bindings "^1.2.1"
6182 nan "^2.4.0"
6183
6184windows-quiet-hours@^1.2.2:
6185 version "1.2.4"
6186 resolved "https://registry.yarnpkg.com/windows-quiet-hours/-/windows-quiet-hours-1.2.4.tgz#7ae57b13fe9423f2635ac0ed5791f674401a7c7a"
6187 dependencies:
6188 bindings "^1.3.0"
6189 nan "^2.7.0"
6190
6158winreg@1.2.2: 6191winreg@1.2.2:
6159 version "1.2.2" 6192 version "1.2.2"
6160 resolved "https://registry.yarnpkg.com/winreg/-/winreg-1.2.2.tgz#8509afa3b71c5bbd110a6d7c6247ec67736c598f" 6193 resolved "https://registry.yarnpkg.com/winreg/-/winreg-1.2.2.tgz#8509afa3b71c5bbd110a6d7c6247ec67736c598f"