aboutsummaryrefslogtreecommitdiffstats
path: root/src/components
diff options
context:
space:
mode:
authorLibravatar Amine El Mouafik <412895+kytwb@users.noreply.github.com>2021-02-08 10:34:45 +0100
committerLibravatar GitHub <noreply@github.com>2021-02-08 10:34:45 +0100
commit035002ceedf78d5ec73eabc0df7f06139939b967 (patch)
tree1c0d1e9531bae05fb65d70b9ea25baf404b74fe1 /src/components
parentdocs: add k0staa as a contributor (#1193) (diff)
downloadferdium-app-035002ceedf78d5ec73eabc0df7f06139939b967.tar.gz
ferdium-app-035002ceedf78d5ec73eabc0df7f06139939b967.tar.zst
ferdium-app-035002ceedf78d5ec73eabc0df7f06139939b967.zip
Synchronize with Franz 5.6.0 (#1033)
Co-authored-by: FranzBot <i18n@meetfranz.com> Co-authored-by: vantezzen <hello@vantezzen.io> Co-authored-by: Makazzz <makazzzpro@live.ca> Co-authored-by: Stefan Malzner <stefan@adlk.io> Co-authored-by: Amine Mouafik <amine@mouafik.fr>
Diffstat (limited to 'src/components')
-rw-r--r--src/components/auth/Pricing.js29
-rw-r--r--src/components/auth/SetupAssistant.js319
-rw-r--r--src/components/layout/Sidebar.js3
-rw-r--r--src/components/services/content/ServiceView.js73
-rw-r--r--src/components/services/content/ServiceWebview.js5
-rw-r--r--src/components/services/content/Services.js6
-rw-r--r--src/components/services/tabs/TabItem.js87
-rw-r--r--src/components/settings/services/EditServiceForm.js13
-rw-r--r--src/components/settings/settings/EditSettingsForm.js14
-rw-r--r--src/components/ui/FeatureList.js2
-rw-r--r--src/components/ui/Modal/index.js4
11 files changed, 461 insertions, 94 deletions
diff --git a/src/components/auth/Pricing.js b/src/components/auth/Pricing.js
index 593cb9c4b..4f5a76c8a 100644
--- a/src/components/auth/Pricing.js
+++ b/src/components/auth/Pricing.js
@@ -67,6 +67,15 @@ const messages = defineMessages({
67}); 67});
68 68
69const styles = theme => ({ 69const styles = theme => ({
70 root: {
71 width: '500px !important',
72 textAlign: 'center',
73 padding: 20,
74 zIndex: 100,
75
76 '& h1': {
77 },
78 },
70 container: { 79 container: {
71 position: 'relative', 80 position: 'relative',
72 marginLeft: -150, 81 marginLeft: -150,
@@ -86,8 +95,8 @@ const styles = theme => ({
86 featureContainer: { 95 featureContainer: {
87 width: 300, 96 width: 300,
88 position: 'absolute', 97 position: 'absolute',
89 left: 'calc(100% / 2 + 225px)', 98 left: 'calc(100% / 2 + 250px)',
90 top: 155, 99 marginTop: 20,
91 background: theme.signup.pricing.feature.background, 100 background: theme.signup.pricing.feature.background,
92 height: 'auto', 101 height: 'auto',
93 padding: 20, 102 padding: 20,
@@ -174,8 +183,8 @@ export default @injectSheet(styles) @observer class Signup extends Component {
174 const [intPart, fractionPart] = (price).toString().split('.'); 183 const [intPart, fractionPart] = (price).toString().split('.');
175 184
176 return ( 185 return (
177 <div className={classnames('auth__scroll-container', classes.container)}> 186 <>
178 <div className={classnames('auth__container', 'auth__container--signup', classes.content)}> 187 <div className={classnames('auth__container', classes.root, classes.container)}>
179 <form className="franz-form auth__form"> 188 <form className="franz-form auth__form">
180 {isLoadingRequiredData ? <Loader /> : ( 189 {isLoadingRequiredData ? <Loader /> : (
181 <img 190 <img
@@ -212,7 +221,7 @@ export default @injectSheet(styles) @observer class Signup extends Component {
212 <p className={classnames(classes.price, classes.trialPrice)}> 221 <p className={classnames(classes.price, classes.trialPrice)}>
213 <span className={classes.figure}> 222 <span className={classes.figure}>
214 {currency} 223 {currency}
215 0 224 0
216 </span> 225 </span>
217 <sup>00</sup> 226 <sup>00</sup>
218 </p> 227 </p>
@@ -234,7 +243,7 @@ export default @injectSheet(styles) @observer class Signup extends Component {
234 </ul> 243 </ul>
235 </div> 244 </div>
236 {trialActivationError && ( 245 {trialActivationError && (
237 <p className={classes.error}>{intl.formatMessage(messages.activationError)}</p> 246 <p className={classes.error}>{intl.formatMessage(messages.activationError)}</p>
238 )} 247 )}
239 <Button 248 <Button
240 label={intl.formatMessage(!canSkipTrial ? messages.ctaStart : messages.ctaAccept)} 249 label={intl.formatMessage(!canSkipTrial ? messages.ctaStart : messages.ctaAccept)}
@@ -244,9 +253,9 @@ export default @injectSheet(styles) @observer class Signup extends Component {
244 disabled={isLoadingRequiredData || isActivatingTrial} 253 disabled={isLoadingRequiredData || isActivatingTrial}
245 /> 254 />
246 {canSkipTrial && ( 255 {canSkipTrial && (
247 <p className={classes.skipLink}> 256 <p className={classes.skipLink}>
248 <a href="#/">{intl.formatMessage(messages.ctaSkip)}</a> 257 <a href="#/">{intl.formatMessage(messages.ctaSkip)}</a>
249 </p> 258 </p>
250 )} 259 )}
251 </form> 260 </form>
252 </div> 261 </div>
@@ -256,7 +265,7 @@ export default @injectSheet(styles) @observer class Signup extends Component {
256 </H2> 265 </H2>
257 <FeatureList /> 266 <FeatureList />
258 </div> 267 </div>
259 </div> 268 </>
260 ); 269 );
261 } 270 }
262} 271}
diff --git a/src/components/auth/SetupAssistant.js b/src/components/auth/SetupAssistant.js
new file mode 100644
index 000000000..e03cf9101
--- /dev/null
+++ b/src/components/auth/SetupAssistant.js
@@ -0,0 +1,319 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react';
4import { defineMessages, intlShape } from 'react-intl';
5import injectSheet from 'react-jss';
6import classnames from 'classnames';
7
8import { Input, Button } from '@meetfranz/forms';
9import { Badge } from '@meetfranz/ui';
10import Modal from '../ui/Modal';
11import Infobox from '../ui/Infobox';
12import Appear from '../ui/effects/Appear';
13
14import { CDN_URL } from '../../config';
15
16const SLACK_ID = 'slack';
17
18const messages = defineMessages({
19 headline: {
20 id: 'setupAssistant.headline',
21 defaultMessage: '!!!Let\'s get started',
22 },
23 subHeadline: {
24 id: 'setupAssistant.subheadline',
25 defaultMessage: '!!!Choose from our most used services and get back on top of your messaging now.',
26 },
27 submitButtonLabel: {
28 id: 'setupAssistant.submit.label',
29 defaultMessage: '!!!Let\'s go',
30 },
31 inviteSuccessInfo: {
32 id: 'invite.successInfo',
33 defaultMessage: '!!!Invitations sent successfully',
34 },
35});
36
37const styles = theme => ({
38 root: {
39 width: '500px !important',
40 textAlign: 'center',
41 padding: 20,
42
43 '& h1': {
44 },
45 },
46 servicesGrid: {
47 display: 'flex',
48 flexWrap: 'wrap',
49 justifyContent: 'space-between',
50 },
51 serviceContainer: {
52 background: theme.colorBackground,
53 position: 'relative',
54 width: '32%',
55 display: 'flex',
56 alignItems: 'center',
57 flexDirection: 'column',
58 justifyContent: 'center',
59 padding: 20,
60 borderRadius: theme.borderRadius,
61 marginBottom: 10,
62 opacity: 0.5,
63 transition: 'all 0.25s',
64 border: [3, 'solid', 'transparent'],
65
66 '& h2': {
67 margin: [10, 0, 0],
68 color: theme.colorText,
69 },
70
71 '&:hover': {
72 border: [3, 'solid', theme.brandPrimary],
73 '& $serviceIcon': {
74 },
75 },
76
77 },
78 selected: {
79 border: [3, 'solid', theme.brandPrimary],
80 background: `${theme.brandPrimary}47`,
81 opacity: 1,
82 },
83 serviceIcon: {
84 width: 50,
85 transition: 'all 0.25s',
86 },
87
88 slackModalContent: {
89 textAlign: 'center',
90
91 '& img': {
92 width: 50,
93 marginBottom: 20,
94 },
95 },
96 modalActionContainer: {
97 display: 'flex',
98 flexDirection: 'column',
99 justifyContent: 'center',
100 alignItems: 'center',
101 },
102 ctaCancel: {
103 background: 'none !important',
104 },
105 slackBadge: {
106 position: 'absolute',
107 bottom: 4,
108 height: 'auto',
109 padding: '0px 4px',
110 borderRadius: theme.borderRadiusSmall,
111 margin: 0,
112 display: 'flex',
113 overflow: 'hidden',
114 },
115 clearSlackWorkspace: {
116 background: theme.inputPrefixColor,
117 marginLeft: 5,
118 height: '100%',
119 color: theme.colorText,
120 display: 'inline-flex',
121 justifyContent: 'center',
122 alignItems: 'center',
123 marginRight: -4,
124 padding: [0, 5],
125 },
126});
127
128@injectSheet(styles) @observer
129class SetupAssistant extends Component {
130 static propTypes = {
131 classes: PropTypes.object.isRequired,
132 onSubmit: PropTypes.func.isRequired,
133 isInviteSuccessful: PropTypes.bool,
134 services: PropTypes.object.isRequired,
135 isSettingUpServices: PropTypes.bool.isRequired,
136 };
137
138 static defaultProps = {
139 isInviteSuccessful: false,
140 };
141
142 static contextTypes = {
143 intl: intlShape,
144 };
145
146 state = {
147 services: [{
148 id: 'whatsapp',
149 }, {
150 id: 'messenger',
151 }, {
152 id: 'gmail',
153 }],
154 isSlackModalOpen: false,
155 slackWorkspace: '',
156 };
157
158 slackWorkspaceHandler() {
159 const { slackWorkspace = '', services } = this.state;
160
161 const sanitizedWorkspace = slackWorkspace.trim().replace(/^https?:\/\//, '');
162
163 if (sanitizedWorkspace) {
164 const index = services.findIndex(s => s.id === SLACK_ID);
165
166 if (index === -1) {
167 const newServices = services;
168 newServices.push({ id: SLACK_ID, team: sanitizedWorkspace });
169 this.setState({ services: newServices });
170 }
171 }
172
173 this.setState({
174 isSlackModalOpen: false,
175 slackWorkspace: sanitizedWorkspace,
176 });
177 }
178
179 render() {
180 const { intl } = this.context;
181 const {
182 classes, isInviteSuccessful, onSubmit, services, isSettingUpServices,
183 } = this.props;
184 const { isSlackModalOpen, slackWorkspace, services: addedServices } = this.state;
185
186 return (
187 <div className={`auth__container ${classes.root}`}>
188 {this.state.showSuccessInfo && isInviteSuccessful && (
189 <Appear>
190 <Infobox
191 type="success"
192 icon="checkbox-marked-circle-outline"
193 dismissable
194 >
195 {intl.formatMessage(messages.inviteSuccessInfo)}
196 </Infobox>
197 </Appear>
198 )}
199
200 <img
201 src="./assets/images/logo.svg"
202 className="auth__logo"
203 alt=""
204 />
205 <h1>
206 {intl.formatMessage(messages.headline)}
207 </h1>
208 <h2>
209 {intl.formatMessage(messages.subHeadline)}
210 </h2>
211 <div className={classnames('grid', classes.servicesGrid)}>
212 {Object.keys(services).map((id) => {
213 const service = services[id];
214 return (
215 <button
216 className={classnames({
217 [classes.serviceContainer]: true,
218 [classes.selected]: this.state.services.findIndex(s => s.id === id) !== -1,
219 })}
220 key={id}
221 onClick={() => {
222 const index = this.state.services.findIndex(s => s.id === id);
223 if (index === -1) {
224 if (id === SLACK_ID) {
225 this.setState({ isSlackModalOpen: true });
226 } else {
227 addedServices.push({ id });
228 }
229 } else {
230 addedServices.splice(index, 1);
231 if (id === SLACK_ID) {
232 this.setState({
233 slackWorkspace: '',
234 });
235 }
236 }
237
238 this.setState({ services: addedServices });
239 }}
240 type="button"
241 >
242 <img
243 src={`${CDN_URL}/recipes/dist/${id}/src/icon.svg`}
244 className={classes.serviceIcon}
245 alt=""
246 />
247 <h2>
248 {service.name}
249 </h2>
250 {id === SLACK_ID && slackWorkspace && (
251 <Badge type="secondary" className={classes.slackBadge}>
252 {slackWorkspace}
253 <button
254 type="button"
255 className={classes.clearSlackWorkspace}
256 onClick={() => {
257 this.setState({
258 slackWorkspace: '',
259 });
260 }}
261 >
262 x
263 </button>
264 </Badge>
265 )}
266 </button>
267 );
268 })}
269 </div>
270 <Modal
271 isOpen={isSlackModalOpen}
272 // isBlocking={false}
273 close={() => this.setState({ isSlackModalOpen: false })}
274 >
275 <div className={classes.slackModalContent}>
276 <img src={`${CDN_URL}/recipes/dist/slack/src/icon.svg`} alt="" />
277 <h1>Create your first Slack workspace</h1>
278 <form onSubmit={(e) => {
279 e.preventDefault();
280 this.slackWorkspaceHandler();
281 }}
282 >
283 <Input
284 suffix=".slack.com"
285 placeholder="workspace-url"
286 onChange={e => this.setState({ slackWorkspace: e.target.value })}
287 value={slackWorkspace}
288 />
289 <div className={classes.modalActionContainer}>
290 <Button
291 type="submit"
292 label="Save"
293 />
294 <Button
295 type="link"
296 buttonType="secondary"
297 label="Cancel"
298 className={classes.ctaCancel}
299 onClick={() => this.setState({ slackWorkspace: '' })}
300 />
301 </div>
302 </form>
303 </div>
304 </Modal>
305 <Button
306 type="button"
307 className="auth__button"
308 // disabled={!atLeastOneEmailAddress}
309 label={intl.formatMessage(messages.submitButtonLabel)}
310 onClick={() => onSubmit(this.state.services)}
311 busy={isSettingUpServices}
312 disabled={isSettingUpServices || addedServices.length === 0}
313 />
314 </div>
315 );
316 }
317}
318
319export default SetupAssistant;
diff --git a/src/components/layout/Sidebar.js b/src/components/layout/Sidebar.js
index 90bbe86e9..a47e74db0 100644
--- a/src/components/layout/Sidebar.js
+++ b/src/components/layout/Sidebar.js
@@ -60,6 +60,7 @@ export default @inject('stores', 'actions') @observer class Sidebar extends Comp
60 isAppMuted: PropTypes.bool.isRequired, 60 isAppMuted: PropTypes.bool.isRequired,
61 isWorkspaceDrawerOpen: PropTypes.bool.isRequired, 61 isWorkspaceDrawerOpen: PropTypes.bool.isRequired,
62 toggleWorkspaceDrawer: PropTypes.func.isRequired, 62 toggleWorkspaceDrawer: PropTypes.func.isRequired,
63 isTodosServiceActive: PropTypes.bool.isRequired,
63 }; 64 };
64 65
65 static contextTypes = { 66 static contextTypes = {
@@ -96,6 +97,7 @@ export default @inject('stores', 'actions') @observer class Sidebar extends Comp
96 toggleWorkspaceDrawer, 97 toggleWorkspaceDrawer,
97 stores, 98 stores,
98 actions, 99 actions,
100 isTodosServiceActive,
99 } = this.props; 101 } = this.props;
100 const { intl } = this.context; 102 const { intl } = this.context;
101 const todosToggleMessage = ( 103 const todosToggleMessage = (
@@ -140,6 +142,7 @@ export default @inject('stores', 'actions') @observer class Sidebar extends Comp
140 todoActions.toggleTodosPanel(); 142 todoActions.toggleTodosPanel();
141 this.updateToolTip(); 143 this.updateToolTip();
142 }} 144 }}
145 disblaed={isTodosServiceActive}
143 className={`sidebar__button sidebar__button--todos ${todosStore.isTodosPanelVisible ? 'is-active' : ''}`} 146 className={`sidebar__button sidebar__button--todos ${todosStore.isTodosPanelVisible ? 'is-active' : ''}`}
144 data-tip={`${intl.formatMessage(todosToggleMessage)} (${ctrlKey}+T)`} 147 data-tip={`${intl.formatMessage(todosToggleMessage)} (${ctrlKey}+T)`}
145 > 148 >
diff --git a/src/components/services/content/ServiceView.js b/src/components/services/content/ServiceView.js
index d91016c71..444d5fea4 100644
--- a/src/components/services/content/ServiceView.js
+++ b/src/components/services/content/ServiceView.js
@@ -1,6 +1,6 @@
1import React, { Component, Fragment } from 'react'; 1import React, { Component, Fragment } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { autorun, reaction } from 'mobx'; 3import { autorun } from 'mobx';
4import { observer, inject } from 'mobx-react'; 4import { observer, inject } from 'mobx-react';
5import classnames from 'classnames'; 5import classnames from 'classnames';
6 6
@@ -27,11 +27,7 @@ export default @inject('stores', 'actions') @observer class ServiceView extends
27 stores: PropTypes.shape({ 27 stores: PropTypes.shape({
28 settings: PropTypes.instanceOf(SettingsStore).isRequired, 28 settings: PropTypes.instanceOf(SettingsStore).isRequired,
29 }).isRequired, 29 }).isRequired,
30 actions: PropTypes.shape({ 30 isSpellcheckerEnabled: PropTypes.bool.isRequired,
31 service: PropTypes.shape({
32 setHibernation: PropTypes.func.isRequired,
33 }).isRequired,
34 }).isRequired,
35 }; 31 };
36 32
37 static defaultProps = { 33 static defaultProps = {
@@ -50,12 +46,6 @@ export default @inject('stores', 'actions') @observer class ServiceView extends
50 46
51 forceRepaintTimeout = null; 47 forceRepaintTimeout = null;
52 48
53 constructor(props) {
54 super(props);
55
56 this.startHibernationTimer = this.startHibernationTimer.bind(this);
57 }
58
59 componentDidMount() { 49 componentDidMount() {
60 this.autorunDisposer = autorun(() => { 50 this.autorunDisposer = autorun(() => {
61 if (this.props.service.isActive) { 51 if (this.props.service.isActive) {
@@ -65,32 +55,6 @@ export default @inject('stores', 'actions') @observer class ServiceView extends
65 }, 100); 55 }, 100);
66 } 56 }
67 }); 57 });
68
69 reaction(
70 () => this.props.service.isActive,
71 () => {
72 if (!this.props.service.isActive && this.props.stores.settings.all.app.hibernate) {
73 // Service is inactive - start hibernation countdown
74 this.startHibernationTimer();
75 } else {
76 if (this.hibernationTimer) {
77 // Service is active but we have an active hibernation timer: Clear timeout
78 clearTimeout(this.hibernationTimer);
79 }
80
81 // Service is active, wake up service from hibernation
82 this.props.actions.service.setHibernation({
83 serviceId: this.props.service.id,
84 hibernating: false,
85 });
86 }
87 },
88 );
89
90 // Start hibernation counter if we are in background
91 if (!this.props.service.isActive && this.props.stores.settings.all.app.hibernate) {
92 this.startHibernationTimer();
93 }
94 } 58 }
95 59
96 componentWillUnmount() { 60 componentWillUnmount() {
@@ -110,19 +74,6 @@ export default @inject('stores', 'actions') @observer class ServiceView extends
110 }); 74 });
111 }; 75 };
112 76
113 startHibernationTimer() {
114 const timerDuration = (Number(this.props.stores.settings.all.app.hibernationStrategy) || 300) * 1000;
115
116 const hibernationTimer = setTimeout(() => {
117 this.props.actions.service.setHibernation({
118 serviceId: this.props.service.id,
119 hibernating: true,
120 });
121 }, timerDuration);
122
123 this.hibernationTimer = hibernationTimer;
124 }
125
126 render() { 77 render() {
127 const { 78 const {
128 detachService, 79 detachService,
@@ -132,6 +83,7 @@ export default @inject('stores', 'actions') @observer class ServiceView extends
132 edit, 83 edit,
133 enable, 84 enable,
134 stores, 85 stores,
86 isSpellcheckerEnabled,
135 } = this.props; 87 } = this.props;
136 88
137 const { 89 const {
@@ -193,22 +145,19 @@ export default @inject('stores', 'actions') @observer class ServiceView extends
193 </Fragment> 145 </Fragment>
194 ) : ( 146 ) : (
195 <> 147 <>
196 {(!service.isHibernating || service.disableHibernation) ? ( 148 {(!service.isHibernating || service.isHibernationEnabled) ? (
197 <> 149 <>
198 {showNavBar && ( 150 {showNavBar && (
199 <WebControlsScreen service={service} /> 151 <WebControlsScreen service={service} />
200 )} 152 )}
201 <ServiceWebview 153 {!service.isHibernating && (
202 service={service} 154 <ServiceWebview
203 setWebviewReference={setWebviewReference} 155 service={service}
204 detachService={detachService} 156 setWebviewReference={setWebviewReference}
205 /> 157 detachService={detachService}
206 {/* {service.lostRecipeConnection && ( 158 isSpellcheckerEnabled={isSpellcheckerEnabled}
207 <ConnectionLostBanner
208 name={service.name}
209 reload={reload}
210 /> 159 />
211 )} */} 160 )}
212 </> 161 </>
213 ) : ( 162 ) : (
214 <div> 163 <div>
diff --git a/src/components/services/content/ServiceWebview.js b/src/components/services/content/ServiceWebview.js
index 2e3354279..4edbde5e2 100644
--- a/src/components/services/content/ServiceWebview.js
+++ b/src/components/services/content/ServiceWebview.js
@@ -15,6 +15,7 @@ class ServiceWebview extends Component {
15 service: PropTypes.instanceOf(ServiceModel).isRequired, 15 service: PropTypes.instanceOf(ServiceModel).isRequired,
16 setWebviewReference: PropTypes.func.isRequired, 16 setWebviewReference: PropTypes.func.isRequired,
17 detachService: PropTypes.func.isRequired, 17 detachService: PropTypes.func.isRequired,
18 isSpellcheckerEnabled: PropTypes.bool.isRequired,
18 }; 19 };
19 20
20 @observable webview = null; 21 @observable webview = null;
@@ -55,6 +56,7 @@ class ServiceWebview extends Component {
55 const { 56 const {
56 service, 57 service,
57 setWebviewReference, 58 setWebviewReference,
59 isSpellcheckerEnabled,
58 } = this.props; 60 } = this.props;
59 61
60 const preloadScript = path.join(__dirname, '../../../', 'webview', 'recipe.js'); 62 const preloadScript = path.join(__dirname, '../../../', 'webview', 'recipe.js');
@@ -70,7 +72,7 @@ class ServiceWebview extends Component {
70 autosize 72 autosize
71 src={service.url} 73 src={service.url}
72 preload={preloadScript} 74 preload={preloadScript}
73 partition={`persist:service-${service.id}`} 75 partition={service.partition}
74 onDidAttach={() => { 76 onDidAttach={() => {
75 setWebviewReference({ 77 setWebviewReference({
76 serviceId: service.id, 78 serviceId: service.id,
@@ -81,6 +83,7 @@ class ServiceWebview extends Component {
81 useragent={service.userAgent} 83 useragent={service.userAgent}
82 disablewebsecurity={service.recipe.disablewebsecurity ? true : undefined} 84 disablewebsecurity={service.recipe.disablewebsecurity ? true : undefined}
83 allowpopups 85 allowpopups
86 webpreferences={`spellcheck=${isSpellcheckerEnabled ? 1 : 0}`}
84 /> 87 />
85 ); 88 );
86 } 89 }
diff --git a/src/components/services/content/Services.js b/src/components/services/content/Services.js
index da2ee0b9e..f679eeed0 100644
--- a/src/components/services/content/Services.js
+++ b/src/components/services/content/Services.js
@@ -10,6 +10,7 @@ import injectSheet from 'react-jss';
10import ServiceView from './ServiceView'; 10import ServiceView from './ServiceView';
11import Appear from '../../ui/effects/Appear'; 11import Appear from '../../ui/effects/Appear';
12import serverlessLogin from '../../../helpers/serverless-helpers'; 12import serverlessLogin from '../../../helpers/serverless-helpers';
13import { TODOS_RECIPE_ID } from '../../../features/todos';
13 14
14const messages = defineMessages({ 15const messages = defineMessages({
15 welcome: { 16 welcome: {
@@ -58,6 +59,7 @@ export default @injectSheet(styles) @inject('actions') @observer class Services
58 hasActivatedTrial: PropTypes.bool.isRequired, 59 hasActivatedTrial: PropTypes.bool.isRequired,
59 classes: PropTypes.object.isRequired, 60 classes: PropTypes.object.isRequired,
60 actions: PropTypes.object.isRequired, 61 actions: PropTypes.object.isRequired,
62 isSpellcheckerEnabled: PropTypes.bool.isRequired,
61 }; 63 };
62 64
63 static defaultProps = { 65 static defaultProps = {
@@ -111,6 +113,7 @@ export default @injectSheet(styles) @inject('actions') @observer class Services
111 userHasCompletedSignup, 113 userHasCompletedSignup,
112 hasActivatedTrial, 114 hasActivatedTrial,
113 classes, 115 classes,
116 isSpellcheckerEnabled,
114 } = this.props; 117 } = this.props;
115 118
116 const { 119 const {
@@ -168,7 +171,7 @@ export default @injectSheet(styles) @inject('actions') @observer class Services
168 </div> 171 </div>
169 </Appear> 172 </Appear>
170 )} 173 )}
171 {services.map(service => ( 174 {services.filter(service => service.recipe.id !== TODOS_RECIPE_ID).map(service => (
172 <ServiceView 175 <ServiceView
173 key={service.id} 176 key={service.id}
174 service={service} 177 service={service}
@@ -186,6 +189,7 @@ export default @injectSheet(styles) @inject('actions') @observer class Services
186 redirect: false, 189 redirect: false,
187 })} 190 })}
188 upgrade={() => openSettings({ path: 'user' })} 191 upgrade={() => openSettings({ path: 'user' })}
192 isSpellcheckerEnabled={isSpellcheckerEnabled}
189 /> 193 />
190 ))} 194 ))}
191 </div> 195 </div>
diff --git a/src/components/services/tabs/TabItem.js b/src/components/services/tabs/TabItem.js
index efa5fa60c..479f151a6 100644
--- a/src/components/services/tabs/TabItem.js
+++ b/src/components/services/tabs/TabItem.js
@@ -5,9 +5,14 @@ import PropTypes from 'prop-types';
5import { observer } from 'mobx-react'; 5import { observer } from 'mobx-react';
6import classnames from 'classnames'; 6import classnames from 'classnames';
7import { SortableElement } from 'react-sortable-hoc'; 7import { SortableElement } from 'react-sortable-hoc';
8import injectSheet from 'react-jss';
9import ms from 'ms';
8 10
11import { observable, autorun } from 'mobx';
9import ServiceModel from '../../../models/Service'; 12import ServiceModel from '../../../models/Service';
10import { ctrlKey } from '../../../environment'; 13import { ctrlKey, cmdKey } from '../../../environment';
14
15const IS_SERVICE_DEBUGGING_ENABLED = (localStorage.getItem('debug') || '').includes('Franz:Service');
11 16
12const { Menu } = remote; 17const { Menu } = remote;
13 18
@@ -50,9 +55,38 @@ const messages = defineMessages({
50 }, 55 },
51}); 56});
52 57
53@observer 58const styles = {
54class TabItem extends Component { 59 pollIndicator: {
60 position: 'absolute',
61 bottom: 2,
62 width: 10,
63 height: 10,
64 borderRadius: 5,
65 background: 'gray',
66 transition: 'background 0.5s',
67 },
68 pollIndicatorPoll: {
69 left: 2,
70 },
71 pollIndicatorAnswer: {
72 left: 14,
73 },
74 polled: {
75 background: 'yellow !important',
76 transition: 'background 0.1s',
77 },
78 pollAnswered: {
79 background: 'green !important',
80 transition: 'background 0.1s',
81 },
82 stale: {
83 background: 'red !important',
84 },
85};
86
87@injectSheet(styles) @observer class TabItem extends Component {
55 static propTypes = { 88 static propTypes = {
89 classes: PropTypes.object.isRequired,
56 service: PropTypes.instanceOf(ServiceModel).isRequired, 90 service: PropTypes.instanceOf(ServiceModel).isRequired,
57 clickHandler: PropTypes.func.isRequired, 91 clickHandler: PropTypes.func.isRequired,
58 shortcutIndex: PropTypes.number.isRequired, 92 shortcutIndex: PropTypes.number.isRequired,
@@ -71,8 +105,33 @@ class TabItem extends Component {
71 intl: intlShape, 105 intl: intlShape,
72 }; 106 };
73 107
108 @observable isPolled = false;
109
110 @observable isPollAnswered = false;
111
112 componentDidMount() {
113 const { service } = this.props;
114
115 if (IS_SERVICE_DEBUGGING_ENABLED) {
116 autorun(() => {
117 if (Date.now() - service.lastPoll < ms('0.2s')) {
118 this.isPolled = true;
119
120 setTimeout(() => { this.isPolled = false; }, ms('1s'));
121 }
122
123 if (Date.now() - service.lastPollAnswer < ms('0.2s')) {
124 this.isPollAnswered = true;
125
126 setTimeout(() => { this.isPollAnswered = false; }, ms('1s'));
127 }
128 });
129 }
130 }
131
74 render() { 132 render() {
75 const { 133 const {
134 classes,
76 service, 135 service,
77 clickHandler, 136 clickHandler,
78 shortcutIndex, 137 shortcutIndex,
@@ -97,6 +156,7 @@ class TabItem extends Component {
97 }, { 156 }, {
98 label: intl.formatMessage(messages.reload), 157 label: intl.formatMessage(messages.reload),
99 click: reload, 158 click: reload,
159 accelerator: `${cmdKey}+R`,
100 }, { 160 }, {
101 label: intl.formatMessage(messages.edit), 161 label: intl.formatMessage(messages.edit),
102 click: () => openSettings({ 162 click: () => openSettings({
@@ -141,7 +201,7 @@ class TabItem extends Component {
141 201
142 </span> 202 </span>
143 )} 203 )}
144 {service.isHibernating && !service.disableHibernation && ( 204 {service.isHibernating && !service.isHibernationEnabled && (
145 <span className="tab-item__message-count hibernating"> 205 <span className="tab-item__message-count hibernating">
146 206
147 </span> 207 </span>
@@ -153,6 +213,7 @@ class TabItem extends Component {
153 return ( 213 return (
154 <li 214 <li
155 className={classnames({ 215 className={classnames({
216 [classes.stale]: IS_SERVICE_DEBUGGING_ENABLED && service.lostRecipeConnection,
156 'tab-item': true, 217 'tab-item': true,
157 'is-active': service.isActive, 218 'is-active': service.isActive,
158 'has-custom-icon': service.hasCustomIcon, 219 'has-custom-icon': service.hasCustomIcon,
@@ -168,6 +229,24 @@ class TabItem extends Component {
168 alt="" 229 alt=""
169 /> 230 />
170 {notificationBadge} 231 {notificationBadge}
232 {IS_SERVICE_DEBUGGING_ENABLED && (
233 <>
234 <div
235 className={classnames({
236 [classes.pollIndicator]: true,
237 [classes.pollIndicatorPoll]: true,
238 [classes.polled]: this.isPolled,
239 })}
240 />
241 <div
242 className={classnames({
243 [classes.pollIndicator]: true,
244 [classes.pollIndicatorAnswer]: true,
245 [classes.pollAnswered]: this.isPollAnswered,
246 })}
247 />
248 </>
249 )}
171 </li> 250 </li>
172 ); 251 );
173 } 252 }
diff --git a/src/components/settings/services/EditServiceForm.js b/src/components/settings/services/EditServiceForm.js
index f1e70ce59..80f60b3e1 100644
--- a/src/components/settings/services/EditServiceForm.js
+++ b/src/components/settings/services/EditServiceForm.js
@@ -20,7 +20,6 @@ import Select from '../../ui/Select';
20import PremiumFeatureContainer from '../../ui/PremiumFeatureContainer'; 20import PremiumFeatureContainer from '../../ui/PremiumFeatureContainer';
21import LimitReachedInfobox from '../../../features/serviceLimit/components/LimitReachedInfobox'; 21import LimitReachedInfobox from '../../../features/serviceLimit/components/LimitReachedInfobox';
22import { serviceLimitStore } from '../../../features/serviceLimit'; 22import { serviceLimitStore } from '../../../features/serviceLimit';
23
24import { isMac } from '../../../environment'; 23import { isMac } from '../../../environment';
25 24
26const messages = defineMessages({ 25const messages = defineMessages({
@@ -96,9 +95,9 @@ const messages = defineMessages({
96 id: 'settings.service.form.isMutedInfo', 95 id: 'settings.service.form.isMutedInfo',
97 defaultMessage: '!!!When disabled, all notification sounds and audio playback are muted', 96 defaultMessage: '!!!When disabled, all notification sounds and audio playback are muted',
98 }, 97 },
99 disableHibernationInfo: { 98 isHibernationEnabledInfo: {
100 id: 'settings.service.form.disableHibernationInfo', 99 id: 'settings.service.form.isHibernatedEnabledInfo',
101 defaultMessage: '!!!You currently have hibernation enabled but you can disable hibernation for individual services using this option.', 100 defaultMessage: '!!!When enabled, a service will be shut down after a period of time to save system resources.',
102 }, 101 },
103 headlineNotifications: { 102 headlineNotifications: {
104 id: 'settings.service.form.headlineNotifications', 103 id: 'settings.service.form.headlineNotifications',
@@ -375,9 +374,9 @@ export default @observer class EditServiceForm extends Component {
375 <Toggle field={form.$('isEnabled')} /> 374 <Toggle field={form.$('isEnabled')} />
376 {isHibernationFeatureActive && ( 375 {isHibernationFeatureActive && (
377 <> 376 <>
378 <Toggle field={form.$('disableHibernation')} /> 377 <Toggle field={form.$('isHibernationEnabled')} />
379 <p className="settings__help"> 378 <p className="settings__help">
380 {intl.formatMessage(messages.disableHibernationInfo)} 379 {intl.formatMessage(messages.isHibernationEnabledInfo)}
381 </p> 380 </p>
382 </> 381 </>
383 )} 382 )}
@@ -409,7 +408,7 @@ export default @observer class EditServiceForm extends Component {
409 gaEventInfo={{ category: 'User', event: 'upgrade', label: 'spellchecker' }} 408 gaEventInfo={{ category: 'User', event: 'upgrade', label: 'spellchecker' }}
410 > 409 >
411 <div className="settings__settings-group"> 410 <div className="settings__settings-group">
412 <Select field={form.$('spellcheckerLanguage')} multiple /> 411 <Select field={form.$('spellcheckerLanguage')} />
413 </div> 412 </div>
414 </PremiumFeatureContainer> 413 </PremiumFeatureContainer>
415 )} 414 )}
diff --git a/src/components/settings/settings/EditSettingsForm.js b/src/components/settings/settings/EditSettingsForm.js
index cd772214f..f20c38bc9 100644
--- a/src/components/settings/settings/EditSettingsForm.js
+++ b/src/components/settings/settings/EditSettingsForm.js
@@ -173,6 +173,8 @@ export default @observer class EditSettingsForm extends Component {
173 isAdaptableDarkModeEnabled: PropTypes.bool.isRequired, 173 isAdaptableDarkModeEnabled: PropTypes.bool.isRequired,
174 isNightlyEnabled: PropTypes.bool.isRequired, 174 isNightlyEnabled: PropTypes.bool.isRequired,
175 openProcessManager: PropTypes.func.isRequired, 175 openProcessManager: PropTypes.func.isRequired,
176 hasAddedTodosAsService: PropTypes.bool.isRequired,
177 isOnline: PropTypes.bool.isRequired,
176 }; 178 };
177 179
178 static contextTypes = { 180 static contextTypes = {
@@ -227,6 +229,8 @@ export default @observer class EditSettingsForm extends Component {
227 openProcessManager, 229 openProcessManager,
228 isTodosActivated, 230 isTodosActivated,
229 isNightlyEnabled, 231 isNightlyEnabled,
232 hasAddedTodosAsService,
233 isOnline,
230 } = this.props; 234 } = this.props;
231 const { intl } = this.context; 235 const { intl } = this.context;
232 236
@@ -346,7 +350,7 @@ export default @observer class EditSettingsForm extends Component {
346 350
347 <Hr /> 351 <Hr />
348 352
349 {isTodosEnabled && ( 353 {isTodosEnabled && !hasAddedTodosAsService && (
350 <> 354 <>
351 <Toggle field={form.$('enableTodos')} /> 355 <Toggle field={form.$('enableTodos')} />
352 {isTodosActivated && ( 356 {isTodosActivated && (
@@ -535,10 +539,10 @@ export default @observer class EditSettingsForm extends Component {
535 <Toggle 539 <Toggle
536 field={form.$('enableSpellchecking')} 540 field={form.$('enableSpellchecking')}
537 /> 541 />
538 {form.$('enableSpellchecking').value && !isMac && ( 542 {!isMac && form.$('enableSpellchecking').value && (
539 <Select field={form.$('spellcheckerLanguage')} multiple /> 543 <Select field={form.$('spellcheckerLanguage')} />
540 )} 544 )}
541 {form.$('enableSpellchecking').value && isMac && ( 545 {isMac && form.$('enableSpellchecking').value && (
542 <p>{intl.formatMessage(messages.spellCheckerLanguageInfo)}</p> 546 <p>{intl.formatMessage(messages.spellCheckerLanguageInfo)}</p>
543 )} 547 )}
544 </Fragment> 548 </Fragment>
@@ -625,7 +629,7 @@ export default @observer class EditSettingsForm extends Component {
625 buttonType="secondary" 629 buttonType="secondary"
626 label={intl.formatMessage(updateButtonLabelMessage)} 630 label={intl.formatMessage(updateButtonLabelMessage)}
627 onClick={checkForUpdates} 631 onClick={checkForUpdates}
628 disabled={!automaticUpdates || isCheckingForUpdates || isUpdateAvailable} 632 disabled={!automaticUpdates || isCheckingForUpdates || isUpdateAvailable || !isOnline}
629 loaded={!isCheckingForUpdates || !isUpdateAvailable} 633 loaded={!isCheckingForUpdates || !isUpdateAvailable}
630 /> 634 />
631 )} 635 )}
diff --git a/src/components/ui/FeatureList.js b/src/components/ui/FeatureList.js
index dbc2a9078..72c799819 100644
--- a/src/components/ui/FeatureList.js
+++ b/src/components/ui/FeatureList.js
@@ -76,7 +76,7 @@ export class FeatureList extends Component {
76 static propTypes = { 76 static propTypes = {
77 className: PropTypes.string, 77 className: PropTypes.string,
78 featureClassName: PropTypes.string, 78 featureClassName: PropTypes.string,
79 plan: PropTypes.oneOf(Object.values(PLANS)), 79 plan: PropTypes.oneOf(Object.keys(PLANS)),
80 }; 80 };
81 81
82 static defaultProps = { 82 static defaultProps = {
diff --git a/src/components/ui/Modal/index.js b/src/components/ui/Modal/index.js
index 0af521452..a9fa0cd1b 100644
--- a/src/components/ui/Modal/index.js
+++ b/src/components/ui/Modal/index.js
@@ -41,8 +41,6 @@ export default @injectCSS(styles) class Modal extends Component {
41 showClose, 41 showClose,
42 } = this.props; 42 } = this.props;
43 43
44 const appRoot = document.getElementById('root');
45
46 return ( 44 return (
47 <ReactModal 45 <ReactModal
48 isOpen={isOpen} 46 isOpen={isOpen}
@@ -55,7 +53,7 @@ export default @injectCSS(styles) class Modal extends Component {
55 portal={portal} 53 portal={portal}
56 onRequestClose={close} 54 onRequestClose={close}
57 shouldCloseOnOverlayClick={shouldCloseOnOverlayClick} 55 shouldCloseOnOverlayClick={shouldCloseOnOverlayClick}
58 appElement={appRoot} 56 appElement={document.getElementById('root')}
59 > 57 >
60 {showClose && close && ( 58 {showClose && close && (
61 <button 59 <button