diff options
author | Stefan Malzner <stefan@adlk.io> | 2018-01-08 10:34:00 +0100 |
---|---|---|
committer | Stefan Malzner <stefan@adlk.io> | 2018-01-08 10:34:00 +0100 |
commit | 91540e15eb2484a097587a38440b05897bb87638 (patch) | |
tree | dba412401da044ba203fcacb56018bfd7ed193eb /src | |
parent | Sort languages by name (diff) | |
parent | Add color to service icons (diff) | |
download | ferdium-app-91540e15eb2484a097587a38440b05897bb87638.tar.gz ferdium-app-91540e15eb2484a097587a38440b05897bb87638.tar.zst ferdium-app-91540e15eb2484a097587a38440b05897bb87638.zip |
Merge branch 'develop' into i18n
# Conflicts:
# src/i18n/languages.js
Diffstat (limited to 'src')
49 files changed, 944 insertions, 232 deletions
diff --git a/src/actions/app.js b/src/actions/app.js index e4f648fc9..e6f7f22ba 100644 --- a/src/actions/app.js +++ b/src/actions/app.js | |||
@@ -25,4 +25,5 @@ export default { | |||
25 | overrideSystemMute: PropTypes.bool, | 25 | overrideSystemMute: PropTypes.bool, |
26 | }, | 26 | }, |
27 | toggleMuteApp: {}, | 27 | toggleMuteApp: {}, |
28 | clearAllCache: {}, | ||
28 | }; | 29 | }; |
diff --git a/src/actions/service.js b/src/actions/service.js index e3100e986..5d483b12a 100644 --- a/src/actions/service.js +++ b/src/actions/service.js | |||
@@ -25,6 +25,9 @@ export default { | |||
25 | serviceId: PropTypes.string.isRequired, | 25 | serviceId: PropTypes.string.isRequired, |
26 | redirect: PropTypes.string, | 26 | redirect: PropTypes.string, |
27 | }, | 27 | }, |
28 | clearCache: { | ||
29 | serviceId: PropTypes.string.isRequired, | ||
30 | }, | ||
28 | setUnreadMessageCount: { | 31 | setUnreadMessageCount: { |
29 | serviceId: PropTypes.string.isRequired, | 32 | serviceId: PropTypes.string.isRequired, |
30 | count: PropTypes.object.isRequired, | 33 | count: PropTypes.object.isRequired, |
diff --git a/src/api/LocalApi.js b/src/api/LocalApi.js index 6f2b049d6..3f84f8a0b 100644 --- a/src/api/LocalApi.js +++ b/src/api/LocalApi.js | |||
@@ -15,4 +15,12 @@ export default class LocalApi { | |||
15 | removeKey(key) { | 15 | removeKey(key) { |
16 | return this.local.removeKey(key); | 16 | return this.local.removeKey(key); |
17 | } | 17 | } |
18 | |||
19 | getAppCacheSize() { | ||
20 | return this.local.getAppCacheSize(); | ||
21 | } | ||
22 | |||
23 | clearAppCache() { | ||
24 | return this.local.clearAppCache(); | ||
25 | } | ||
18 | } | 26 | } |
diff --git a/src/api/ServicesApi.js b/src/api/ServicesApi.js index 3cb40ba0d..36ed9482f 100644 --- a/src/api/ServicesApi.js +++ b/src/api/ServicesApi.js | |||
@@ -1,5 +1,6 @@ | |||
1 | export default class ServicesApi { | 1 | export default class ServicesApi { |
2 | constructor(server) { | 2 | constructor(server, local) { |
3 | this.local = local; | ||
3 | this.server = server; | 4 | this.server = server; |
4 | } | 5 | } |
5 | 6 | ||
@@ -30,4 +31,8 @@ export default class ServicesApi { | |||
30 | reorder(data) { | 31 | reorder(data) { |
31 | return this.server.reorderService(data); | 32 | return this.server.reorderService(data); |
32 | } | 33 | } |
34 | |||
35 | clearCache(serviceId) { | ||
36 | return this.local.clearCache(serviceId); | ||
37 | } | ||
33 | } | 38 | } |
diff --git a/src/api/server/LocalApi.js b/src/api/server/LocalApi.js index eba236f16..e95d750ac 100644 --- a/src/api/server/LocalApi.js +++ b/src/api/server/LocalApi.js | |||
@@ -1,4 +1,9 @@ | |||
1 | import SettingsModel from '../../models/Settings'; | 1 | import { remote } from 'electron'; |
2 | import du from 'du'; | ||
3 | |||
4 | import { getServicePartitionsDirectory } from '../../helpers/service-helpers.js'; | ||
5 | |||
6 | const { session } = remote; | ||
2 | 7 | ||
3 | export default class LocalApi { | 8 | export default class LocalApi { |
4 | // App | 9 | // App |
@@ -15,7 +20,7 @@ export default class LocalApi { | |||
15 | async getAppSettings() { | 20 | async getAppSettings() { |
16 | const settingsString = localStorage.getItem('app'); | 21 | const settingsString = localStorage.getItem('app'); |
17 | try { | 22 | try { |
18 | const settings = new SettingsModel(JSON.parse(settingsString) || {}); | 23 | const settings = JSON.parse(settingsString) || {}; |
19 | console.debug('LocalApi::getAppSettings resolves', settings); | 24 | console.debug('LocalApi::getAppSettings resolves', settings); |
20 | 25 | ||
21 | return settings; | 26 | return settings; |
@@ -32,4 +37,31 @@ export default class LocalApi { | |||
32 | localStorage.setItem('app', JSON.stringify(settings)); | 37 | localStorage.setItem('app', JSON.stringify(settings)); |
33 | } | 38 | } |
34 | } | 39 | } |
40 | |||
41 | // Services | ||
42 | async getAppCacheSize() { | ||
43 | const partitionsDir = getServicePartitionsDirectory(); | ||
44 | return new Promise((resolve, reject) => { | ||
45 | du(partitionsDir, (err, size) => { | ||
46 | if (err) reject(err); | ||
47 | |||
48 | console.debug('LocalApi::getAppCacheSize resolves', size); | ||
49 | resolve(size); | ||
50 | }); | ||
51 | }); | ||
52 | } | ||
53 | |||
54 | async clearCache(serviceId) { | ||
55 | const s = session.fromPartition(`persist:service-${serviceId}`); | ||
56 | |||
57 | console.debug('LocalApi::clearCache resolves', serviceId); | ||
58 | return new Promise(resolve => s.clearCache(resolve)); | ||
59 | } | ||
60 | |||
61 | async clearAppCache() { | ||
62 | const s = session.defaultSession; | ||
63 | |||
64 | console.debug('LocalApi::clearCache clearAppCache'); | ||
65 | return new Promise(resolve => s.clearCache(resolve)); | ||
66 | } | ||
35 | } | 67 | } |
diff --git a/src/api/server/ServerApi.js b/src/api/server/ServerApi.js index 644bf20cd..a684ff98b 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 { |
@@ -20,6 +22,10 @@ import { | |||
20 | loadRecipeConfig, | 22 | loadRecipeConfig, |
21 | } from '../../helpers/recipe-helpers'; | 23 | } from '../../helpers/recipe-helpers'; |
22 | 24 | ||
25 | import { | ||
26 | removeServicePartitionDirectory, | ||
27 | } from '../../helpers/service-helpers.js'; | ||
28 | |||
23 | module.paths.unshift( | 29 | module.paths.unshift( |
24 | getDevRecipeDirectory(), | 30 | getDevRecipeDirectory(), |
25 | getRecipeDirectory(), | 31 | getRecipeDirectory(), |
@@ -165,27 +171,65 @@ export default class ServerApi { | |||
165 | throw request; | 171 | throw request; |
166 | } | 172 | } |
167 | const serviceData = await request.json(); | 173 | const serviceData = await request.json(); |
174 | |||
175 | if (data.iconFile) { | ||
176 | const iconUrl = await this.uploadServiceIcon(serviceData.data.id, data.iconFile); | ||
177 | |||
178 | serviceData.data.iconUrl = iconUrl; | ||
179 | } | ||
180 | |||
168 | const service = Object.assign(serviceData, { data: await this._prepareServiceModel(serviceData.data) }); | 181 | const service = Object.assign(serviceData, { data: await this._prepareServiceModel(serviceData.data) }); |
169 | 182 | ||
170 | console.debug('ServerApi::createService resolves', service); | 183 | console.debug('ServerApi::createService resolves', service); |
171 | return service; | 184 | return service; |
172 | } | 185 | } |
173 | 186 | ||
174 | async updateService(recipeId, data) { | 187 | async updateService(serviceId, rawData) { |
175 | const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/service/${recipeId}`, this._prepareAuthRequest({ | 188 | const data = rawData; |
189 | |||
190 | if (data.iconFile) { | ||
191 | await this.uploadServiceIcon(serviceId, data.iconFile); | ||
192 | } | ||
193 | |||
194 | const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/service/${serviceId}`, this._prepareAuthRequest({ | ||
176 | method: 'PUT', | 195 | method: 'PUT', |
177 | body: JSON.stringify(data), | 196 | body: JSON.stringify(data), |
178 | })); | 197 | })); |
198 | |||
179 | if (!request.ok) { | 199 | if (!request.ok) { |
180 | throw request; | 200 | throw request; |
181 | } | 201 | } |
202 | |||
182 | const serviceData = await request.json(); | 203 | const serviceData = await request.json(); |
204 | |||
183 | const service = Object.assign(serviceData, { data: await this._prepareServiceModel(serviceData.data) }); | 205 | const service = Object.assign(serviceData, { data: await this._prepareServiceModel(serviceData.data) }); |
184 | 206 | ||
185 | console.debug('ServerApi::updateService resolves', service); | 207 | console.debug('ServerApi::updateService resolves', service); |
186 | return service; | 208 | return service; |
187 | } | 209 | } |
188 | 210 | ||
211 | async uploadServiceIcon(serviceId, icon) { | ||
212 | const formData = new FormData(); | ||
213 | formData.append('icon', icon); | ||
214 | |||
215 | const requestData = this._prepareAuthRequest({ | ||
216 | method: 'PUT', | ||
217 | body: formData, | ||
218 | }); | ||
219 | |||
220 | delete requestData.headers['Content-Type']; | ||
221 | |||
222 | const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/service/${serviceId}`, requestData); | ||
223 | |||
224 | if (!request.ok) { | ||
225 | throw request; | ||
226 | } | ||
227 | |||
228 | const serviceData = await request.json(); | ||
229 | |||
230 | return serviceData.data.iconUrl; | ||
231 | } | ||
232 | |||
189 | async reorderService(data) { | 233 | async reorderService(data) { |
190 | const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/service/reorder`, this._prepareAuthRequest({ | 234 | const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/service/reorder`, this._prepareAuthRequest({ |
191 | method: 'PUT', | 235 | method: 'PUT', |
@@ -208,6 +252,8 @@ export default class ServerApi { | |||
208 | } | 252 | } |
209 | const data = await request.json(); | 253 | const data = await request.json(); |
210 | 254 | ||
255 | removeServicePartitionDirectory(id, true); | ||
256 | |||
211 | console.debug('ServerApi::deleteService resolves', data); | 257 | console.debug('ServerApi::deleteService resolves', data); |
212 | return data; | 258 | return data; |
213 | } | 259 | } |
@@ -303,18 +349,25 @@ export default class ServerApi { | |||
303 | 349 | ||
304 | fs.ensureDirSync(recipeTempDirectory); | 350 | fs.ensureDirSync(recipeTempDirectory); |
305 | const res = await fetch(packageUrl); | 351 | const res = await fetch(packageUrl); |
352 | console.debug('Recipe downloaded', recipeId); | ||
306 | const buffer = await res.buffer(); | 353 | const buffer = await res.buffer(); |
307 | fs.writeFileSync(archivePath, buffer); | 354 | fs.writeFileSync(archivePath, buffer); |
308 | 355 | ||
309 | tar.x({ | 356 | await sleep(10); |
357 | |||
358 | await tar.x({ | ||
310 | file: archivePath, | 359 | file: archivePath, |
311 | cwd: recipeTempDirectory, | 360 | cwd: recipeTempDirectory, |
312 | sync: true, | 361 | preservePaths: true, |
362 | unlink: true, | ||
363 | preserveOwner: false, | ||
364 | onwarn: x => console.log('warn', recipeId, x), | ||
313 | }); | 365 | }); |
314 | 366 | ||
367 | await sleep(10); | ||
368 | |||
315 | const { id } = fs.readJsonSync(path.join(recipeTempDirectory, 'package.json')); | 369 | const { id } = fs.readJsonSync(path.join(recipeTempDirectory, 'package.json')); |
316 | const recipeDirectory = path.join(recipesDirectory, id); | 370 | const recipeDirectory = path.join(recipesDirectory, id); |
317 | |||
318 | fs.copySync(recipeTempDirectory, recipeDirectory); | 371 | fs.copySync(recipeTempDirectory, recipeDirectory); |
319 | fs.remove(recipeTempDirectory); | 372 | fs.remove(recipeTempDirectory); |
320 | fs.remove(path.join(recipesDirectory, recipeId, 'recipe.tar.gz')); | 373 | fs.remove(path.join(recipesDirectory, recipeId, 'recipe.tar.gz')); |
diff --git a/src/app.js b/src/app.js index a0b88611c..8e62776d2 100644 --- a/src/app.js +++ b/src/app.js | |||
@@ -105,3 +105,5 @@ window.addEventListener('load', () => { | |||
105 | // Prevent drag and drop into window from redirecting | 105 | // Prevent drag and drop into window from redirecting |
106 | window.addEventListener('dragover', event => event.preventDefault()); | 106 | window.addEventListener('dragover', event => event.preventDefault()); |
107 | window.addEventListener('drop', event => event.preventDefault()); | 107 | window.addEventListener('drop', event => event.preventDefault()); |
108 | window.addEventListener('dragover', event => event.stopPropagation()); | ||
109 | window.addEventListener('drop', event => event.stopPropagation()); | ||
diff --git a/src/components/services/content/ServiceWebview.js b/src/components/services/content/ServiceWebview.js index faa356d3d..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 | }); |
@@ -105,7 +106,6 @@ export default class ServiceWebview extends Component { | |||
105 | onUpdateTargetUrl={this.updateTargetUrl} | 106 | onUpdateTargetUrl={this.updateTargetUrl} |
106 | useragent={service.userAgent} | 107 | useragent={service.userAgent} |
107 | muted={isAppMuted || service.isMuted} | 108 | muted={isAppMuted || service.isMuted} |
108 | disablewebsecurity | ||
109 | allowpopups | 109 | allowpopups |
110 | /> | 110 | /> |
111 | )} | 111 | )} |
diff --git a/src/components/services/tabs/TabItem.js b/src/components/services/tabs/TabItem.js index 8403d9462..7aed8fda7 100644 --- a/src/components/services/tabs/TabItem.js +++ b/src/components/services/tabs/TabItem.js | |||
@@ -126,7 +126,7 @@ class TabItem extends Component { | |||
126 | const menu = Menu.buildFromTemplate(menuTemplate); | 126 | const menu = Menu.buildFromTemplate(menuTemplate); |
127 | 127 | ||
128 | let notificationBadge = null; | 128 | let notificationBadge = null; |
129 | if ((showMessageBadgeWhenMutedSetting || service.isNotificationEnabled) && showMessageBadgesEvenWhenMuted) { | 129 | if ((showMessageBadgeWhenMutedSetting || service.isNotificationEnabled) && showMessageBadgesEvenWhenMuted && service.isBadgeEnabled) { |
130 | notificationBadge = ( | 130 | notificationBadge = ( |
131 | <span> | 131 | <span> |
132 | {service.unreadDirectMessageCount > 0 && ( | 132 | {service.unreadDirectMessageCount > 0 && ( |
diff --git a/src/components/settings/recipes/RecipesDashboard.js b/src/components/settings/recipes/RecipesDashboard.js index b6ade5da4..4610c69a5 100644 --- a/src/components/settings/recipes/RecipesDashboard.js +++ b/src/components/settings/recipes/RecipesDashboard.js | |||
@@ -16,6 +16,10 @@ const messages = defineMessages({ | |||
16 | id: 'settings.recipes.headline', | 16 | id: 'settings.recipes.headline', |
17 | defaultMessage: '!!!Available Services', | 17 | defaultMessage: '!!!Available Services', |
18 | }, | 18 | }, |
19 | searchService: { | ||
20 | id: 'settings.searchService', | ||
21 | defaultMessage: '!!!Search service', | ||
22 | }, | ||
19 | mostPopularRecipes: { | 23 | mostPopularRecipes: { |
20 | id: 'settings.recipes.mostPopular', | 24 | id: 'settings.recipes.mostPopular', |
21 | defaultMessage: '!!!Most popular', | 25 | defaultMessage: '!!!Most popular', |
@@ -81,13 +85,7 @@ export default class RecipesDashboard extends Component { | |||
81 | return ( | 85 | return ( |
82 | <div className="settings__main"> | 86 | <div className="settings__main"> |
83 | <div className="settings__header"> | 87 | <div className="settings__header"> |
84 | <SearchInput | 88 | <h1>{intl.formatMessage(messages.headline)}</h1> |
85 | className="settings__search-header" | ||
86 | defaultValue={intl.formatMessage(messages.headline)} | ||
87 | onChange={e => searchRecipes(e)} | ||
88 | onReset={() => resetSearch()} | ||
89 | throttle | ||
90 | /> | ||
91 | </div> | 89 | </div> |
92 | <div className="settings__body recipes"> | 90 | <div className="settings__body recipes"> |
93 | {serviceStatus.length > 0 && serviceStatus.includes('created') && ( | 91 | {serviceStatus.length > 0 && serviceStatus.includes('created') && ( |
@@ -101,7 +99,13 @@ export default class RecipesDashboard extends Component { | |||
101 | </Infobox> | 99 | </Infobox> |
102 | </Appear> | 100 | </Appear> |
103 | )} | 101 | )} |
104 | {/* {!searchNeedle && ( */} | 102 | <SearchInput |
103 | placeholder={intl.formatMessage(messages.searchService)} | ||
104 | onChange={e => searchRecipes(e)} | ||
105 | onReset={() => resetSearch()} | ||
106 | autoFocus | ||
107 | throttle | ||
108 | /> | ||
105 | <div className="recipes__navigation"> | 109 | <div className="recipes__navigation"> |
106 | <Link | 110 | <Link |
107 | to="/settings/recipes" | 111 | to="/settings/recipes" |
diff --git a/src/components/settings/services/EditServiceForm.js b/src/components/settings/services/EditServiceForm.js index 36cefe87c..f6f2df2f3 100644 --- a/src/components/settings/services/EditServiceForm.js +++ b/src/components/settings/services/EditServiceForm.js | |||
@@ -13,6 +13,7 @@ import Tabs, { TabItem } from '../../ui/Tabs'; | |||
13 | import Input from '../../ui/Input'; | 13 | import Input from '../../ui/Input'; |
14 | import Toggle from '../../ui/Toggle'; | 14 | import Toggle from '../../ui/Toggle'; |
15 | import Button from '../../ui/Button'; | 15 | import Button from '../../ui/Button'; |
16 | import ImageUpload from '../../ui/ImageUpload'; | ||
16 | 17 | ||
17 | const messages = defineMessages({ | 18 | const messages = defineMessages({ |
18 | saveService: { | 19 | saveService: { |
@@ -47,6 +48,10 @@ const messages = defineMessages({ | |||
47 | id: 'settings.service.form.tabOnPremise', | 48 | id: 'settings.service.form.tabOnPremise', |
48 | defaultMessage: '!!!Self hosted ⭐️', | 49 | defaultMessage: '!!!Self hosted ⭐️', |
49 | }, | 50 | }, |
51 | useHostedService: { | ||
52 | id: 'settings.service.form.useHostedService', | ||
53 | defaultMessage: '!!!Use the hosted {name} service.', | ||
54 | }, | ||
50 | customUrlValidationError: { | 55 | customUrlValidationError: { |
51 | id: 'settings.service.form.customUrlValidationError', | 56 | id: 'settings.service.form.customUrlValidationError', |
52 | defaultMessage: '!!!Could not validate custom {name} server.', | 57 | defaultMessage: '!!!Could not validate custom {name} server.', |
@@ -67,6 +72,26 @@ const messages = defineMessages({ | |||
67 | id: 'settings.service.form.isMutedInfo', | 72 | id: 'settings.service.form.isMutedInfo', |
68 | defaultMessage: '!!!When disabled, all notification sounds and audio playback are muted', | 73 | defaultMessage: '!!!When disabled, all notification sounds and audio playback are muted', |
69 | }, | 74 | }, |
75 | headlineNotifications: { | ||
76 | id: 'settings.service.form.headlineNotifications', | ||
77 | defaultMessage: '!!!Notifications', | ||
78 | }, | ||
79 | headlineBadges: { | ||
80 | id: 'settings.service.form.headlineBadges', | ||
81 | defaultMessage: '!!!Unread message badges', | ||
82 | }, | ||
83 | headlineGeneral: { | ||
84 | id: 'settings.service.form.headlineGeneral', | ||
85 | defaultMessage: '!!!General', | ||
86 | }, | ||
87 | iconDelete: { | ||
88 | id: 'settings.service.form.iconDelete', | ||
89 | defaultMessage: '!!!Delete', | ||
90 | }, | ||
91 | iconUpload: { | ||
92 | id: 'settings.service.form.iconUpload', | ||
93 | defaultMessage: '!!!Drop your image, or click here', | ||
94 | }, | ||
70 | }); | 95 | }); |
71 | 96 | ||
72 | @observer | 97 | @observer |
@@ -108,9 +133,13 @@ export default class EditServiceForm extends Component { | |||
108 | this.props.form.submit({ | 133 | this.props.form.submit({ |
109 | onSuccess: async (form) => { | 134 | onSuccess: async (form) => { |
110 | const values = form.values(); | 135 | const values = form.values(); |
111 | |||
112 | let isValid = true; | 136 | let isValid = true; |
113 | 137 | ||
138 | const files = form.$('customIcon').files; | ||
139 | if (files) { | ||
140 | values.iconFile = files[0]; | ||
141 | } | ||
142 | |||
114 | if (recipe.validateUrl && values.customUrl) { | 143 | if (recipe.validateUrl && values.customUrl) { |
115 | this.setState({ isValidatingCustomUrl: true }); | 144 | this.setState({ isValidatingCustomUrl: true }); |
116 | try { | 145 | try { |
@@ -166,6 +195,13 @@ export default class EditServiceForm extends Component { | |||
166 | /> | 195 | /> |
167 | ); | 196 | ); |
168 | 197 | ||
198 | let activeTabIndex = 0; | ||
199 | if (recipe.hasHostedOption && service.team) { | ||
200 | activeTabIndex = 1; | ||
201 | } else if (recipe.hasHostedOption && service.customUrl) { | ||
202 | activeTabIndex = 2; | ||
203 | } | ||
204 | |||
169 | return ( | 205 | return ( |
170 | <div className="settings__main"> | 206 | <div className="settings__main"> |
171 | <div className="settings__header"> | 207 | <div className="settings__header"> |
@@ -195,14 +231,25 @@ export default class EditServiceForm extends Component { | |||
195 | </div> | 231 | </div> |
196 | <div className="settings__body"> | 232 | <div className="settings__body"> |
197 | <form onSubmit={e => this.submit(e)} id="form"> | 233 | <form onSubmit={e => this.submit(e)} id="form"> |
198 | <Input field={form.$('name')} focus /> | 234 | <div className="service-name"> |
235 | <Input field={form.$('name')} focus /> | ||
236 | </div> | ||
199 | {(recipe.hasTeamId || recipe.hasCustomUrl) && ( | 237 | {(recipe.hasTeamId || recipe.hasCustomUrl) && ( |
200 | <Tabs | 238 | <Tabs |
201 | active={service.customUrl ? 1 : 0} | 239 | active={activeTabIndex} |
202 | > | 240 | > |
241 | {recipe.hasHostedOption && ( | ||
242 | <TabItem title={recipe.name}> | ||
243 | {intl.formatMessage(messages.useHostedService, { name: recipe.name })} | ||
244 | </TabItem> | ||
245 | )} | ||
203 | {recipe.hasTeamId && ( | 246 | {recipe.hasTeamId && ( |
204 | <TabItem title={intl.formatMessage(messages.tabHosted)}> | 247 | <TabItem title={intl.formatMessage(messages.tabHosted)}> |
205 | <Input field={form.$('team')} suffix={recipe.urlInputSuffix} /> | 248 | <Input |
249 | field={form.$('team')} | ||
250 | prefix={recipe.urlInputPrefix} | ||
251 | suffix={recipe.urlInputSuffix} | ||
252 | /> | ||
206 | </TabItem> | 253 | </TabItem> |
207 | )} | 254 | )} |
208 | {recipe.hasCustomUrl && ( | 255 | {recipe.hasCustomUrl && ( |
@@ -230,21 +277,42 @@ export default class EditServiceForm extends Component { | |||
230 | )} | 277 | )} |
231 | </Tabs> | 278 | </Tabs> |
232 | )} | 279 | )} |
233 | <div className="settings__options"> | 280 | <div className="service-flex-grid"> |
234 | <Toggle field={form.$('isNotificationEnabled')} /> | 281 | <div className="settings__options"> |
235 | {recipe.hasIndirectMessages && ( | 282 | <div className="settings__settings-group"> |
236 | <div> | 283 | <h3>{intl.formatMessage(messages.headlineNotifications)}</h3> |
237 | <Toggle field={form.$('isIndirectMessageBadgeEnabled')} /> | 284 | <Toggle field={form.$('isNotificationEnabled')} /> |
285 | <Toggle field={form.$('isMuted')} /> | ||
238 | <p className="settings__help"> | 286 | <p className="settings__help"> |
239 | {intl.formatMessage(messages.indirectMessageInfo)} | 287 | {intl.formatMessage(messages.isMutedInfo)} |
240 | </p> | 288 | </p> |
241 | </div> | 289 | </div> |
242 | )} | 290 | |
243 | <Toggle field={form.$('isMuted')} /> | 291 | <div className="settings__settings-group"> |
244 | <p className="settings__help"> | 292 | <h3>{intl.formatMessage(messages.headlineBadges)}</h3> |
245 | {intl.formatMessage(messages.isMutedInfo)} | 293 | <Toggle field={form.$('isBadgeEnabled')} /> |
246 | </p> | 294 | {recipe.hasIndirectMessages && form.$('isBadgeEnabled').value && ( |
247 | <Toggle field={form.$('isEnabled')} /> | 295 | <div> |
296 | <Toggle field={form.$('isIndirectMessageBadgeEnabled')} /> | ||
297 | <p className="settings__help"> | ||
298 | {intl.formatMessage(messages.indirectMessageInfo)} | ||
299 | </p> | ||
300 | </div> | ||
301 | )} | ||
302 | </div> | ||
303 | |||
304 | <div className="settings__settings-group"> | ||
305 | <h3>{intl.formatMessage(messages.headlineGeneral)}</h3> | ||
306 | <Toggle field={form.$('isEnabled')} /> | ||
307 | </div> | ||
308 | </div> | ||
309 | <div className="service-icon"> | ||
310 | <ImageUpload | ||
311 | field={form.$('customIcon')} | ||
312 | textDelete={intl.formatMessage(messages.iconDelete)} | ||
313 | textUpload={intl.formatMessage(messages.iconUpload)} | ||
314 | /> | ||
315 | </div> | ||
248 | </div> | 316 | </div> |
249 | {recipe.message && ( | 317 | {recipe.message && ( |
250 | <p className="settings__message"> | 318 | <p className="settings__message"> |
diff --git a/src/components/settings/services/ServicesDashboard.js b/src/components/settings/services/ServicesDashboard.js index 5f146b5f3..20e451f01 100644 --- a/src/components/settings/services/ServicesDashboard.js +++ b/src/components/settings/services/ServicesDashboard.js | |||
@@ -15,10 +15,18 @@ const messages = defineMessages({ | |||
15 | id: 'settings.services.headline', | 15 | id: 'settings.services.headline', |
16 | defaultMessage: '!!!Your services', | 16 | defaultMessage: '!!!Your services', |
17 | }, | 17 | }, |
18 | searchService: { | ||
19 | id: 'settings.searchService', | ||
20 | defaultMessage: '!!!Search service', | ||
21 | }, | ||
18 | noServicesAdded: { | 22 | noServicesAdded: { |
19 | id: 'settings.services.noServicesAdded', | 23 | id: 'settings.services.noServicesAdded', |
20 | defaultMessage: '!!!You haven\'t added any services yet.', | 24 | defaultMessage: '!!!You haven\'t added any services yet.', |
21 | }, | 25 | }, |
26 | noServiceFound: { | ||
27 | id: 'settings.recipes.nothingFound', | ||
28 | defaultMessage: '!!!Sorry, but no service matched your search term.', | ||
29 | }, | ||
22 | discoverServices: { | 30 | discoverServices: { |
23 | id: 'settings.services.discoverServices', | 31 | id: 'settings.services.discoverServices', |
24 | defaultMessage: '!!!Discover services', | 32 | defaultMessage: '!!!Discover services', |
@@ -53,7 +61,13 @@ export default class ServicesDashboard extends Component { | |||
53 | servicesRequestFailed: PropTypes.bool.isRequired, | 61 | servicesRequestFailed: PropTypes.bool.isRequired, |
54 | retryServicesRequest: PropTypes.func.isRequired, | 62 | retryServicesRequest: PropTypes.func.isRequired, |
55 | status: MobxPropTypes.arrayOrObservableArray.isRequired, | 63 | status: MobxPropTypes.arrayOrObservableArray.isRequired, |
64 | searchNeedle: PropTypes.string, | ||
56 | }; | 65 | }; |
66 | |||
67 | static defaultProps = { | ||
68 | searchNeedle: '', | ||
69 | } | ||
70 | |||
57 | static contextTypes = { | 71 | static contextTypes = { |
58 | intl: intlShape, | 72 | intl: intlShape, |
59 | }; | 73 | }; |
@@ -69,20 +83,24 @@ export default class ServicesDashboard extends Component { | |||
69 | servicesRequestFailed, | 83 | servicesRequestFailed, |
70 | retryServicesRequest, | 84 | retryServicesRequest, |
71 | status, | 85 | status, |
86 | searchNeedle, | ||
72 | } = this.props; | 87 | } = this.props; |
73 | const { intl } = this.context; | 88 | const { intl } = this.context; |
74 | 89 | ||
75 | return ( | 90 | return ( |
76 | <div className="settings__main"> | 91 | <div className="settings__main"> |
77 | <div className="settings__header"> | 92 | <div className="settings__header"> |
78 | <SearchInput | 93 | <h1>{intl.formatMessage(messages.headline)}</h1> |
79 | className="settings__search-header" | ||
80 | defaultValue={intl.formatMessage(messages.headline)} | ||
81 | onChange={needle => filterServices({ needle })} | ||
82 | onReset={() => resetFilter()} | ||
83 | /> | ||
84 | </div> | 94 | </div> |
85 | <div className="settings__body"> | 95 | <div className="settings__body"> |
96 | {!isLoading && ( | ||
97 | <SearchInput | ||
98 | placeholder={intl.formatMessage(messages.searchService)} | ||
99 | onChange={needle => filterServices({ needle })} | ||
100 | onReset={() => resetFilter()} | ||
101 | autoFocus | ||
102 | /> | ||
103 | )} | ||
86 | {!isLoading && servicesRequestFailed && ( | 104 | {!isLoading && servicesRequestFailed && ( |
87 | <div> | 105 | <div> |
88 | <Infobox | 106 | <Infobox |
@@ -121,7 +139,7 @@ export default class ServicesDashboard extends Component { | |||
121 | </Appear> | 139 | </Appear> |
122 | )} | 140 | )} |
123 | 141 | ||
124 | {!isLoading && services.length === 0 && ( | 142 | {!isLoading && services.length === 0 && !searchNeedle && ( |
125 | <div className="align-middle settings__empty-state"> | 143 | <div className="align-middle settings__empty-state"> |
126 | <p className="settings__empty-text"> | 144 | <p className="settings__empty-text"> |
127 | <span className="emoji"> | 145 | <span className="emoji"> |
@@ -132,6 +150,16 @@ export default class ServicesDashboard extends Component { | |||
132 | <Link to="/settings/recipes" className="button">{intl.formatMessage(messages.discoverServices)}</Link> | 150 | <Link to="/settings/recipes" className="button">{intl.formatMessage(messages.discoverServices)}</Link> |
133 | </div> | 151 | </div> |
134 | )} | 152 | )} |
153 | {!isLoading && services.length === 0 && searchNeedle && ( | ||
154 | <div className="align-middle settings__empty-state"> | ||
155 | <p className="settings__empty-text"> | ||
156 | <span className="emoji"> | ||
157 | <img src="./assets/images/emoji/dontknow.png" alt="" /> | ||
158 | </span> | ||
159 | {intl.formatMessage(messages.noServiceFound)} | ||
160 | </p> | ||
161 | </div> | ||
162 | )} | ||
135 | {isLoading ? ( | 163 | {isLoading ? ( |
136 | <Loader /> | 164 | <Loader /> |
137 | ) : ( | 165 | ) : ( |
diff --git a/src/components/settings/settings/EditSettingsForm.js b/src/components/settings/settings/EditSettingsForm.js index 878e46d6d..72aa5a8af 100644 --- a/src/components/settings/settings/EditSettingsForm.js +++ b/src/components/settings/settings/EditSettingsForm.js | |||
@@ -40,6 +40,18 @@ const messages = defineMessages({ | |||
40 | id: 'settings.app.translationHelp', | 40 | id: 'settings.app.translationHelp', |
41 | defaultMessage: '!!!Help us to translate Franz into your language.', | 41 | defaultMessage: '!!!Help us to translate Franz into your language.', |
42 | }, | 42 | }, |
43 | subheadlineCache: { | ||
44 | id: 'settings.app.subheadlineCache', | ||
45 | defaultMessage: '!!!Cache', | ||
46 | }, | ||
47 | cacheInfo: { | ||
48 | id: 'settings.app.cacheInfo', | ||
49 | defaultMessage: '!!!Franz cache is currently using {size} of disk space.', | ||
50 | }, | ||
51 | buttonClearAllCache: { | ||
52 | id: 'settings.app.buttonClearAllCache', | ||
53 | defaultMessage: '!!!Clear cache', | ||
54 | }, | ||
43 | buttonSearchForUpdate: { | 55 | buttonSearchForUpdate: { |
44 | id: 'settings.app.buttonSearchForUpdate', | 56 | id: 'settings.app.buttonSearchForUpdate', |
45 | defaultMessage: '!!!Check for updates', | 57 | defaultMessage: '!!!Check for updates', |
@@ -64,10 +76,6 @@ const messages = defineMessages({ | |||
64 | id: 'settings.app.currentVersion', | 76 | id: 'settings.app.currentVersion', |
65 | defaultMessage: '!!!Current version:', | 77 | defaultMessage: '!!!Current version:', |
66 | }, | 78 | }, |
67 | restartRequired: { | ||
68 | id: 'settings.app.restartRequired', | ||
69 | defaultMessage: '!!!Changes require restart', | ||
70 | }, | ||
71 | }); | 79 | }); |
72 | 80 | ||
73 | @observer | 81 | @observer |
@@ -81,6 +89,9 @@ export default class EditSettingsForm extends Component { | |||
81 | isUpdateAvailable: PropTypes.bool.isRequired, | 89 | isUpdateAvailable: PropTypes.bool.isRequired, |
82 | noUpdateAvailable: PropTypes.bool.isRequired, | 90 | noUpdateAvailable: PropTypes.bool.isRequired, |
83 | updateIsReadyToInstall: PropTypes.bool.isRequired, | 91 | updateIsReadyToInstall: PropTypes.bool.isRequired, |
92 | isClearingAllCache: PropTypes.bool.isRequired, | ||
93 | onClearAllCache: PropTypes.func.isRequired, | ||
94 | cacheSize: PropTypes.string.isRequired, | ||
84 | }; | 95 | }; |
85 | 96 | ||
86 | static contextTypes = { | 97 | static contextTypes = { |
@@ -107,6 +118,9 @@ export default class EditSettingsForm extends Component { | |||
107 | isUpdateAvailable, | 118 | isUpdateAvailable, |
108 | noUpdateAvailable, | 119 | noUpdateAvailable, |
109 | updateIsReadyToInstall, | 120 | updateIsReadyToInstall, |
121 | isClearingAllCache, | ||
122 | onClearAllCache, | ||
123 | cacheSize, | ||
110 | } = this.props; | 124 | } = this.props; |
111 | const { intl } = this.context; | 125 | const { intl } = this.context; |
112 | 126 | ||
@@ -158,8 +172,26 @@ export default class EditSettingsForm extends Component { | |||
158 | {/* Advanced */} | 172 | {/* Advanced */} |
159 | <h2 id="advanced">{intl.formatMessage(messages.headlineAdvanced)}</h2> | 173 | <h2 id="advanced">{intl.formatMessage(messages.headlineAdvanced)}</h2> |
160 | <Toggle field={form.$('enableSpellchecking')} /> | 174 | <Toggle field={form.$('enableSpellchecking')} /> |
161 | <p className="settings__help">{intl.formatMessage(messages.restartRequired)}</p> | ||
162 | {/* <Select field={form.$('spellcheckingLanguage')} /> */} | 175 | {/* <Select field={form.$('spellcheckingLanguage')} /> */} |
176 | <div className="settings__settings-group"> | ||
177 | <h3> | ||
178 | {intl.formatMessage(messages.subheadlineCache)} | ||
179 | </h3> | ||
180 | <p> | ||
181 | {intl.formatMessage(messages.cacheInfo, { | ||
182 | size: cacheSize, | ||
183 | })} | ||
184 | </p> | ||
185 | <p> | ||
186 | <Button | ||
187 | buttonType="secondary" | ||
188 | label={intl.formatMessage(messages.buttonClearAllCache)} | ||
189 | onClick={onClearAllCache} | ||
190 | disabled={isClearingAllCache} | ||
191 | loaded={!isClearingAllCache} | ||
192 | /> | ||
193 | </p> | ||
194 | </div> | ||
163 | 195 | ||
164 | {/* Updates */} | 196 | {/* Updates */} |
165 | <h2 id="updates">{intl.formatMessage(messages.headlineUpdates)}</h2> | 197 | <h2 id="updates">{intl.formatMessage(messages.headlineUpdates)}</h2> |
@@ -170,6 +202,7 @@ export default class EditSettingsForm extends Component { | |||
170 | /> | 202 | /> |
171 | ) : ( | 203 | ) : ( |
172 | <Button | 204 | <Button |
205 | buttonType="secondary" | ||
173 | label={intl.formatMessage(updateButtonLabelMessage)} | 206 | label={intl.formatMessage(updateButtonLabelMessage)} |
174 | onClick={checkForUpdates} | 207 | onClick={checkForUpdates} |
175 | disabled={isCheckingForUpdates || isUpdateAvailable} | 208 | disabled={isCheckingForUpdates || isUpdateAvailable} |
diff --git a/src/components/ui/ImageUpload.js b/src/components/ui/ImageUpload.js new file mode 100644 index 000000000..81c3b8da6 --- /dev/null +++ b/src/components/ui/ImageUpload.js | |||
@@ -0,0 +1,108 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { observer } from 'mobx-react'; | ||
4 | import { Field } from 'mobx-react-form'; | ||
5 | // import Loader from 'react-loader'; | ||
6 | import classnames from 'classnames'; | ||
7 | import Dropzone from 'react-dropzone'; | ||
8 | |||
9 | @observer | ||
10 | export default class ImageUpload extends Component { | ||
11 | static propTypes = { | ||
12 | field: PropTypes.instanceOf(Field).isRequired, | ||
13 | className: PropTypes.string, | ||
14 | multiple: PropTypes.bool, | ||
15 | textDelete: PropTypes.string.isRequired, | ||
16 | textUpload: PropTypes.string.isRequired, | ||
17 | }; | ||
18 | |||
19 | static defaultProps = { | ||
20 | className: null, | ||
21 | multiple: false, | ||
22 | }; | ||
23 | |||
24 | state = { | ||
25 | path: null, | ||
26 | } | ||
27 | |||
28 | onDrop(acceptedFiles) { | ||
29 | const { field } = this.props; | ||
30 | |||
31 | acceptedFiles.forEach((file) => { | ||
32 | this.setState({ | ||
33 | path: file.path, | ||
34 | }); | ||
35 | this.props.field.onDrop(file); | ||
36 | }); | ||
37 | |||
38 | field.set(''); | ||
39 | } | ||
40 | |||
41 | dropzoneRef = null; | ||
42 | |||
43 | render() { | ||
44 | const { | ||
45 | field, | ||
46 | className, | ||
47 | multiple, | ||
48 | textDelete, | ||
49 | textUpload, | ||
50 | } = this.props; | ||
51 | |||
52 | const cssClasses = classnames({ | ||
53 | 'image-upload__dropzone': true, | ||
54 | [`${className}`]: className, | ||
55 | }); | ||
56 | |||
57 | return ( | ||
58 | <div className="image-upload-wrapper"> | ||
59 | <label className="franz-form__label" htmlFor="iconUpload">{field.label}</label> | ||
60 | <div className="image-upload"> | ||
61 | {(field.value && field.value !== 'delete') || this.state.path ? ( | ||
62 | <div> | ||
63 | <div | ||
64 | className="image-upload__preview" | ||
65 | style={({ | ||
66 | backgroundImage: `url("${this.state.path || field.value}")`, | ||
67 | })} | ||
68 | /> | ||
69 | <div className="image-upload__action"> | ||
70 | <button | ||
71 | type="button" | ||
72 | onClick={() => { | ||
73 | if (field.value) { | ||
74 | field.set('delete'); | ||
75 | } else { | ||
76 | this.setState({ | ||
77 | path: null, | ||
78 | }); | ||
79 | } | ||
80 | }} | ||
81 | > | ||
82 | <i className="mdi mdi-delete" /> | ||
83 | <p> | ||
84 | {textDelete} | ||
85 | </p> | ||
86 | </button> | ||
87 | <div className="image-upload__action-background" /> | ||
88 | </div> | ||
89 | </div> | ||
90 | ) : ( | ||
91 | <Dropzone | ||
92 | ref={(node) => { this.dropzoneRef = node; }} | ||
93 | onDrop={this.onDrop.bind(this)} | ||
94 | className={cssClasses} | ||
95 | multiple={multiple} | ||
96 | accept="image/jpeg, image/png" | ||
97 | > | ||
98 | <i className="mdi mdi-file-image" /> | ||
99 | <p> | ||
100 | {textUpload} | ||
101 | </p> | ||
102 | </Dropzone> | ||
103 | )} | ||
104 | </div> | ||
105 | </div> | ||
106 | ); | ||
107 | } | ||
108 | } | ||
diff --git a/src/components/ui/SearchInput.js b/src/components/ui/SearchInput.js index bca412cef..a94cde201 100644 --- a/src/components/ui/SearchInput.js +++ b/src/components/ui/SearchInput.js | |||
@@ -9,36 +9,46 @@ import { debounce } from 'lodash'; | |||
9 | export default class SearchInput extends Component { | 9 | export default class SearchInput extends Component { |
10 | static propTypes = { | 10 | static propTypes = { |
11 | value: PropTypes.string, | 11 | value: PropTypes.string, |
12 | defaultValue: PropTypes.string, | 12 | placeholder: PropTypes.string, |
13 | className: PropTypes.string, | 13 | className: PropTypes.string, |
14 | onChange: PropTypes.func, | 14 | onChange: PropTypes.func, |
15 | onReset: PropTypes.func, | 15 | onReset: PropTypes.func, |
16 | name: PropTypes.string, | 16 | name: PropTypes.string, |
17 | throttle: PropTypes.bool, | 17 | throttle: PropTypes.bool, |
18 | throttleDelay: PropTypes.number, | 18 | throttleDelay: PropTypes.number, |
19 | autoFocus: PropTypes.bool, | ||
19 | }; | 20 | }; |
20 | 21 | ||
21 | static defaultProps = { | 22 | static defaultProps = { |
22 | value: '', | 23 | value: '', |
23 | defaultValue: '', | 24 | placeholder: '', |
24 | className: '', | 25 | className: '', |
25 | name: uuidv1(), | 26 | name: uuidv1(), |
26 | throttle: false, | 27 | throttle: false, |
27 | throttleDelay: 250, | 28 | throttleDelay: 250, |
28 | onChange: () => null, | 29 | onChange: () => null, |
29 | onReset: () => null, | 30 | onReset: () => null, |
31 | autoFocus: false, | ||
30 | } | 32 | } |
31 | 33 | ||
32 | constructor(props) { | 34 | constructor(props) { |
33 | super(props); | 35 | super(props); |
34 | 36 | ||
35 | this.state = { | 37 | this.state = { |
36 | value: props.value || props.defaultValue, | 38 | value: props.value, |
37 | }; | 39 | }; |
38 | 40 | ||
39 | this.throttledOnChange = debounce(this.throttledOnChange, this.props.throttleDelay); | 41 | this.throttledOnChange = debounce(this.throttledOnChange, this.props.throttleDelay); |
40 | } | 42 | } |
41 | 43 | ||
44 | componentDidMount() { | ||
45 | const { autoFocus } = this.props; | ||
46 | |||
47 | if (autoFocus) { | ||
48 | this.input.focus(); | ||
49 | } | ||
50 | } | ||
51 | |||
42 | onChange(e) { | 52 | onChange(e) { |
43 | const { throttle, onChange } = this.props; | 53 | const { throttle, onChange } = this.props; |
44 | const { value } = e.target; | 54 | const { value } = e.target; |
@@ -52,26 +62,6 @@ export default class SearchInput extends Component { | |||
52 | } | 62 | } |
53 | } | 63 | } |
54 | 64 | ||
55 | onClick() { | ||
56 | const { defaultValue } = this.props; | ||
57 | const { value } = this.state; | ||
58 | |||
59 | if (value === defaultValue) { | ||
60 | this.setState({ value: '' }); | ||
61 | } | ||
62 | |||
63 | this.input.focus(); | ||
64 | } | ||
65 | |||
66 | onBlur() { | ||
67 | const { defaultValue } = this.props; | ||
68 | const { value } = this.state; | ||
69 | |||
70 | if (value === '') { | ||
71 | this.setState({ value: defaultValue }); | ||
72 | } | ||
73 | } | ||
74 | |||
75 | throttledOnChange(e) { | 65 | throttledOnChange(e) { |
76 | const { onChange } = this.props; | 66 | const { onChange } = this.props; |
77 | 67 | ||
@@ -79,8 +69,8 @@ export default class SearchInput extends Component { | |||
79 | } | 69 | } |
80 | 70 | ||
81 | reset() { | 71 | reset() { |
82 | const { defaultValue, onReset } = this.props; | 72 | const { onReset } = this.props; |
83 | this.setState({ value: defaultValue }); | 73 | this.setState({ value: '' }); |
84 | 74 | ||
85 | onReset(); | 75 | onReset(); |
86 | } | 76 | } |
@@ -88,7 +78,7 @@ export default class SearchInput extends Component { | |||
88 | input = null; | 78 | input = null; |
89 | 79 | ||
90 | render() { | 80 | render() { |
91 | const { className, name, defaultValue } = this.props; | 81 | const { className, name, placeholder } = this.props; |
92 | const { value } = this.state; | 82 | const { value } = this.state; |
93 | 83 | ||
94 | return ( | 84 | return ( |
@@ -101,18 +91,16 @@ export default class SearchInput extends Component { | |||
101 | <label | 91 | <label |
102 | htmlFor={name} | 92 | htmlFor={name} |
103 | className="mdi mdi-magnify" | 93 | className="mdi mdi-magnify" |
104 | onClick={() => this.onClick()} | ||
105 | /> | 94 | /> |
106 | <input | 95 | <input |
107 | name={name} | 96 | name={name} |
108 | type="text" | 97 | type="text" |
98 | placeholder={placeholder} | ||
109 | value={value} | 99 | value={value} |
110 | onChange={e => this.onChange(e)} | 100 | onChange={e => this.onChange(e)} |
111 | onClick={() => this.onClick()} | ||
112 | onBlur={() => this.onBlur()} | ||
113 | ref={(ref) => { this.input = ref; }} | 101 | ref={(ref) => { this.input = ref; }} |
114 | /> | 102 | /> |
115 | {value !== defaultValue && value.length > 0 && ( | 103 | {value.length > 0 && ( |
116 | <span | 104 | <span |
117 | className="mdi mdi-close-circle-outline" | 105 | className="mdi mdi-close-circle-outline" |
118 | onClick={() => this.reset()} | 106 | onClick={() => this.reset()} |
diff --git a/src/components/ui/SubscriptionPopup.js b/src/components/ui/SubscriptionPopup.js index 5aae2c47a..528d02907 100644 --- a/src/components/ui/SubscriptionPopup.js +++ b/src/components/ui/SubscriptionPopup.js | |||
@@ -58,7 +58,6 @@ export default class SubscriptionPopup extends Component { | |||
58 | 58 | ||
59 | autosize | 59 | autosize |
60 | src={encodeURI(url)} | 60 | src={encodeURI(url)} |
61 | disablewebsecurity | ||
62 | onDidNavigate={completeCheck} | 61 | onDidNavigate={completeCheck} |
63 | // onNewWindow={(event, url, frameName, options) => | 62 | // onNewWindow={(event, url, frameName, options) => |
64 | // openWindow({ event, url, frameName, options })} | 63 | // openWindow({ event, url, frameName, options })} |
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/EditServiceScreen.js b/src/containers/settings/EditServiceScreen.js index 191ef447b..c26195a1e 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', |
@@ -42,6 +46,10 @@ const messages = defineMessages({ | |||
42 | id: 'settings.service.form.indirectMessages', | 46 | id: 'settings.service.form.indirectMessages', |
43 | defaultMessage: '!!!Show message badge for all new messages', | 47 | defaultMessage: '!!!Show message badge for all new messages', |
44 | }, | 48 | }, |
49 | icon: { | ||
50 | id: 'settings.service.form.icon', | ||
51 | defaultMessage: '!!!Custom icon', | ||
52 | }, | ||
45 | }); | 53 | }); |
46 | 54 | ||
47 | @inject('stores', 'actions') @observer | 55 | @inject('stores', 'actions') @observer |
@@ -88,11 +96,22 @@ export default class EditServiceScreen extends Component { | |||
88 | value: service.isNotificationEnabled, | 96 | value: service.isNotificationEnabled, |
89 | default: true, | 97 | default: true, |
90 | }, | 98 | }, |
99 | isBadgeEnabled: { | ||
100 | label: intl.formatMessage(messages.enableBadge), | ||
101 | value: service.isBadgeEnabled, | ||
102 | default: true, | ||
103 | }, | ||
91 | isMuted: { | 104 | isMuted: { |
92 | label: intl.formatMessage(messages.enableAudio), | 105 | label: intl.formatMessage(messages.enableAudio), |
93 | value: !service.isMuted, | 106 | value: !service.isMuted, |
94 | default: true, | 107 | default: true, |
95 | }, | 108 | }, |
109 | customIcon: { | ||
110 | label: intl.formatMessage(messages.icon), | ||
111 | value: service.hasCustomUploadedIcon ? service.icon : false, | ||
112 | default: null, | ||
113 | type: 'file', | ||
114 | }, | ||
96 | }, | 115 | }, |
97 | }; | 116 | }; |
98 | 117 | ||
@@ -118,11 +137,22 @@ export default class EditServiceScreen extends Component { | |||
118 | }); | 137 | }); |
119 | } | 138 | } |
120 | 139 | ||
140 | // More fine grained and use case specific validation rules | ||
121 | if (recipe.hasTeamId && recipe.hasCustomUrl) { | 141 | if (recipe.hasTeamId && recipe.hasCustomUrl) { |
122 | config.fields.team.validate = [oneRequired(['team', 'customUrl'])]; | 142 | config.fields.team.validate = [oneRequired(['team', 'customUrl'])]; |
123 | config.fields.customUrl.validate = [url, oneRequired(['team', 'customUrl'])]; | 143 | config.fields.customUrl.validate = [url, oneRequired(['team', 'customUrl'])]; |
124 | } | 144 | } |
125 | 145 | ||
146 | // If a service can be hosted and has a teamId or customUrl | ||
147 | if (recipe.hasHostedOption && (recipe.hasTeamId || recipe.hasCustomUrl)) { | ||
148 | if (config.fields.team) { | ||
149 | config.fields.team.validate = []; | ||
150 | } | ||
151 | if (config.fields.customUrl) { | ||
152 | config.fields.customUrl.validate = [url]; | ||
153 | } | ||
154 | } | ||
155 | |||
126 | if (recipe.hasIndirectMessages) { | 156 | if (recipe.hasIndirectMessages) { |
127 | Object.assign(config.fields, { | 157 | Object.assign(config.fields, { |
128 | isIndirectMessageBadgeEnabled: { | 158 | isIndirectMessageBadgeEnabled: { |
@@ -179,6 +209,12 @@ export default class EditServiceScreen extends Component { | |||
179 | return (<div>Loading...</div>); | 209 | return (<div>Loading...</div>); |
180 | } | 210 | } |
181 | 211 | ||
212 | if (!recipe) { | ||
213 | return ( | ||
214 | <div>something went wrong</div> | ||
215 | ); | ||
216 | } | ||
217 | |||
182 | const form = this.prepareForm(recipe, service); | 218 | const form = this.prepareForm(recipe, service); |
183 | 219 | ||
184 | return ( | 220 | return ( |
diff --git a/src/containers/settings/EditSettingsScreen.js b/src/containers/settings/EditSettingsScreen.js index 1a297f41f..1fa7ce8bc 100644 --- a/src/containers/settings/EditSettingsScreen.js +++ b/src/containers/settings/EditSettingsScreen.js | |||
@@ -193,8 +193,17 @@ export default class EditSettingsScreen extends Component { | |||
193 | } | 193 | } |
194 | 194 | ||
195 | render() { | 195 | render() { |
196 | const { updateStatus, updateStatusTypes } = this.props.stores.app; | 196 | const { |
197 | const { checkForUpdates, installUpdate } = this.props.actions.app; | 197 | updateStatus, |
198 | cacheSize, | ||
199 | updateStatusTypes, | ||
200 | isClearingAllCache, | ||
201 | } = this.props.stores.app; | ||
202 | const { | ||
203 | checkForUpdates, | ||
204 | installUpdate, | ||
205 | clearAllCache, | ||
206 | } = this.props.actions.app; | ||
198 | const form = this.prepareForm(); | 207 | const form = this.prepareForm(); |
199 | 208 | ||
200 | return ( | 209 | return ( |
@@ -207,6 +216,9 @@ export default class EditSettingsScreen extends Component { | |||
207 | noUpdateAvailable={updateStatus === updateStatusTypes.NOT_AVAILABLE} | 216 | noUpdateAvailable={updateStatus === updateStatusTypes.NOT_AVAILABLE} |
208 | updateIsReadyToInstall={updateStatus === updateStatusTypes.DOWNLOADED} | 217 | updateIsReadyToInstall={updateStatus === updateStatusTypes.DOWNLOADED} |
209 | onSubmit={d => this.onSubmit(d)} | 218 | onSubmit={d => this.onSubmit(d)} |
219 | cacheSize={cacheSize} | ||
220 | isClearingAllCache={isClearingAllCache} | ||
221 | onClearAllCache={clearAllCache} | ||
210 | /> | 222 | /> |
211 | ); | 223 | ); |
212 | } | 224 | } |
@@ -223,6 +235,7 @@ EditSettingsScreen.wrappedComponent.propTypes = { | |||
223 | launchOnStartup: PropTypes.func.isRequired, | 235 | launchOnStartup: PropTypes.func.isRequired, |
224 | checkForUpdates: PropTypes.func.isRequired, | 236 | checkForUpdates: PropTypes.func.isRequired, |
225 | installUpdate: PropTypes.func.isRequired, | 237 | installUpdate: PropTypes.func.isRequired, |
238 | clearAllCache: PropTypes.func.isRequired, | ||
226 | }).isRequired, | 239 | }).isRequired, |
227 | settings: PropTypes.shape({ | 240 | settings: PropTypes.shape({ |
228 | update: PropTypes.func.isRequired, | 241 | update: PropTypes.func.isRequired, |
diff --git a/src/containers/settings/ServicesScreen.js b/src/containers/settings/ServicesScreen.js index 8cfe5efbf..12db1bcd3 100644 --- a/src/containers/settings/ServicesScreen.js +++ b/src/containers/settings/ServicesScreen.js | |||
@@ -53,6 +53,7 @@ export default class ServicesScreen extends Component { | |||
53 | goTo={router.push} | 53 | goTo={router.push} |
54 | servicesRequestFailed={services.allServicesRequest.wasExecuted && services.allServicesRequest.isError} | 54 | servicesRequestFailed={services.allServicesRequest.wasExecuted && services.allServicesRequest.isError} |
55 | retryServicesRequest={() => services.allServicesRequest.reload()} | 55 | retryServicesRequest={() => services.allServicesRequest.reload()} |
56 | searchNeedle={services.filterNeedle} | ||
56 | /> | 57 | /> |
57 | ); | 58 | ); |
58 | } | 59 | } |
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/electron/ipc-api/autoUpdate.js b/src/electron/ipc-api/autoUpdate.js index 7bc193e2d..ba49a2f97 100644 --- a/src/electron/ipc-api/autoUpdate.js +++ b/src/electron/ipc-api/autoUpdate.js | |||
@@ -1,8 +1,9 @@ | |||
1 | import { app, ipcMain } from 'electron'; | 1 | import { app, ipcMain } from 'electron'; |
2 | import { autoUpdater } from 'electron-updater'; | 2 | import { autoUpdater } from 'electron-updater'; |
3 | import { isDevMode } from '../../environment.js'; | ||
3 | 4 | ||
4 | export default (params) => { | 5 | export default (params) => { |
5 | if (process.platform === 'darwin' || process.platform === 'win32') { | 6 | if (!isDevMode && (process.platform === 'darwin' || process.platform === 'win32')) { |
6 | // autoUpdater.setFeedURL(updateUrl); | 7 | // autoUpdater.setFeedURL(updateUrl); |
7 | ipcMain.on('autoUpdate', (event, args) => { | 8 | ipcMain.on('autoUpdate', (event, args) => { |
8 | try { | 9 | try { |
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/service-helpers.js b/src/helpers/service-helpers.js new file mode 100644 index 000000000..5f63f6b7c --- /dev/null +++ b/src/helpers/service-helpers.js | |||
@@ -0,0 +1,20 @@ | |||
1 | import path from 'path'; | ||
2 | import { remote } from 'electron'; | ||
3 | import fs from 'fs-extra'; | ||
4 | |||
5 | const app = remote.app; | ||
6 | |||
7 | export function getServicePartitionsDirectory() { | ||
8 | return path.join(app.getPath('userData'), 'Partitions'); | ||
9 | } | ||
10 | |||
11 | export function removeServicePartitionDirectory(id = '', addServicePrefix = false) { | ||
12 | const servicePartition = path.join(getServicePartitionsDirectory(), `${addServicePrefix ? 'service-' : ''}${id}`); | ||
13 | |||
14 | return fs.remove(servicePartition); | ||
15 | } | ||
16 | |||
17 | export async function getServiceIdsFromPartitions() { | ||
18 | const files = await fs.readdir(getServicePartitionsDirectory()); | ||
19 | return files.filter(n => n !== '__chrome_extension'); | ||
20 | } | ||
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/locales/en-US.json b/src/i18n/locales/en-US.json index 48b408e59..7fc9eac1c 100644 --- a/src/i18n/locales/en-US.json +++ b/src/i18n/locales/en-US.json | |||
@@ -66,6 +66,7 @@ | |||
66 | "sidebar.unmuteApp": "Enable notifications & 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.searchService": "Search service", | ||
69 | "settings.account.headline": "Account", | 70 | "settings.account.headline": "Account", |
70 | "settings.account.headlineSubscription": "Your subscription", | 71 | "settings.account.headlineSubscription": "Your subscription", |
71 | "settings.account.headlineUpgrade": "Upgrade your account & support Franz", | 72 | "settings.account.headlineUpgrade": "Upgrade your account & support Franz", |
@@ -110,6 +111,7 @@ | |||
110 | "settings.service.form.editServiceHeadline": "Edit {name}", | 111 | "settings.service.form.editServiceHeadline": "Edit {name}", |
111 | "settings.service.form.tabHosted": "Hosted", | 112 | "settings.service.form.tabHosted": "Hosted", |
112 | "settings.service.form.tabOnPremise": "Self hosted ⭐️", | 113 | "settings.service.form.tabOnPremise": "Self hosted ⭐️", |
114 | "settings.service.form.useHostedService": "Use the hosted {name} service.", | ||
113 | "settings.service.form.customUrlValidationError": "Could not validate custom {name} server.", | 115 | "settings.service.form.customUrlValidationError": "Could not validate custom {name} server.", |
114 | "settings.service.form.customUrlPremiumInfo": "To add self hosted services, you need a Franz Premium Supporter Account.", | 116 | "settings.service.form.customUrlPremiumInfo": "To add self hosted services, you need a Franz Premium Supporter Account.", |
115 | "settings.service.form.customUrlUpgradeAccount": "Upgrade your account", | 117 | "settings.service.form.customUrlUpgradeAccount": "Upgrade your account", |
@@ -117,11 +119,18 @@ | |||
117 | "settings.service.form.name": "Name", | 119 | "settings.service.form.name": "Name", |
118 | "settings.service.form.enableService": "Enable service", | 120 | "settings.service.form.enableService": "Enable service", |
119 | "settings.service.form.enableNotification": "Enable notifications", | 121 | "settings.service.form.enableNotification": "Enable notifications", |
122 | "settings.service.form.enableBadge": "Show unread message badges", | ||
120 | "settings.service.form.team": "Team", | 123 | "settings.service.form.team": "Team", |
121 | "settings.service.form.customUrl": "Custom server", | 124 | "settings.service.form.customUrl": "Custom server", |
122 | "settings.service.form.indirectMessages": "Show message badge for all new messages", | 125 | "settings.service.form.indirectMessages": "Show message badge for all new messages", |
123 | "settings.service.form.enableAudio": "Enable audio", | 126 | "settings.service.form.enableAudio": "Enable audio", |
124 | "settings.service.form.isMutedInfo": "When disabled, all notification sounds and audio playback are muted", | 127 | "settings.service.form.isMutedInfo": "When disabled, all notification sounds and audio playback are muted", |
128 | "settings.service.form.headlineNotifications": "Notifications", | ||
129 | "settings.service.form.headlineBadges": "Unread message badges", | ||
130 | "settings.service.form.headlineGeneral": "General", | ||
131 | "settings.service.form.icon": "Custom icon", | ||
132 | "settings.service.form.iconDelete": "Delete", | ||
133 | "settings.service.form.iconUpload": "Drop your image, or click here", | ||
125 | "settings.service.error.headline": "Error", | 134 | "settings.service.error.headline": "Error", |
126 | "settings.service.error.goBack": "Back to services", | 135 | "settings.service.error.goBack": "Back to services", |
127 | "settings.service.error.message": "Could not load service recipe.", | 136 | "settings.service.error.message": "Could not load service recipe.", |
@@ -144,6 +153,9 @@ | |||
144 | "settings.app.updateStatusSearching": "Is searching for update", | 153 | "settings.app.updateStatusSearching": "Is searching for update", |
145 | "settings.app.updateStatusAvailable": "Update available, downloading...", | 154 | "settings.app.updateStatusAvailable": "Update available, downloading...", |
146 | "settings.app.updateStatusUpToDate": "You are using the latest version of Franz", | 155 | "settings.app.updateStatusUpToDate": "You are using the latest version of Franz", |
156 | "settings.app.subheadlineCache": "Cache", | ||
157 | "settings.app.cacheInfo": "Franz cache is currently using {size} of disk space.", | ||
158 | "settings.app.buttonClearAllCache": "Clear cache", | ||
147 | "settings.app.form.autoLaunchOnStart": "Launch Franz on start", | 159 | "settings.app.form.autoLaunchOnStart": "Launch Franz on start", |
148 | "settings.app.form.autoLaunchInBackground": "Open in background", | 160 | "settings.app.form.autoLaunchInBackground": "Open in background", |
149 | "settings.app.form.enableSystemTray": "Show Franz in system tray", | 161 | "settings.app.form.enableSystemTray": "Show Franz in system tray", |
diff --git a/src/index.html b/src/index.html index 05a93e37b..9e5acd705 100644 --- a/src/index.html +++ b/src/index.html | |||
@@ -23,6 +23,24 @@ | |||
23 | s.async = true; | 23 | s.async = true; |
24 | s.setAttribute('src', lrHost + '/livereload.js'); | 24 | s.setAttribute('src', lrHost + '/livereload.js'); |
25 | document.body.appendChild(s); | 25 | document.body.appendChild(s); |
26 | |||
27 | s.onload = () => { | ||
28 | console.log('livereload loaded'); | ||
29 | const originalReloadBehaviour = window._onLiveReloadFileChanged; | ||
30 | |||
31 | window._onLiveReloadFileChanged = (file) => { | ||
32 | if (!file.path.includes('/build/webview/') && !file.path.includes('/build/index.js') && !file.path.includes('/build/electron/')) { | ||
33 | originalReloadBehaviour(file); | ||
34 | } else { | ||
35 | if (file.path.includes('/build/webview/')) { | ||
36 | console.log('Livereload: Reloading all webvies'); | ||
37 | const webviews = document.querySelectorAll('webview').forEach(webview => webview.reload()); | ||
38 | } else { | ||
39 | console.log('Livereload: skip reload as only main process files have changed'); | ||
40 | } | ||
41 | } | ||
42 | } | ||
43 | } | ||
26 | })(); | 44 | })(); |
27 | } | 45 | } |
28 | </script> | 46 | </script> |
diff --git a/src/index.js b/src/index.js index 6a08e5e5a..f82bb3590 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,14 +27,24 @@ 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 | ||
36 | if (isSecondInstance) { | 46 | if (isSecondInstance) { |
47 | console.log('An instance of Franz is already running. Exiting...'); | ||
37 | app.exit(); | 48 | app.exit(); |
38 | } | 49 | } |
39 | 50 | ||
@@ -176,3 +187,15 @@ app.on('activate', () => { | |||
176 | mainWindow.show(); | 187 | mainWindow.show(); |
177 | } | 188 | } |
178 | }); | 189 | }); |
190 | |||
191 | app.on('will-finish-launching', () => { | ||
192 | // Protocol handler for osx | ||
193 | app.on('open-url', (event, url) => { | ||
194 | event.preventDefault(); | ||
195 | console.log(`open-url event: ${url}`); | ||
196 | handleDeepLink(mainWindow, url); | ||
197 | }); | ||
198 | }); | ||
199 | |||
200 | // Register App URL | ||
201 | app.setAsDefaultProtocolClient('franz'); | ||
diff --git a/src/lib/Menu.js b/src/lib/Menu.js index d9c30466b..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 | { |
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..423510c7d 100644 --- a/src/models/Service.js +++ b/src/models/Service.js | |||
@@ -22,8 +22,10 @@ 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 iconUrl = ''; |
28 | @observable hasCustomUploadedIcon = false; | ||
27 | @observable hasCrashed = false; | 29 | @observable hasCrashed = false; |
28 | 30 | ||
29 | constructor(data, recipe) { | 31 | constructor(data, recipe) { |
@@ -41,7 +43,8 @@ export default class Service { | |||
41 | this.name = data.name || this.name; | 43 | this.name = data.name || this.name; |
42 | this.team = data.team || this.team; | 44 | this.team = data.team || this.team; |
43 | this.customUrl = data.customUrl || this.customUrl; | 45 | this.customUrl = data.customUrl || this.customUrl; |
44 | this.customIconUrl = data.customIconUrl || this.customIconUrl; | 46 | // this.customIconUrl = data.customIconUrl || this.customIconUrl; |
47 | this.iconUrl = data.iconUrl || this.iconUrl; | ||
45 | 48 | ||
46 | this.order = data.order !== undefined | 49 | this.order = data.order !== undefined |
47 | ? data.order : this.order; | 50 | ? data.order : this.order; |
@@ -52,11 +55,16 @@ export default class Service { | |||
52 | this.isNotificationEnabled = data.isNotificationEnabled !== undefined | 55 | this.isNotificationEnabled = data.isNotificationEnabled !== undefined |
53 | ? data.isNotificationEnabled : this.isNotificationEnabled; | 56 | ? data.isNotificationEnabled : this.isNotificationEnabled; |
54 | 57 | ||
58 | this.isBadgeEnabled = data.isBadgeEnabled !== undefined | ||
59 | ? data.isBadgeEnabled : this.isBadgeEnabled; | ||
60 | |||
55 | this.isIndirectMessageBadgeEnabled = data.isIndirectMessageBadgeEnabled !== undefined | 61 | this.isIndirectMessageBadgeEnabled = data.isIndirectMessageBadgeEnabled !== undefined |
56 | ? data.isIndirectMessageBadgeEnabled : this.isIndirectMessageBadgeEnabled; | 62 | ? data.isIndirectMessageBadgeEnabled : this.isIndirectMessageBadgeEnabled; |
57 | 63 | ||
58 | this.isMuted = data.isMuted !== undefined ? data.isMuted : this.isMuted; | 64 | this.isMuted = data.isMuted !== undefined ? data.isMuted : this.isMuted; |
59 | 65 | ||
66 | this.hasCustomUploadedIcon = data.hasCustomIcon !== undefined ? data.hasCustomIcon : this.hasCustomUploadedIcon; | ||
67 | |||
60 | this.recipe = recipe; | 68 | this.recipe = recipe; |
61 | 69 | ||
62 | autorun(() => { | 70 | autorun(() => { |
@@ -93,15 +101,15 @@ export default class Service { | |||
93 | } | 101 | } |
94 | 102 | ||
95 | @computed get icon() { | 103 | @computed get icon() { |
96 | if (this.hasCustomIcon) { | 104 | if (this.iconUrl) { |
97 | return this.customIconUrl; | 105 | return this.iconUrl; |
98 | } | 106 | } |
99 | 107 | ||
100 | return path.join(this.recipe.path, 'icon.svg'); | 108 | return path.join(this.recipe.path, 'icon.svg'); |
101 | } | 109 | } |
102 | 110 | ||
103 | @computed get hasCustomIcon() { | 111 | @computed get hasCustomIcon() { |
104 | return (this.customIconUrl !== ''); | 112 | return Boolean(this.iconUrl); |
105 | } | 113 | } |
106 | 114 | ||
107 | @computed get iconPNG() { | 115 | @computed get iconPNG() { |
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 17ec832cf..e33f50f05 100644 --- a/src/stores/AppStore.js +++ b/src/stores/AppStore.js | |||
@@ -1,10 +1,11 @@ | |||
1 | import { remote, ipcRenderer, shell } from 'electron'; | 1 | import { remote, ipcRenderer, shell } from 'electron'; |
2 | import { action, observable } from 'mobx'; | 2 | import { action, computed, observable } from 'mobx'; |
3 | import moment from 'moment'; | 3 | import moment from 'moment'; |
4 | import key from 'keymaster'; | 4 | import key from 'keymaster'; |
5 | import { getDoNotDisturb } from '@meetfranz/electron-notification-state'; | 5 | import { getDoNotDisturb } from '@meetfranz/electron-notification-state'; |
6 | import idleTimer from '@paulcbetts/system-idle-time'; | 6 | import idleTimer from '@paulcbetts/system-idle-time'; |
7 | import AutoLaunch from 'auto-launch'; | 7 | import AutoLaunch from 'auto-launch'; |
8 | import prettyBytes from 'pretty-bytes'; | ||
8 | 9 | ||
9 | import Store from './lib/Store'; | 10 | import Store from './lib/Store'; |
10 | import Request from './lib/Request'; | 11 | import Request from './lib/Request'; |
@@ -14,7 +15,10 @@ import locales from '../i18n/translations'; | |||
14 | import { gaEvent } from '../lib/analytics'; | 15 | import { gaEvent } from '../lib/analytics'; |
15 | import Miner from '../lib/Miner'; | 16 | import Miner from '../lib/Miner'; |
16 | 17 | ||
17 | const { app, powerMonitor } = remote; | 18 | import { getServiceIdsFromPartitions, removeServicePartitionDirectory } from '../helpers/service-helpers.js'; |
19 | |||
20 | const { app } = remote; | ||
21 | |||
18 | const defaultLocale = DEFAULT_APP_SETTINGS.locale; | 22 | const defaultLocale = DEFAULT_APP_SETTINGS.locale; |
19 | const autoLauncher = new AutoLaunch({ | 23 | const autoLauncher = new AutoLaunch({ |
20 | name: 'Franz', | 24 | name: 'Franz', |
@@ -30,6 +34,8 @@ export default class AppStore extends Store { | |||
30 | }; | 34 | }; |
31 | 35 | ||
32 | @observable healthCheckRequest = new Request(this.api.app, 'health'); | 36 | @observable healthCheckRequest = new Request(this.api.app, 'health'); |
37 | @observable getAppCacheSizeRequest = new Request(this.api.local, 'getAppCacheSize'); | ||
38 | @observable clearAppCacheRequest = new Request(this.api.local, 'clearAppCache'); | ||
33 | 39 | ||
34 | @observable autoLaunchOnStart = true; | 40 | @observable autoLaunchOnStart = true; |
35 | 41 | ||
@@ -47,6 +53,8 @@ export default class AppStore extends Store { | |||
47 | 53 | ||
48 | @observable isSystemMuteOverridden = false; | 54 | @observable isSystemMuteOverridden = false; |
49 | 55 | ||
56 | @observable isClearingAllCache = false; | ||
57 | |||
50 | constructor(...args) { | 58 | constructor(...args) { |
51 | super(...args); | 59 | super(...args); |
52 | 60 | ||
@@ -61,6 +69,7 @@ export default class AppStore extends Store { | |||
61 | this.actions.app.healthCheck.listen(this._healthCheck.bind(this)); | 69 | this.actions.app.healthCheck.listen(this._healthCheck.bind(this)); |
62 | this.actions.app.muteApp.listen(this._muteApp.bind(this)); | 70 | this.actions.app.muteApp.listen(this._muteApp.bind(this)); |
63 | this.actions.app.toggleMuteApp.listen(this._toggleMuteApp.bind(this)); | 71 | this.actions.app.toggleMuteApp.listen(this._toggleMuteApp.bind(this)); |
72 | this.actions.app.clearAllCache.listen(this._clearAllCache.bind(this)); | ||
64 | 73 | ||
65 | this.registerReactions([ | 74 | this.registerReactions([ |
66 | this._offlineCheck.bind(this), | 75 | this._offlineCheck.bind(this), |
@@ -116,15 +125,31 @@ export default class AppStore extends Store { | |||
116 | } | 125 | } |
117 | }); | 126 | }); |
118 | 127 | ||
128 | // Handle deep linking (franz://) | ||
129 | ipcRenderer.on('navigateFromDeepLink', (event, data) => { | ||
130 | const { url } = data; | ||
131 | if (!url) return; | ||
132 | |||
133 | this.stores.router.push(data.url); | ||
134 | }); | ||
135 | |||
136 | const TIMEOUT = 5000; | ||
119 | // Check system idle time every minute | 137 | // Check system idle time every minute |
120 | setInterval(() => { | 138 | setInterval(() => { |
121 | this.idleTime = idleTimer.getIdleTime(); | 139 | this.idleTime = idleTimer.getIdleTime(); |
122 | }, 60000); | 140 | }, TIMEOUT); |
123 | 141 | ||
124 | // Reload all services after a healthy nap | 142 | // Reload all services after a healthy nap |
125 | powerMonitor.on('resume', () => { | 143 | // Alternative solution for powerMonitor as the resume event is not fired |
126 | setTimeout(window.location.reload, 5000); | 144 | // More information: https://github.com/electron/electron/issues/1615 |
127 | }); | 145 | let lastTime = (new Date()).getTime(); |
146 | setInterval(() => { | ||
147 | const currentTime = (new Date()).getTime(); | ||
148 | if (currentTime > (lastTime + TIMEOUT + 2000)) { | ||
149 | this._reactivateServices(); | ||
150 | } | ||
151 | lastTime = currentTime; | ||
152 | }, TIMEOUT); | ||
128 | 153 | ||
129 | // Set active the next service | 154 | // Set active the next service |
130 | key( | 155 | key( |
@@ -149,6 +174,10 @@ export default class AppStore extends Store { | |||
149 | this._healthCheck(); | 174 | this._healthCheck(); |
150 | } | 175 | } |
151 | 176 | ||
177 | @computed get cacheSize() { | ||
178 | return prettyBytes(this.getAppCacheSizeRequest.execute().result || 0); | ||
179 | } | ||
180 | |||
152 | // Actions | 181 | // Actions |
153 | @action _notify({ title, options, notificationId, serviceId = null }) { | 182 | @action _notify({ title, options, notificationId, serviceId = null }) { |
154 | if (this.stores.settings.all.isAppMuted) return; | 183 | if (this.stores.settings.all.isAppMuted) return; |
@@ -239,6 +268,23 @@ export default class AppStore extends Store { | |||
239 | this._muteApp({ isMuted: !this.stores.settings.all.isAppMuted }); | 268 | this._muteApp({ isMuted: !this.stores.settings.all.isAppMuted }); |
240 | } | 269 | } |
241 | 270 | ||
271 | @action async _clearAllCache() { | ||
272 | this.isClearingAllCache = true; | ||
273 | const clearAppCache = this.clearAppCacheRequest.execute(); | ||
274 | const allServiceIds = await getServiceIdsFromPartitions(); | ||
275 | const allOrphanedServiceIds = allServiceIds.filter(id => !this.stores.services.all.find(s => id.replace('service-', '') === s.id)); | ||
276 | |||
277 | await Promise.all(allOrphanedServiceIds.map(id => removeServicePartitionDirectory(id))); | ||
278 | |||
279 | await Promise.all(this.stores.services.all.map(s => this.actions.service.clearCache({ serviceId: s.id }))); | ||
280 | |||
281 | await clearAppCache._promise; | ||
282 | |||
283 | this.getAppCacheSizeRequest.execute(); | ||
284 | |||
285 | this.isClearingAllCache = false; | ||
286 | } | ||
287 | |||
242 | // Reactions | 288 | // Reactions |
243 | _offlineCheck() { | 289 | _offlineCheck() { |
244 | if (!this.isOnline) { | 290 | if (!this.isOnline) { |
@@ -255,8 +301,10 @@ export default class AppStore extends Store { | |||
255 | _setLocale() { | 301 | _setLocale() { |
256 | const locale = this.stores.settings.all.locale; | 302 | const locale = this.stores.settings.all.locale; |
257 | 303 | ||
258 | if (locale && locale !== this.locale) { | 304 | if (locale && Object.prototype.hasOwnProperty.call(locales, locale) && locale !== this.locale) { |
259 | this.locale = locale; | 305 | this.locale = locale; |
306 | } else if (!locale) { | ||
307 | this.locale = this._getDefaultLocale(); | ||
260 | } | 308 | } |
261 | } | 309 | } |
262 | 310 | ||
@@ -281,6 +329,10 @@ export default class AppStore extends Store { | |||
281 | locale = defaultLocale; | 329 | locale = defaultLocale; |
282 | } | 330 | } |
283 | 331 | ||
332 | if (!locale) { | ||
333 | locale = DEFAULT_APP_SETTINGS.fallbackLocale; | ||
334 | } | ||
335 | |||
284 | return locale; | 336 | return locale; |
285 | } | 337 | } |
286 | 338 | ||
@@ -343,6 +395,16 @@ export default class AppStore extends Store { | |||
343 | return autoLauncher.isEnabled() || false; | 395 | return autoLauncher.isEnabled() || false; |
344 | } | 396 | } |
345 | 397 | ||
398 | _reactivateServices(retryCount = 0) { | ||
399 | if (!this.isOnline) { | ||
400 | console.debug('reactivateServices: computer is offline, trying again in 5s, retries:', retryCount); | ||
401 | setTimeout(() => this._reactivateServices(retryCount + 1), 5000); | ||
402 | } else { | ||
403 | console.debug('reactivateServices: reload all services'); | ||
404 | this.actions.service.reloadAll(); | ||
405 | } | ||
406 | } | ||
407 | |||
346 | _systemDND() { | 408 | _systemDND() { |
347 | const dnd = getDoNotDisturb(); | 409 | const dnd = getDoNotDisturb(); |
348 | if (dnd === this.stores.settings.all.isAppMuted || !this.isSystemMuteOverriden) { | 410 | if (dnd === this.stores.settings.all.isAppMuted || !this.isSystemMuteOverriden) { |
diff --git a/src/stores/ServicesStore.js b/src/stores/ServicesStore.js index b04aafd78..7300a76c8 100644 --- a/src/stores/ServicesStore.js +++ b/src/stores/ServicesStore.js | |||
@@ -16,6 +16,7 @@ export default class ServicesStore extends Store { | |||
16 | @observable updateServiceRequest = new Request(this.api.services, 'update'); | 16 | @observable updateServiceRequest = new Request(this.api.services, 'update'); |
17 | @observable reorderServicesRequest = new Request(this.api.services, 'reorder'); | 17 | @observable reorderServicesRequest = new Request(this.api.services, 'reorder'); |
18 | @observable deleteServiceRequest = new Request(this.api.services, 'delete'); | 18 | @observable deleteServiceRequest = new Request(this.api.services, 'delete'); |
19 | @observable clearCacheRequest = new Request(this.api.services, 'clearCache'); | ||
19 | 20 | ||
20 | @observable filterNeedle = null; | 21 | @observable filterNeedle = null; |
21 | 22 | ||
@@ -31,6 +32,7 @@ export default class ServicesStore extends Store { | |||
31 | this.actions.service.createFromLegacyService.listen(this._createFromLegacyService.bind(this)); | 32 | this.actions.service.createFromLegacyService.listen(this._createFromLegacyService.bind(this)); |
32 | this.actions.service.updateService.listen(this._updateService.bind(this)); | 33 | this.actions.service.updateService.listen(this._updateService.bind(this)); |
33 | this.actions.service.deleteService.listen(this._deleteService.bind(this)); | 34 | this.actions.service.deleteService.listen(this._deleteService.bind(this)); |
35 | this.actions.service.clearCache.listen(this._clearCache.bind(this)); | ||
34 | this.actions.service.setWebviewReference.listen(this._setWebviewReference.bind(this)); | 36 | this.actions.service.setWebviewReference.listen(this._setWebviewReference.bind(this)); |
35 | this.actions.service.focusService.listen(this._focusService.bind(this)); | 37 | this.actions.service.focusService.listen(this._focusService.bind(this)); |
36 | this.actions.service.focusActiveService.listen(this._focusActiveService.bind(this)); | 38 | this.actions.service.focusActiveService.listen(this._focusActiveService.bind(this)); |
@@ -172,9 +174,29 @@ export default class ServicesStore extends Store { | |||
172 | const data = this._cleanUpTeamIdAndCustomUrl(service.recipe.id, serviceData); | 174 | const data = this._cleanUpTeamIdAndCustomUrl(service.recipe.id, serviceData); |
173 | const request = this.updateServiceRequest.execute(serviceId, data); | 175 | const request = this.updateServiceRequest.execute(serviceId, data); |
174 | 176 | ||
177 | const newData = serviceData; | ||
178 | if (serviceData.iconFile) { | ||
179 | await request._promise; | ||
180 | |||
181 | newData.iconUrl = request.result.data.iconUrl; | ||
182 | newData.hasCustomUploadedIcon = true; | ||
183 | } | ||
184 | |||
175 | this.allServicesRequest.patch((result) => { | 185 | this.allServicesRequest.patch((result) => { |
176 | if (!result) return; | 186 | if (!result) return; |
177 | Object.assign(result.find(c => c.id === serviceId), serviceData); | 187 | |
188 | // patch custom icon deletion | ||
189 | if (data.customIcon === 'delete') { | ||
190 | data.iconUrl = ''; | ||
191 | data.hasCustomUploadedIcon = false; | ||
192 | } | ||
193 | |||
194 | // patch custom icon url | ||
195 | if (data.customIconUrl) { | ||
196 | data.iconUrl = data.customIconUrl; | ||
197 | } | ||
198 | |||
199 | Object.assign(result.find(c => c.id === serviceId), newData); | ||
178 | }); | 200 | }); |
179 | 201 | ||
180 | await request._promise; | 202 | await request._promise; |
@@ -205,6 +227,13 @@ export default class ServicesStore extends Store { | |||
205 | gaEvent('Service', 'delete', service.recipe.id); | 227 | gaEvent('Service', 'delete', service.recipe.id); |
206 | } | 228 | } |
207 | 229 | ||
230 | @action async _clearCache({ serviceId }) { | ||
231 | this.clearCacheRequest.reset(); | ||
232 | const request = this.clearCacheRequest.execute(serviceId); | ||
233 | await request._promise; | ||
234 | gaEvent('Service', 'clear cache'); | ||
235 | } | ||
236 | |||
208 | @action _setActive({ serviceId }) { | 237 | @action _setActive({ serviceId }) { |
209 | const service = this.one(serviceId); | 238 | const service = this.one(serviceId); |
210 | 239 | ||
@@ -297,7 +326,7 @@ export default class ServicesStore extends Store { | |||
297 | }); | 326 | }); |
298 | } else if (channel === 'notification') { | 327 | } else if (channel === 'notification') { |
299 | const options = args[0].options; | 328 | const options = args[0].options; |
300 | if (service.recipe.hasNotificationSound || service.isMuted) { | 329 | if (service.recipe.hasNotificationSound || service.isMuted || this.stores.settings.all.isAppMuted) { |
301 | Object.assign(options, { | 330 | Object.assign(options, { |
302 | silent: true, | 331 | silent: true, |
303 | }); | 332 | }); |
@@ -316,7 +345,7 @@ export default class ServicesStore extends Store { | |||
316 | } | 345 | } |
317 | } else if (channel === 'avatar') { | 346 | } else if (channel === 'avatar') { |
318 | const url = args[0]; | 347 | const url = args[0]; |
319 | if (service.customIconUrl !== url) { | 348 | if (service.iconUrl !== url && !service.hasCustomUploadedIcon) { |
320 | service.customIconUrl = url; | 349 | service.customIconUrl = url; |
321 | 350 | ||
322 | this.actions.service.updateService({ | 351 | this.actions.service.updateService({ |
@@ -327,6 +356,10 @@ export default class ServicesStore extends Store { | |||
327 | redirect: false, | 356 | redirect: false, |
328 | }); | 357 | }); |
329 | } | 358 | } |
359 | } else if (channel === 'new-window') { | ||
360 | const url = args[0]; | ||
361 | |||
362 | this.actions.app.openExternalUrl({ url }); | ||
330 | } | 363 | } |
331 | } | 364 | } |
332 | 365 | ||
@@ -368,7 +401,7 @@ export default class ServicesStore extends Store { | |||
368 | const service = this.one(serviceId); | 401 | const service = this.one(serviceId); |
369 | service.resetMessageCount(); | 402 | service.resetMessageCount(); |
370 | 403 | ||
371 | service.webview.reload(); | 404 | service.webview.loadURL(service.url); |
372 | } | 405 | } |
373 | 406 | ||
374 | @action _reloadActive() { | 407 | @action _reloadActive() { |
@@ -491,18 +524,19 @@ export default class ServicesStore extends Store { | |||
491 | const showMessageBadgeWhenMuted = this.stores.settings.all.showMessageBadgeWhenMuted; | 524 | const showMessageBadgeWhenMuted = this.stores.settings.all.showMessageBadgeWhenMuted; |
492 | const showMessageBadgesEvenWhenMuted = this.stores.ui.showMessageBadgesEvenWhenMuted; | 525 | const showMessageBadgesEvenWhenMuted = this.stores.ui.showMessageBadgesEvenWhenMuted; |
493 | 526 | ||
494 | const unreadDirectMessageCount = this.enabled | 527 | const unreadDirectMessageCount = this.allDisplayed |
495 | .filter(s => (showMessageBadgeWhenMuted || s.isNotificationEnabled) && showMessageBadgesEvenWhenMuted) | 528 | .filter(s => (showMessageBadgeWhenMuted || s.isNotificationEnabled) && showMessageBadgesEvenWhenMuted && s.isBadgeEnabled) |
496 | .map(s => s.unreadDirectMessageCount) | 529 | .map(s => s.unreadDirectMessageCount) |
497 | .reduce((a, b) => a + b, 0); | 530 | .reduce((a, b) => a + b, 0); |
498 | 531 | ||
499 | const unreadIndirectMessageCount = this.enabled | 532 | const unreadIndirectMessageCount = this.allDisplayed |
500 | .filter(s => (showMessageBadgeWhenMuted || s.isIndirectMessageBadgeEnabled) && showMessageBadgesEvenWhenMuted) | 533 | .filter(s => (showMessageBadgeWhenMuted && showMessageBadgesEvenWhenMuted) && (s.isBadgeEnabled && s.isIndirectMessageBadgeEnabled)) |
501 | .map(s => s.unreadIndirectMessageCount) | 534 | .map(s => s.unreadIndirectMessageCount) |
502 | .reduce((a, b) => a + b, 0); | 535 | .reduce((a, b) => a + b, 0); |
503 | 536 | ||
504 | // We can't just block this earlier, otherwise the mobx reaction won't be aware of the vars to watch in some cases | 537 | // We can't just block this earlier, otherwise the mobx reaction won't be aware of the vars to watch in some cases |
505 | if (showMessageBadgesEvenWhenMuted) { | 538 | if (showMessageBadgesEvenWhenMuted) { |
539 | console.log('set badge', unreadDirectMessageCount, unreadIndirectMessageCount); | ||
506 | this.actions.app.setBadge({ | 540 | this.actions.app.setBadge({ |
507 | unreadDirectMessageCount, | 541 | unreadDirectMessageCount, |
508 | unreadIndirectMessageCount, | 542 | unreadIndirectMessageCount, |
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/styles/button.scss b/src/styles/button.scss index 75d2cb1d4..8d2adbbcc 100644 --- a/src/styles/button.scss +++ b/src/styles/button.scss | |||
@@ -48,6 +48,18 @@ | |||
48 | } | 48 | } |
49 | } | 49 | } |
50 | 50 | ||
51 | &.franz-form__button--warning { | ||
52 | background: $theme-brand-warning; | ||
53 | |||
54 | &:hover { | ||
55 | background: darken($theme-brand-warning, 5%); | ||
56 | } | ||
57 | |||
58 | &:active { | ||
59 | background: lighten($theme-brand-warning, 5%); | ||
60 | } | ||
61 | } | ||
62 | |||
51 | &.franz-form__button--inverted { | 63 | &.franz-form__button--inverted { |
52 | background: none; | 64 | background: none; |
53 | padding: 10px 20px; | 65 | padding: 10px 20px; |
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/image-upload.scss b/src/styles/image-upload.scss new file mode 100644 index 000000000..06176a7af --- /dev/null +++ b/src/styles/image-upload.scss | |||
@@ -0,0 +1,91 @@ | |||
1 | .image-upload { | ||
2 | position: absolute; | ||
3 | width: 140px; | ||
4 | height: 140px; | ||
5 | border: 1px solid $theme-gray-lighter; | ||
6 | border-radius: $theme-border-radius-small; | ||
7 | background: $theme-gray-lightest; | ||
8 | overflow: hidden; | ||
9 | margin-top: 5px; | ||
10 | |||
11 | &__preview, | ||
12 | &__action { | ||
13 | position: absolute; | ||
14 | top: 0; | ||
15 | left: 0; | ||
16 | right: 0; | ||
17 | } | ||
18 | |||
19 | &__preview { | ||
20 | z-index: 1; | ||
21 | background-size: cover; | ||
22 | background-size: 100%; | ||
23 | background-repeat: no-repeat; | ||
24 | background-position: center center; | ||
25 | border-radius: 3px; | ||
26 | } | ||
27 | |||
28 | &__action { | ||
29 | position: relative; | ||
30 | z-index: 10; | ||
31 | opacity: 0; | ||
32 | transition: opacity 0.5s; | ||
33 | display: flex; | ||
34 | justify-content: center; | ||
35 | |||
36 | &-background { | ||
37 | position: absolute; | ||
38 | top: 0; | ||
39 | left: 0; | ||
40 | right: 0; | ||
41 | bottom: 0; | ||
42 | background: rgba($theme-gray, 0.7); | ||
43 | z-index: 10; | ||
44 | } | ||
45 | |||
46 | button { | ||
47 | position: relative; | ||
48 | z-index: 100; | ||
49 | color: #FFF; | ||
50 | |||
51 | .mdi { | ||
52 | color: #FFF; | ||
53 | } | ||
54 | } | ||
55 | } | ||
56 | |||
57 | &__dropzone { | ||
58 | text-align: center; | ||
59 | border-radius: 5px; | ||
60 | padding: 10px; | ||
61 | display: flex; | ||
62 | align-items: center; | ||
63 | justify-content: center; | ||
64 | flex-direction: column; | ||
65 | } | ||
66 | |||
67 | &__dropzone, | ||
68 | button { | ||
69 | .mdi { | ||
70 | margin-bottom: 5px; | ||
71 | } | ||
72 | |||
73 | p { | ||
74 | font-size: 10px; | ||
75 | line-height: 10px; | ||
76 | } | ||
77 | } | ||
78 | |||
79 | &:hover { | ||
80 | .image-upload__action { | ||
81 | opacity: 1; | ||
82 | } | ||
83 | } | ||
84 | } | ||
85 | |||
86 | .image-upload-wrapper { | ||
87 | .mdi { | ||
88 | font-size: 40px; | ||
89 | color: $theme-gray-light; | ||
90 | } | ||
91 | } \ No newline at end of file | ||
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/main.scss b/src/styles/main.scss index 0a082729c..261396f6f 100644 --- a/src/styles/main.scss +++ b/src/styles/main.scss | |||
@@ -35,3 +35,4 @@ $mdi-font-path: '../node_modules/mdi/fonts'; | |||
35 | @import './button.scss'; | 35 | @import './button.scss'; |
36 | @import './searchInput.scss'; | 36 | @import './searchInput.scss'; |
37 | @import './select.scss'; | 37 | @import './select.scss'; |
38 | @import './image-upload.scss'; | ||
diff --git a/src/styles/searchInput.scss b/src/styles/searchInput.scss index 28ff09fc4..633a31e09 100644 --- a/src/styles/searchInput.scss +++ b/src/styles/searchInput.scss | |||
@@ -1,4 +1,20 @@ | |||
1 | .search-input { | 1 | .search-input { |
2 | width: 100%; | 2 | width: 100%; |
3 | height: auto; | 3 | height: auto; |
4 | display: flex; | ||
5 | align-items: center; | ||
6 | padding: 0 10px; | ||
7 | border-radius: 30px; | ||
8 | background: $theme-gray-lightest; | ||
9 | padding: 5px 10px; | ||
10 | @extend %headline; | ||
11 | color: $theme-gray-light; | ||
12 | |||
13 | input { | ||
14 | padding-left: 10px; | ||
15 | background: none; | ||
16 | border: 0; | ||
17 | flex: 1; | ||
18 | color: $theme-gray-light; | ||
19 | } | ||
4 | } | 20 | } |
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 73cef0813..2182c9b5f 100644 --- a/src/styles/settings.scss +++ b/src/styles/settings.scss | |||
@@ -111,6 +111,26 @@ | |||
111 | &::-webkit-scrollbar-thumb:window-inactive { | 111 | &::-webkit-scrollbar-thumb:window-inactive { |
112 | background: none; | 112 | background: none; |
113 | } | 113 | } |
114 | |||
115 | .service-flex-grid { | ||
116 | display: flex; | ||
117 | } | ||
118 | |||
119 | .service-name { | ||
120 | flex: 1px; | ||
121 | } | ||
122 | |||
123 | .service-icon { | ||
124 | width: 140px; | ||
125 | float: right; | ||
126 | margin-top: 30px; | ||
127 | margin-left: 40px; | ||
128 | |||
129 | label { | ||
130 | font-weight: bold; | ||
131 | letter-spacing: -0.1px; | ||
132 | } | ||
133 | } | ||
114 | } | 134 | } |
115 | 135 | ||
116 | .settings__close { | 136 | .settings__close { |
@@ -129,30 +149,28 @@ | |||
129 | } | 149 | } |
130 | } | 150 | } |
131 | 151 | ||
132 | .settings__search-header { | 152 | .search-input { |
133 | display: flex; | 153 | margin-bottom: 30px; |
134 | align-items: center; | 154 | } |
135 | padding: 0 10px; | ||
136 | border-radius: $theme-border-radius; | ||
137 | transition: background $theme-transition-time; | ||
138 | @extend %headline; | ||
139 | font-size: 22px; | ||
140 | |||
141 | &:hover { | ||
142 | background: darken($theme-gray-lighter, 5%); | ||
143 | } | ||
144 | 155 | ||
145 | input { | 156 | &__options { |
146 | padding-left: 10px; | 157 | margin-top: 20px; |
147 | background: none; | 158 | flex: 1; |
148 | border: 0; | ||
149 | flex: 1; | ||
150 | @extend %headline; | ||
151 | } | ||
152 | } | 159 | } |
153 | 160 | ||
154 | .settings__options { | 161 | &__settings-group { |
155 | margin-top: 30px; | 162 | margin-top: 10px; |
163 | |||
164 | h3 { | ||
165 | font-weight: bold; | ||
166 | margin: 25px 0 15px; | ||
167 | color: $theme-gray-light; | ||
168 | letter-spacing: -0.1px; | ||
169 | |||
170 | &:first-of-type { | ||
171 | margin-top: 0; | ||
172 | } | ||
173 | } | ||
156 | } | 174 | } |
157 | 175 | ||
158 | .settings__message { | 176 | .settings__message { |
@@ -173,10 +191,6 @@ | |||
173 | margin: -10px 0 20px 55px;; | 191 | margin: -10px 0 20px 55px;; |
174 | font-size: 12px; | 192 | font-size: 12px; |
175 | color: $theme-gray-light; | 193 | color: $theme-gray-light; |
176 | |||
177 | &:last-of-type { | ||
178 | margin-bottom: 30px; | ||
179 | } | ||
180 | } | 194 | } |
181 | 195 | ||
182 | .settings__controls { | 196 | .settings__controls { |
@@ -312,12 +326,6 @@ | |||
312 | } | 326 | } |
313 | } | 327 | } |
314 | 328 | ||
315 | // @include element(add-service-teaser) { | ||
316 | // height: auto; | ||
317 | // margin-top: 20px; | ||
318 | // display: block; | ||
319 | // text-align: center; | ||
320 | // } | ||
321 | .emoji { | 329 | .emoji { |
322 | display: block; | 330 | display: block; |
323 | font-size: 40px; | 331 | font-size: 40px; |
diff --git a/src/styles/welcome.scss b/src/styles/welcome.scss index cfdcc80ad..46299b966 100644 --- a/src/styles/welcome.scss +++ b/src/styles/welcome.scss | |||
@@ -73,15 +73,8 @@ | |||
73 | width: 35px; | 73 | width: 35px; |
74 | height: 35px; | 74 | height: 35px; |
75 | margin: 0 10px 15px; | 75 | margin: 0 10px 15px; |
76 | filter: grayscale(1) | ||
77 | opacity(0.5); | ||
78 | transition: 0.5s filter, 0.5s opacity; | 76 | transition: 0.5s filter, 0.5s opacity; |
79 | 77 | ||
80 | &:hover { | ||
81 | filter: grayscale(0); | ||
82 | opacity: (1); | ||
83 | } | ||
84 | |||
85 | img { | 78 | img { |
86 | width: 35px; | 79 | width: 35px; |
87 | } | 80 | } |
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/notifications.js b/src/webview/notifications.js index 4f602bfdb..2020bbdc6 100644 --- a/src/webview/notifications.js +++ b/src/webview/notifications.js | |||
@@ -16,7 +16,9 @@ class Notification { | |||
16 | })); | 16 | })); |
17 | 17 | ||
18 | ipcRenderer.once(`notification-onclick:${this.notificationId}`, () => { | 18 | ipcRenderer.once(`notification-onclick:${this.notificationId}`, () => { |
19 | this.onclick(); | 19 | if (typeof this.onclick === 'function') { |
20 | this.onclick(); | ||
21 | } | ||
20 | }); | 22 | }); |
21 | } | 23 | } |
22 | 24 | ||
diff --git a/src/webview/plugin.js b/src/webview/plugin.js index c877132b1..d9e021e6d 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'; |
7 | import './notifications.js'; | 9 | import './notifications'; |
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,34 @@ 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); |
42 | |||
43 | // Patching window.open | ||
44 | const originalWindowOpen = window.open; | ||
45 | |||
46 | window.open = (url, frameName, features) => { | ||
47 | // We need to differentiate if the link should be opened in a popup or in the systems default browser | ||
48 | if (!frameName && !features) { | ||
49 | return ipcRenderer.sendToHost('new-window', url); | ||
50 | } | ||
51 | |||
52 | return originalWindowOpen(url, frameName, features); | ||
53 | }; | ||
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 | ||