diff options
30 files changed, 269 insertions, 63 deletions
diff --git a/electron-builder.yml b/electron-builder.yml index 03e59e462..96bd63cc2 100644 --- a/electron-builder.yml +++ b/electron-builder.yml | |||
@@ -34,3 +34,7 @@ linux: | |||
34 | nsis: | 34 | nsis: |
35 | perMachine: false | 35 | perMachine: false |
36 | 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 8a5eee7b2..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": { |
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/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/services/content/ServiceWebview.js b/src/components/services/content/ServiceWebview.js index faa356d3d..842fde76d 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/settings/account/AccountDashboard.js b/src/components/settings/account/AccountDashboard.js index 75dbdef49..89fa07800 100644 --- a/src/components/settings/account/AccountDashboard.js +++ b/src/components/settings/account/AccountDashboard.js | |||
@@ -28,6 +28,10 @@ const messages = defineMessages({ | |||
28 | id: 'settings.account.headlineInvoices', | 28 | id: 'settings.account.headlineInvoices', |
29 | defaultMessage: '!!Invoices', | 29 | defaultMessage: '!!Invoices', |
30 | }, | 30 | }, |
31 | headlineDangerZone: { | ||
32 | id: 'settings.account.headlineDangerZone', | ||
33 | defaultMessage: '!!Danger Zone', | ||
34 | }, | ||
31 | manageSubscriptionButtonLabel: { | 35 | manageSubscriptionButtonLabel: { |
32 | id: 'settings.account.manageSubscription.label', | 36 | id: 'settings.account.manageSubscription.label', |
33 | defaultMessage: '!!!Manage your subscription', | 37 | defaultMessage: '!!!Manage your subscription', |
@@ -72,6 +76,18 @@ const messages = defineMessages({ | |||
72 | id: 'settings.account.mining.cancel', | 76 | id: 'settings.account.mining.cancel', |
73 | defaultMessage: '!!!Cancel mining', | 77 | defaultMessage: '!!!Cancel mining', |
74 | }, | 78 | }, |
79 | deleteAccount: { | ||
80 | id: 'settings.account.deleteAccount', | ||
81 | defaultMessage: '!!!Delete account', | ||
82 | }, | ||
83 | deleteInfo: { | ||
84 | id: 'settings.account.deleteInfo', | ||
85 | defaultMessage: '!!!If you don\'t need your Franz account any longer, you can delete your account and all related data here.', | ||
86 | }, | ||
87 | deleteEmailSent: { | ||
88 | id: 'settings.account.deleteEmailSent', | ||
89 | defaultMessage: '!!!You have received an email with a link to confirm your account deletion. Your account and data cannot be restored!', | ||
90 | }, | ||
75 | }); | 91 | }); |
76 | 92 | ||
77 | @observer | 93 | @observer |
@@ -90,6 +106,9 @@ export default class AccountDashboard extends Component { | |||
90 | openExternalUrl: PropTypes.func.isRequired, | 106 | openExternalUrl: PropTypes.func.isRequired, |
91 | onCloseSubscriptionWindow: PropTypes.func.isRequired, | 107 | onCloseSubscriptionWindow: PropTypes.func.isRequired, |
92 | stopMiner: PropTypes.func.isRequired, | 108 | stopMiner: PropTypes.func.isRequired, |
109 | deleteAccount: PropTypes.func.isRequired, | ||
110 | isLoadingDeleteAccount: PropTypes.bool.isRequired, | ||
111 | isDeleteAccountSuccessful: PropTypes.bool.isRequired, | ||
93 | }; | 112 | }; |
94 | 113 | ||
95 | static contextTypes = { | 114 | static contextTypes = { |
@@ -111,6 +130,9 @@ export default class AccountDashboard extends Component { | |||
111 | retryUserInfoRequest, | 130 | retryUserInfoRequest, |
112 | onCloseSubscriptionWindow, | 131 | onCloseSubscriptionWindow, |
113 | stopMiner, | 132 | stopMiner, |
133 | deleteAccount, | ||
134 | isLoadingDeleteAccount, | ||
135 | isDeleteAccountSuccessful, | ||
114 | } = this.props; | 136 | } = this.props; |
115 | const { intl } = this.context; | 137 | const { intl } = this.context; |
116 | 138 | ||
@@ -201,7 +223,7 @@ export default class AccountDashboard extends Component { | |||
201 | /> | 223 | /> |
202 | </div> | 224 | </div> |
203 | </div> | 225 | </div> |
204 | <div className="account__box account__box--last"> | 226 | <div className="account__box"> |
205 | <h2>{intl.formatMessage(messages.headlineInvoices)}</h2> | 227 | <h2>{intl.formatMessage(messages.headlineInvoices)}</h2> |
206 | <table className="invoices"> | 228 | <table className="invoices"> |
207 | <tbody> | 229 | <tbody> |
@@ -230,7 +252,7 @@ export default class AccountDashboard extends Component { | |||
230 | 252 | ||
231 | {user.isMiner && ( | 253 | {user.isMiner && ( |
232 | <div className="account franz-form"> | 254 | <div className="account franz-form"> |
233 | <div className="account__box"> | 255 | <div className="account__box account__box--last"> |
234 | <h2>{intl.formatMessage(messages.headlineSubscription)}</h2> | 256 | <h2>{intl.formatMessage(messages.headlineSubscription)}</h2> |
235 | <div className="account__subscription"> | 257 | <div className="account__subscription"> |
236 | <div> | 258 | <div> |
@@ -267,7 +289,7 @@ export default class AccountDashboard extends Component { | |||
267 | <Loader /> | 289 | <Loader /> |
268 | ) : ( | 290 | ) : ( |
269 | <div className="account franz-form"> | 291 | <div className="account franz-form"> |
270 | <div className="account__box account__box--last"> | 292 | <div className="account__box"> |
271 | <h2>{intl.formatMessage(messages.headlineUpgrade)}</h2> | 293 | <h2>{intl.formatMessage(messages.headlineUpgrade)}</h2> |
272 | <SubscriptionForm | 294 | <SubscriptionForm |
273 | onCloseWindow={onCloseSubscriptionWindow} | 295 | onCloseWindow={onCloseSubscriptionWindow} |
@@ -276,8 +298,29 @@ export default class AccountDashboard extends Component { | |||
276 | </div> | 298 | </div> |
277 | ) | 299 | ) |
278 | )} | 300 | )} |
301 | |||
302 | <div className="account franz-form"> | ||
303 | <div className="account__box"> | ||
304 | <h2>{intl.formatMessage(messages.headlineDangerZone)}</h2> | ||
305 | {!isDeleteAccountSuccessful && ( | ||
306 | <div className="account__subscription"> | ||
307 | <p>{intl.formatMessage(messages.deleteInfo)}</p> | ||
308 | <Button | ||
309 | label={intl.formatMessage(messages.deleteAccount)} | ||
310 | buttonType="danger" | ||
311 | onClick={() => deleteAccount()} | ||
312 | loaded={!isLoadingDeleteAccount} | ||
313 | /> | ||
314 | </div> | ||
315 | )} | ||
316 | {isDeleteAccountSuccessful && ( | ||
317 | <p>{intl.formatMessage(messages.deleteEmailSent)}</p> | ||
318 | )} | ||
319 | </div> | ||
320 | </div> | ||
279 | </div> | 321 | </div> |
280 | )} | 322 | )} |
323 | |||
281 | </div> | 324 | </div> |
282 | <ReactTooltip place="right" type="dark" effect="solid" /> | 325 | <ReactTooltip place="right" type="dark" effect="solid" /> |
283 | </div> | 326 | </div> |
diff --git a/src/components/settings/settings/EditSettingsForm.js b/src/components/settings/settings/EditSettingsForm.js index 878e46d6d..ff398aa33 100644 --- a/src/components/settings/settings/EditSettingsForm.js +++ b/src/components/settings/settings/EditSettingsForm.js | |||
@@ -64,10 +64,6 @@ const messages = defineMessages({ | |||
64 | id: 'settings.app.currentVersion', | 64 | id: 'settings.app.currentVersion', |
65 | defaultMessage: '!!!Current version:', | 65 | defaultMessage: '!!!Current version:', |
66 | }, | 66 | }, |
67 | restartRequired: { | ||
68 | id: 'settings.app.restartRequired', | ||
69 | defaultMessage: '!!!Changes require restart', | ||
70 | }, | ||
71 | }); | 67 | }); |
72 | 68 | ||
73 | @observer | 69 | @observer |
@@ -158,7 +154,6 @@ export default class EditSettingsForm extends Component { | |||
158 | {/* Advanced */} | 154 | {/* Advanced */} |
159 | <h2 id="advanced">{intl.formatMessage(messages.headlineAdvanced)}</h2> | 155 | <h2 id="advanced">{intl.formatMessage(messages.headlineAdvanced)}</h2> |
160 | <Toggle field={form.$('enableSpellchecking')} /> | 156 | <Toggle field={form.$('enableSpellchecking')} /> |
161 | <p className="settings__help">{intl.formatMessage(messages.restartRequired)}</p> | ||
162 | {/* <Select field={form.$('spellcheckingLanguage')} /> */} | 157 | {/* <Select field={form.$('spellcheckingLanguage')} /> */} |
163 | 158 | ||
164 | {/* Updates */} | 159 | {/* Updates */} |
diff --git a/src/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 e6d8958e6..e66594c59 100644 --- a/src/config.js +++ b/src/config.js | |||
@@ -14,7 +14,8 @@ export const DEFAULT_APP_SETTINGS = { | |||
14 | showMessageBadgeWhenMuted: true, | 14 | showMessageBadgeWhenMuted: true, |
15 | enableSpellchecking: true, | 15 | enableSpellchecking: true, |
16 | // spellcheckingLanguage: 'auto', | 16 | // spellcheckingLanguage: 'auto', |
17 | locale: 'en-US', | 17 | locale: '', |
18 | fallbackLocale: 'en-US', | ||
18 | beta: false, | 19 | beta: false, |
19 | isAppMuted: false, | 20 | isAppMuted: false, |
20 | }; | 21 | }; |
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/electron/deepLinking.js b/src/electron/deepLinking.js new file mode 100644 index 000000000..16e68b914 --- /dev/null +++ b/src/electron/deepLinking.js | |||
@@ -0,0 +1,5 @@ | |||
1 | export default function handleDeepLink(window, rawUrl) { | ||
2 | const url = rawUrl.replace('franz://', ''); | ||
3 | |||
4 | window.webContents.send('navigateFromDeepLink', { url }); | ||
5 | } | ||
diff --git a/src/helpers/async-helpers.js b/src/helpers/async-helpers.js new file mode 100644 index 000000000..2ef01ee09 --- /dev/null +++ b/src/helpers/async-helpers.js | |||
@@ -0,0 +1,5 @@ | |||
1 | /* eslint-disable import/prefer-default-export */ | ||
2 | |||
3 | export function sleep(ms = 0) { | ||
4 | return new Promise(r => setTimeout(r, ms)); | ||
5 | } | ||
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/en-US.json b/src/i18n/locales/en-US.json index 380871668..48b408e59 100644 --- a/src/i18n/locales/en-US.json +++ b/src/i18n/locales/en-US.json | |||
@@ -70,6 +70,7 @@ | |||
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", |
diff --git a/src/index.js b/src/index.js index 6a08e5e5a..76b1d8352 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 | ||
@@ -137,6 +147,8 @@ const createWindow = () => { | |||
137 | 147 | ||
138 | mainWindow.on('show', () => { | 148 | mainWindow.on('show', () => { |
139 | mainWindow.setSkipTaskbar(false); | 149 | mainWindow.setSkipTaskbar(false); |
150 | |||
151 | handleDeepLink(mainWindow, 'franz://settings/services/add/msteams'); | ||
140 | }); | 152 | }); |
141 | 153 | ||
142 | app.mainWindow = mainWindow; | 154 | app.mainWindow = mainWindow; |
@@ -176,3 +188,15 @@ app.on('activate', () => { | |||
176 | mainWindow.show(); | 188 | mainWindow.show(); |
177 | } | 189 | } |
178 | }); | 190 | }); |
191 | |||
192 | app.on('will-finish-launching', () => { | ||
193 | // Protocol handler for osx | ||
194 | app.on('open-url', (event, url) => { | ||
195 | event.preventDefault(); | ||
196 | console.log(`open-url event: ${url}`); | ||
197 | handleDeepLink(mainWindow, url); | ||
198 | }); | ||
199 | }); | ||
200 | |||
201 | // Register App URL | ||
202 | app.setAsDefaultProtocolClient('franz'); | ||
diff --git a/src/lib/Menu.js b/src/lib/Menu.js index 6624ab75e..d9c30466b 100644 --- a/src/lib/Menu.js +++ b/src/lib/Menu.js | |||
@@ -249,7 +249,7 @@ export default class FranzMenu { | |||
249 | } | 249 | } |
250 | 250 | ||
251 | @computed get serviceTpl() { | 251 | @computed get serviceTpl() { |
252 | const services = this.stores.services.enabled; | 252 | const services = this.stores.services.allDisplayed; |
253 | 253 | ||
254 | if (this.stores.user.isLoggedIn) { | 254 | if (this.stores.user.isLoggedIn) { |
255 | return services.map((service, i) => ({ | 255 | return services.map((service, i) => ({ |
diff --git a/src/models/Settings.js b/src/models/Settings.js index 35bfe0d05..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 { |
@@ -17,4 +17,8 @@ export default class Settings { | |||
17 | constructor(data) { | 17 | constructor(data) { |
18 | Object.assign(this, data); | 18 | Object.assign(this, data); |
19 | } | 19 | } |
20 | |||
21 | update(data) { | ||
22 | extendObservable(this, data); | ||
23 | } | ||
20 | } | 24 | } |
diff --git a/src/stores/AppStore.js b/src/stores/AppStore.js index 6125a7cff..5a6c12ee1 100644 --- a/src/stores/AppStore.js +++ b/src/stores/AppStore.js | |||
@@ -116,6 +116,14 @@ export default class AppStore extends Store { | |||
116 | } | 116 | } |
117 | }); | 117 | }); |
118 | 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 | |||
119 | // Check system idle time every minute | 127 | // Check system idle time every minute |
120 | setInterval(() => { | 128 | setInterval(() => { |
121 | this.idleTime = idleTimer.getIdleTime(); | 129 | this.idleTime = idleTimer.getIdleTime(); |
@@ -138,7 +146,7 @@ export default class AppStore extends Store { | |||
138 | this.actions.service.setActivePrev(); | 146 | this.actions.service.setActivePrev(); |
139 | }); | 147 | }); |
140 | 148 | ||
141 | // Global Mute | 149 | // Global Mute |
142 | key( | 150 | key( |
143 | '⌘+shift+m ctrl+shift+m', () => { | 151 | '⌘+shift+m ctrl+shift+m', () => { |
144 | this.actions.app.toggleMuteApp(); | 152 | this.actions.app.toggleMuteApp(); |
@@ -163,6 +171,11 @@ export default class AppStore extends Store { | |||
163 | }); | 171 | }); |
164 | 172 | ||
165 | this.actions.service.setActive({ serviceId }); | 173 | this.actions.service.setActive({ serviceId }); |
174 | |||
175 | if (!isMac) { | ||
176 | const mainWindow = remote.getCurrentWindow(); | ||
177 | mainWindow.restore(); | ||
178 | } | ||
166 | } | 179 | } |
167 | }; | 180 | }; |
168 | } | 181 | } |
@@ -250,8 +263,10 @@ export default class AppStore extends Store { | |||
250 | _setLocale() { | 263 | _setLocale() { |
251 | const locale = this.stores.settings.all.locale; | 264 | const locale = this.stores.settings.all.locale; |
252 | 265 | ||
253 | if (locale && locale !== this.locale) { | 266 | if (locale && Object.prototype.hasOwnProperty.call(locales, locale) && locale !== this.locale) { |
254 | this.locale = locale; | 267 | this.locale = locale; |
268 | } else if (!locale) { | ||
269 | this.locale = this._getDefaultLocale(); | ||
255 | } | 270 | } |
256 | } | 271 | } |
257 | 272 | ||
@@ -276,6 +291,10 @@ export default class AppStore extends Store { | |||
276 | locale = defaultLocale; | 291 | locale = defaultLocale; |
277 | } | 292 | } |
278 | 293 | ||
294 | if (!locale) { | ||
295 | locale = DEFAULT_APP_SETTINGS.fallbackLocale; | ||
296 | } | ||
297 | |||
279 | return locale; | 298 | return locale; |
280 | } | 299 | } |
281 | 300 | ||
diff --git a/src/stores/SettingsStore.js b/src/stores/SettingsStore.js index 33473f16d..da99a720f 100644 --- a/src/stores/SettingsStore.js +++ b/src/stores/SettingsStore.js | |||
@@ -26,7 +26,7 @@ export default class SettingsStore extends Store { | |||
26 | } | 26 | } |
27 | 27 | ||
28 | @computed get all() { | 28 | @computed get all() { |
29 | return this.allSettingsRequest.result || new SettingsModel(); | 29 | return new SettingsModel(this.allSettingsRequest.result); |
30 | } | 30 | } |
31 | 31 | ||
32 | @action async _update({ settings }) { | 32 | @action async _update({ settings }) { |
diff --git a/src/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/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..73cef0813 100644 --- a/src/styles/settings.scss +++ b/src/styles/settings.scss | |||
@@ -281,6 +281,10 @@ | |||
281 | margin-left: auto; | 281 | margin-left: auto; |
282 | } | 282 | } |
283 | 283 | ||
284 | .franz-form__button { | ||
285 | white-space: nowrap; | ||
286 | } | ||
287 | |||
284 | div { | 288 | div { |
285 | height: auto; | 289 | height: auto; |
286 | } | 290 | } |
diff --git a/src/styles/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/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..cf38169d3 100644 --- a/src/webview/plugin.js +++ b/src/webview/plugin.js | |||
@@ -1,14 +1,14 @@ | |||
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'; | 10 | import './ime.js'; |
9 | 11 | ||
10 | const spellchecker = new Spellchecker(); | ||
11 | |||
12 | ipcRenderer.on('initializeRecipe', (e, data) => { | 12 | ipcRenderer.on('initializeRecipe', (e, data) => { |
13 | const modulePath = path.join(data.recipe.path, 'webview.js'); | 13 | const modulePath = path.join(data.recipe.path, 'webview.js'); |
14 | // Delete module from cache | 14 | // Delete module from cache |
@@ -21,20 +21,22 @@ ipcRenderer.on('initializeRecipe', (e, data) => { | |||
21 | } | 21 | } |
22 | }); | 22 | }); |
23 | 23 | ||
24 | const spellchecker = new Spellchecker(); | ||
25 | spellchecker.initialize(); | ||
26 | |||
27 | const contextMenuBuilder = new ContextMenuBuilder(spellchecker.handler, null, isDevMode); | ||
28 | |||
29 | new ContextMenuListener((info) => { // eslint-disable-line | ||
30 | contextMenuBuilder.showPopupMenu(info); | ||
31 | }); | ||
32 | |||
24 | ipcRenderer.on('settings-update', (e, data) => { | 33 | ipcRenderer.on('settings-update', (e, data) => { |
25 | if (data.enableSpellchecking) { | 34 | console.log('settings-update', data); |
26 | if (!spellchecker.isEnabled) { | 35 | 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 | }); | 36 | }); |
37 | 37 | ||
38 | // initSpellche | ||
39 | |||
38 | document.addEventListener('DOMContentLoaded', () => { | 40 | document.addEventListener('DOMContentLoaded', () => { |
39 | ipcRenderer.sendToHost('hello'); | 41 | ipcRenderer.sendToHost('hello'); |
40 | }, false); | 42 | }, 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 | ||