diff options
66 files changed, 556 insertions, 193 deletions
diff --git a/.travis.yml b/.travis.yml index b3ebc4f25..78c1e3693 100644 --- a/.travis.yml +++ b/.travis.yml | |||
@@ -9,8 +9,6 @@ matrix: | |||
9 | language: node_js | 9 | language: node_js |
10 | 10 | ||
11 | before_script: | 11 | before_script: |
12 | - yarn add global gulp-cli@1.2.2 | ||
13 | - yarn add global gulpjs/gulp#4.0 | ||
14 | - yarn add global node-sass | 12 | - yarn add global node-sass |
15 | - yarn install | 13 | - yarn install |
16 | 14 | ||
@@ -19,7 +17,7 @@ script: | |||
19 | - travis_wait yarn build | 17 | - travis_wait yarn build |
20 | 18 | ||
21 | node_js: | 19 | node_js: |
22 | - '7' | 20 | - '8' |
23 | 21 | ||
24 | before_install: | 22 | before_install: |
25 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt install libx11-dev libxext-dev libxss-dev libxkbfile-dev; fi | 23 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt install libx11-dev libxext-dev libxss-dev libxkbfile-dev; fi |
@@ -31,12 +31,6 @@ $ brew install yarn | |||
31 | ##### Linux | 31 | ##### Linux |
32 | [Install Yarn on Linux](https://yarnpkg.com/lang/en/docs/install/) | 32 | [Install Yarn on Linux](https://yarnpkg.com/lang/en/docs/install/) |
33 | 33 | ||
34 | #### Install Gulp 4 | ||
35 | ```bash | ||
36 | $ yarn add global gulp-cli@1.2.2 | ||
37 | $ yarn add global gulpjs/gulp#4.0 | ||
38 | ``` | ||
39 | |||
40 | #### Fix native modules to match current electron node version | 34 | #### Fix native modules to match current electron node version |
41 | ```bash | 35 | ```bash |
42 | $ yarn run rebuild | 36 | $ yarn run rebuild |
diff --git a/appveyor.yml b/appveyor.yml index a8e55d25f..d9296b1f6 100644 --- a/appveyor.yml +++ b/appveyor.yml | |||
@@ -11,8 +11,6 @@ version: 5.0.0.{build} | |||
11 | install: | 11 | install: |
12 | - ps: Install-Product node 8 | 12 | - ps: Install-Product node 8 |
13 | - yarn cache clean | 13 | - yarn cache clean |
14 | - yarn add global gulp-cli@1.2.2 | ||
15 | - yarn add global gulpjs/gulp#4.0 | ||
16 | - yarn install | 14 | - yarn install |
17 | 15 | ||
18 | # cache: | 16 | # cache: |
diff --git a/build-helpers/images/icon.png b/build-helpers/images/icon.png index e0b4935b5..9c39f06e0 100644 --- a/build-helpers/images/icon.png +++ b/build-helpers/images/icon.png | |||
Binary files differ | |||
diff --git a/build-helpers/images/icons/1024x1024.png b/build-helpers/images/icons/1024x1024.png index e0b4935b5..9c39f06e0 100644 --- a/build-helpers/images/icons/1024x1024.png +++ b/build-helpers/images/icons/1024x1024.png | |||
Binary files differ | |||
diff --git a/build-helpers/images/icons/128x128.png b/build-helpers/images/icons/128x128.png new file mode 100644 index 000000000..8eb811283 --- /dev/null +++ b/build-helpers/images/icons/128x128.png | |||
Binary files differ | |||
diff --git a/build-helpers/images/icons/16x16.png b/build-helpers/images/icons/16x16.png new file mode 100644 index 000000000..2523dd8a7 --- /dev/null +++ b/build-helpers/images/icons/16x16.png | |||
Binary files differ | |||
diff --git a/build-helpers/images/icons/24x24.png b/build-helpers/images/icons/24x24.png new file mode 100644 index 000000000..9cb17a0b1 --- /dev/null +++ b/build-helpers/images/icons/24x24.png | |||
Binary files differ | |||
diff --git a/build-helpers/images/icons/256x256.png b/build-helpers/images/icons/256x256.png new file mode 100644 index 000000000..5742147b8 --- /dev/null +++ b/build-helpers/images/icons/256x256.png | |||
Binary files differ | |||
diff --git a/build-helpers/images/icons/32x32.png b/build-helpers/images/icons/32x32.png new file mode 100644 index 000000000..09b88a805 --- /dev/null +++ b/build-helpers/images/icons/32x32.png | |||
Binary files differ | |||
diff --git a/build-helpers/images/icons/48x48.png b/build-helpers/images/icons/48x48.png new file mode 100644 index 000000000..c9d2f331c --- /dev/null +++ b/build-helpers/images/icons/48x48.png | |||
Binary files differ | |||
diff --git a/build-helpers/images/icons/512x512.png b/build-helpers/images/icons/512x512.png new file mode 100644 index 000000000..9d5f1c658 --- /dev/null +++ b/build-helpers/images/icons/512x512.png | |||
Binary files differ | |||
diff --git a/build-helpers/images/icons/64x64.png b/build-helpers/images/icons/64x64.png new file mode 100644 index 000000000..7842217ed --- /dev/null +++ b/build-helpers/images/icons/64x64.png | |||
Binary files differ | |||
diff --git a/build-helpers/images/icons/96x96.png b/build-helpers/images/icons/96x96.png new file mode 100644 index 000000000..bbae07f45 --- /dev/null +++ b/build-helpers/images/icons/96x96.png | |||
Binary files differ | |||
diff --git a/electron-builder.yml b/electron-builder.yml index adc8f8ae9..96bd63cc2 100644 --- a/electron-builder.yml +++ b/electron-builder.yml | |||
@@ -24,10 +24,17 @@ linux: | |||
24 | icon: ./build-helpers/images/icons | 24 | icon: ./build-helpers/images/icons |
25 | category: Network;InstantMessaging; | 25 | category: Network;InstantMessaging; |
26 | executableName: franz | 26 | executableName: franz |
27 | synopsis: "Messaging app for WhatsApp, Slack, Telegram, HipChat, Hangouts and many many more." | ||
28 | description: "Franz is your messaging app / former Emperor of Austria and combines chat & messaging services into one application. Franz currently supports Slack, WhatsApp, WeChat, HipChat, Facebook Messenger, Telegram, Google Hangouts, GroupMe, Skype and many more. You can download Franz for free for Mac & Windows." | ||
27 | target: | 29 | target: |
28 | - target: AppImage | 30 | - target: AppImage |
29 | - target: deb | 31 | - target: deb |
32 | - target: tar.gz | ||
30 | 33 | ||
31 | nsis: | 34 | nsis: |
32 | perMachine: false | 35 | perMachine: false |
33 | oneClick: true | 36 | oneClick: true |
37 | |||
38 | protocols: | ||
39 | name: Franz | ||
40 | schemes: [franz] | ||
diff --git a/gulpfile.babel.js b/gulpfile.babel.js index d947974b3..b50001b2d 100644 --- a/gulpfile.babel.js +++ b/gulpfile.babel.js | |||
@@ -110,7 +110,11 @@ export function watch() { | |||
110 | } | 110 | } |
111 | 111 | ||
112 | export function webserver() { | 112 | export function webserver() { |
113 | gulp.src(paths.dest) | 113 | gulp.src([ |
114 | paths.dest, | ||
115 | `!${paths.dest}/electron/**`, | ||
116 | `!${paths.dest}/webview/**`, | ||
117 | ]) | ||
114 | .pipe(server({ | 118 | .pipe(server({ |
115 | livereload: true, | 119 | livereload: true, |
116 | })); | 120 | })); |
diff --git a/package.json b/package.json index 54df530ca..9c111d336 100644 --- a/package.json +++ b/package.json | |||
@@ -6,6 +6,7 @@ | |||
6 | "description": "Messaging app for WhatsApp, Slack, Telegram, HipChat, Hangouts and many many more.", | 6 | "description": "Messaging app for WhatsApp, Slack, Telegram, HipChat, Hangouts and many many more.", |
7 | "copyright": "adlk x franz - Stefan Malzner", | 7 | "copyright": "adlk x franz - Stefan Malzner", |
8 | "main": "index.js", | 8 | "main": "index.js", |
9 | "homepage": "https://meetfranz.com", | ||
9 | "repository": "https://github.com/meetfranz/franz.git", | 10 | "repository": "https://github.com/meetfranz/franz.git", |
10 | "private": true, | 11 | "private": true, |
11 | "scripts": { | 12 | "scripts": { |
@@ -38,6 +39,7 @@ | |||
38 | "electron-updater": "^2.4.3", | 39 | "electron-updater": "^2.4.3", |
39 | "electron-window-state": "^4.1.0", | 40 | "electron-window-state": "^4.1.0", |
40 | "fs-extra": "^3.0.1", | 41 | "fs-extra": "^3.0.1", |
42 | "gulp-cli": "1.2.2", | ||
41 | "ini": "^1.3.4", | 43 | "ini": "^1.3.4", |
42 | "jshashes": "^1.0.6", | 44 | "jshashes": "^1.0.6", |
43 | "jsonwebtoken": "^7.4.1", | 45 | "jsonwebtoken": "^7.4.1", |
diff --git a/src/actions/app.js b/src/actions/app.js index 25ff9344d..e4f648fc9 100644 --- a/src/actions/app.js +++ b/src/actions/app.js | |||
@@ -22,6 +22,7 @@ export default { | |||
22 | healthCheck: {}, | 22 | healthCheck: {}, |
23 | muteApp: { | 23 | muteApp: { |
24 | isMuted: PropTypes.bool.isRequired, | 24 | isMuted: PropTypes.bool.isRequired, |
25 | overrideSystemMute: PropTypes.bool, | ||
25 | }, | 26 | }, |
26 | toggleMuteApp: {}, | 27 | toggleMuteApp: {}, |
27 | }; | 28 | }; |
diff --git a/src/actions/user.js b/src/actions/user.js index fe32b8a05..ccf1fa56a 100644 --- a/src/actions/user.js +++ b/src/actions/user.js | |||
@@ -27,4 +27,5 @@ export default { | |||
27 | importLegacyServices: PropTypes.arrayOf(PropTypes.shape({ | 27 | importLegacyServices: PropTypes.arrayOf(PropTypes.shape({ |
28 | recipe: PropTypes.string.isRequired, | 28 | recipe: PropTypes.string.isRequired, |
29 | })).isRequired, | 29 | })).isRequired, |
30 | delete: {}, | ||
30 | }; | 31 | }; |
diff --git a/src/api/UserApi.js b/src/api/UserApi.js index e8fd75bed..edfb88988 100644 --- a/src/api/UserApi.js +++ b/src/api/UserApi.js | |||
@@ -46,4 +46,8 @@ export default class UserApi { | |||
46 | getLegacyServices() { | 46 | getLegacyServices() { |
47 | return this.server.getLegacyServices(); | 47 | return this.server.getLegacyServices(); |
48 | } | 48 | } |
49 | |||
50 | delete() { | ||
51 | return this.server.deleteAccount(); | ||
52 | } | ||
49 | } | 53 | } |
diff --git a/src/api/server/LocalApi.js b/src/api/server/LocalApi.js index eba236f16..79ac6e12f 100644 --- a/src/api/server/LocalApi.js +++ b/src/api/server/LocalApi.js | |||
@@ -1,5 +1,3 @@ | |||
1 | import SettingsModel from '../../models/Settings'; | ||
2 | |||
3 | export default class LocalApi { | 1 | export default class LocalApi { |
4 | // App | 2 | // App |
5 | async updateAppSettings(data) { | 3 | async updateAppSettings(data) { |
@@ -15,7 +13,7 @@ export default class LocalApi { | |||
15 | async getAppSettings() { | 13 | async getAppSettings() { |
16 | const settingsString = localStorage.getItem('app'); | 14 | const settingsString = localStorage.getItem('app'); |
17 | try { | 15 | try { |
18 | const settings = new SettingsModel(JSON.parse(settingsString) || {}); | 16 | const settings = JSON.parse(settingsString) || {}; |
19 | console.debug('LocalApi::getAppSettings resolves', settings); | 17 | console.debug('LocalApi::getAppSettings resolves', settings); |
20 | 18 | ||
21 | return settings; | 19 | return settings; |
diff --git a/src/api/server/ServerApi.js b/src/api/server/ServerApi.js index f25f02eaa..8b3136d27 100644 --- a/src/api/server/ServerApi.js +++ b/src/api/server/ServerApi.js | |||
@@ -12,6 +12,8 @@ import NewsModel from '../../models/News'; | |||
12 | import UserModel from '../../models/User'; | 12 | import UserModel from '../../models/User'; |
13 | import OrderModel from '../../models/Order'; | 13 | import OrderModel from '../../models/Order'; |
14 | 14 | ||
15 | import { sleep } from '../../helpers/async-helpers'; | ||
16 | |||
15 | import { API } from '../../environment'; | 17 | import { API } from '../../environment'; |
16 | 18 | ||
17 | import { | 19 | import { |
@@ -125,6 +127,19 @@ export default class ServerApi { | |||
125 | return user; | 127 | return user; |
126 | } | 128 | } |
127 | 129 | ||
130 | async deleteAccount() { | ||
131 | const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/me`, this._prepareAuthRequest({ | ||
132 | method: 'DELETE', | ||
133 | })); | ||
134 | if (!request.ok) { | ||
135 | throw request; | ||
136 | } | ||
137 | const data = await request.json(); | ||
138 | |||
139 | console.debug('ServerApi::deleteAccount resolves', data); | ||
140 | return data; | ||
141 | } | ||
142 | |||
128 | // Services | 143 | // Services |
129 | async getServices() { | 144 | async getServices() { |
130 | const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/me/services`, this._prepareAuthRequest({ | 145 | const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/me/services`, this._prepareAuthRequest({ |
@@ -290,18 +305,25 @@ export default class ServerApi { | |||
290 | 305 | ||
291 | fs.ensureDirSync(recipeTempDirectory); | 306 | fs.ensureDirSync(recipeTempDirectory); |
292 | const res = await fetch(packageUrl); | 307 | const res = await fetch(packageUrl); |
308 | console.debug('Recipe downloaded', recipeId); | ||
293 | const buffer = await res.buffer(); | 309 | const buffer = await res.buffer(); |
294 | fs.writeFileSync(archivePath, buffer); | 310 | fs.writeFileSync(archivePath, buffer); |
295 | 311 | ||
296 | tar.x({ | 312 | await sleep(10); |
313 | |||
314 | await tar.x({ | ||
297 | file: archivePath, | 315 | file: archivePath, |
298 | cwd: recipeTempDirectory, | 316 | cwd: recipeTempDirectory, |
299 | sync: true, | 317 | preservePaths: true, |
318 | unlink: true, | ||
319 | preserveOwner: false, | ||
320 | onwarn: x => console.log('warn', recipeId, x), | ||
300 | }); | 321 | }); |
301 | 322 | ||
323 | await sleep(10); | ||
324 | |||
302 | const { id } = fs.readJsonSync(path.join(recipeTempDirectory, 'package.json')); | 325 | const { id } = fs.readJsonSync(path.join(recipeTempDirectory, 'package.json')); |
303 | const recipeDirectory = path.join(recipesDirectory, id); | 326 | const recipeDirectory = path.join(recipesDirectory, id); |
304 | |||
305 | fs.copySync(recipeTempDirectory, recipeDirectory); | 327 | fs.copySync(recipeTempDirectory, recipeDirectory); |
306 | fs.remove(recipeTempDirectory); | 328 | fs.remove(recipeTempDirectory); |
307 | fs.remove(path.join(recipesDirectory, recipeId, 'recipe.tar.gz')); | 329 | fs.remove(path.join(recipesDirectory, recipeId, 'recipe.tar.gz')); |
diff --git a/src/components/auth/Import.js b/src/components/auth/Import.js index 06493a0fd..078244434 100644 --- a/src/components/auth/Import.js +++ b/src/components/auth/Import.js | |||
@@ -24,7 +24,7 @@ const messages = defineMessages({ | |||
24 | }, | 24 | }, |
25 | skipButtonLabel: { | 25 | skipButtonLabel: { |
26 | id: 'import.skip.label', | 26 | id: 'import.skip.label', |
27 | defaultMessage: '!!!I want add services manually', | 27 | defaultMessage: '!!!I want to add services manually', |
28 | }, | 28 | }, |
29 | }); | 29 | }); |
30 | 30 | ||
diff --git a/src/components/auth/Welcome.js b/src/components/auth/Welcome.js index 06b10ecfe..eb9fbb847 100644 --- a/src/components/auth/Welcome.js +++ b/src/components/auth/Welcome.js | |||
@@ -55,12 +55,16 @@ export default class Login extends Component { | |||
55 | </div> | 55 | </div> |
56 | <div className="welcome__featured-services"> | 56 | <div className="welcome__featured-services"> |
57 | {recipes.map(recipe => ( | 57 | {recipes.map(recipe => ( |
58 | <img | 58 | <div |
59 | key={recipe.id} | 59 | key={recipe.id} |
60 | src={recipe.icons.svg} | ||
61 | className="welcome__featured-service" | 60 | className="welcome__featured-service" |
62 | alt="" | 61 | > |
63 | /> | 62 | <img |
63 | key={recipe.id} | ||
64 | src={recipe.icons.svg} | ||
65 | alt="" | ||
66 | /> | ||
67 | </div> | ||
64 | ))} | 68 | ))} |
65 | </div> | 69 | </div> |
66 | </div> | 70 | </div> |
diff --git a/src/components/layout/Sidebar.js b/src/components/layout/Sidebar.js index cb2ecc8ce..915ebeace 100644 --- a/src/components/layout/Sidebar.js +++ b/src/components/layout/Sidebar.js | |||
@@ -17,12 +17,12 @@ const messages = defineMessages({ | |||
17 | defaultMessage: '!!!Add new service', | 17 | defaultMessage: '!!!Add new service', |
18 | }, | 18 | }, |
19 | mute: { | 19 | mute: { |
20 | id: 'sidebar.mute', | 20 | id: 'sidebar.muteApp', |
21 | defaultMessage: '!!!Disable audio', | 21 | defaultMessage: '!!!Disable notifications & audio', |
22 | }, | 22 | }, |
23 | unmute: { | 23 | unmute: { |
24 | id: 'sidebar.unmute', | 24 | id: 'sidebar.unmuteApp', |
25 | defaultMessage: '!!!Enable audio', | 25 | defaultMessage: '!!!Enable notifications & audio', |
26 | }, | 26 | }, |
27 | }); | 27 | }); |
28 | 28 | ||
diff --git a/src/components/services/content/ServiceWebview.js b/src/components/services/content/ServiceWebview.js index 11911e834..c146abf4e 100644 --- a/src/components/services/content/ServiceWebview.js +++ b/src/components/services/content/ServiceWebview.js | |||
@@ -65,6 +65,7 @@ export default class ServiceWebview extends Component { | |||
65 | 65 | ||
66 | const webviewClasses = classnames({ | 66 | const webviewClasses = classnames({ |
67 | services__webview: true, | 67 | services__webview: true, |
68 | 'services__webview-wrapper': true, | ||
68 | 'is-active': service.isActive, | 69 | 'is-active': service.isActive, |
69 | 'services__webview--force-repaint': this.state.forceRepaint, | 70 | 'services__webview--force-repaint': this.state.forceRepaint, |
70 | }); | 71 | }); |
diff --git a/src/components/services/tabs/TabBarSortableList.js b/src/components/services/tabs/TabBarSortableList.js index 2daf55676..489027d57 100644 --- a/src/components/services/tabs/TabBarSortableList.js +++ b/src/components/services/tabs/TabBarSortableList.js | |||
@@ -17,6 +17,8 @@ class TabBarSortableList extends Component { | |||
17 | deleteService: PropTypes.func.isRequired, | 17 | deleteService: PropTypes.func.isRequired, |
18 | disableService: PropTypes.func.isRequired, | 18 | disableService: PropTypes.func.isRequired, |
19 | enableService: PropTypes.func.isRequired, | 19 | enableService: PropTypes.func.isRequired, |
20 | showMessageBadgeWhenMutedSetting: PropTypes.bool.isRequired, | ||
21 | showMessageBadgesEvenWhenMuted: PropTypes.bool.isRequired, | ||
20 | } | 22 | } |
21 | 23 | ||
22 | render() { | 24 | render() { |
@@ -30,6 +32,8 @@ class TabBarSortableList extends Component { | |||
30 | disableService, | 32 | disableService, |
31 | enableService, | 33 | enableService, |
32 | openSettings, | 34 | openSettings, |
35 | showMessageBadgeWhenMutedSetting, | ||
36 | showMessageBadgesEvenWhenMuted, | ||
33 | } = this.props; | 37 | } = this.props; |
34 | 38 | ||
35 | return ( | 39 | return ( |
@@ -50,6 +54,8 @@ class TabBarSortableList extends Component { | |||
50 | disableService={() => disableService({ serviceId: service.id })} | 54 | disableService={() => disableService({ serviceId: service.id })} |
51 | enableService={() => enableService({ serviceId: service.id })} | 55 | enableService={() => enableService({ serviceId: service.id })} |
52 | openSettings={openSettings} | 56 | openSettings={openSettings} |
57 | showMessageBadgeWhenMutedSetting={showMessageBadgeWhenMutedSetting} | ||
58 | showMessageBadgesEvenWhenMuted={showMessageBadgesEvenWhenMuted} | ||
53 | /> | 59 | /> |
54 | ))} | 60 | ))} |
55 | {/* <li> | 61 | {/* <li> |
diff --git a/src/components/services/tabs/TabItem.js b/src/components/services/tabs/TabItem.js index a7136c43f..7aed8fda7 100644 --- a/src/components/services/tabs/TabItem.js +++ b/src/components/services/tabs/TabItem.js | |||
@@ -63,6 +63,8 @@ class TabItem extends Component { | |||
63 | deleteService: PropTypes.func.isRequired, | 63 | deleteService: PropTypes.func.isRequired, |
64 | disableService: PropTypes.func.isRequired, | 64 | disableService: PropTypes.func.isRequired, |
65 | enableService: PropTypes.func.isRequired, | 65 | enableService: PropTypes.func.isRequired, |
66 | showMessageBadgeWhenMutedSetting: PropTypes.bool.isRequired, | ||
67 | showMessageBadgesEvenWhenMuted: PropTypes.bool.isRequired, | ||
66 | }; | 68 | }; |
67 | 69 | ||
68 | static contextTypes = { | 70 | static contextTypes = { |
@@ -81,6 +83,8 @@ class TabItem extends Component { | |||
81 | disableService, | 83 | disableService, |
82 | enableService, | 84 | enableService, |
83 | openSettings, | 85 | openSettings, |
86 | showMessageBadgeWhenMutedSetting, | ||
87 | showMessageBadgesEvenWhenMuted, | ||
84 | } = this.props; | 88 | } = this.props; |
85 | const { intl } = this.context; | 89 | const { intl } = this.context; |
86 | 90 | ||
@@ -121,6 +125,26 @@ class TabItem extends Component { | |||
121 | }]; | 125 | }]; |
122 | const menu = Menu.buildFromTemplate(menuTemplate); | 126 | const menu = Menu.buildFromTemplate(menuTemplate); |
123 | 127 | ||
128 | let notificationBadge = null; | ||
129 | if ((showMessageBadgeWhenMutedSetting || service.isNotificationEnabled) && showMessageBadgesEvenWhenMuted && service.isBadgeEnabled) { | ||
130 | notificationBadge = ( | ||
131 | <span> | ||
132 | {service.unreadDirectMessageCount > 0 && ( | ||
133 | <span className="tab-item__message-count"> | ||
134 | {service.unreadDirectMessageCount} | ||
135 | </span> | ||
136 | )} | ||
137 | {service.unreadIndirectMessageCount > 0 | ||
138 | && service.unreadDirectMessageCount === 0 | ||
139 | && service.isIndirectMessageBadgeEnabled && ( | ||
140 | <span className="tab-item__message-count is-indirect"> | ||
141 | • | ||
142 | </span> | ||
143 | )} | ||
144 | </span> | ||
145 | ); | ||
146 | } | ||
147 | |||
124 | return ( | 148 | return ( |
125 | <li | 149 | <li |
126 | className={classnames({ | 150 | className={classnames({ |
@@ -138,18 +162,7 @@ class TabItem extends Component { | |||
138 | className="tab-item__icon" | 162 | className="tab-item__icon" |
139 | alt="" | 163 | alt="" |
140 | /> | 164 | /> |
141 | {service.unreadDirectMessageCount > 0 && ( | 165 | {notificationBadge} |
142 | <span className="tab-item__message-count"> | ||
143 | {service.unreadDirectMessageCount} | ||
144 | </span> | ||
145 | )} | ||
146 | {service.unreadIndirectMessageCount > 0 | ||
147 | && service.unreadDirectMessageCount === 0 | ||
148 | && service.isIndirectMessageBadgeEnabled && ( | ||
149 | <span className="tab-item__message-count is-indirect"> | ||
150 | • | ||
151 | </span> | ||
152 | )} | ||
153 | </li> | 166 | </li> |
154 | ); | 167 | ); |
155 | } | 168 | } |
diff --git a/src/components/services/tabs/Tabbar.js b/src/components/services/tabs/Tabbar.js index 9da1090b7..ceb88c51c 100644 --- a/src/components/services/tabs/Tabbar.js +++ b/src/components/services/tabs/Tabbar.js | |||
@@ -18,6 +18,8 @@ export default class TabBar extends Component { | |||
18 | toggleAudio: PropTypes.func.isRequired, | 18 | toggleAudio: PropTypes.func.isRequired, |
19 | deleteService: PropTypes.func.isRequired, | 19 | deleteService: PropTypes.func.isRequired, |
20 | updateService: PropTypes.func.isRequired, | 20 | updateService: PropTypes.func.isRequired, |
21 | showMessageBadgeWhenMutedSetting: PropTypes.bool.isRequired, | ||
22 | showMessageBadgesEvenWhenMuted: PropTypes.bool.isRequired, | ||
21 | } | 23 | } |
22 | 24 | ||
23 | onSortEnd = ({ oldIndex, newIndex }) => { | 25 | onSortEnd = ({ oldIndex, newIndex }) => { |
@@ -64,6 +66,8 @@ export default class TabBar extends Component { | |||
64 | toggleNotifications, | 66 | toggleNotifications, |
65 | toggleAudio, | 67 | toggleAudio, |
66 | deleteService, | 68 | deleteService, |
69 | showMessageBadgeWhenMutedSetting, | ||
70 | showMessageBadgesEvenWhenMuted, | ||
67 | } = this.props; | 71 | } = this.props; |
68 | 72 | ||
69 | return ( | 73 | return ( |
@@ -85,6 +89,8 @@ export default class TabBar extends Component { | |||
85 | axis="y" | 89 | axis="y" |
86 | lockAxis="y" | 90 | lockAxis="y" |
87 | helperClass="is-reordering" | 91 | helperClass="is-reordering" |
92 | showMessageBadgeWhenMutedSetting={showMessageBadgeWhenMutedSetting} | ||
93 | showMessageBadgesEvenWhenMuted={showMessageBadgesEvenWhenMuted} | ||
88 | /> | 94 | /> |
89 | </div> | 95 | </div> |
90 | ); | 96 | ); |
diff --git a/src/components/settings/account/AccountDashboard.js b/src/components/settings/account/AccountDashboard.js index 75dbdef49..89fa07800 100644 --- a/src/components/settings/account/AccountDashboard.js +++ b/src/components/settings/account/AccountDashboard.js | |||
@@ -28,6 +28,10 @@ const messages = defineMessages({ | |||
28 | id: 'settings.account.headlineInvoices', | 28 | id: 'settings.account.headlineInvoices', |
29 | defaultMessage: '!!Invoices', | 29 | defaultMessage: '!!Invoices', |
30 | }, | 30 | }, |
31 | headlineDangerZone: { | ||
32 | id: 'settings.account.headlineDangerZone', | ||
33 | defaultMessage: '!!Danger Zone', | ||
34 | }, | ||
31 | manageSubscriptionButtonLabel: { | 35 | manageSubscriptionButtonLabel: { |
32 | id: 'settings.account.manageSubscription.label', | 36 | id: 'settings.account.manageSubscription.label', |
33 | defaultMessage: '!!!Manage your subscription', | 37 | defaultMessage: '!!!Manage your subscription', |
@@ -72,6 +76,18 @@ const messages = defineMessages({ | |||
72 | id: 'settings.account.mining.cancel', | 76 | id: 'settings.account.mining.cancel', |
73 | defaultMessage: '!!!Cancel mining', | 77 | defaultMessage: '!!!Cancel mining', |
74 | }, | 78 | }, |
79 | deleteAccount: { | ||
80 | id: 'settings.account.deleteAccount', | ||
81 | defaultMessage: '!!!Delete account', | ||
82 | }, | ||
83 | deleteInfo: { | ||
84 | id: 'settings.account.deleteInfo', | ||
85 | defaultMessage: '!!!If you don\'t need your Franz account any longer, you can delete your account and all related data here.', | ||
86 | }, | ||
87 | deleteEmailSent: { | ||
88 | id: 'settings.account.deleteEmailSent', | ||
89 | defaultMessage: '!!!You have received an email with a link to confirm your account deletion. Your account and data cannot be restored!', | ||
90 | }, | ||
75 | }); | 91 | }); |
76 | 92 | ||
77 | @observer | 93 | @observer |
@@ -90,6 +106,9 @@ export default class AccountDashboard extends Component { | |||
90 | openExternalUrl: PropTypes.func.isRequired, | 106 | openExternalUrl: PropTypes.func.isRequired, |
91 | onCloseSubscriptionWindow: PropTypes.func.isRequired, | 107 | onCloseSubscriptionWindow: PropTypes.func.isRequired, |
92 | stopMiner: PropTypes.func.isRequired, | 108 | stopMiner: PropTypes.func.isRequired, |
109 | deleteAccount: PropTypes.func.isRequired, | ||
110 | isLoadingDeleteAccount: PropTypes.bool.isRequired, | ||
111 | isDeleteAccountSuccessful: PropTypes.bool.isRequired, | ||
93 | }; | 112 | }; |
94 | 113 | ||
95 | static contextTypes = { | 114 | static contextTypes = { |
@@ -111,6 +130,9 @@ export default class AccountDashboard extends Component { | |||
111 | retryUserInfoRequest, | 130 | retryUserInfoRequest, |
112 | onCloseSubscriptionWindow, | 131 | onCloseSubscriptionWindow, |
113 | stopMiner, | 132 | stopMiner, |
133 | deleteAccount, | ||
134 | isLoadingDeleteAccount, | ||
135 | isDeleteAccountSuccessful, | ||
114 | } = this.props; | 136 | } = this.props; |
115 | const { intl } = this.context; | 137 | const { intl } = this.context; |
116 | 138 | ||
@@ -201,7 +223,7 @@ export default class AccountDashboard extends Component { | |||
201 | /> | 223 | /> |
202 | </div> | 224 | </div> |
203 | </div> | 225 | </div> |
204 | <div className="account__box account__box--last"> | 226 | <div className="account__box"> |
205 | <h2>{intl.formatMessage(messages.headlineInvoices)}</h2> | 227 | <h2>{intl.formatMessage(messages.headlineInvoices)}</h2> |
206 | <table className="invoices"> | 228 | <table className="invoices"> |
207 | <tbody> | 229 | <tbody> |
@@ -230,7 +252,7 @@ export default class AccountDashboard extends Component { | |||
230 | 252 | ||
231 | {user.isMiner && ( | 253 | {user.isMiner && ( |
232 | <div className="account franz-form"> | 254 | <div className="account franz-form"> |
233 | <div className="account__box"> | 255 | <div className="account__box account__box--last"> |
234 | <h2>{intl.formatMessage(messages.headlineSubscription)}</h2> | 256 | <h2>{intl.formatMessage(messages.headlineSubscription)}</h2> |
235 | <div className="account__subscription"> | 257 | <div className="account__subscription"> |
236 | <div> | 258 | <div> |
@@ -267,7 +289,7 @@ export default class AccountDashboard extends Component { | |||
267 | <Loader /> | 289 | <Loader /> |
268 | ) : ( | 290 | ) : ( |
269 | <div className="account franz-form"> | 291 | <div className="account franz-form"> |
270 | <div className="account__box account__box--last"> | 292 | <div className="account__box"> |
271 | <h2>{intl.formatMessage(messages.headlineUpgrade)}</h2> | 293 | <h2>{intl.formatMessage(messages.headlineUpgrade)}</h2> |
272 | <SubscriptionForm | 294 | <SubscriptionForm |
273 | onCloseWindow={onCloseSubscriptionWindow} | 295 | onCloseWindow={onCloseSubscriptionWindow} |
@@ -276,8 +298,29 @@ export default class AccountDashboard extends Component { | |||
276 | </div> | 298 | </div> |
277 | ) | 299 | ) |
278 | )} | 300 | )} |
301 | |||
302 | <div className="account franz-form"> | ||
303 | <div className="account__box"> | ||
304 | <h2>{intl.formatMessage(messages.headlineDangerZone)}</h2> | ||
305 | {!isDeleteAccountSuccessful && ( | ||
306 | <div className="account__subscription"> | ||
307 | <p>{intl.formatMessage(messages.deleteInfo)}</p> | ||
308 | <Button | ||
309 | label={intl.formatMessage(messages.deleteAccount)} | ||
310 | buttonType="danger" | ||
311 | onClick={() => deleteAccount()} | ||
312 | loaded={!isLoadingDeleteAccount} | ||
313 | /> | ||
314 | </div> | ||
315 | )} | ||
316 | {isDeleteAccountSuccessful && ( | ||
317 | <p>{intl.formatMessage(messages.deleteEmailSent)}</p> | ||
318 | )} | ||
319 | </div> | ||
320 | </div> | ||
279 | </div> | 321 | </div> |
280 | )} | 322 | )} |
323 | |||
281 | </div> | 324 | </div> |
282 | <ReactTooltip place="right" type="dark" effect="solid" /> | 325 | <ReactTooltip place="right" type="dark" effect="solid" /> |
283 | </div> | 326 | </div> |
diff --git a/src/components/settings/services/EditServiceForm.js b/src/components/settings/services/EditServiceForm.js index 36cefe87c..4458c4c5a 100644 --- a/src/components/settings/services/EditServiceForm.js +++ b/src/components/settings/services/EditServiceForm.js | |||
@@ -47,6 +47,10 @@ const messages = defineMessages({ | |||
47 | id: 'settings.service.form.tabOnPremise', | 47 | id: 'settings.service.form.tabOnPremise', |
48 | defaultMessage: '!!!Self hosted ⭐️', | 48 | defaultMessage: '!!!Self hosted ⭐️', |
49 | }, | 49 | }, |
50 | useHostedService: { | ||
51 | id: 'settings.service.form.useHostedService', | ||
52 | defaultMessage: '!!!Use the hosted {name} service.', | ||
53 | }, | ||
50 | customUrlValidationError: { | 54 | customUrlValidationError: { |
51 | id: 'settings.service.form.customUrlValidationError', | 55 | id: 'settings.service.form.customUrlValidationError', |
52 | defaultMessage: '!!!Could not validate custom {name} server.', | 56 | defaultMessage: '!!!Could not validate custom {name} server.', |
@@ -67,6 +71,18 @@ const messages = defineMessages({ | |||
67 | id: 'settings.service.form.isMutedInfo', | 71 | id: 'settings.service.form.isMutedInfo', |
68 | defaultMessage: '!!!When disabled, all notification sounds and audio playback are muted', | 72 | defaultMessage: '!!!When disabled, all notification sounds and audio playback are muted', |
69 | }, | 73 | }, |
74 | headlineNotifications: { | ||
75 | id: 'settings.service.form.headlineNotifications', | ||
76 | defaultMessage: '!!!Notifications', | ||
77 | }, | ||
78 | headlineBadges: { | ||
79 | id: 'settings.service.form.headlineBadges', | ||
80 | defaultMessage: '!!!Unread message dadges', | ||
81 | }, | ||
82 | headlineGeneral: { | ||
83 | id: 'settings.service.form.headlineGeneral', | ||
84 | defaultMessage: '!!!General', | ||
85 | }, | ||
70 | }); | 86 | }); |
71 | 87 | ||
72 | @observer | 88 | @observer |
@@ -108,7 +124,6 @@ export default class EditServiceForm extends Component { | |||
108 | this.props.form.submit({ | 124 | this.props.form.submit({ |
109 | onSuccess: async (form) => { | 125 | onSuccess: async (form) => { |
110 | const values = form.values(); | 126 | const values = form.values(); |
111 | |||
112 | let isValid = true; | 127 | let isValid = true; |
113 | 128 | ||
114 | if (recipe.validateUrl && values.customUrl) { | 129 | if (recipe.validateUrl && values.customUrl) { |
@@ -166,6 +181,13 @@ export default class EditServiceForm extends Component { | |||
166 | /> | 181 | /> |
167 | ); | 182 | ); |
168 | 183 | ||
184 | let activeTabIndex = 0; | ||
185 | if (recipe.hasHostedOption && service.team) { | ||
186 | activeTabIndex = 1; | ||
187 | } else if (recipe.hasHostedOption && service.customUrl) { | ||
188 | activeTabIndex = 2; | ||
189 | } | ||
190 | |||
169 | return ( | 191 | return ( |
170 | <div className="settings__main"> | 192 | <div className="settings__main"> |
171 | <div className="settings__header"> | 193 | <div className="settings__header"> |
@@ -198,11 +220,20 @@ export default class EditServiceForm extends Component { | |||
198 | <Input field={form.$('name')} focus /> | 220 | <Input field={form.$('name')} focus /> |
199 | {(recipe.hasTeamId || recipe.hasCustomUrl) && ( | 221 | {(recipe.hasTeamId || recipe.hasCustomUrl) && ( |
200 | <Tabs | 222 | <Tabs |
201 | active={service.customUrl ? 1 : 0} | 223 | active={activeTabIndex} |
202 | > | 224 | > |
225 | {recipe.hasHostedOption && ( | ||
226 | <TabItem title={recipe.name}> | ||
227 | {intl.formatMessage(messages.useHostedService, { name: recipe.name })} | ||
228 | </TabItem> | ||
229 | )} | ||
203 | {recipe.hasTeamId && ( | 230 | {recipe.hasTeamId && ( |
204 | <TabItem title={intl.formatMessage(messages.tabHosted)}> | 231 | <TabItem title={intl.formatMessage(messages.tabHosted)}> |
205 | <Input field={form.$('team')} suffix={recipe.urlInputSuffix} /> | 232 | <Input |
233 | field={form.$('team')} | ||
234 | prefix={recipe.urlInputPrefix} | ||
235 | suffix={recipe.urlInputSuffix} | ||
236 | /> | ||
206 | </TabItem> | 237 | </TabItem> |
207 | )} | 238 | )} |
208 | {recipe.hasCustomUrl && ( | 239 | {recipe.hasCustomUrl && ( |
@@ -231,20 +262,32 @@ export default class EditServiceForm extends Component { | |||
231 | </Tabs> | 262 | </Tabs> |
232 | )} | 263 | )} |
233 | <div className="settings__options"> | 264 | <div className="settings__options"> |
234 | <Toggle field={form.$('isNotificationEnabled')} /> | 265 | <div className="settings__settings-group"> |
235 | {recipe.hasIndirectMessages && ( | 266 | <h3>{intl.formatMessage(messages.headlineNotifications)}</h3> |
236 | <div> | 267 | <Toggle field={form.$('isNotificationEnabled')} /> |
237 | <Toggle field={form.$('isIndirectMessageBadgeEnabled')} /> | 268 | <Toggle field={form.$('isMuted')} /> |
238 | <p className="settings__help"> | 269 | <p className="settings__help"> |
239 | {intl.formatMessage(messages.indirectMessageInfo)} | 270 | {intl.formatMessage(messages.isMutedInfo)} |
240 | </p> | 271 | </p> |
241 | </div> | 272 | </div> |
242 | )} | 273 | |
243 | <Toggle field={form.$('isMuted')} /> | 274 | <div className="settings__settings-group"> |
244 | <p className="settings__help"> | 275 | <h3>{intl.formatMessage(messages.headlineBadges)}</h3> |
245 | {intl.formatMessage(messages.isMutedInfo)} | 276 | <Toggle field={form.$('isBadgeEnabled')} /> |
246 | </p> | 277 | {recipe.hasIndirectMessages && form.$('isBadgeEnabled').value && ( |
247 | <Toggle field={form.$('isEnabled')} /> | 278 | <div> |
279 | <Toggle field={form.$('isIndirectMessageBadgeEnabled')} /> | ||
280 | <p className="settings__help"> | ||
281 | {intl.formatMessage(messages.indirectMessageInfo)} | ||
282 | </p> | ||
283 | </div> | ||
284 | )} | ||
285 | </div> | ||
286 | |||
287 | <div className="settings__settings-group"> | ||
288 | <h3>{intl.formatMessage(messages.headlineGeneral)}</h3> | ||
289 | <Toggle field={form.$('isEnabled')} /> | ||
290 | </div> | ||
248 | </div> | 291 | </div> |
249 | {recipe.message && ( | 292 | {recipe.message && ( |
250 | <p className="settings__message"> | 293 | <p className="settings__message"> |
diff --git a/src/components/settings/settings/EditSettingsForm.js b/src/components/settings/settings/EditSettingsForm.js index 4ce9b7ab2..ff398aa33 100644 --- a/src/components/settings/settings/EditSettingsForm.js +++ b/src/components/settings/settings/EditSettingsForm.js | |||
@@ -64,10 +64,6 @@ const messages = defineMessages({ | |||
64 | id: 'settings.app.currentVersion', | 64 | id: 'settings.app.currentVersion', |
65 | defaultMessage: '!!!Current version:', | 65 | defaultMessage: '!!!Current version:', |
66 | }, | 66 | }, |
67 | restartRequired: { | ||
68 | id: 'settings.app.restartRequired', | ||
69 | defaultMessage: '!!!Changes require restart', | ||
70 | }, | ||
71 | }); | 67 | }); |
72 | 68 | ||
73 | @observer | 69 | @observer |
@@ -142,6 +138,7 @@ export default class EditSettingsForm extends Component { | |||
142 | {/* Appearance */} | 138 | {/* Appearance */} |
143 | <h2 id="apperance">{intl.formatMessage(messages.headlineAppearance)}</h2> | 139 | <h2 id="apperance">{intl.formatMessage(messages.headlineAppearance)}</h2> |
144 | <Toggle field={form.$('showDisabledServices')} /> | 140 | <Toggle field={form.$('showDisabledServices')} /> |
141 | <Toggle field={form.$('showMessageBadgeWhenMuted')} /> | ||
145 | 142 | ||
146 | {/* Language */} | 143 | {/* Language */} |
147 | <h2 id="language">{intl.formatMessage(messages.headlineLanguage)}</h2> | 144 | <h2 id="language">{intl.formatMessage(messages.headlineLanguage)}</h2> |
@@ -157,7 +154,6 @@ export default class EditSettingsForm extends Component { | |||
157 | {/* Advanced */} | 154 | {/* Advanced */} |
158 | <h2 id="advanced">{intl.formatMessage(messages.headlineAdvanced)}</h2> | 155 | <h2 id="advanced">{intl.formatMessage(messages.headlineAdvanced)}</h2> |
159 | <Toggle field={form.$('enableSpellchecking')} /> | 156 | <Toggle field={form.$('enableSpellchecking')} /> |
160 | <p className="settings__help">{intl.formatMessage(messages.restartRequired)}</p> | ||
161 | {/* <Select field={form.$('spellcheckingLanguage')} /> */} | 157 | {/* <Select field={form.$('spellcheckingLanguage')} /> */} |
162 | 158 | ||
163 | {/* Updates */} | 159 | {/* Updates */} |
diff --git a/src/components/ui/Button.js b/src/components/ui/Button.js index 07e94192f..554206cb7 100644 --- a/src/components/ui/Button.js +++ b/src/components/ui/Button.js | |||
@@ -68,7 +68,7 @@ export default class Button extends Component { | |||
68 | loaded={loaded} | 68 | loaded={loaded} |
69 | lines={10} | 69 | lines={10} |
70 | scale={0.4} | 70 | scale={0.4} |
71 | color={buttonType === '' ? '#FFF' : '#373a3c'} | 71 | color={buttonType !== 'secondary' ? '#FFF' : '#373a3c'} |
72 | component="span" | 72 | component="span" |
73 | /> | 73 | /> |
74 | {label} | 74 | {label} |
diff --git a/src/config.js b/src/config.js index b3e00c92c..e66594c59 100644 --- a/src/config.js +++ b/src/config.js | |||
@@ -11,9 +11,11 @@ export const DEFAULT_APP_SETTINGS = { | |||
11 | enableSystemTray: true, | 11 | enableSystemTray: true, |
12 | minimizeToSystemTray: false, | 12 | minimizeToSystemTray: false, |
13 | showDisabledServices: true, | 13 | showDisabledServices: true, |
14 | showMessageBadgeWhenMuted: true, | ||
14 | enableSpellchecking: true, | 15 | enableSpellchecking: true, |
15 | // spellcheckingLanguage: 'auto', | 16 | // spellcheckingLanguage: 'auto', |
16 | locale: 'en-US', | 17 | locale: '', |
18 | fallbackLocale: 'en-US', | ||
17 | beta: false, | 19 | beta: false, |
18 | isAppMuted: false, | 20 | isAppMuted: false, |
19 | }; | 21 | }; |
diff --git a/src/containers/layout/AppLayoutContainer.js b/src/containers/layout/AppLayoutContainer.js index 7c6ceccd6..e4a9d60c3 100644 --- a/src/containers/layout/AppLayoutContainer.js +++ b/src/containers/layout/AppLayoutContainer.js | |||
@@ -73,13 +73,11 @@ export default class AppLayoutContainer extends Component { | |||
73 | ); | 73 | ); |
74 | } | 74 | } |
75 | 75 | ||
76 | const isMuted = settings.all.isAppMuted || app.isSystemMuted; | ||
77 | |||
78 | const sidebar = ( | 76 | const sidebar = ( |
79 | <Sidebar | 77 | <Sidebar |
80 | services={services.allDisplayed} | 78 | services={services.allDisplayed} |
81 | setActive={setActive} | 79 | setActive={setActive} |
82 | isAppMuted={isMuted} | 80 | isAppMuted={settings.all.isAppMuted} |
83 | openSettings={openSettings} | 81 | openSettings={openSettings} |
84 | closeSettings={closeSettings} | 82 | closeSettings={closeSettings} |
85 | reorder={reorder} | 83 | reorder={reorder} |
@@ -89,6 +87,8 @@ export default class AppLayoutContainer extends Component { | |||
89 | deleteService={deleteService} | 87 | deleteService={deleteService} |
90 | updateService={updateService} | 88 | updateService={updateService} |
91 | toggleMuteApp={toggleMuteApp} | 89 | toggleMuteApp={toggleMuteApp} |
90 | showMessageBadgeWhenMutedSetting={settings.all.showMessageBadgeWhenMuted} | ||
91 | showMessageBadgesEvenWhenMuted={ui.showMessageBadgesEvenWhenMuted} | ||
92 | /> | 92 | /> |
93 | ); | 93 | ); |
94 | 94 | ||
@@ -99,7 +99,7 @@ export default class AppLayoutContainer extends Component { | |||
99 | setWebviewReference={setWebviewReference} | 99 | setWebviewReference={setWebviewReference} |
100 | openWindow={openWindow} | 100 | openWindow={openWindow} |
101 | reload={reload} | 101 | reload={reload} |
102 | isAppMuted={isMuted} | 102 | isAppMuted={settings.all.isAppMuted} |
103 | update={updateService} | 103 | update={updateService} |
104 | /> | 104 | /> |
105 | ); | 105 | ); |
diff --git a/src/containers/settings/AccountScreen.js b/src/containers/settings/AccountScreen.js index a1ac8bda3..008c495d4 100644 --- a/src/containers/settings/AccountScreen.js +++ b/src/containers/settings/AccountScreen.js | |||
@@ -69,6 +69,7 @@ export default class AccountScreen extends Component { | |||
69 | render() { | 69 | render() { |
70 | const { user, payment, app } = this.props.stores; | 70 | const { user, payment, app } = this.props.stores; |
71 | const { openExternalUrl } = this.props.actions.app; | 71 | const { openExternalUrl } = this.props.actions.app; |
72 | const { user: userActions } = this.props.actions; | ||
72 | 73 | ||
73 | const isLoadingUserInfo = user.getUserInfoRequest.isExecuting; | 74 | const isLoadingUserInfo = user.getUserInfoRequest.isExecuting; |
74 | const isLoadingOrdersInfo = payment.ordersDataRequest.isExecuting; | 75 | const isLoadingOrdersInfo = payment.ordersDataRequest.isExecuting; |
@@ -89,6 +90,9 @@ export default class AccountScreen extends Component { | |||
89 | openExternalUrl={url => openExternalUrl({ url })} | 90 | openExternalUrl={url => openExternalUrl({ url })} |
90 | onCloseSubscriptionWindow={() => this.onCloseWindow()} | 91 | onCloseSubscriptionWindow={() => this.onCloseWindow()} |
91 | stopMiner={() => this.stopMiner()} | 92 | stopMiner={() => this.stopMiner()} |
93 | deleteAccount={userActions.delete} | ||
94 | isLoadingDeleteAccount={user.deleteAccountRequest.isExecuting} | ||
95 | isDeleteAccountSuccessful={user.deleteAccountRequest.wasExecuted && !user.deleteAccountRequest.isError} | ||
92 | /> | 96 | /> |
93 | ); | 97 | ); |
94 | } | 98 | } |
@@ -109,6 +113,7 @@ AccountScreen.wrappedComponent.propTypes = { | |||
109 | }).isRequired, | 113 | }).isRequired, |
110 | user: PropTypes.shape({ | 114 | user: PropTypes.shape({ |
111 | update: PropTypes.func.isRequired, | 115 | update: PropTypes.func.isRequired, |
116 | delete: PropTypes.func.isRequired, | ||
112 | }).isRequired, | 117 | }).isRequired, |
113 | }).isRequired, | 118 | }).isRequired, |
114 | }; | 119 | }; |
diff --git a/src/containers/settings/EditServiceScreen.js b/src/containers/settings/EditServiceScreen.js index 191ef447b..3c52152b1 100644 --- a/src/containers/settings/EditServiceScreen.js +++ b/src/containers/settings/EditServiceScreen.js | |||
@@ -26,6 +26,10 @@ const messages = defineMessages({ | |||
26 | id: 'settings.service.form.enableNotification', | 26 | id: 'settings.service.form.enableNotification', |
27 | defaultMessage: '!!!Enable Notifications', | 27 | defaultMessage: '!!!Enable Notifications', |
28 | }, | 28 | }, |
29 | enableBadge: { | ||
30 | id: 'settings.service.form.enableBadge', | ||
31 | defaultMessage: '!!!Show unread message badges', | ||
32 | }, | ||
29 | enableAudio: { | 33 | enableAudio: { |
30 | id: 'settings.service.form.enableAudio', | 34 | id: 'settings.service.form.enableAudio', |
31 | defaultMessage: '!!!Enable audio', | 35 | defaultMessage: '!!!Enable audio', |
@@ -88,6 +92,11 @@ export default class EditServiceScreen extends Component { | |||
88 | value: service.isNotificationEnabled, | 92 | value: service.isNotificationEnabled, |
89 | default: true, | 93 | default: true, |
90 | }, | 94 | }, |
95 | isBadgeEnabled: { | ||
96 | label: intl.formatMessage(messages.enableBadge), | ||
97 | value: service.isBadgeEnabled, | ||
98 | default: true, | ||
99 | }, | ||
91 | isMuted: { | 100 | isMuted: { |
92 | label: intl.formatMessage(messages.enableAudio), | 101 | label: intl.formatMessage(messages.enableAudio), |
93 | value: !service.isMuted, | 102 | value: !service.isMuted, |
@@ -118,11 +127,22 @@ export default class EditServiceScreen extends Component { | |||
118 | }); | 127 | }); |
119 | } | 128 | } |
120 | 129 | ||
130 | // More fine grained and use case specific validation rules | ||
121 | if (recipe.hasTeamId && recipe.hasCustomUrl) { | 131 | if (recipe.hasTeamId && recipe.hasCustomUrl) { |
122 | config.fields.team.validate = [oneRequired(['team', 'customUrl'])]; | 132 | config.fields.team.validate = [oneRequired(['team', 'customUrl'])]; |
123 | config.fields.customUrl.validate = [url, oneRequired(['team', 'customUrl'])]; | 133 | config.fields.customUrl.validate = [url, oneRequired(['team', 'customUrl'])]; |
124 | } | 134 | } |
125 | 135 | ||
136 | // If a service can be hosted and has a teamId or customUrl | ||
137 | if (recipe.hasHostedOption && (recipe.hasTeamId || recipe.hasCustomUrl)) { | ||
138 | if (config.fields.team) { | ||
139 | config.fields.team.validate = []; | ||
140 | } | ||
141 | if (config.fields.customUrl) { | ||
142 | config.fields.customUrl.validate = [url]; | ||
143 | } | ||
144 | } | ||
145 | |||
126 | if (recipe.hasIndirectMessages) { | 146 | if (recipe.hasIndirectMessages) { |
127 | Object.assign(config.fields, { | 147 | Object.assign(config.fields, { |
128 | isIndirectMessageBadgeEnabled: { | 148 | isIndirectMessageBadgeEnabled: { |
diff --git a/src/containers/settings/EditSettingsScreen.js b/src/containers/settings/EditSettingsScreen.js index 62e255dab..45ded9e5c 100644 --- a/src/containers/settings/EditSettingsScreen.js +++ b/src/containers/settings/EditSettingsScreen.js | |||
@@ -43,6 +43,10 @@ const messages = defineMessages({ | |||
43 | id: 'settings.app.form.showDisabledServices', | 43 | id: 'settings.app.form.showDisabledServices', |
44 | defaultMessage: '!!!Display disabled services tabs', | 44 | defaultMessage: '!!!Display disabled services tabs', |
45 | }, | 45 | }, |
46 | showMessageBadgeWhenMuted: { | ||
47 | id: 'settings.app.form.showMessagesBadgesWhenMuted', | ||
48 | defaultMessage: '!!!Show unread message badge when notifications are disabled', | ||
49 | }, | ||
46 | enableSpellchecking: { | 50 | enableSpellchecking: { |
47 | id: 'settings.app.form.enableSpellchecking', | 51 | id: 'settings.app.form.enableSpellchecking', |
48 | defaultMessage: '!!!Enable spell checking', | 52 | defaultMessage: '!!!Enable spell checking', |
@@ -85,6 +89,7 @@ export default class EditSettingsScreen extends Component { | |||
85 | enableSystemTray: settingsData.enableSystemTray, | 89 | enableSystemTray: settingsData.enableSystemTray, |
86 | minimizeToSystemTray: settingsData.minimizeToSystemTray, | 90 | minimizeToSystemTray: settingsData.minimizeToSystemTray, |
87 | showDisabledServices: settingsData.showDisabledServices, | 91 | showDisabledServices: settingsData.showDisabledServices, |
92 | showMessageBadgeWhenMuted: settingsData.showMessageBadgeWhenMuted, | ||
88 | enableSpellchecking: settingsData.enableSpellchecking, | 93 | enableSpellchecking: settingsData.enableSpellchecking, |
89 | // spellcheckingLanguage: settingsData.spellcheckingLanguage, | 94 | // spellcheckingLanguage: settingsData.spellcheckingLanguage, |
90 | locale: settingsData.locale, | 95 | locale: settingsData.locale, |
@@ -154,6 +159,11 @@ export default class EditSettingsScreen extends Component { | |||
154 | value: settings.all.showDisabledServices, | 159 | value: settings.all.showDisabledServices, |
155 | default: DEFAULT_APP_SETTINGS.showDisabledServices, | 160 | default: DEFAULT_APP_SETTINGS.showDisabledServices, |
156 | }, | 161 | }, |
162 | showMessageBadgeWhenMuted: { | ||
163 | label: intl.formatMessage(messages.showMessageBadgeWhenMuted), | ||
164 | value: settings.all.showMessageBadgeWhenMuted, | ||
165 | default: DEFAULT_APP_SETTINGS.showMessageBadgeWhenMuted, | ||
166 | }, | ||
157 | enableSpellchecking: { | 167 | enableSpellchecking: { |
158 | label: intl.formatMessage(messages.enableSpellchecking), | 168 | label: intl.formatMessage(messages.enableSpellchecking), |
159 | value: settings.all.enableSpellchecking, | 169 | value: settings.all.enableSpellchecking, |
diff --git a/src/electron/deepLinking.js b/src/electron/deepLinking.js new file mode 100644 index 000000000..ef23fd3c5 --- /dev/null +++ b/src/electron/deepLinking.js | |||
@@ -0,0 +1,7 @@ | |||
1 | export default function handleDeepLink(window, rawUrl) { | ||
2 | const url = rawUrl.replace('franz://', ''); | ||
3 | |||
4 | if (!url) return; | ||
5 | |||
6 | window.webContents.send('navigateFromDeepLink', { url }); | ||
7 | } | ||
diff --git a/src/helpers/async-helpers.js b/src/helpers/async-helpers.js new file mode 100644 index 000000000..2ef01ee09 --- /dev/null +++ b/src/helpers/async-helpers.js | |||
@@ -0,0 +1,5 @@ | |||
1 | /* eslint-disable import/prefer-default-export */ | ||
2 | |||
3 | export function sleep(ms = 0) { | ||
4 | return new Promise(r => setTimeout(r, ms)); | ||
5 | } | ||
diff --git a/src/helpers/webview-ime-focus-helpers.js b/src/helpers/webview-ime-focus-helpers.js deleted file mode 100644 index 2593a5f26..000000000 --- a/src/helpers/webview-ime-focus-helpers.js +++ /dev/null | |||
@@ -1,38 +0,0 @@ | |||
1 | module.exports.releaseDocumentFocus = () => { | ||
2 | const element = document.createElement('span'); | ||
3 | document.body.appendChild(element); | ||
4 | |||
5 | const range = document.createRange(); | ||
6 | range.setStart(element, 0); | ||
7 | |||
8 | const selection = window.getSelection(); | ||
9 | selection.removeAllRanges(); | ||
10 | selection.addRange(range); | ||
11 | selection.removeAllRanges(); | ||
12 | |||
13 | document.body.removeChild(element); | ||
14 | }; | ||
15 | |||
16 | module.exports.claimDocumentFocus = () => { | ||
17 | const { activeElement } = document; | ||
18 | const selection = window.getSelection(); | ||
19 | |||
20 | let selectionStart; | ||
21 | let selectionEnd; | ||
22 | let range; | ||
23 | |||
24 | if (activeElement) ({ selectionStart, selectionEnd } = activeElement); | ||
25 | if (selection.rangeCount) range = selection.getRangeAt(0); | ||
26 | |||
27 | const restoreOriginalSelection = () => { | ||
28 | if (selectionStart >= 0 && selectionEnd >= 0) { | ||
29 | activeElement.selectionStart = selectionStart; | ||
30 | activeElement.selectionEnd = selectionEnd; | ||
31 | } else if (range) { | ||
32 | selection.addRange(range); | ||
33 | } | ||
34 | }; | ||
35 | |||
36 | exports.releaseDocumentFocus(); | ||
37 | window.requestAnimationFrame(restoreOriginalSelection); | ||
38 | }; | ||
diff --git a/src/i18n/languages.js b/src/i18n/languages.js index 677b09405..f32c345af 100644 --- a/src/i18n/languages.js +++ b/src/i18n/languages.js | |||
@@ -4,6 +4,7 @@ export const APP_LOCALES = { | |||
4 | 'zh-HANT': 'Chinese (Traditional)', | 4 | 'zh-HANT': 'Chinese (Traditional)', |
5 | cs: 'Czech', | 5 | cs: 'Czech', |
6 | nl: 'Dutch', | 6 | nl: 'Dutch', |
7 | es: 'Spanish', | ||
7 | fr: 'French', | 8 | fr: 'French', |
8 | ka: 'Georgian', | 9 | ka: 'Georgian', |
9 | de: 'German', | 10 | de: 'German', |
diff --git a/src/i18n/locales/el.json b/src/i18n/locales/el.json index 5717a18b1..459d097f3 100644 --- a/src/i18n/locales/el.json +++ b/src/i18n/locales/el.json | |||
@@ -3,7 +3,7 @@ | |||
3 | "global.notConnectedToTheInternet" : "You are not connected to the internet.", | 3 | "global.notConnectedToTheInternet" : "You are not connected to the internet.", |
4 | "import.headline" : "Import your Franz 4 services", | 4 | "import.headline" : "Import your Franz 4 services", |
5 | "import.notSupportedHeadline" : "Services not yet supported in Franz 5", | 5 | "import.notSupportedHeadline" : "Services not yet supported in Franz 5", |
6 | "import.skip.label" : "I want add services manually", | 6 | "import.skip.label" : "I want to add services manually", |
7 | "import.submit.label" : "Import services", | 7 | "import.submit.label" : "Import services", |
8 | "infobar.buttonChangelog" : "What is new?", | 8 | "infobar.buttonChangelog" : "What is new?", |
9 | "infobar.buttonInstallUpdate" : "Restart & install update", | 9 | "infobar.buttonInstallUpdate" : "Restart & install update", |
diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json index 8de5e5e02..567537d75 100644 --- a/src/i18n/locales/en-US.json +++ b/src/i18n/locales/en-US.json | |||
@@ -39,7 +39,7 @@ | |||
39 | "import.headline": "Import your Franz 4 services", | 39 | "import.headline": "Import your Franz 4 services", |
40 | "import.notSupportedHeadline": "Services not yet supported in Franz 5", | 40 | "import.notSupportedHeadline": "Services not yet supported in Franz 5", |
41 | "import.submit.label": "Import services", | 41 | "import.submit.label": "Import services", |
42 | "import.skip.label": "I want add services manually", | 42 | "import.skip.label": "I want to add services manually", |
43 | "invite.submit.label": "Send invites", | 43 | "invite.submit.label": "Send invites", |
44 | "invite.headline.friends": "Invite 3 of your friends or colleagues", | 44 | "invite.headline.friends": "Invite 3 of your friends or colleagues", |
45 | "invite.name.label": "Name", | 45 | "invite.name.label": "Name", |
@@ -62,14 +62,15 @@ | |||
62 | "infobar.requiredRequestsFailed": "Could not load services and user information", | 62 | "infobar.requiredRequestsFailed": "Could not load services and user information", |
63 | "sidebar.settings": "Settings", | 63 | "sidebar.settings": "Settings", |
64 | "sidebar.addNewService": "Add new service", | 64 | "sidebar.addNewService": "Add new service", |
65 | "sidebar.mute": "Disable audio", | 65 | "sidebar.muteApp": "Disable notifications & audio", |
66 | "sidebar.unmute": "Enable audio", | 66 | "sidebar.unmuteApp": "Enable notifications & audio", |
67 | "services.welcome": "Welcome to Franz", | 67 | "services.welcome": "Welcome to Franz", |
68 | "services.getStarted": "Get started", | 68 | "services.getStarted": "Get started", |
69 | "settings.account.headline": "Account", | 69 | "settings.account.headline": "Account", |
70 | "settings.account.headlineSubscription": "Your subscription", | 70 | "settings.account.headlineSubscription": "Your subscription", |
71 | "settings.account.headlineUpgrade": "Upgrade your account & support Franz", | 71 | "settings.account.headlineUpgrade": "Upgrade your account & support Franz", |
72 | "settings.account.headlineInvoices": "Invoices", | 72 | "settings.account.headlineInvoices": "Invoices", |
73 | "settings.account.headlineDangerZone": "Danger Zone", | ||
73 | "settings.account.manageSubscription.label": "Manage your subscription", | 74 | "settings.account.manageSubscription.label": "Manage your subscription", |
74 | "settings.account.accountType.basic": "Basic Account", | 75 | "settings.account.accountType.basic": "Basic Account", |
75 | "settings.account.accountType.premium": "Premium Supporter Account", | 76 | "settings.account.accountType.premium": "Premium Supporter Account", |
@@ -86,6 +87,9 @@ | |||
86 | "settings.account.mining.active": "You are right now performing {hashes} calculations per second.", | 87 | "settings.account.mining.active": "You are right now performing {hashes} calculations per second.", |
87 | "settings.account.mining.moreInformation": "Get more information", | 88 | "settings.account.mining.moreInformation": "Get more information", |
88 | "settings.account.mining.cancel": "Cancel mining", | 89 | "settings.account.mining.cancel": "Cancel mining", |
90 | "settings.account.deleteAccount": "Delete account", | ||
91 | "settings.account.deleteInfo": "If you don't need your Franz account any longer, you can delete your account and all related data here.", | ||
92 | "settings.account.deleteEmailSent": "You have received an email with a link to confirm your account deletion. Your account and data cannot be restored!", | ||
89 | "settings.navigation.availableServices": "Available services", | 93 | "settings.navigation.availableServices": "Available services", |
90 | "settings.navigation.yourServices": "Your services", | 94 | "settings.navigation.yourServices": "Your services", |
91 | "settings.navigation.account": "Account", | 95 | "settings.navigation.account": "Account", |
@@ -106,6 +110,7 @@ | |||
106 | "settings.service.form.editServiceHeadline": "Edit {name}", | 110 | "settings.service.form.editServiceHeadline": "Edit {name}", |
107 | "settings.service.form.tabHosted": "Hosted", | 111 | "settings.service.form.tabHosted": "Hosted", |
108 | "settings.service.form.tabOnPremise": "Self hosted ⭐️", | 112 | "settings.service.form.tabOnPremise": "Self hosted ⭐️", |
113 | "settings.service.form.useHostedService": "Use the hosted {name} service.", | ||
109 | "settings.service.form.customUrlValidationError": "Could not validate custom {name} server.", | 114 | "settings.service.form.customUrlValidationError": "Could not validate custom {name} server.", |
110 | "settings.service.form.customUrlPremiumInfo": "To add self hosted services, you need a Franz Premium Supporter Account.", | 115 | "settings.service.form.customUrlPremiumInfo": "To add self hosted services, you need a Franz Premium Supporter Account.", |
111 | "settings.service.form.customUrlUpgradeAccount": "Upgrade your account", | 116 | "settings.service.form.customUrlUpgradeAccount": "Upgrade your account", |
@@ -113,11 +118,15 @@ | |||
113 | "settings.service.form.name": "Name", | 118 | "settings.service.form.name": "Name", |
114 | "settings.service.form.enableService": "Enable service", | 119 | "settings.service.form.enableService": "Enable service", |
115 | "settings.service.form.enableNotification": "Enable notifications", | 120 | "settings.service.form.enableNotification": "Enable notifications", |
121 | "settings.service.form.enableBadge": "Show unread message badges", | ||
116 | "settings.service.form.team": "Team", | 122 | "settings.service.form.team": "Team", |
117 | "settings.service.form.customUrl": "Custom server", | 123 | "settings.service.form.customUrl": "Custom server", |
118 | "settings.service.form.indirectMessages": "Show message badge for all new messages", | 124 | "settings.service.form.indirectMessages": "Show message badge for all new messages", |
119 | "settings.service.form.enableAudio": "Enable audio", | 125 | "settings.service.form.enableAudio": "Enable audio", |
120 | "settings.service.form.isMutedInfo": "When disabled, all notification sounds and audio playback are muted", | 126 | "settings.service.form.isMutedInfo": "When disabled, all notification sounds and audio playback are muted", |
127 | "settings.service.form.headlineNotifications": "Notifications", | ||
128 | "settings.service.form.headlineBadges": "Unread message badges", | ||
129 | "settings.service.form.headlineGeneral": "General", | ||
121 | "settings.service.error.headline": "Error", | 130 | "settings.service.error.headline": "Error", |
122 | "settings.service.error.goBack": "Back to services", | 131 | "settings.service.error.goBack": "Back to services", |
123 | "settings.service.error.message": "Could not load service recipe.", | 132 | "settings.service.error.message": "Could not load service recipe.", |
@@ -148,6 +157,7 @@ | |||
148 | "settings.app.form.language": "Language", | 157 | "settings.app.form.language": "Language", |
149 | "settings.app.form.enableSpellchecking": "Enable spell checking", | 158 | "settings.app.form.enableSpellchecking": "Enable spell checking", |
150 | "settings.app.form.showDisabledServices": "Display disabled services tabs", | 159 | "settings.app.form.showDisabledServices": "Display disabled services tabs", |
160 | "settings.app.form.showMessagesBadgesWhenMuted": "Show unread message badge when notifications are disabled", | ||
151 | "settings.app.form.beta": "Include beta versions", | 161 | "settings.app.form.beta": "Include beta versions", |
152 | "settings.app.translationHelp": "Help us to translate Franz into your language.", | 162 | "settings.app.translationHelp": "Help us to translate Franz into your language.", |
153 | "settings.app.currentVersion": "Current version:", | 163 | "settings.app.currentVersion": "Current version:", |
@@ -188,5 +198,5 @@ | |||
188 | "service.crashHandler.action": "Reload {name}", | 198 | "service.crashHandler.action": "Reload {name}", |
189 | "service.crashHandler.autoReload": "Trying to automatically restore {name} in {seconds} seconds", | 199 | "service.crashHandler.autoReload": "Trying to automatically restore {name} in {seconds} seconds", |
190 | "service.disabledHandler.headline": "{name} is disabled", | 200 | "service.disabledHandler.headline": "{name} is disabled", |
191 | "service.disabledHandler.action": "Enable {name}" | 201 | "service.disabledHandler.action": "Enable {name}" |
192 | } | 202 | } |
diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index 5717a18b1..459d097f3 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json | |||
@@ -3,7 +3,7 @@ | |||
3 | "global.notConnectedToTheInternet" : "You are not connected to the internet.", | 3 | "global.notConnectedToTheInternet" : "You are not connected to the internet.", |
4 | "import.headline" : "Import your Franz 4 services", | 4 | "import.headline" : "Import your Franz 4 services", |
5 | "import.notSupportedHeadline" : "Services not yet supported in Franz 5", | 5 | "import.notSupportedHeadline" : "Services not yet supported in Franz 5", |
6 | "import.skip.label" : "I want add services manually", | 6 | "import.skip.label" : "I want to add services manually", |
7 | "import.submit.label" : "Import services", | 7 | "import.submit.label" : "Import services", |
8 | "infobar.buttonChangelog" : "What is new?", | 8 | "infobar.buttonChangelog" : "What is new?", |
9 | "infobar.buttonInstallUpdate" : "Restart & install update", | 9 | "infobar.buttonInstallUpdate" : "Restart & install update", |
diff --git a/src/index.js b/src/index.js index 6a08e5e5a..a047e2bc1 100644 --- a/src/index.js +++ b/src/index.js | |||
@@ -8,6 +8,7 @@ import { isDevMode, isWindows } from './environment'; | |||
8 | import ipcApi from './electron/ipc-api'; | 8 | import ipcApi from './electron/ipc-api'; |
9 | import Tray from './lib/Tray'; | 9 | import Tray from './lib/Tray'; |
10 | import Settings from './electron/Settings'; | 10 | import Settings from './electron/Settings'; |
11 | import handleDeepLink from './electron/deepLinking'; | ||
11 | import { appId } from './package.json'; // eslint-disable-line import/no-unresolved | 12 | import { appId } from './package.json'; // eslint-disable-line import/no-unresolved |
12 | import './electron/exception'; | 13 | import './electron/exception'; |
13 | 14 | ||
@@ -26,10 +27,19 @@ if (isWindows) { | |||
26 | } | 27 | } |
27 | 28 | ||
28 | // Force single window | 29 | // Force single window |
29 | const isSecondInstance = app.makeSingleInstance(() => { | 30 | const isSecondInstance = app.makeSingleInstance((argv) => { |
30 | if (mainWindow) { | 31 | if (mainWindow) { |
31 | if (mainWindow.isMinimized()) mainWindow.restore(); | 32 | if (mainWindow.isMinimized()) mainWindow.restore(); |
32 | mainWindow.focus(); | 33 | mainWindow.focus(); |
34 | |||
35 | if (process.platform === 'win32') { | ||
36 | // Keep only command line / deep linked arguments | ||
37 | const url = argv.slice(1); | ||
38 | |||
39 | if (url) { | ||
40 | handleDeepLink(mainWindow, url.toString()); | ||
41 | } | ||
42 | } | ||
33 | } | 43 | } |
34 | }); | 44 | }); |
35 | 45 | ||
@@ -176,3 +186,15 @@ app.on('activate', () => { | |||
176 | mainWindow.show(); | 186 | mainWindow.show(); |
177 | } | 187 | } |
178 | }); | 188 | }); |
189 | |||
190 | app.on('will-finish-launching', () => { | ||
191 | // Protocol handler for osx | ||
192 | app.on('open-url', (event, url) => { | ||
193 | event.preventDefault(); | ||
194 | console.log(`open-url event: ${url}`); | ||
195 | handleDeepLink(mainWindow, url); | ||
196 | }); | ||
197 | }); | ||
198 | |||
199 | // Register App URL | ||
200 | app.setAsDefaultProtocolClient('franz'); | ||
diff --git a/src/lib/Menu.js b/src/lib/Menu.js index 6624ab75e..d01666d49 100644 --- a/src/lib/Menu.js +++ b/src/lib/Menu.js | |||
@@ -167,7 +167,7 @@ export default class FranzMenu { | |||
167 | label: 'Settings', | 167 | label: 'Settings', |
168 | accelerator: 'CmdOrCtrl+,', | 168 | accelerator: 'CmdOrCtrl+,', |
169 | click: () => { | 169 | click: () => { |
170 | this.actions.ui.openSettings({ path: '' }); | 170 | this.actions.ui.openSettings({ path: 'app' }); |
171 | }, | 171 | }, |
172 | }, | 172 | }, |
173 | { | 173 | { |
@@ -249,7 +249,7 @@ export default class FranzMenu { | |||
249 | } | 249 | } |
250 | 250 | ||
251 | @computed get serviceTpl() { | 251 | @computed get serviceTpl() { |
252 | const services = this.stores.services.enabled; | 252 | const services = this.stores.services.allDisplayed; |
253 | 253 | ||
254 | if (this.stores.user.isLoggedIn) { | 254 | if (this.stores.user.isLoggedIn) { |
255 | return services.map((service, i) => ({ | 255 | return services.map((service, i) => ({ |
diff --git a/src/models/Recipe.js b/src/models/Recipe.js index 9971df77c..1fc23ac89 100644 --- a/src/models/Recipe.js +++ b/src/models/Recipe.js | |||
@@ -1,10 +1,11 @@ | |||
1 | import emailParser from 'address-rfc2822'; | 1 | import emailParser from 'address-rfc2822'; |
2 | import semver from 'semver'; | ||
2 | 3 | ||
3 | export default class Recipe { | 4 | export default class Recipe { |
4 | id = ''; | 5 | id = ''; |
5 | name = ''; | 6 | name = ''; |
6 | description = ''; | 7 | description = ''; |
7 | version = '1.0'; | 8 | version = ''; |
8 | path = ''; | 9 | path = ''; |
9 | 10 | ||
10 | serviceURL = ''; | 11 | serviceURL = ''; |
@@ -15,6 +16,7 @@ export default class Recipe { | |||
15 | hasTeamId = false; | 16 | hasTeamId = false; |
16 | hasPredefinedUrl = false; | 17 | hasPredefinedUrl = false; |
17 | hasCustomUrl = false; | 18 | hasCustomUrl = false; |
19 | hasHostedOption = false; | ||
18 | urlInputPrefix = ''; | 20 | urlInputPrefix = ''; |
19 | urlInputSuffix = ''; | 21 | urlInputSuffix = ''; |
20 | 22 | ||
@@ -30,6 +32,10 @@ export default class Recipe { | |||
30 | throw Error(`Recipe '${data.name}' requires Id`); | 32 | throw Error(`Recipe '${data.name}' requires Id`); |
31 | } | 33 | } |
32 | 34 | ||
35 | if (!semver.valid(data.version)) { | ||
36 | throw Error(`Version ${data.version} of recipe '${data.name}' is not a valid semver version`); | ||
37 | } | ||
38 | |||
33 | this.id = data.id || this.id; | 39 | this.id = data.id || this.id; |
34 | this.name = data.name || this.name; | 40 | this.name = data.name || this.name; |
35 | this.rawAuthor = data.author || this.author; | 41 | this.rawAuthor = data.author || this.author; |
@@ -45,6 +51,7 @@ export default class Recipe { | |||
45 | this.hasTeamId = data.config.hasTeamId || this.hasTeamId; | 51 | this.hasTeamId = data.config.hasTeamId || this.hasTeamId; |
46 | this.hasPredefinedUrl = data.config.hasPredefinedUrl || this.hasPredefinedUrl; | 52 | this.hasPredefinedUrl = data.config.hasPredefinedUrl || this.hasPredefinedUrl; |
47 | this.hasCustomUrl = data.config.hasCustomUrl || this.hasCustomUrl; | 53 | this.hasCustomUrl = data.config.hasCustomUrl || this.hasCustomUrl; |
54 | this.hasHostedOption = data.config.hasHostedOption || this.hasHostedOption; | ||
48 | 55 | ||
49 | this.urlInputPrefix = data.config.urlInputPrefix || this.urlInputPrefix; | 56 | this.urlInputPrefix = data.config.urlInputPrefix || this.urlInputPrefix; |
50 | this.urlInputSuffix = data.config.urlInputSuffix || this.urlInputSuffix; | 57 | this.urlInputSuffix = data.config.urlInputSuffix || this.urlInputSuffix; |
diff --git a/src/models/Service.js b/src/models/Service.js index 958e4b11e..0b19440e7 100644 --- a/src/models/Service.js +++ b/src/models/Service.js | |||
@@ -22,6 +22,7 @@ export default class Service { | |||
22 | @observable team = ''; | 22 | @observable team = ''; |
23 | @observable customUrl = ''; | 23 | @observable customUrl = ''; |
24 | @observable isNotificationEnabled = true; | 24 | @observable isNotificationEnabled = true; |
25 | @observable isBadgeEnabled = true; | ||
25 | @observable isIndirectMessageBadgeEnabled = true; | 26 | @observable isIndirectMessageBadgeEnabled = true; |
26 | @observable customIconUrl = ''; | 27 | @observable customIconUrl = ''; |
27 | @observable hasCrashed = false; | 28 | @observable hasCrashed = false; |
@@ -52,6 +53,9 @@ export default class Service { | |||
52 | this.isNotificationEnabled = data.isNotificationEnabled !== undefined | 53 | this.isNotificationEnabled = data.isNotificationEnabled !== undefined |
53 | ? data.isNotificationEnabled : this.isNotificationEnabled; | 54 | ? data.isNotificationEnabled : this.isNotificationEnabled; |
54 | 55 | ||
56 | this.isBadgeEnabled = data.isBadgeEnabled !== undefined | ||
57 | ? data.isBadgeEnabled : this.isBadgeEnabled; | ||
58 | |||
55 | this.isIndirectMessageBadgeEnabled = data.isIndirectMessageBadgeEnabled !== undefined | 59 | this.isIndirectMessageBadgeEnabled = data.isIndirectMessageBadgeEnabled !== undefined |
56 | ? data.isIndirectMessageBadgeEnabled : this.isIndirectMessageBadgeEnabled; | 60 | ? data.isIndirectMessageBadgeEnabled : this.isIndirectMessageBadgeEnabled; |
57 | 61 | ||
diff --git a/src/models/Settings.js b/src/models/Settings.js index 3b352f9aa..ca44da258 100644 --- a/src/models/Settings.js +++ b/src/models/Settings.js | |||
@@ -1,4 +1,4 @@ | |||
1 | import { observable } from 'mobx'; | 1 | import { observable, extendObservable } from 'mobx'; |
2 | import { DEFAULT_APP_SETTINGS } from '../config'; | 2 | import { DEFAULT_APP_SETTINGS } from '../config'; |
3 | 3 | ||
4 | export default class Settings { | 4 | export default class Settings { |
@@ -8,6 +8,7 @@ export default class Settings { | |||
8 | @observable enableSystemTray = DEFAULT_APP_SETTINGS.enableSystemTray; | 8 | @observable enableSystemTray = DEFAULT_APP_SETTINGS.enableSystemTray; |
9 | @observable minimizeToSystemTray = DEFAULT_APP_SETTINGS.minimizeToSystemTray; | 9 | @observable minimizeToSystemTray = DEFAULT_APP_SETTINGS.minimizeToSystemTray; |
10 | @observable showDisabledServices = DEFAULT_APP_SETTINGS.showDisabledServices; | 10 | @observable showDisabledServices = DEFAULT_APP_SETTINGS.showDisabledServices; |
11 | @observable showMessageBadgeWhenMuted = DEFAULT_APP_SETTINGS.showMessageBadgeWhenMuted; | ||
11 | @observable enableSpellchecking = DEFAULT_APP_SETTINGS.enableSpellchecking; | 12 | @observable enableSpellchecking = DEFAULT_APP_SETTINGS.enableSpellchecking; |
12 | @observable locale = DEFAULT_APP_SETTINGS.locale; | 13 | @observable locale = DEFAULT_APP_SETTINGS.locale; |
13 | @observable beta = DEFAULT_APP_SETTINGS.beta; | 14 | @observable beta = DEFAULT_APP_SETTINGS.beta; |
@@ -16,4 +17,8 @@ export default class Settings { | |||
16 | constructor(data) { | 17 | constructor(data) { |
17 | Object.assign(this, data); | 18 | Object.assign(this, data); |
18 | } | 19 | } |
20 | |||
21 | update(data) { | ||
22 | extendObservable(this, data); | ||
23 | } | ||
19 | } | 24 | } |
diff --git a/src/stores/AppStore.js b/src/stores/AppStore.js index 0b7c60bce..5a6c12ee1 100644 --- a/src/stores/AppStore.js +++ b/src/stores/AppStore.js | |||
@@ -45,7 +45,7 @@ export default class AppStore extends Store { | |||
45 | miner = null; | 45 | miner = null; |
46 | @observable minerHashrate = 0.0; | 46 | @observable minerHashrate = 0.0; |
47 | 47 | ||
48 | @observable isSystemMuted = false; | 48 | @observable isSystemMuteOverridden = false; |
49 | 49 | ||
50 | constructor(...args) { | 50 | constructor(...args) { |
51 | super(...args); | 51 | super(...args); |
@@ -67,6 +67,7 @@ export default class AppStore extends Store { | |||
67 | this._setLocale.bind(this), | 67 | this._setLocale.bind(this), |
68 | this._handleMiner.bind(this), | 68 | this._handleMiner.bind(this), |
69 | this._handleMinerThrottle.bind(this), | 69 | this._handleMinerThrottle.bind(this), |
70 | this._muteAppHandler.bind(this), | ||
70 | ]); | 71 | ]); |
71 | } | 72 | } |
72 | 73 | ||
@@ -115,6 +116,14 @@ export default class AppStore extends Store { | |||
115 | } | 116 | } |
116 | }); | 117 | }); |
117 | 118 | ||
119 | // Handle deep linking (franz://) | ||
120 | ipcRenderer.on('navigateFromDeepLink', (event, data) => { | ||
121 | const { url } = data; | ||
122 | if (!url) return; | ||
123 | |||
124 | this.stores.router.push(data.url); | ||
125 | }); | ||
126 | |||
118 | // Check system idle time every minute | 127 | // Check system idle time every minute |
119 | setInterval(() => { | 128 | setInterval(() => { |
120 | this.idleTime = idleTimer.getIdleTime(); | 129 | this.idleTime = idleTimer.getIdleTime(); |
@@ -137,7 +146,7 @@ export default class AppStore extends Store { | |||
137 | this.actions.service.setActivePrev(); | 146 | this.actions.service.setActivePrev(); |
138 | }); | 147 | }); |
139 | 148 | ||
140 | // Global Mute | 149 | // Global Mute |
141 | key( | 150 | key( |
142 | '⌘+shift+m ctrl+shift+m', () => { | 151 | '⌘+shift+m ctrl+shift+m', () => { |
143 | this.actions.app.toggleMuteApp(); | 152 | this.actions.app.toggleMuteApp(); |
@@ -150,6 +159,8 @@ export default class AppStore extends Store { | |||
150 | 159 | ||
151 | // Actions | 160 | // Actions |
152 | @action _notify({ title, options, notificationId, serviceId = null }) { | 161 | @action _notify({ title, options, notificationId, serviceId = null }) { |
162 | if (this.stores.settings.all.isAppMuted) return; | ||
163 | |||
153 | const notification = new window.Notification(title, options); | 164 | const notification = new window.Notification(title, options); |
154 | notification.onclick = (e) => { | 165 | notification.onclick = (e) => { |
155 | if (serviceId) { | 166 | if (serviceId) { |
@@ -160,6 +171,11 @@ export default class AppStore extends Store { | |||
160 | }); | 171 | }); |
161 | 172 | ||
162 | this.actions.service.setActive({ serviceId }); | 173 | this.actions.service.setActive({ serviceId }); |
174 | |||
175 | if (!isMac) { | ||
176 | const mainWindow = remote.getCurrentWindow(); | ||
177 | mainWindow.restore(); | ||
178 | } | ||
163 | } | 179 | } |
164 | }; | 180 | }; |
165 | } | 181 | } |
@@ -217,7 +233,9 @@ export default class AppStore extends Store { | |||
217 | this.healthCheckRequest.execute(); | 233 | this.healthCheckRequest.execute(); |
218 | } | 234 | } |
219 | 235 | ||
220 | @action _muteApp({ isMuted }) { | 236 | @action _muteApp({ isMuted, overrideSystemMute = true }) { |
237 | this.isSystemMuteOverriden = overrideSystemMute; | ||
238 | |||
221 | this.actions.settings.update({ | 239 | this.actions.settings.update({ |
222 | settings: { | 240 | settings: { |
223 | isAppMuted: isMuted, | 241 | isAppMuted: isMuted, |
@@ -245,8 +263,10 @@ export default class AppStore extends Store { | |||
245 | _setLocale() { | 263 | _setLocale() { |
246 | const locale = this.stores.settings.all.locale; | 264 | const locale = this.stores.settings.all.locale; |
247 | 265 | ||
248 | if (locale && locale !== this.locale) { | 266 | if (locale && Object.prototype.hasOwnProperty.call(locales, locale) && locale !== this.locale) { |
249 | this.locale = locale; | 267 | this.locale = locale; |
268 | } else if (!locale) { | ||
269 | this.locale = this._getDefaultLocale(); | ||
250 | } | 270 | } |
251 | } | 271 | } |
252 | 272 | ||
@@ -271,6 +291,10 @@ export default class AppStore extends Store { | |||
271 | locale = defaultLocale; | 291 | locale = defaultLocale; |
272 | } | 292 | } |
273 | 293 | ||
294 | if (!locale) { | ||
295 | locale = DEFAULT_APP_SETTINGS.fallbackLocale; | ||
296 | } | ||
297 | |||
274 | return locale; | 298 | return locale; |
275 | } | 299 | } |
276 | 300 | ||
@@ -296,6 +320,14 @@ export default class AppStore extends Store { | |||
296 | } | 320 | } |
297 | } | 321 | } |
298 | 322 | ||
323 | _muteAppHandler() { | ||
324 | const showMessageBadgesEvenWhenMuted = this.stores.ui.showMessageBadgesEvenWhenMuted; | ||
325 | |||
326 | if (!showMessageBadgesEvenWhenMuted) { | ||
327 | this.actions.app.setBadge({ unreadDirectMessageCount: 0, unreadIndirectMessageCount: 0 }); | ||
328 | } | ||
329 | } | ||
330 | |||
299 | // Helpers | 331 | // Helpers |
300 | async _appStartsCounter() { | 332 | async _appStartsCounter() { |
301 | // we need to wait until the settings request is resolved | 333 | // we need to wait until the settings request is resolved |
@@ -326,6 +358,12 @@ export default class AppStore extends Store { | |||
326 | } | 358 | } |
327 | 359 | ||
328 | _systemDND() { | 360 | _systemDND() { |
329 | this.isSystemMuted = getDoNotDisturb(); | 361 | const dnd = getDoNotDisturb(); |
362 | if (dnd === this.stores.settings.all.isAppMuted || !this.isSystemMuteOverriden) { | ||
363 | this.actions.app.muteApp({ | ||
364 | isMuted: dnd, | ||
365 | overrideSystemMute: false, | ||
366 | }); | ||
367 | } | ||
330 | } | 368 | } |
331 | } | 369 | } |
diff --git a/src/stores/ServicesStore.js b/src/stores/ServicesStore.js index 22c376c06..66f37af26 100644 --- a/src/stores/ServicesStore.js +++ b/src/stores/ServicesStore.js | |||
@@ -297,7 +297,7 @@ export default class ServicesStore extends Store { | |||
297 | }); | 297 | }); |
298 | } else if (channel === 'notification') { | 298 | } else if (channel === 'notification') { |
299 | const options = args[0].options; | 299 | const options = args[0].options; |
300 | if (service.recipe.hasNotificationSound || service.isMuted) { | 300 | if (service.recipe.hasNotificationSound || service.isMuted || this.stores.settings.all.isAppMuted) { |
301 | Object.assign(options, { | 301 | Object.assign(options, { |
302 | silent: true, | 302 | silent: true, |
303 | }); | 303 | }); |
@@ -488,19 +488,26 @@ export default class ServicesStore extends Store { | |||
488 | } | 488 | } |
489 | 489 | ||
490 | _getUnreadMessageCountReaction() { | 490 | _getUnreadMessageCountReaction() { |
491 | const unreadDirectMessageCount = this.enabled | 491 | const showMessageBadgeWhenMuted = this.stores.settings.all.showMessageBadgeWhenMuted; |
492 | const showMessageBadgesEvenWhenMuted = this.stores.ui.showMessageBadgesEvenWhenMuted; | ||
493 | |||
494 | const unreadDirectMessageCount = this.allDisplayed | ||
495 | .filter(s => (showMessageBadgeWhenMuted || s.isNotificationEnabled) && showMessageBadgesEvenWhenMuted && s.isBadgeEnabled) | ||
492 | .map(s => s.unreadDirectMessageCount) | 496 | .map(s => s.unreadDirectMessageCount) |
493 | .reduce((a, b) => a + b, 0); | 497 | .reduce((a, b) => a + b, 0); |
494 | 498 | ||
495 | const unreadIndirectMessageCount = this.enabled | 499 | const unreadIndirectMessageCount = this.allDisplayed |
496 | .filter(s => s.isIndirectMessageBadgeEnabled) | 500 | .filter(s => (showMessageBadgeWhenMuted || s.isIndirectMessageBadgeEnabled) && showMessageBadgesEvenWhenMuted && s.isBadgeEnabled) |
497 | .map(s => s.unreadIndirectMessageCount) | 501 | .map(s => s.unreadIndirectMessageCount) |
498 | .reduce((a, b) => a + b, 0); | 502 | .reduce((a, b) => a + b, 0); |
499 | 503 | ||
500 | this.actions.app.setBadge({ | 504 | // We can't just block this earlier, otherwise the mobx reaction won't be aware of the vars to watch in some cases |
501 | unreadDirectMessageCount, | 505 | if (showMessageBadgesEvenWhenMuted) { |
502 | unreadIndirectMessageCount, | 506 | this.actions.app.setBadge({ |
503 | }); | 507 | unreadDirectMessageCount, |
508 | unreadIndirectMessageCount, | ||
509 | }); | ||
510 | } | ||
504 | } | 511 | } |
505 | 512 | ||
506 | _logoutReaction() { | 513 | _logoutReaction() { |
diff --git a/src/stores/SettingsStore.js b/src/stores/SettingsStore.js index 30058f41d..da99a720f 100644 --- a/src/stores/SettingsStore.js +++ b/src/stores/SettingsStore.js | |||
@@ -5,6 +5,7 @@ import Store from './lib/Store'; | |||
5 | import Request from './lib/Request'; | 5 | import Request from './lib/Request'; |
6 | import CachedRequest from './lib/CachedRequest'; | 6 | import CachedRequest from './lib/CachedRequest'; |
7 | import { gaEvent } from '../lib/analytics'; | 7 | import { gaEvent } from '../lib/analytics'; |
8 | import SettingsModel from '../models/Settings'; | ||
8 | 9 | ||
9 | export default class SettingsStore extends Store { | 10 | export default class SettingsStore extends Store { |
10 | @observable allSettingsRequest = new CachedRequest(this.api.local, 'getSettings'); | 11 | @observable allSettingsRequest = new CachedRequest(this.api.local, 'getSettings'); |
@@ -25,7 +26,7 @@ export default class SettingsStore extends Store { | |||
25 | } | 26 | } |
26 | 27 | ||
27 | @computed get all() { | 28 | @computed get all() { |
28 | return this.allSettingsRequest.result || {}; | 29 | return new SettingsModel(this.allSettingsRequest.result); |
29 | } | 30 | } |
30 | 31 | ||
31 | @action async _update({ settings }) { | 32 | @action async _update({ settings }) { |
diff --git a/src/stores/UIStore.js b/src/stores/UIStore.js index cb45b88b5..5e9cc9ba7 100644 --- a/src/stores/UIStore.js +++ b/src/stores/UIStore.js | |||
@@ -1,4 +1,4 @@ | |||
1 | import { action, observable } from 'mobx'; | 1 | import { action, observable, computed } from 'mobx'; |
2 | 2 | ||
3 | import Store from './lib/Store'; | 3 | import Store from './lib/Store'; |
4 | 4 | ||
@@ -14,6 +14,12 @@ export default class UIStore extends Store { | |||
14 | this.actions.ui.toggleServiceUpdatedInfoBar.listen(this._toggleServiceUpdatedInfoBar.bind(this)); | 14 | this.actions.ui.toggleServiceUpdatedInfoBar.listen(this._toggleServiceUpdatedInfoBar.bind(this)); |
15 | } | 15 | } |
16 | 16 | ||
17 | @computed get showMessageBadgesEvenWhenMuted() { | ||
18 | const settings = this.stores.settings.all; | ||
19 | |||
20 | return (settings.isAppMuted && settings.showMessageBadgeWhenMuted) || !settings.isAppMuted; | ||
21 | } | ||
22 | |||
17 | // Actions | 23 | // Actions |
18 | @action _openSettings({ path = '/settings' }) { | 24 | @action _openSettings({ path = '/settings' }) { |
19 | const settingsPath = path !== '/settings' ? `/settings/${path}` : path; | 25 | const settingsPath = path !== '/settings' ? `/settings/${path}` : path; |
diff --git a/src/stores/UserStore.js b/src/stores/UserStore.js index 1cb2ecac3..09000dcdb 100644 --- a/src/stores/UserStore.js +++ b/src/stores/UserStore.js | |||
@@ -26,6 +26,7 @@ export default class UserStore extends Store { | |||
26 | @observable getUserInfoRequest = new CachedRequest(this.api.user, 'getInfo'); | 26 | @observable getUserInfoRequest = new CachedRequest(this.api.user, 'getInfo'); |
27 | @observable updateUserInfoRequest = new Request(this.api.user, 'updateInfo'); | 27 | @observable updateUserInfoRequest = new Request(this.api.user, 'updateInfo'); |
28 | @observable getLegacyServicesRequest = new CachedRequest(this.api.user, 'getLegacyServices'); | 28 | @observable getLegacyServicesRequest = new CachedRequest(this.api.user, 'getLegacyServices'); |
29 | @observable deleteAccountRequest = new CachedRequest(this.api.user, 'delete'); | ||
29 | 30 | ||
30 | @observable isImportLegacyServicesExecuting = false; | 31 | @observable isImportLegacyServicesExecuting = false; |
31 | @observable isImportLegacyServicesCompleted = false; | 32 | @observable isImportLegacyServicesCompleted = false; |
@@ -57,6 +58,7 @@ export default class UserStore extends Store { | |||
57 | this.actions.user.update.listen(this._update.bind(this)); | 58 | this.actions.user.update.listen(this._update.bind(this)); |
58 | this.actions.user.resetStatus.listen(this._resetStatus.bind(this)); | 59 | this.actions.user.resetStatus.listen(this._resetStatus.bind(this)); |
59 | this.actions.user.importLegacyServices.listen(this._importLegacyServices.bind(this)); | 60 | this.actions.user.importLegacyServices.listen(this._importLegacyServices.bind(this)); |
61 | this.actions.user.delete.listen(this._delete.bind(this)); | ||
60 | 62 | ||
61 | // Reactions | 63 | // Reactions |
62 | this.registerReactions([ | 64 | this.registerReactions([ |
@@ -212,6 +214,10 @@ export default class UserStore extends Store { | |||
212 | this.isImportLegacyServicesCompleted = true; | 214 | this.isImportLegacyServicesCompleted = true; |
213 | } | 215 | } |
214 | 216 | ||
217 | @action async _delete() { | ||
218 | this.deleteAccountRequest.execute(); | ||
219 | } | ||
220 | |||
215 | // This is a mobx autorun which forces the user to login if not authenticated | 221 | // This is a mobx autorun which forces the user to login if not authenticated |
216 | _requireAuthenticatedUser = () => { | 222 | _requireAuthenticatedUser = () => { |
217 | if (this.isTokenExpired) { | 223 | if (this.isTokenExpired) { |
diff --git a/src/styles/content-tabs.scss b/src/styles/content-tabs.scss index aa3c8594b..47dfea2c4 100644 --- a/src/styles/content-tabs.scss +++ b/src/styles/content-tabs.scss | |||
@@ -12,15 +12,17 @@ | |||
12 | flex: 1; | 12 | flex: 1; |
13 | // border: 1px solid $theme-gray-lightest; | 13 | // border: 1px solid $theme-gray-lightest; |
14 | color: $theme-gray-dark; | 14 | color: $theme-gray-dark; |
15 | background: $theme-gray-lightest; | 15 | background: linear-gradient($theme-gray-lightest 80%, darken($theme-gray-lightest, 3%)); |
16 | border-bottom: 1px solid $theme-gray-lighter; | 16 | border-right: 1px solid $theme-gray-lighter; |
17 | box-shadow: inset 0px -3px 10px rgba(black, 0.05); | 17 | transition: background $theme-transition-time; |
18 | transition: all $theme-transition-time; | 18 | |
19 | &:last-of-type { | ||
20 | border-right: 0; | ||
21 | } | ||
19 | 22 | ||
20 | &.is-active { | 23 | &.is-active { |
21 | background: $theme-brand-primary; | 24 | background: $theme-brand-primary; |
22 | color: #FFF; | 25 | color: #FFF; |
23 | border-bottom: 1px solid $theme-brand-primary; | ||
24 | box-shadow: none; | 26 | box-shadow: none; |
25 | } | 27 | } |
26 | } | 28 | } |
diff --git a/src/styles/input.scss b/src/styles/input.scss index 814dce5f8..7042f56e8 100644 --- a/src/styles/input.scss +++ b/src/styles/input.scss | |||
@@ -47,6 +47,10 @@ | |||
47 | padding: 8px; | 47 | padding: 8px; |
48 | // font-size: 18px; | 48 | // font-size: 18px; |
49 | color: $theme-gray; | 49 | color: $theme-gray; |
50 | |||
51 | &::placeholder { | ||
52 | color: lighten($theme-gray-light, 10%); | ||
53 | } | ||
50 | } | 54 | } |
51 | 55 | ||
52 | .franz-form__input-prefix, | 56 | .franz-form__input-prefix, |
diff --git a/src/styles/services.scss b/src/styles/services.scss index 282c15121..9f6cfc772 100644 --- a/src/styles/services.scss +++ b/src/styles/services.scss | |||
@@ -24,6 +24,7 @@ | |||
24 | display: inline-flex; | 24 | display: inline-flex; |
25 | width: 0px; | 25 | width: 0px; |
26 | height: 0px; | 26 | height: 0px; |
27 | background: $theme-gray-lighter; | ||
27 | } | 28 | } |
28 | 29 | ||
29 | &.is-active { | 30 | &.is-active { |
diff --git a/src/styles/settings.scss b/src/styles/settings.scss index 6e93094b4..b29ed5468 100644 --- a/src/styles/settings.scss +++ b/src/styles/settings.scss | |||
@@ -151,8 +151,23 @@ | |||
151 | } | 151 | } |
152 | } | 152 | } |
153 | 153 | ||
154 | .settings__options { | 154 | &__options { |
155 | margin-top: 30px; | 155 | margin-top: 20px; |
156 | } | ||
157 | |||
158 | &__settings-group { | ||
159 | margin-top: 10px; | ||
160 | |||
161 | h3 { | ||
162 | font-weight: bold; | ||
163 | margin: 25px 0 15px; | ||
164 | color: $theme-gray-light; | ||
165 | letter-spacing: -0.1px; | ||
166 | |||
167 | &:first-of-type { | ||
168 | margin-top: 0; | ||
169 | } | ||
170 | } | ||
156 | } | 171 | } |
157 | 172 | ||
158 | .settings__message { | 173 | .settings__message { |
@@ -173,10 +188,6 @@ | |||
173 | margin: -10px 0 20px 55px;; | 188 | margin: -10px 0 20px 55px;; |
174 | font-size: 12px; | 189 | font-size: 12px; |
175 | color: $theme-gray-light; | 190 | color: $theme-gray-light; |
176 | |||
177 | &:last-of-type { | ||
178 | margin-bottom: 30px; | ||
179 | } | ||
180 | } | 191 | } |
181 | 192 | ||
182 | .settings__controls { | 193 | .settings__controls { |
@@ -281,6 +292,10 @@ | |||
281 | margin-left: auto; | 292 | margin-left: auto; |
282 | } | 293 | } |
283 | 294 | ||
295 | .franz-form__button { | ||
296 | white-space: nowrap; | ||
297 | } | ||
298 | |||
284 | div { | 299 | div { |
285 | height: auto; | 300 | height: auto; |
286 | } | 301 | } |
diff --git a/src/styles/tabs.scss b/src/styles/tabs.scss index 3ffc53558..ac48aabd6 100644 --- a/src/styles/tabs.scss +++ b/src/styles/tabs.scss | |||
@@ -78,6 +78,26 @@ | |||
78 | } | 78 | } |
79 | } | 79 | } |
80 | 80 | ||
81 | .tab-item__info-badge { | ||
82 | width: 17px; | ||
83 | height: 17px; | ||
84 | background: $theme-gray-light; | ||
85 | color: $theme-gray-lighter; | ||
86 | border-radius: 20px; | ||
87 | padding: 0px 5px; | ||
88 | font-size: 11px; | ||
89 | position: absolute; | ||
90 | right: 8px; | ||
91 | bottom: 8px; | ||
92 | display: flex; | ||
93 | justify-content: center; | ||
94 | align-items: center; | ||
95 | |||
96 | &.is-indirect { | ||
97 | padding-top: 0px; | ||
98 | } | ||
99 | } | ||
100 | |||
81 | &.is-reordering { | 101 | &.is-reordering { |
82 | z-index: 99999; | 102 | z-index: 99999; |
83 | } | 103 | } |
diff --git a/src/styles/welcome.scss b/src/styles/welcome.scss index 5365921fb..cfdcc80ad 100644 --- a/src/styles/welcome.scss +++ b/src/styles/welcome.scss | |||
@@ -58,17 +58,32 @@ | |||
58 | } | 58 | } |
59 | 59 | ||
60 | &__featured-services { | 60 | &__featured-services { |
61 | margin-top: 150px; | ||
62 | text-align: center; | 61 | text-align: center; |
63 | margin-top: 80px; | 62 | width: 480px; |
63 | margin: 80px auto 0 auto; | ||
64 | display: flex; | ||
65 | align-items: center; | ||
66 | flex-wrap: wrap; | ||
67 | background: #FFF; | ||
68 | border-radius: 6px; | ||
69 | padding: 20px 20px 5px; | ||
64 | } | 70 | } |
65 | 71 | ||
66 | &__featured-service { | 72 | &__featured-service { |
67 | width: 35px; | 73 | width: 35px; |
68 | margin-right: 30px; | 74 | height: 35px; |
75 | margin: 0 10px 15px; | ||
76 | filter: grayscale(1) | ||
77 | opacity(0.5); | ||
78 | transition: 0.5s filter, 0.5s opacity; | ||
79 | |||
80 | &:hover { | ||
81 | filter: grayscale(0); | ||
82 | opacity: (1); | ||
83 | } | ||
69 | 84 | ||
70 | &:last-of-type { | 85 | img { |
71 | margin-right: 0; | 86 | width: 35px; |
72 | } | 87 | } |
73 | } | 88 | } |
74 | } | 89 | } |
diff --git a/src/webview/ime.js b/src/webview/ime.js deleted file mode 100644 index 43df6267c..000000000 --- a/src/webview/ime.js +++ /dev/null | |||
@@ -1,10 +0,0 @@ | |||
1 | const { ipcRenderer } = require('electron'); | ||
2 | const { claimDocumentFocus } = require('../helpers/webview-ime-focus-helpers'); | ||
3 | |||
4 | ipcRenderer.on('claim-document-focus', claimDocumentFocus); | ||
5 | |||
6 | window.addEventListener('DOMContentLoaded', () => { | ||
7 | if (document.querySelector('[autofocus]')) { | ||
8 | ipcRenderer.sendToHost('autofocus'); | ||
9 | } | ||
10 | }); | ||
diff --git a/src/webview/lib/RecipeWebview.js b/src/webview/lib/RecipeWebview.js index 048beea69..be29142af 100644 --- a/src/webview/lib/RecipeWebview.js +++ b/src/webview/lib/RecipeWebview.js | |||
@@ -40,8 +40,8 @@ class RecipeWebview { | |||
40 | && this.countCache.indirect === indirect) return; | 40 | && this.countCache.indirect === indirect) return; |
41 | 41 | ||
42 | const count = { | 42 | const count = { |
43 | direct, | 43 | direct: direct > 0 ? direct : 0, |
44 | indirect, | 44 | indirect: indirect > 0 ? indirect : 0, |
45 | }; | 45 | }; |
46 | 46 | ||
47 | ipcRenderer.sendToHost('messages', count); | 47 | ipcRenderer.sendToHost('messages', count); |
diff --git a/src/webview/plugin.js b/src/webview/plugin.js index c877132b1..9903ee07a 100644 --- a/src/webview/plugin.js +++ b/src/webview/plugin.js | |||
@@ -1,13 +1,12 @@ | |||
1 | import { ipcRenderer } from 'electron'; | 1 | import { ipcRenderer } from 'electron'; |
2 | import { ContextMenuListener, ContextMenuBuilder } from 'electron-spellchecker'; | ||
2 | import path from 'path'; | 3 | import path from 'path'; |
3 | 4 | ||
5 | import { isDevMode } from '../environment'; | ||
4 | import RecipeWebview from './lib/RecipeWebview'; | 6 | import RecipeWebview from './lib/RecipeWebview'; |
5 | 7 | ||
6 | import Spellchecker from './spellchecker.js'; | 8 | import Spellchecker from './spellchecker.js'; |
7 | import './notifications.js'; | 9 | import './notifications.js'; |
8 | import './ime.js'; | ||
9 | |||
10 | const spellchecker = new Spellchecker(); | ||
11 | 10 | ||
12 | ipcRenderer.on('initializeRecipe', (e, data) => { | 11 | ipcRenderer.on('initializeRecipe', (e, data) => { |
13 | const modulePath = path.join(data.recipe.path, 'webview.js'); | 12 | const modulePath = path.join(data.recipe.path, 'webview.js'); |
@@ -21,20 +20,22 @@ ipcRenderer.on('initializeRecipe', (e, data) => { | |||
21 | } | 20 | } |
22 | }); | 21 | }); |
23 | 22 | ||
23 | const spellchecker = new Spellchecker(); | ||
24 | spellchecker.initialize(); | ||
25 | |||
26 | const contextMenuBuilder = new ContextMenuBuilder(spellchecker.handler, null, isDevMode); | ||
27 | |||
28 | new ContextMenuListener((info) => { // eslint-disable-line | ||
29 | contextMenuBuilder.showPopupMenu(info); | ||
30 | }); | ||
31 | |||
24 | ipcRenderer.on('settings-update', (e, data) => { | 32 | ipcRenderer.on('settings-update', (e, data) => { |
25 | if (data.enableSpellchecking) { | 33 | console.log('settings-update', data); |
26 | if (!spellchecker.isEnabled) { | 34 | spellchecker.toggleSpellchecker(data.enableSpellchecking); |
27 | spellchecker.enable(); | ||
28 | |||
29 | // TODO: this does not work yet, needs more testing | ||
30 | // if (data.spellcheckingLanguage !== 'auto') { | ||
31 | // console.log('set spellchecking language to', data.spellcheckingLanguage); | ||
32 | // spellchecker.switchLanguage(data.spellcheckingLanguage); | ||
33 | // } | ||
34 | } | ||
35 | } | ||
36 | }); | 35 | }); |
37 | 36 | ||
37 | // initSpellche | ||
38 | |||
38 | document.addEventListener('DOMContentLoaded', () => { | 39 | document.addEventListener('DOMContentLoaded', () => { |
39 | ipcRenderer.sendToHost('hello'); | 40 | ipcRenderer.sendToHost('hello'); |
40 | }, false); | 41 | }, false); |
diff --git a/src/webview/spellchecker.js b/src/webview/spellchecker.js index 5beb77e03..a504a4039 100644 --- a/src/webview/spellchecker.js +++ b/src/webview/spellchecker.js | |||
@@ -1,30 +1,63 @@ | |||
1 | import { SpellCheckHandler, ContextMenuListener, ContextMenuBuilder } from 'electron-spellchecker'; | 1 | import { SpellCheckHandler } from 'electron-spellchecker'; |
2 | 2 | ||
3 | import { isMac } from '../environment'; | 3 | import { isMac } from '../environment'; |
4 | 4 | ||
5 | export default class Spellchecker { | 5 | export default class Spellchecker { |
6 | isEnabled = false; | 6 | isInitialized = false; |
7 | spellchecker = null; | 7 | handler = null; |
8 | initRetries = 0; | ||
9 | DOMCheckInterval = null; | ||
10 | |||
11 | get inputs() { | ||
12 | return document.querySelectorAll('input[type="text"], [contenteditable="true"], textarea'); | ||
13 | } | ||
14 | |||
15 | initialize() { | ||
16 | this.handler = new SpellCheckHandler(); | ||
8 | 17 | ||
9 | enable() { | ||
10 | this.spellchecker = new SpellCheckHandler(); | ||
11 | if (!isMac) { | 18 | if (!isMac) { |
12 | this.spellchecker.attachToInput(); | 19 | this.attach(); |
13 | this.spellchecker.switchLanguage(navigator.language); | 20 | } else { |
21 | this.isInitialized = true; | ||
22 | } | ||
23 | } | ||
24 | |||
25 | attach() { | ||
26 | let initFailed = false; | ||
27 | |||
28 | if (this.initRetries > 3) { | ||
29 | console.error('Could not initialize spellchecker'); | ||
30 | return; | ||
14 | } | 31 | } |
15 | 32 | ||
16 | const contextMenuBuilder = new ContextMenuBuilder(this.spellchecker); | 33 | try { |
34 | this.handler.attachToInput(); | ||
35 | this.handler.switchLanguage(navigator.language); | ||
36 | } catch (err) { | ||
37 | initFailed = true; | ||
38 | this.initRetries = +1; | ||
39 | setTimeout(() => { this.attach(); console.warn('Spellchecker init failed, trying again in 5s'); }, 5000); | ||
40 | } | ||
17 | 41 | ||
18 | new ContextMenuListener((info) => { // eslint-disable-line | 42 | if (!initFailed) { |
19 | contextMenuBuilder.showPopupMenu(info); | 43 | this.isInitialized = true; |
44 | } | ||
45 | } | ||
46 | |||
47 | toggleSpellchecker(enable = false) { | ||
48 | this.inputs.forEach((input) => { | ||
49 | input.setAttribute('spellcheck', enable); | ||
20 | }); | 50 | }); |
51 | |||
52 | this.intervalHandler(enable); | ||
21 | } | 53 | } |
22 | 54 | ||
23 | // TODO: this does not work yet, needs more testing | 55 | intervalHandler(enable) { |
24 | // switchLanguage(language) { | 56 | clearInterval(this.DOMCheckInterval); |
25 | // if (language !== 'auto') { | 57 | |
26 | // this.spellchecker.switchLanguage(language); | 58 | if (enable) { |
27 | // } | 59 | this.DOMCheckInterval = setInterval(() => this.toggleSpellchecker(enable), 30000); |
28 | // } | 60 | } |
61 | } | ||
29 | } | 62 | } |
30 | 63 | ||
@@ -2974,6 +2974,29 @@ gulp-babel@^6.1.2: | |||
2974 | through2 "^2.0.0" | 2974 | through2 "^2.0.0" |
2975 | vinyl-sourcemaps-apply "^0.2.0" | 2975 | vinyl-sourcemaps-apply "^0.2.0" |
2976 | 2976 | ||
2977 | gulp-cli@1.2.2: | ||
2978 | version "1.2.2" | ||
2979 | resolved "https://registry.yarnpkg.com/gulp-cli/-/gulp-cli-1.2.2.tgz#7392def6316c6e7939a4f296f3f540151ae3a275" | ||
2980 | dependencies: | ||
2981 | archy "^1.0.0" | ||
2982 | chalk "^1.1.0" | ||
2983 | fancy-log "^1.1.0" | ||
2984 | gulplog "^1.0.0" | ||
2985 | interpret "^1.0.0" | ||
2986 | liftoff "^2.1.0" | ||
2987 | lodash.isfunction "^3.0.8" | ||
2988 | lodash.isplainobject "^4.0.4" | ||
2989 | lodash.isstring "^4.0.1" | ||
2990 | lodash.sortby "^4.5.0" | ||
2991 | matchdep "^1.0.0" | ||
2992 | mute-stdout "^1.0.0" | ||
2993 | pretty-hrtime "^1.0.0" | ||
2994 | semver-greatest-satisfied-range "^1.0.0" | ||
2995 | tildify "^1.0.0" | ||
2996 | v8flags "^2.0.9" | ||
2997 | wreck "^6.3.0" | ||
2998 | yargs "^3.28.0" | ||
2999 | |||
2977 | gulp-cli@^1.0.0: | 3000 | gulp-cli@^1.0.0: |
2978 | version "1.4.0" | 3001 | version "1.4.0" |
2979 | resolved "https://registry.yarnpkg.com/gulp-cli/-/gulp-cli-1.4.0.tgz#6f5bbe2cd0bdb4849d12cf9e1246a5861f8b4f88" | 3002 | resolved "https://registry.yarnpkg.com/gulp-cli/-/gulp-cli-1.4.0.tgz#6f5bbe2cd0bdb4849d12cf9e1246a5861f8b4f88" |
@@ -3768,7 +3791,7 @@ levn@^0.3.0, levn@~0.3.0: | |||
3768 | prelude-ls "~1.1.2" | 3791 | prelude-ls "~1.1.2" |
3769 | type-check "~0.3.2" | 3792 | type-check "~0.3.2" |
3770 | 3793 | ||
3771 | liftoff@^2.3.0: | 3794 | liftoff@^2.1.0, liftoff@^2.3.0: |
3772 | version "2.3.0" | 3795 | version "2.3.0" |
3773 | resolved "https://registry.yarnpkg.com/liftoff/-/liftoff-2.3.0.tgz#a98f2ff67183d8ba7cfaca10548bd7ff0550b385" | 3796 | resolved "https://registry.yarnpkg.com/liftoff/-/liftoff-2.3.0.tgz#a98f2ff67183d8ba7cfaca10548bd7ff0550b385" |
3774 | dependencies: | 3797 | dependencies: |